diff --git a/.docker/nautilus_trader.dockerfile b/.docker/nautilus_trader.dockerfile index 9b720d7d4bb9..352b83255f08 100644 --- a/.docker/nautilus_trader.dockerfile +++ b/.docker/nautilus_trader.dockerfile @@ -34,7 +34,7 @@ RUN (cd nautilus_core && cargo build --release) COPY nautilus_trader ./nautilus_trader COPY README.md ./ -RUN poetry install --only main +RUN poetry install --only main --all-extras RUN poetry build -f wheel RUN python -m pip install ./dist/*whl --force RUN find /usr/local/lib/python3.11/site-packages -name "*.pyc" -exec rm -f {} \; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e9084e29caa..aec13c95b984 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -190,7 +190,7 @@ jobs: fail-fast: false matrix: arch: [x64] - os: [ubuntu-20.04, ubuntu-latest, macos-latest, macos-13, windows-latest] + os: [ubuntu-20.04, ubuntu-latest, windows-latest] python-version: ["3.9", "3.10", "3.11"] name: publish-wheels - Python ${{ matrix.python-version }} (${{ matrix.arch }} ${{ matrix.os }}) runs-on: ${{ matrix.os }} diff --git a/.gitignore b/.gitignore index fd010ec802dc..787348286769 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.obj *.lib *.log +*.patch *.prof *.rdb *.xml @@ -35,6 +36,7 @@ dist/ env/ log/ logs/ +secrets/ *temp/ *target/ venv*/ @@ -50,5 +52,6 @@ examples/backtest/notebooks/catalog nautilus_trader/**/.gitignore nautilus_trader/test_kit/mocks/.nautilus/ tests/test_data/catalog/ +bench_data/ !tests/integration_tests/adapters/betfair/responses/*.log diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e13eca158542..77dd68a912af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: end-of-file-fixer types_or: [python, cython, rust] - id: trailing-whitespace - types_or: [python, cython] + types_or: [rust, cython, python] - id: debug-statements - id: detect-private-key - id: check-builtin-literals @@ -60,7 +60,7 @@ repos: # Python/Cython formatting and linting ############################################################################## - repo: https://github.com/asottile/add-trailing-comma - rev: v3.0.0 + rev: v3.0.1 hooks: - id: add-trailing-comma name: add-trailing-comma @@ -76,7 +76,7 @@ repos: exclude: "docs/_pygments/monokai.py" - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.280 + rev: v0.0.285 hooks: - id: ruff args: ["--fix"] @@ -105,7 +105,7 @@ repos: ] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.4.1 + rev: v1.5.1 hooks: - id: mypy args: [ diff --git a/Makefile b/Makefile index adb7f77a9b7e..d454d443617e 100644 --- a/Makefile +++ b/Makefile @@ -45,9 +45,12 @@ format: (cd nautilus_core && cargo +nightly fmt) .PHONY: pre-commit -pre-commit: format +pre-commit: pre-commit run --all-files +.PHONY: pre-flight +pre-flight: format pre-commit cargo-test build-debug pytest + .PHONY: ruff ruff: ruff check . --fix diff --git a/README.md b/README.md index 27391b7e7e90..7a7b792c5c3a 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ | Platform | Rust | Python | | :----------------- | :------ | :----- | -| `Linux (x86_64)` | 1.71.0+ | 3.9+ | -| `macOS (x86_64)` | 1.71.0+ | 3.9+ | -| `macOS (arm64)` | 1.71.0+ | 3.9+ | -| `Windows (x86_64)` | 1.71.0+ | 3.9+ | +| `Linux (x86_64)` | 1.71.1+ | 3.9+ | +| `macOS (x86_64)` | 1.71.1+ | 3.9+ | +| `macOS (arm64)` | 1.71.1+ | 3.9+ | +| `Windows (x86_64)` | 1.71.1+ | 3.9+ | - **Website:** https://nautilustrader.io - **Docs:** https://docs.nautilustrader.io @@ -233,6 +233,7 @@ A `Makefile` is provided to automate most installation and build tasks for devel - `make clean` -- **CAUTION** Cleans all non-source artifacts from the repository - `make docs` -- Builds the documentation HTML using Sphinx - `make pre-commit` -- Runs the pre-commit checks over all files +- `make pre-flight` -- Runs pre-commit, makes a clean debug build and runs all tests (except performance tests) - `make ruff` -- Runs ruff over all files using the `pyproject.toml` config - `make pytest` -- Runs all tests with `pytest` (except performance tests) - `make pytest-coverage` -- Same as `make pytest` and additionally runs with test coverage and produces a report diff --git a/RELEASES.md b/RELEASES.md index 2660a5f2af6f..e19fc6563d31 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,39 @@ +# NautilusTrader 1.177.0 Beta + +Released on 26th August 2023 (UTC). + +This release includes a large breaking change to quote tick bid and ask price property and +parameter naming. This was done in the interest of maintaining our generally explicit naming +standards, and has caused confusion for some users in the past. Data using 'bid' and 'ask' columns should +still work with the legacy data wranglers, as columns are renamed under the hood to accommodate +this change. + +### Enhancements +- Added `ActorExecutor` with `Actor` API for creating and running threaded tasks in live environments +- Added `OrderEmulated` event and associated `OrderStatus.EMULATED` enum variant +- Added `OrderReleased` event and associated `OrderStatus.RELEASED` enum variant +- Added `BacktestVenueConfig.use_position_ids` option (default true to retain current behavior) +- Added `Cache.exec_spawn_total_quantity(...)` convenience method +- Added `Cache.exec_spawn_total_filled_qty(...)` convenience method +- Added `Cache.exec_spawn_total_leaves_qty(...)` convenience method +- Added `WebSocketClient.send_text`, thanks @twitu +- Implemented string interning for `TimeEvent` + +### Breaking Changes +- Renamed `QuoteTick.bid` to `bid_price` including all associated parameters (for explicit naming standards) +- Renamed `QuoteTick.ask` to `ask_price` including all associated parameters (for explicit naming standards) + +### Fixes +- Fixed execution algorithm `position_id` assignment in `HEDGING` mode +- Fixed `OrderMatchingEngine` processing of emulated orders +- Fixed `OrderEmulator` processing of exec algorithm orders +- Fixed `ExecutionEngine` processing of exec algorithm orders (exec spawn IDs) +- Fixed `Cache` emulated order indexing (were not being properly discarded from the set when closed) +- Fixed `RedisCacheDatabase` loading of transformed `LIMIT` orders +- Fixed a connection issue with the IB client, thanks @dkharrat and @rsmb7z + +--- + # NautilusTrader 1.176.0 Beta Released on 31st July 2023 (UTC). @@ -16,7 +52,7 @@ Released on 31st July 2023 (UTC). - Added `BinanceExecClientConfig.use_reduce_only` option (default true to retain current behavior) - Added `BinanceExecClientConfig.use_position_ids` option (default true to retain current behavior) - Added `BinanceExecClientConfig.treat_expired_as_canceled` option (default false to retain current behavior) -- Added `BacktestVenueConfig.use_reduct_only` option (default true to retain current behaviour) +- Added `BacktestVenueConfig.use_reduce_only` option (default true to retain current behavior) - Added `MessageBus.is_pending_request(...)` method - Added `Level` API for core `OrderBook` (exposes the bid and ask levels for the order book) - Added `Actor.is_pending_request(...)` convenience method @@ -53,7 +89,7 @@ Released on 31st July 2023 (UTC). - Fixed Binance commission rates requests for `InstrumentProvider` - Fixed Binance `TriggerType` parsing #1154, thanks for reporting @davidblom603 - Fixed Binance order parsing of invalid orders in execution reports #1157, thanks for reporting @graceyangfan -- Fixed `BinanceOrderType` members to include undocumented `INSURANCE_FUND`, thanks for reporting @Tzumx +- Extended `BinanceOrderType` enum members to include undocumented `INSURANCE_FUND`, thanks for reporting @Tzumx - Extended `BinanceSpotPermissions` enum members #1161, thanks for reporting @davidblom603 --- @@ -74,7 +110,7 @@ We recommend you do not upgrade to this version if you're using the Betfair adap - Added core Rust `SocketClient` based on `tokio` `TcpStream`, thanks @twitu - Added `quote_quantity` parameter to determine if order quantity is denominated in quote currency - Added `trigger_instrument_id` parameter to trigger emulated orders from alternative instrument prices -- Added `use_random_ids` to `add_venue(...)` method, controls whether venue order, position and trade IDs will be random UUID4s (no change to current behaviour) +- Added `use_random_ids` to `add_venue(...)` method, controls whether venue order, position and trade IDs will be random UUID4s (no change to current behavior) - Added `ExecEngineConfig.filter_unclaimed_external_orders` options, if unclaimed order events with an `EXTERNAL` strategy ID should be filtered/dropped - Changed `BinanceHttpClient` to use new core HTTP client - Defined public API for data, can now import directly from `nautilus_trader.model.data` (denest namespace) @@ -442,7 +478,7 @@ Released on 12th December 2022 (UTC). ### Fixes - Fixed `OrderBook` sorting for bid side, thanks @gaugau3000 -- Fixed `MARKET_TO_LIMIT` order initial fill behaviour +- Fixed `MARKET_TO_LIMIT` order initial fill behavior - Fixed `BollingerBands` indicator mid-band calculations, thanks zhp (Discord) --- @@ -640,10 +676,10 @@ Released on September 14th 2022 (UTC). - De-cythonized live data and execution client base classes for usability ### Fixes -- Fixed limit order `IOC` and `FOK` behaviour, thanks @limx0 for identifying +- Fixed limit order `IOC` and `FOK` behavior, thanks @limx0 for identifying - Fixed FTX `CryptoFuture` instrument parsing, thanks @limx0 - Fixed missing imports in data catalog example notebook, thanks @gaugau3000 -- Fixed order update behaviour, affected orders: +- Fixed order update behavior, affected orders: - `LIMIT_IF_TOUCHED` - `MARKET_IF_TOUCHED` - `MARKET_TO_LIMIT` @@ -808,7 +844,7 @@ None ### Enhancements - Improved error handling for invalid state triggers -- Improved component state transition behaviour and logging +- Improved component state transition behavior and logging - Improved `TradingNode` disposal flow - Implemented core monotonic clock - Implemented logging in Rust @@ -882,7 +918,7 @@ Released on 10th May 2022 (UTC). - The `bypass_logging` config option will also now bypass the `BacktestEngine` logger ### Fixes -- Fixed behaviour of `IOC` and `FOK` time in force instructions +- Fixed behavior of `IOC` and `FOK` time in force instructions - Fixed Binance bar resolution parsing --- @@ -2159,7 +2195,7 @@ symbol string, a primary `Venue`, `AssetClass` and `AssetType` properties. This is a patch release which applies various fixes and refactorings. -The behaviour of the `StopLimitOrder` continued to be fixed and refined. +The behavior of the `StopLimitOrder` continued to be fixed and refined. `SimulatedExchange` was refactored further to reduce complexity. ### Breaking Changes @@ -2170,7 +2206,7 @@ None ### Fixes - `TRIGGERED` states in order FSM -- `StopLimitOrder` triggering behaviour +- `StopLimitOrder` triggering behavior - `OrderFactory.stop_limit` missing `post_only` and `hidden` - `Order` and `StopLimitOrder` `__repr__` string (duplicate id) @@ -2181,10 +2217,10 @@ None ## Release Notes The main thrust of this release is to refine some subtleties relating to order -matching and amendment behaviour for improved realism. This involved a fairly substantial refactoring +matching and amendment behavior for improved realism. This involved a fairly substantial refactoring of `SimulatedExchange` to manage its complexity, and support extending the order types. -The `post_only` flag for LIMIT orders now results in the expected behaviour regarding +The `post_only` flag for LIMIT orders now results in the expected behavior regarding when a marketable limit order will become a liquidity `TAKER` during order placement and amendment. @@ -2198,7 +2234,7 @@ None - Add `risk` subpackage to group risk components ### Fixes -- `StopLimitOrder` triggering behaviour +- `StopLimitOrder` triggering behavior - All flake8 warnings --- diff --git a/docs/api_reference/common.md b/docs/api_reference/common.md index 5c4984757dc8..ee437c874438 100644 --- a/docs/api_reference/common.md +++ b/docs/api_reference/common.md @@ -32,6 +32,16 @@ :member-order: bysource ``` +## Executor + +```{eval-rst} +.. automodule:: nautilus_trader.common.executor + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + ```{eval-rst} .. automodule:: nautilus_trader.common.factories :show-inheritance: diff --git a/docs/api_reference/execution.md b/docs/api_reference/execution.md index 4e6436170188..801daad1b54c 100644 --- a/docs/api_reference/execution.md +++ b/docs/api_reference/execution.md @@ -38,6 +38,14 @@ :member-order: bysource ``` +```{eval-rst} +.. automodule:: nautilus_trader.execution.manager + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + ```{eval-rst} .. automodule:: nautilus_trader.execution.matching_core :show-inheritance: diff --git a/docs/concepts/execution.md b/docs/concepts/execution.md index 76c985d93e05..c625ea319f44 100644 --- a/docs/concepts/execution.md +++ b/docs/concepts/execution.md @@ -278,7 +278,7 @@ cpdef list orders_for_exec_algorithm( As well as more specifically querying the orders for a certain execution series/spawn: ```python -cpdef list orders_for_exec_spawn(self, ClientOrderId client_order_id): +cpdef list orders_for_exec_spawn(self, ClientOrderId exec_spawn_id): """ Return all orders for the given execution spawn ID (if found). @@ -286,7 +286,7 @@ cpdef list orders_for_exec_spawn(self, ClientOrderId client_order_id): Parameters ---------- - client_order_id : ClientOrderId + exec_spawn_id : ClientOrderId The execution algorithm spawning primary (original) client order ID. Returns diff --git a/docs/concepts/orders.md b/docs/concepts/orders.md index d34467051ec7..7bcf743a760f 100644 --- a/docs/concepts/orders.md +++ b/docs/concepts/orders.md @@ -63,10 +63,10 @@ important for market makers, or traders seeking to restrict the order to a liqui ### Reduce Only An order which is set as `reduce_only` will only ever reduce an existing position on an instrument, and -never open a new position (if already flat). The exact behaviour of this instruction can vary between -exchanges, however the behaviour as per the Nautilus `SimulatedExchange` is typical of a live exchange. +never open a new position (if already flat). The exact behavior of this instruction can vary between +exchanges, however the behavior as per the Nautilus `SimulatedExchange` is typical of a live exchange. -- Order will be cancelled if the associated position is closed (becomes flat) +- Order will be canceled if the associated position is closed (becomes flat) - Order quantity will be reduced as the associated positions size reduces ### Display Quantity diff --git a/docs/developer_guide/rust.md b/docs/developer_guide/rust.md index 82a2e17ccbc1..d44a162f0f93 100644 --- a/docs/developer_guide/rust.md +++ b/docs/developer_guide/rust.md @@ -23,8 +23,8 @@ their own standard libraries. Great care will be taken with the use of Rusts `unsafe` facility - which just enables a small set of additional language features, thereby changing the contract between the interface and caller, shifting some responsibility for guaranteeing correctness -from the Rust compiler, and onto us. The goal is to realize the advantages of the `unsafe` facility, whilst avoiding _any_ undefined behaviour. -The definition for what the Rust language designers consider undefined behaviour can be found in the [language reference](https://doc.rust-lang.org/stable/reference/behavior-considered-undefined.html). +from the Rust compiler, and onto us. The goal is to realize the advantages of the `unsafe` facility, whilst avoiding _any_ undefined behavior. +The definition for what the Rust language designers consider undefined behavior can be found in the [language reference](https://doc.rust-lang.org/stable/reference/behavior-considered-undefined.html). ## Safety Policy To maintain the high standards of correctness the project strives for, it's necessary to specify a reasonable policy diff --git a/docs/getting_started/quick_start.md b/docs/getting_started/quick_start.md index 3364b8b35d17..2eb5c7c7a6fa 100644 --- a/docs/getting_started/quick_start.md +++ b/docs/getting_started/quick_start.md @@ -14,7 +14,7 @@ deleted when the container is deleted. - To get started, install docker: - Go to [docker.com](https://docs.docker.com/get-docker/) and follow the instructions - From a terminal, download the latest image - - `docker pull ghcr.io/nautechsystems/jupyterlab:develop` + - `docker pull ghcr.io/nautechsystems/jupyterlab:develop --platform linux/amd64` - Run the docker container, exposing the jupyter port: - `docker run -p 8888:8888 ghcr.io/nautechsystems/jupyterlab:develop` - Open your web browser to `localhost:{port}` @@ -196,7 +196,7 @@ venue = BacktestVenueConfig( oms_type="NETTING", account_type="MARGIN", base_currency="USD", - starting_balances=["1_000_000 USD"] + starting_balances=["1_000_000 USD"], ) ``` diff --git a/docs/guides/backtest_example.md b/docs/guides/backtest_example.md index 3ae1f091ed95..dd8cca91e57f 100644 --- a/docs/guides/backtest_example.md +++ b/docs/guides/backtest_example.md @@ -74,8 +74,8 @@ def parser(line): dt = pd.Timestamp(datetime.datetime.strptime(ts.decode(), "%Y%m%d %H%M%S%f"), tz='UTC') yield QuoteTick( instrument_id=AUDUSD.id, - bid=Price.from_str(bid.decode()), - ask=Price.from_str(ask.decode()), + bid_price=Price.from_str(bid.decode()), + ask_price=Price.from_str(ask.decode()), bid_size=Quantity.from_int(100_000), ask_size=Quantity.from_int(100_000), ts_event=dt_to_unix_nanos(dt), diff --git a/docs/guides/loading_external_data.md b/docs/guides/loading_external_data.md index e1e5014c76f5..a7f0f48cb22c 100644 --- a/docs/guides/loading_external_data.md +++ b/docs/guides/loading_external_data.md @@ -66,8 +66,8 @@ def parser(data, instrument_id): dt = pd.Timestamp(datetime.datetime.strptime(data['timestamp'].decode(), "%Y%m%d %H%M%S%f"), tz='UTC') yield QuoteTick( instrument_id=instrument_id, - bid=Price.from_str(data['bid'].decode()), - ask=Price.from_str(data['ask'].decode()), + bid_price=Price.from_str(data["bid_price"].decode()), + ask_price=Price.from_str(data["ask_price"].decode()), bid_size=Quantity.from_int(100_000), ask_size=Quantity.from_int(100_000), ts_event=dt_to_unix_nanos(dt), @@ -158,7 +158,7 @@ process_files( glob_path=input_files, reader=CSVReader( block_parser=lambda x: parser(x, instrument_id=instrument.id), - header=['timestamp', 'bid', 'ask', 'volume'], + header=["timestamp", "bid", "ask", "volume"], chunked=False, as_dataframe=False, ), diff --git a/examples/backtest/betfair_backtest_orderbook_imbalance.py b/examples/backtest/betfair_backtest_orderbook_imbalance.py index e1010de84f15..1c0d44ec79fb 100644 --- a/examples/backtest/betfair_backtest_orderbook_imbalance.py +++ b/examples/backtest/betfair_backtest_orderbook_imbalance.py @@ -31,8 +31,8 @@ from nautilus_trader.model.enums import OmsType from nautilus_trader.model.identifiers import ClientId from nautilus_trader.model.objects import Money -from nautilus_trader.test_kit.providers import TestInstrumentProvider from tests.integration_tests.adapters.betfair.test_kit import BetfairDataProvider +from tests.integration_tests.adapters.betfair.test_kit import betting_instrument if __name__ == "__main__": @@ -54,12 +54,12 @@ # Add instruments instruments = [ - TestInstrumentProvider.betting_instrument( + betting_instrument( market_id="1.166811431", selection_id="19248890", selection_handicap="0.0", ), - TestInstrumentProvider.betting_instrument( + betting_instrument( market_id="1.166811431", selection_id="38848248", selection_handicap="0.0", diff --git a/examples/live/binance_futures_testnet_ema_cross_bracket.py b/examples/live/binance_futures_testnet_ema_cross_bracket.py index dc48a57ef0b1..24e62cfa4846 100644 --- a/examples/live/binance_futures_testnet_ema_cross_bracket.py +++ b/examples/live/binance_futures_testnet_ema_cross_bracket.py @@ -26,6 +26,7 @@ from nautilus_trader.config import LiveExecEngineConfig from nautilus_trader.config import LoggingConfig from nautilus_trader.config import TradingNodeConfig +from nautilus_trader.config.common import CacheConfig from nautilus_trader.examples.strategies.ema_cross_bracket import EMACrossBracket from nautilus_trader.examples.strategies.ema_cross_bracket import EMACrossBracketConfig from nautilus_trader.live.node import TradingNode @@ -45,7 +46,16 @@ reconciliation=True, reconciliation_lookback_mins=1440, ), - cache_database=CacheDatabaseConfig(type="in-memory"), + cache=CacheConfig( + # snapshot_orders=True, + # snapshot_positions=True, + # snapshot_positions_interval=5.0, + ), + cache_database=CacheDatabaseConfig( + type="in-memory", + flush_on_start=False, + timestamps_as_iso8601=True, + ), data_clients={ "BINANCE": BinanceDataClientConfig( api_key=None, # "YOUR_BINANCE_TESTNET_API_KEY" @@ -87,7 +97,7 @@ bar_type="ETHUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL", fast_ema_period=10, slow_ema_period=20, - bracket_distance_atr=1.0, + bracket_distance_atr=3.0, trade_size=Decimal("0.010"), order_id_tag="001", emulation_trigger="BID_ASK", diff --git a/examples/live/binance_futures_testnet_market_maker.py b/examples/live/binance_futures_testnet_market_maker.py index e65b8860b97b..f44fb0394c14 100644 --- a/examples/live/binance_futures_testnet_market_maker.py +++ b/examples/live/binance_futures_testnet_market_maker.py @@ -26,6 +26,7 @@ from nautilus_trader.config import LiveExecEngineConfig from nautilus_trader.config import LoggingConfig from nautilus_trader.config import TradingNodeConfig +from nautilus_trader.config.common import CacheConfig from nautilus_trader.examples.strategies.volatility_market_maker import VolatilityMarketMaker from nautilus_trader.examples.strategies.volatility_market_maker import VolatilityMarketMakerConfig from nautilus_trader.live.node import TradingNode @@ -50,8 +51,14 @@ reconciliation_lookback_mins=1440, filter_position_reports=True, ), + cache=CacheConfig( + # snapshot_orders=True, + # snapshot_positions=True, + # snapshot_positions_interval=5.0, + ), cache_database=CacheDatabaseConfig( type="in-memory", + flush_on_start=False, timestamps_as_iso8601=True, ), data_clients={ diff --git a/examples/notebooks/external_data_backtest.ipynb b/examples/notebooks/external_data_backtest.ipynb index 93d816a6064b..12a256f0df5b 100644 --- a/examples/notebooks/external_data_backtest.ipynb +++ b/examples/notebooks/external_data_backtest.ipynb @@ -61,8 +61,8 @@ " dt = pd.Timestamp(datetime.datetime.strptime(ts.decode(), \"%Y%m%d %H%M%S%f\"), tz='UTC')\n", " yield QuoteTick(\n", " instrument_id=AUDUSD.id,\n", - " bid=Price.from_str(bid.decode()),\n", - " ask=Price.from_str(ask.decode()),\n", + " bid_price=Price.from_str(bid.decode()),\n", + " ask_price=Price.from_str(ask.decode()),\n", " bid_size=Quantity.from_int(100_000),\n", " ask_size=Quantity.from_int(100_000),\n", " ts_event=dt_to_unix_nanos(dt),\n", diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 1d92f678dd34..4ec21608a767 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" dependencies = [ "memchr", ] @@ -92,11 +92,17 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstyle" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" + [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arrayvec" @@ -336,13 +342,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -364,9 +370,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -400,9 +406,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "bitvec" @@ -570,7 +576,7 @@ version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b922faaf31122819ec80c4047cc684c6979a087366c069611e33649bf98e18d" dependencies = [ - "clap", + "clap 3.2.25", "heck", "indexmap 1.9.3", "log", @@ -585,11 +591,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -608,7 +615,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", - "time", + "time 0.1.45", "wasm-bindgen", "winapi", ] @@ -670,13 +677,32 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", "bitflags 1.3.2", - "clap_lex", + "clap_lex 0.2.4", "indexmap 1.9.3", "strsim", "termcolor", "textwrap", ] +[[package]] +name = "clap" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d5f1946157a96594eb2d2c10eb7ad9a2b27518cb3000209dec700c35df9197d" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78116e32a042dd73c2901f0dc30790d20ff3447f3e3472fad359e8c3d282bcd6" +dependencies = [ + "anstyle", + "clap_lex 0.5.1", +] + [[package]] name = "clap_lex" version = "0.2.4" @@ -686,6 +712,12 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + [[package]] name = "comfy-table" version = "7.0.1" @@ -761,19 +793,19 @@ dependencies = [ [[package]] name = "criterion" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", - "atty", "cast", "ciborium", - "clap", + "clap 4.4.0", "criterion-plot", + "is-terminal", "itertools 0.10.5", - "lazy_static", "num-traits", + "once_cell", "oorandom", "plotters", "rayon", @@ -912,9 +944,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.5.0" +version = "5.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28" dependencies = [ "cfg-if", "hashbrown 0.14.0", @@ -1023,7 +1055,7 @@ dependencies = [ "lazy_static", "sqlparser", "strum 0.25.0", - "strum_macros 0.25.1", + "strum_macros 0.25.2", ] [[package]] @@ -1087,6 +1119,12 @@ dependencies = [ "sqlparser", ] +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + [[package]] name = "derive_builder" version = "0.12.0" @@ -1182,9 +1220,9 @@ dependencies = [ [[package]] name = "evalexpr" -version = "11.0.1" +version = "11.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adeb24130b6ffd6a2f4d2768c8cbf28d98b115c9f576896bb3d6926c2296927" +checksum = "1e757e796a66b54d19fa26de38e75c3351eb7a3755c85d7d181a8c61437ff60c" [[package]] name = "fastrand" @@ -1210,14 +1248,23 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1310,7 +1357,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -1372,9 +1419,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "glob" @@ -1478,9 +1525,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" @@ -1504,7 +1551,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -1738,9 +1785,9 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "linux-raw-sys" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "lock_api" @@ -1754,9 +1801,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lz4" @@ -1853,7 +1900,7 @@ dependencies = [ [[package]] name = "nautilus-backtest" -version = "0.7.0" +version = "0.8.0" dependencies = [ "cbindgen", "nautilus-common", @@ -1865,7 +1912,7 @@ dependencies = [ [[package]] name = "nautilus-common" -version = "0.7.0" +version = "0.8.0" dependencies = [ "cbindgen", "chrono", @@ -1876,11 +1923,12 @@ dependencies = [ "serde_json", "strum 0.25.0", "tempfile", + "ustr", ] [[package]] name = "nautilus-core" -version = "0.7.0" +version = "0.8.0" dependencies = [ "cbindgen", "chrono", @@ -1891,12 +1939,13 @@ dependencies = [ "rstest", "serde", "serde_json", + "ustr", "uuid", ] [[package]] name = "nautilus-indicators" -version = "0.7.0" +version = "0.8.0" dependencies = [ "nautilus-core", "nautilus-model", @@ -1905,13 +1954,14 @@ dependencies = [ [[package]] name = "nautilus-model" -version = "0.7.0" +version = "0.8.0" dependencies = [ "anyhow", "cbindgen", "criterion", "derive_builder", "evalexpr", + "float-cmp", "iai", "lazy_static", "nautilus-core", @@ -1919,6 +1969,7 @@ dependencies = [ "rmp-serde", "rstest", "rust_decimal", + "rust_decimal_macros", "serde", "serde_json", "strum 0.25.0", @@ -1929,7 +1980,7 @@ dependencies = [ [[package]] name = "nautilus-network" -version = "0.7.0" +version = "0.8.0" dependencies = [ "anyhow", "criterion", @@ -1949,7 +2000,7 @@ dependencies = [ [[package]] name = "nautilus-persistence" -version = "0.7.0" +version = "0.8.0" dependencies = [ "binary-heap-plus", "chrono", @@ -1969,13 +2020,16 @@ dependencies = [ [[package]] name = "nautilus-pyo3" -version = "0.7.0" +version = "0.8.0" dependencies = [ "nautilus-indicators", "nautilus-model", "nautilus-network", "nautilus-persistence", "pyo3", + "tracing", + "tracing-appender", + "tracing-subscriber", ] [[package]] @@ -2004,9 +2058,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", @@ -2015,9 +2069,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" dependencies = [ "num-traits", ] @@ -2077,9 +2131,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" dependencies = [ "memchr", ] @@ -2119,9 +2173,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" dependencies = [ "bitflags 1.3.2", "cfg-if", @@ -2140,7 +2194,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -2151,18 +2205,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.26.0+1.1.1u" +version = "111.27.0+1.1.1v" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37" +checksum = "06e8f197c82d7511c5b014030c9b1efeda40d7d5f99d23b4ceed3524a5e63f02" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" dependencies = [ "cc", "libc", @@ -2283,12 +2337,12 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "petgraph" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 1.9.3", + "indexmap 2.0.0", ] [[package]] @@ -2331,9 +2385,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -2451,9 +2505,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb88ae05f306b4bfcde40ac4a51dc0b05936a9207a4b75b798c7729c4258a59" +checksum = "e681a6cfdc4adcc93b4d3cf993749a4552018ee0a9b65fc0ccfad74352c72a38" dependencies = [ "cfg-if", "indoc", @@ -2469,7 +2523,8 @@ dependencies = [ [[package]] name = "pyo3-asyncio" version = "0.19.0" -source = "git+https://github.com/nautechsystems/pyo3-asyncio.git#2cdf4d7ed932b6d0a3dc2756189bdd17a050e7f7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2cc34c1f907ca090d7add03dc523acdd91f3a4dab12286604951e2f5152edad" dependencies = [ "futures", "once_cell", @@ -2482,7 +2537,8 @@ dependencies = [ [[package]] name = "pyo3-asyncio-macros" version = "0.19.0" -source = "git+https://github.com/nautechsystems/pyo3-asyncio.git#2cdf4d7ed932b6d0a3dc2756189bdd17a050e7f7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4045f06429547179e4596f5c0b13c82efc8b04296016780133653ed69ce26b3" dependencies = [ "proc-macro2", "quote", @@ -2491,9 +2547,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "554db24f0b3c180a9c0b1268f91287ab3f17c162e15b54caaae5a6b3773396b0" +checksum = "076c73d0bc438f7a4ef6fdd0c3bb4732149136abd952b110ac93e4edb13a6ba5" dependencies = [ "once_cell", "target-lexicon", @@ -2501,9 +2557,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "922ede8759e8600ad4da3195ae41259654b9c55da4f7eec84a0ccc7d067a70a4" +checksum = "e53cee42e77ebe256066ba8aa77eff722b3bb91f3419177cf4cd0f304d3284d9" dependencies = [ "libc", "pyo3-build-config", @@ -2511,9 +2567,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a5caec6a1dd355964a841fcbeeb1b89fe4146c87295573f94228911af3cc5a2" +checksum = "dfeb4c99597e136528c6dd7d5e3de5434d1ceaf487436a3f03b2d56b6fc9efd1" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2523,9 +2579,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0b78ccbb160db1556cdb6fd96c50334c5d4ec44dc5e0a968d0a1208fa0efa8b" +checksum = "947dc12175c254889edc0c02e399476c2f652b4b9ebd123aa655c224de259536" dependencies = [ "proc-macro2", "quote", @@ -2534,9 +2590,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -2610,13 +2666,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "regex-syntax 0.7.4", ] @@ -2631,9 +2687,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ "aho-corasick", "memchr", @@ -2652,6 +2708,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +[[package]] +name = "relative-path" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" + [[package]] name = "rend" version = "0.4.0" @@ -2728,9 +2790,9 @@ dependencies = [ [[package]] name = "rstest" -version = "0.17.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de1bb486a691878cd320c2f0d319ba91eeaa2e894066d8b5f8f117c000e9d962" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" dependencies = [ "futures", "futures-timer", @@ -2740,27 +2802,29 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.17.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290ca1a1c8ca7edb7c3283bd44dc35dd54fdec6253a3912e201ba1072018fca8" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" dependencies = [ "cfg-if", + "glob", "proc-macro2", "quote", + "regex", + "relative-path", "rustc_version", - "syn 1.0.109", + "syn 2.0.29", "unicode-ident", ] [[package]] name = "rust_decimal" -version = "1.31.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a2ab0025103a60ecaaf3abf24db1db240a4e1c15837090d2c32f625ac98abea" +checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" dependencies = [ "arrayvec", "borsh", - "byteorder", "bytes", "num-traits", "rand", @@ -2769,6 +2833,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "rust_decimal_macros" +version = "1.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86444b802de0b10ac5e563b5ddb43b541b9705de4e01a50e82194d2b183c1835" +dependencies = [ + "quote", + "rust_decimal", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2786,11 +2860,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno", "libc", "linux-raw-sys", @@ -2799,13 +2873,13 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.5" +version = "0.21.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36" +checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" dependencies = [ "log", "ring", - "rustls-webpki 0.101.2", + "rustls-webpki 0.101.4", "sct", ] @@ -2832,9 +2906,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.100.1" +version = "0.100.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab" dependencies = [ "ring", "untrusted", @@ -2842,9 +2916,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.2" +version = "0.101.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513722fd73ad80a71f72b61009ea1b584bcfa1483ca93949c8f290298837fa59" +checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" dependencies = [ "ring", "untrusted", @@ -2939,29 +3013,29 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.178" +version = "1.0.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60363bdd39a7be0266a520dab25fdc9241d2f987b08a01e01f0ec6d06a981348" +checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.178" +version = "1.0.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28482318d6641454cb273da158647922d1be6b5a2fcc6165cd89ebdd7ed576b" +checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", @@ -3005,15 +3079,15 @@ checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" [[package]] name = "siphasher" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -3062,6 +3136,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -3113,7 +3197,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ - "strum_macros 0.25.1", + "strum_macros 0.25.2", ] [[package]] @@ -3131,15 +3215,15 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6069ca09d878a33f883cc06aaa9718ede171841d3832450354410b718b097232" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -3155,9 +3239,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.27" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -3196,15 +3280,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "target-lexicon" -version = "0.12.10" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2faeef5759ab89935255b1a4cd98e0baf99d1085e37d36599c625dac49ae8e" +checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "tempfile" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", @@ -3230,22 +3314,22 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -3280,6 +3364,34 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9" +dependencies = [ + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -3316,11 +3428,10 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", @@ -3329,7 +3440,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", "windows-sys", ] @@ -3342,7 +3453,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -3425,6 +3536,17 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" +dependencies = [ + "crossbeam-channel", + "time 0.3.27", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.26" @@ -3433,7 +3555,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -3599,13 +3721,13 @@ dependencies = [ [[package]] name = "ustr" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b050b13c1933490b043b8238a75bc7676cb49292f49768c9350eabb284eaeb87" +source = "git+https://github.com/anderslanglands/ustr#c78ddc25300c4720ffcb5f8ddef6028cef14535f" dependencies = [ "ahash 0.8.3", "byteorder", "lazy_static", "parking_lot", + "serde", ] [[package]] @@ -3693,7 +3815,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -3715,7 +3837,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3742,7 +3864,7 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ - "rustls-webpki 0.100.1", + "rustls-webpki 0.100.2", ] [[package]] @@ -3796,9 +3918,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -3811,45 +3933,45 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "wyz" diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index fa6e393a6c0f..7c7c99b45d62 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -9,41 +9,42 @@ members = [ "network", "network/tokio-tungstenite", "persistence", - "pyo3" + "pyo3", ] [workspace.package] -rust-version = "1.71.0" -version = "0.7.0" +rust-version = "1.71.1" +version = "0.8.0" edition = "2021" authors = ["Nautech Systems "] description = "A high-performance algorithmic trading platform and event-driven backtester" documentation = "https://docs.nautilustrader.io" [workspace.dependencies] -anyhow = "1.0.72" +anyhow = "1.0.75" chrono = "0.4.26" futures = "0.3.28" -pyo3 = "0.19.1" -pyo3-asyncio = { git = "https://github.com/nautechsystems/pyo3-asyncio.git", features = ["tokio-runtime", "tokio", "attributes"] } -pyo3-macros = "0.19.1" +pyo3 = "0.19.2" +pyo3-asyncio = { version = "0.19.0", features = ["tokio-runtime", "tokio", "attributes"] } rand = "0.8.5" rmp-serde = "1.1.2" -rust_decimal = "1.31.0" -rust_decimal_macros = "1.29.1" -serde = { version = "1.0.178", features = ["derive"] } -serde_json = "1.0.104" +rust_decimal = "1.32.0" +rust_decimal_macros = "1.32.0" +serde = { version = "1.0.186", features = ["derive"] } +serde_json = "1.0.105" strum = { version = "0.25.0", features = ["derive"] } -thiserror = "1.0.44" +thiserror = "1.0.47" tracing = "0.1.37" -tokio = { version = "1.29.1", features = ["full"] } +tokio = { version = "1.32.0", features = ["full"] } +ustr = { git = "https://github.com/anderslanglands/ustr", features = ["serde"] } uuid = { version = "1.4.1", features = ["v4"] } # dev-dependencies -criterion = "0.4.0" +criterion = "0.5.1" +float-cmp = "0.9.0" iai = "0.1" -rstest = "0.17.0" -tempfile = "3.6.0" +rstest = "0.18.2" +tempfile = "3.8.0" # build-dependencies cbindgen = "0.24.5" diff --git a/nautilus_core/backtest/cbindgen_cython.toml b/nautilus_core/backtest/cbindgen_cython.toml index 725496201771..21fd84dfbf18 100644 --- a/nautilus_core/backtest/cbindgen_cython.toml +++ b/nautilus_core/backtest/cbindgen_cython.toml @@ -33,7 +33,7 @@ header = '"../includes/backtest.h"' rename_variants = "ScreamingSnakeCase" [export.rename] -"bool" = "uint8_t" +"bool" = "bint" "UnixNanos" = "uint64_t" "TimedeltaNanos" = "int64_t" "TimeEvent" = "TimeEvent_t" diff --git a/nautilus_core/common/Cargo.toml b/nautilus_core/common/Cargo.toml index b4b7c715bb05..0c449ce561ee 100644 --- a/nautilus_core/common/Cargo.toml +++ b/nautilus_core/common/Cargo.toml @@ -19,6 +19,7 @@ serde.workspace = true serde_json.workspace = true pyo3.workspace = true strum.workspace = true +ustr.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/nautilus_core/common/cbindgen.toml b/nautilus_core/common/cbindgen.toml index da13d1f306c3..c463779d0bfe 100644 --- a/nautilus_core/common/cbindgen.toml +++ b/nautilus_core/common/cbindgen.toml @@ -11,6 +11,7 @@ rename_variants = "ScreamingSnakeCase" [export.rename] "bool" = "uint8_t" +"Ustr" = "char*" "UnixNanos" = "uint64_t" "TimedeltaNanos" = "int64_t" "TimeEvent" = "TimeEvent_t" diff --git a/nautilus_core/common/cbindgen_cython.toml b/nautilus_core/common/cbindgen_cython.toml index e114be46279d..2647476c9fbe 100644 --- a/nautilus_core/common/cbindgen_cython.toml +++ b/nautilus_core/common/cbindgen_cython.toml @@ -28,7 +28,8 @@ header = '"../includes/common.h"' rename_variants = "ScreamingSnakeCase" [export.rename] -"bool" = "uint8_t" +"bool" = "bint" +"Ustr" = "char*" "UnixNanos" = "uint64_t" "TimedeltaNanos" = "int64_t" "TimeEvent" = "TimeEvent_t" diff --git a/nautilus_core/common/src/enums.rs b/nautilus_core/common/src/enums.rs index 40a8b0797b53..c02f31a49e3a 100644 --- a/nautilus_core/common/src/enums.rs +++ b/nautilus_core/common/src/enums.rs @@ -17,13 +17,29 @@ use std::{ffi::c_char, fmt::Debug, str::FromStr}; use nautilus_core::string::{cstr_to_string, str_to_cstr}; use serde::{Deserialize, Serialize}; -use strum::{Display, EnumString, FromRepr}; +use strum::{Display, EnumIter, EnumString, FromRepr}; /// The state of a component within the system. #[repr(C)] -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, FromRepr, EnumString, Display)] +#[derive( + Copy, + Clone, + Debug, + Display, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + FromRepr, + EnumIter, + EnumString, + Serialize, + Deserialize, +)] #[strum(ascii_case_insensitive)] #[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +#[allow(non_camel_case_types)] pub enum ComponentState { /// When a component is instantiated, but not yet ready to fulfill its specification. PreInitialized = 0, @@ -57,9 +73,25 @@ pub enum ComponentState { /// A trigger condition for a component within the system. #[repr(C)] -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, FromRepr, EnumString, Display)] +#[derive( + Copy, + Clone, + Debug, + Display, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + FromRepr, + EnumIter, + EnumString, + Serialize, + Deserialize, +)] #[strum(ascii_case_insensitive)] #[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +#[allow(non_camel_case_types)] pub enum ComponentTrigger { /// A trigger for the component to initialize. Initialize = 1, @@ -100,16 +132,19 @@ pub enum ComponentTrigger { Clone, Debug, Hash, - PartialOrd, PartialEq, Eq, + PartialOrd, + Ord, FromRepr, + EnumIter, EnumString, Serialize, Deserialize, )] #[strum(ascii_case_insensitive)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[allow(non_camel_case_types)] pub enum LogLevel { /// The **DBG** debug log level. #[strum(serialize = "DBG", serialize = "DEBUG")] @@ -150,10 +185,24 @@ impl std::fmt::Display for LogLevel { /// The log color for log messages. #[repr(C)] #[derive( - Copy, Clone, Debug, Hash, PartialEq, Eq, FromRepr, EnumString, Display, Serialize, Deserialize, + Copy, + Clone, + Debug, + Display, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + FromRepr, + EnumIter, + EnumString, + Serialize, + Deserialize, )] #[strum(ascii_case_insensitive)] #[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +#[allow(non_camel_case_types)] pub enum LogColor { /// The default/normal log color. #[strum(serialize = "")] @@ -184,6 +233,7 @@ pub enum LogColor { #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, FromRepr, EnumString, Display)] #[strum(ascii_case_insensitive)] #[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +#[allow(non_camel_case_types)] pub enum LogFormat { /// Header log format. This ANSI escape code is used for magenta text color, /// often used for headers or titles in the log output. diff --git a/nautilus_core/common/src/logging.rs b/nautilus_core/common/src/logging.rs index cc2f2285ae0e..f088ab6b45e3 100644 --- a/nautilus_core/common/src/logging.rs +++ b/nautilus_core/common/src/logging.rs @@ -58,7 +58,6 @@ pub struct LogEvent { timestamp: UnixNanos, /// The log level for the event. level: LogLevel, - #[serde(skip_serializing)] /// The color for the log message content. color: LogColor, /// The Nautilus system component the log event originated from. @@ -699,7 +698,7 @@ mod tests { assert_eq!( log_contents, - "{\"timestamp\":1650000000000000,\"level\":\"INFO\",\"component\":\"RiskEngine\",\"message\":\"This is a test.\"}\n" + "{\"timestamp\":1650000000000000,\"level\":\"INFO\",\"color\":\"Normal\",\"component\":\"RiskEngine\",\"message\":\"This is a test.\"}\n" ); } } diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index b0c5d7b7a426..efc4a6f2aab6 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -16,7 +16,6 @@ use std::{ cmp::Ordering, fmt::{Display, Formatter}, - rc::Rc, }; use nautilus_core::{ @@ -25,6 +24,7 @@ use nautilus_core::{ uuid::UUID4, }; use pyo3::ffi; +use ustr::Ustr; #[repr(C)] #[derive(Clone, Debug)] @@ -32,7 +32,7 @@ use pyo3::ffi; /// Represents a time event occurring at the event timestamp. pub struct TimeEvent { /// The event name. - pub name: Box>, + pub name: Ustr, /// The event ID. pub event_id: UUID4, /// The message category @@ -47,7 +47,7 @@ impl TimeEvent { correctness::valid_string(&name, "`TimeEvent` name"); TimeEvent { - name: Box::new(Rc::new(name)), + name: Ustr::from(&name), event_id, ts_event, ts_init, @@ -150,7 +150,7 @@ impl TestTimer { pub fn pop_event(&self, event_id: UUID4, ts_init: UnixNanos) -> TimeEvent { TimeEvent { - name: Box::new(Rc::new(self.name.clone())), + name: Ustr::from(&self.name), event_id, ts_event: self.next_time_ns, ts_init, @@ -181,7 +181,7 @@ impl Iterator for TestTimer { } else { let item = ( TimeEvent { - name: Box::new(Rc::new(self.name.clone())), + name: Ustr::from(&self.name), event_id: UUID4::new(), ts_event: self.next_time_ns, ts_init: self.next_time_ns, diff --git a/nautilus_core/common/src/timer_api.rs b/nautilus_core/common/src/timer_api.rs index 369246ccade5..a515e5bc8309 100644 --- a/nautilus_core/common/src/timer_api.rs +++ b/nautilus_core/common/src/timer_api.rs @@ -35,21 +35,6 @@ pub unsafe extern "C" fn time_event_new( TimeEvent::new(cstr_to_string(name_ptr), event_id, ts_event, ts_init) } -#[no_mangle] -pub extern "C" fn time_event_clone(event: &TimeEvent) -> TimeEvent { - event.clone() -} - -#[no_mangle] -pub extern "C" fn time_event_drop(event: TimeEvent) { - drop(event); // Memory freed here -} - -#[no_mangle] -pub extern "C" fn time_event_name_to_cstr(event: &TimeEvent) -> *const c_char { - str_to_cstr(&event.name) -} - /// Returns a [`TimeEvent`] as a C string pointer. #[no_mangle] pub extern "C" fn time_event_to_cstr(event: &TimeEvent) -> *const c_char { diff --git a/nautilus_core/core/Cargo.toml b/nautilus_core/core/Cargo.toml index 5a6c2490ed08..e27fbb3a9d49 100644 --- a/nautilus_core/core/Cargo.toml +++ b/nautilus_core/core/Cargo.toml @@ -16,6 +16,7 @@ pyo3.workspace = true rmp-serde.workspace = true serde.workspace = true serde_json.workspace = true +ustr.workspace = true uuid.workspace = true [features] diff --git a/nautilus_core/core/cbindgen.toml b/nautilus_core/core/cbindgen.toml index a243ede11ae0..dc7afc38803f 100644 --- a/nautilus_core/core/cbindgen.toml +++ b/nautilus_core/core/cbindgen.toml @@ -18,6 +18,8 @@ include = ["CVec"] rename_variants = "ScreamingSnakeCase" [export.rename] +"bool" = "uint8_t" +"Ustr" = "char*" "UnixNanos" = "uint64_t" "TimedeltaNanos" = "int64_t" "TimeEvent" = "TimeEvent_t" diff --git a/nautilus_core/core/cbindgen_cython.toml b/nautilus_core/core/cbindgen_cython.toml index 4d70f66a9c67..fe883e7d226d 100644 --- a/nautilus_core/core/cbindgen_cython.toml +++ b/nautilus_core/core/cbindgen_cython.toml @@ -27,6 +27,8 @@ include = ["CVec"] rename_variants = "ScreamingSnakeCase" [export.rename] +"bool" = "bint" +"Ustr" = "char*" "UnixNanos" = "uint64_t" "TimedeltaNanos" = "int64_t" "TimeEvent" = "TimeEvent_t" diff --git a/nautilus_core/core/src/correctness.rs b/nautilus_core/core/src/correctness.rs index 4cfbff6c17d2..503f9f1e1b34 100644 --- a/nautilus_core/core/src/correctness.rs +++ b/nautilus_core/core/src/correctness.rs @@ -125,19 +125,23 @@ mod tests { use super::*; - #[rstest(s, case(" a"), case("a "), case("a a"), case(" a "), case("abc"))] - fn test_valid_string_with_valid_value(s: &str) { + #[rstest] + #[case(" a")] + #[case("a ")] + #[case("a a")] + #[case(" a ")] + #[case("abc")] + fn test_valid_string_with_valid_value(#[case] s: &str) { valid_string(s, "value"); } - #[rstest(s, - case(""), // <-- empty string - case(" "), // <-- whitespace-only - case(" "), // <-- whitespace-only string - case("🦀"), // <-- contains Non-ASCII char - )] + #[rstest] + #[case("")] // <-- empty string + #[case(" ")] // <-- whitespace-only + #[case(" ")] // <-- whitespace-only string + #[case("🦀")] // <-- contains Non-ASCII char #[should_panic] - fn test_valid_string_with_invalid_values(s: &str) { + fn test_valid_string_with_invalid_values(#[case] s: &str) { valid_string(s, "value"); } diff --git a/nautilus_core/core/src/cvec.rs b/nautilus_core/core/src/cvec.rs index 111e33fe5838..2c2829c54e17 100644 --- a/nautilus_core/core/src/cvec.rs +++ b/nautilus_core/core/src/cvec.rs @@ -22,7 +22,7 @@ use std::{ /// `CVec` is a C compatible struct that stores an opaque pointer to a block of /// memory, it's length and the capacity of the vector it was allocated from. /// -/// NOTE: Changing the values here may lead to undefined behaviour when the +/// NOTE: Changing the values here may lead to undefined behavior when the /// memory is dropped. #[repr(C)] #[derive(Clone, Copy, Debug)] diff --git a/nautilus_core/core/src/parsing.rs b/nautilus_core/core/src/parsing.rs index 29d9f0dfbbd2..356850dc2537 100644 --- a/nautilus_core/core/src/parsing.rs +++ b/nautilus_core/core/src/parsing.rs @@ -19,6 +19,7 @@ use std::{ }; use serde_json::{Result, Value}; +use ustr::Ustr; use crate::string::cstr_to_string; @@ -79,6 +80,54 @@ pub unsafe fn optional_bytes_to_json(ptr: *const c_char) -> Option>`. +/// +/// # Safety +/// +/// - Assumes `ptr` is a valid C string pointer. +#[must_use] +pub unsafe fn optional_bytes_to_str_map(ptr: *const c_char) -> Option> { + if ptr.is_null() { + None + } else { + let c_str = CStr::from_ptr(ptr); + let bytes = c_str.to_bytes(); + let json_string = std::str::from_utf8(bytes).unwrap(); + let result: Result> = serde_json::from_str(json_string); + match result { + Ok(map) => Some(map), + Err(err) => { + eprintln!("Error parsing JSON: {err}"); + None + } + } + } +} + +/// Convert a C bytes pointer into an owned `Option>`. +/// +/// # Safety +/// +/// - Assumes `ptr` is a valid C string pointer. +#[must_use] +pub unsafe fn optional_bytes_to_str_vec(ptr: *const c_char) -> Option> { + if ptr.is_null() { + None + } else { + let c_str = CStr::from_ptr(ptr); + let bytes = c_str.to_bytes(); + let json_string = std::str::from_utf8(bytes).unwrap(); + let result: Result> = serde_json::from_str(json_string); + match result { + Ok(map) => Some(map), + Err(err) => { + eprintln!("Error parsing JSON: {err}"); + None + } + } + } +} + /// Return the decimal precision inferred from the given string. #[must_use] pub fn precision_from_str(s: &str) -> u8 { @@ -108,6 +157,11 @@ pub unsafe extern "C" fn precision_from_cstr(ptr: *const c_char) -> u8 { precision_from_str(&cstr_to_string(ptr)) } +/// Return a `bool` value from the given `u8`. +pub fn u8_to_bool(value: u8) -> bool { + value != 0 +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// @@ -206,37 +260,31 @@ mod tests { assert_eq!(result, None); } - #[rstest( - s, - expected, - case("", 0), - case("0", 0), - case("1.0", 1), - case("1.00", 2), - case("1.23456789", 8), - case("123456.789101112", 9), - case("0.000000001", 9), - case("1e-1", 1), - case("1e-2", 2), - case("1e-3", 3), - case("1e8", 0) - )] - fn test_precision_from_str(s: &str, expected: u8) { + #[rstest] + #[case("", 0)] + #[case("0", 0)] + #[case("1.0", 1)] + #[case("1.00", 2)] + #[case("1.23456789", 8)] + #[case("123456.789101112", 9)] + #[case("0.000000001", 9)] + #[case("1e-1", 1)] + #[case("1e-2", 2)] + #[case("1e-3", 3)] + #[case("1e8", 0)] + fn test_precision_from_str(#[case] s: &str, #[case] expected: u8) { let result = precision_from_str(s); assert_eq!(result, expected); } - #[rstest( - input, - expected, - case("1e8", 0), - case("123", 0), - case("123.45", 2), - case("123.456789", 6), - case("1.23456789e-2", 2), - case("1.23456789e-12", 12) - )] - fn test_precision_from_cstr(input: &str, expected: u8) { + #[rstest] + #[case("1e8", 0)] + #[case("123", 0)] + #[case("123.45", 2)] + #[case("123.456789", 6)] + #[case("1.23456789e-2", 2)] + #[case("1.23456789e-12", 12)] + fn test_precision_from_cstr(#[case] input: &str, #[case] expected: u8) { let c_str = CString::new(input).unwrap(); assert_eq!(unsafe { precision_from_cstr(c_str.as_ptr()) }, expected); } diff --git a/nautilus_core/core/src/string.rs b/nautilus_core/core/src/string.rs index 145f5fec3130..8fe49eb17864 100644 --- a/nautilus_core/core/src/string.rs +++ b/nautilus_core/core/src/string.rs @@ -13,9 +13,13 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::ffi::{c_char, CStr, CString}; +use std::{ + ffi::{c_char, CStr, CString}, + str, +}; use pyo3::{ffi, types::PyString, FromPyPointer, Python}; +use ustr::Ustr; /// Returns an owned string from a valid Python object pointer. /// @@ -32,6 +36,39 @@ pub unsafe fn pystr_to_string(ptr: *mut ffi::PyObject) -> String { Python::with_gil(|py| PyString::from_borrowed_ptr(py, ptr).to_string()) } +/// Convert a C string pointer into an owned `String`. +/// +/// # Safety +/// +/// - Assumes `ptr` is a valid C string pointer. +/// +/// # Panics +/// +/// - If `ptr` is null. +#[must_use] +pub unsafe fn cstr_to_ustr(ptr: *const c_char) -> Ustr { + assert!(!ptr.is_null(), "`ptr` was NULL"); + Ustr::from(CStr::from_ptr(ptr).to_str().expect("CStr::from_ptr failed")) +} + +/// Convert a C string pointer into an owned `Option`. +/// +/// # Safety +/// +/// - Assumes `ptr` is a valid C string pointer or NULL. +/// +/// # Panics +/// +/// - If `ptr` is null. +#[must_use] +pub unsafe fn optional_cstr_to_ustr(ptr: *const c_char) -> Option { + if !ptr.is_null() { + Some(cstr_to_ustr(ptr)) + } else { + None + } +} + /// Convert a C string pointer into an owned `String`. /// /// # Safety diff --git a/nautilus_core/indicators/src/ema.rs b/nautilus_core/indicators/src/ema.rs index e5fccfeff78a..8512550c1698 100644 --- a/nautilus_core/indicators/src/ema.rs +++ b/nautilus_core/indicators/src/ema.rs @@ -30,8 +30,8 @@ pub struct ExponentialMovingAverage { pub alpha: f64, pub value: f64, pub count: usize, - _has_inputs: bool, - _is_initialized: bool, + has_inputs: bool, + is_initialized: bool, } impl Indicator for ExponentialMovingAverage { @@ -40,99 +40,104 @@ impl Indicator for ExponentialMovingAverage { } fn has_inputs(&self) -> bool { - self._has_inputs + self.has_inputs } fn is_initialized(&self) -> bool { - self._is_initialized + self.is_initialized } fn handle_quote_tick(&mut self, tick: &QuoteTick) { - self.update_raw(tick.extract_price(self.price_type).into()) + self.py_update_raw(tick.extract_price(self.price_type).into()) } fn handle_trade_tick(&mut self, tick: &TradeTick) { - self.update_raw((&tick.price).into()) + self.py_update_raw((&tick.price).into()) } fn handle_bar(&mut self, bar: &Bar) { - self.update_raw((&bar.close).into()) + self.py_update_raw((&bar.close).into()) } fn reset(&mut self) { self.value = 0.0; self.count = 0; - self._has_inputs = false; - self._is_initialized = false; + self.has_inputs = false; + self.is_initialized = false; + } +} + +impl ExponentialMovingAverage { + fn update_raw(&mut self, value: f64) { + if !self.has_inputs { + self.has_inputs = true; + self.value = value; + } + + self.value = self.alpha.mul_add(value, (1.0 - self.alpha) * self.value); + self.count += 1; + + // Initialization logic + if !self.is_initialized && self.count >= self.period { + self.is_initialized = true; + } } } #[pymethods] impl ExponentialMovingAverage { - #[must_use] #[new] - pub fn new(period: usize, price_type: Option) -> Self { + fn new(period: usize, price_type: Option) -> Self { Self { period, price_type: price_type.unwrap_or(PriceType::Last), alpha: 2.0 / (period as f64 + 1.0), value: 0.0, count: 0, - _has_inputs: false, - _is_initialized: false, + has_inputs: false, + is_initialized: false, } } #[getter] #[pyo3(name = "name")] - #[must_use] - pub fn name_py(&self) -> String { + fn py_name(&self) -> String { self.name() } #[pyo3(name = "has_inputs")] - fn has_inputs_py(&self) -> bool { + fn py_has_inputs(&self) -> bool { self.has_inputs() } #[pyo3(name = "is_initialized")] - fn is_initialized(&self) -> bool { - self._is_initialized + fn py_is_initialized(&self) -> bool { + self.is_initialized } #[pyo3(name = "handle_quote_tick")] - fn handle_quote_tick_py(&mut self, tick: &QuoteTick) { - self.update_raw(tick.extract_price(self.price_type).into()) + fn py_handle_quote_tick(&mut self, tick: &QuoteTick) { + self.py_update_raw(tick.extract_price(self.price_type).into()) } #[pyo3(name = "handle_trade_tick")] - fn handle_trade_tick_py(&mut self, tick: &TradeTick) { + fn py_handle_trade_tick(&mut self, tick: &TradeTick) { self.update_raw((&tick.price).into()) } #[pyo3(name = "handle_bar")] - fn handle_bar_py(&mut self, bar: &Bar) { + fn py_handle_bar(&mut self, bar: &Bar) { self.update_raw((&bar.close).into()) } #[pyo3(name = "reset")] - fn reset_py(&mut self) { + fn py_reset(&mut self) { self.reset() } - pub fn update_raw(&mut self, value: f64) { - if !self._has_inputs { - self._has_inputs = true; - self.value = value; - } - - self.value = self.alpha.mul_add(value, (1.0 - self.alpha) * self.value); - self.count += 1; - - // Initialization logic - if !self._is_initialized && self.count >= self.period { - self._is_initialized = true; - } + #[pyo3(name = "update_raw")] + fn py_update_raw(&mut self, value: f64) { + self.update_raw(value) } } @@ -147,15 +152,15 @@ mod tests { fn test_ema_initialized() { let ema = ExponentialMovingAverage::new(20, Some(PriceType::Mid)); let display_str = format!("{ema:?}"); - assert_eq!(display_str, "ExponentialMovingAverage { period: 20, price_type: Mid, alpha: 0.09523809523809523, value: 0.0, count: 0, _has_inputs: false, _is_initialized: false }"); + assert_eq!(display_str, "ExponentialMovingAverage { period: 20, price_type: Mid, alpha: 0.09523809523809523, value: 0.0, count: 0, has_inputs: false, is_initialized: false }"); } #[test] fn test_ema_update_raw() { let mut ema = ExponentialMovingAverage::new(3, Some(PriceType::Mid)); - ema.update_raw(1.0); - ema.update_raw(2.0); - ema.update_raw(3.0); + ema.py_update_raw(1.0); + ema.py_update_raw(2.0); + ema.py_update_raw(3.0); assert!(ema.has_inputs()); assert!(ema.is_initialized()); @@ -166,9 +171,9 @@ mod tests { #[test] fn test_ema_reset() { let mut ema = ExponentialMovingAverage::new(3, Some(PriceType::Mid)); - ema.update_raw(1.0); - ema.update_raw(2.0); - ema.update_raw(3.0); + ema.py_update_raw(1.0); + ema.py_update_raw(2.0); + ema.py_update_raw(3.0); ema.reset(); diff --git a/nautilus_core/model/Cargo.toml b/nautilus_core/model/Cargo.toml index abe6363cb929..f0f72b7b97a1 100644 --- a/nautilus_core/model/Cargo.toml +++ b/nautilus_core/model/Cargo.toml @@ -16,15 +16,16 @@ anyhow.workspace = true pyo3.workspace = true rmp-serde.workspace = true rust_decimal.workspace = true +rust_decimal_macros.workspace = true serde.workspace = true serde_json.workspace = true strum.workspace = true thiserror.workspace = true +ustr.workspace = true derive_builder = "0.12.0" -evalexpr = "11.0.0" +evalexpr = "11.1.0" lazy_static = "1.4.0" tabled = "0.12.2" -ustr = "0.10.0" [features] extension-module = ["pyo3/extension-module", "nautilus-core/extension-module"] @@ -32,6 +33,7 @@ default = [] [dev-dependencies] criterion.workspace = true +float-cmp.workspace = true iai.workspace = true rstest.workspace = true diff --git a/nautilus_core/model/cbindgen.toml b/nautilus_core/model/cbindgen.toml index 80108cde9431..482818d37f22 100644 --- a/nautilus_core/model/cbindgen.toml +++ b/nautilus_core/model/cbindgen.toml @@ -15,6 +15,7 @@ exclude = [ ] [export.rename] +"bool" = "uint8_t" "Ustr" = "char*" "AccountId" = "AccountId_t" "Bar" = "Bar_t" @@ -31,7 +32,14 @@ exclude = [ "InstrumentId" = "InstrumentId_t" "Money" = "Money_t" "OrderBookDelta" = "OrderBookDelta_t" +"OrderInitialized" = "OrderInitialized_t" "OrderDenied" = "OrderDenied_t" +"OrderEmulated" = "OrderEmulated_t" +"OrderReleased" = "OrderReleased_t" +"OrderSubmitted" = "OrderSubmitted_t" +"OrderAccepted" = "OrderAccepted_t" +"OrderRejected" = "OrderRejected_t" +"OrderCanceled" = "OrderCanceled_t" "OrderListId" = "OrderListId_t" "PositionId" = "PositionId_t" "Price" = "Price_t" diff --git a/nautilus_core/model/cbindgen_cython.toml b/nautilus_core/model/cbindgen_cython.toml index 05ad3a97161e..329c454e5c7e 100644 --- a/nautilus_core/model/cbindgen_cython.toml +++ b/nautilus_core/model/cbindgen_cython.toml @@ -31,6 +31,7 @@ exclude = [ ] [export.rename] +"bool" = "bint" "Ustr" = "char*" "AccountId" = "AccountId_t" "Bar" = "Bar_t" @@ -47,7 +48,14 @@ exclude = [ "InstrumentId" = "InstrumentId_t" "Money" = "Money_t" "OrderBookDelta" = "OrderBookDelta_t" +"OrderInitialized" = "OrderInitialized_t" "OrderDenied" = "OrderDenied_t" +"OrderEmulated" = "OrderEmulated_t" +"OrderReleased" = "OrderReleased_t" +"OrderSubmitted" = "OrderSubmitted_t" +"OrderAccepted" = "OrderAccepted_t" +"OrderRejected" = "OrderRejected_t" +"OrderCanceled" = "OrderCanceled_t" "OrderListId" = "OrderListId_t" "PositionId" = "PositionId_t" "Price" = "Price_t" diff --git a/nautilus_core/model/src/data/bar.rs b/nautilus_core/model/src/data/bar.rs index 19e0a6d2c3cd..6b9e6018bf43 100644 --- a/nautilus_core/model/src/data/bar.rs +++ b/nautilus_core/model/src/data/bar.rs @@ -38,7 +38,7 @@ use crate::{ #[pyclass] pub struct BarSpecification { /// The step for binning samples for bar aggregation. - pub step: u64, + pub step: usize, /// The type of bar aggregation. pub aggregation: BarAggregation, /// The price type to use for aggregation. @@ -160,6 +160,31 @@ impl<'de> Deserialize<'de> for BarType { } } +#[pymethods] +impl BarType { + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + CompareOp::Ne => self.ne(other).into_py(py), + _ => py.NotImplemented(), + } + } + + fn __hash__(&self) -> isize { + let mut h = DefaultHasher::new(); + self.hash(&mut h); + h.finish() as isize + } + + fn __str__(&self) -> String { + self.to_string() + } + + fn __repr__(&self) -> String { + format!("{self:?}") + } +} + /// Represents an aggregated bar. #[repr(C)] #[derive(Clone, Copy, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -209,6 +234,7 @@ impl Bar { } } + /// Returns the metadata for the type, for use with serialization formats. pub fn get_metadata( bar_type: &BarType, price_precision: u8, @@ -220,6 +246,44 @@ impl Bar { metadata.insert("size_precision".to_string(), size_precision.to_string()); metadata } + + /// Create a new [`Bar`] extracted from the given [`PyAny`]. + pub fn from_pyobject(obj: &PyAny) -> PyResult { + let bar_type_obj: &PyAny = obj.getattr("bar_type")?.extract()?; + let bar_type_str = bar_type_obj.call_method0("__str__")?.extract()?; + let bar_type = BarType::from_str(bar_type_str) + .map_err(|e| PyValueError::new_err(format!("{}", e))) + .unwrap(); + + let open_py: &PyAny = obj.getattr("open")?; + let price_prec: u8 = open_py.getattr("precision")?.extract()?; + let open_raw: i64 = open_py.getattr("raw")?.extract()?; + let open = Price::from_raw(open_raw, price_prec); + + let high_py: &PyAny = obj.getattr("high")?; + let high_raw: i64 = high_py.getattr("raw")?.extract()?; + let high = Price::from_raw(high_raw, price_prec); + + let low_py: &PyAny = obj.getattr("low")?; + let low_raw: i64 = low_py.getattr("raw")?.extract()?; + let low = Price::from_raw(low_raw, price_prec); + + let close_py: &PyAny = obj.getattr("close")?; + let close_raw: i64 = close_py.getattr("raw")?.extract()?; + let close = Price::from_raw(close_raw, price_prec); + + let volume_py: &PyAny = obj.getattr("volume")?; + let volume_raw: u64 = volume_py.getattr("raw")?.extract()?; + let volume_prec: u8 = volume_py.getattr("precision")?.extract()?; + let volume = Quantity::from_raw(volume_raw, volume_prec); + + let ts_event: UnixNanos = obj.getattr("ts_event")?.extract()?; + let ts_init: UnixNanos = obj.getattr("ts_init")?.extract()?; + + Ok(Self::new( + bar_type, open, high, low, close, volume, ts_event, ts_init, + )) + } } impl Serializable for Bar {} @@ -319,9 +383,8 @@ impl Bar { let json_str = serde_json::to_string(self).map_err(|e| PyValueError::new_err(e.to_string()))?; // Parse JSON into a Python dictionary - let py_dict: Py = PyModule::import(py, "msgspec")? - .getattr("json")? - .call_method("decode", (json_str,), None)? + let py_dict: Py = PyModule::import(py, "json")? + .call_method("loads", (json_str,), None)? .extract()?; Ok(py_dict) } @@ -329,13 +392,13 @@ impl Bar { /// Return a new object from the given dictionary representation. #[staticmethod] pub fn from_dict(py: Python<'_>, values: Py) -> PyResult { - // Serialize to JSON bytes - let json_bytes: Vec = PyModule::import(py, "msgspec")? - .getattr("json")? - .call_method("encode", (values,), None)? + // Extract to JSON string + let json_str: String = PyModule::import(py, "json")? + .call_method("dumps", (values,), None)? .extract()?; + // Deserialize to object - let instance = serde_json::from_slice(&json_bytes) + let instance = serde_json::from_slice(&json_str.into_bytes()) .map_err(|e| PyValueError::new_err(e.to_string()))?; Ok(instance) } @@ -636,6 +699,18 @@ mod tests { }); } + #[test] + fn test_from_pyobject() { + pyo3::prepare_freethreaded_python(); + let bar = create_stub_bar(); + + Python::with_gil(|py| { + let bar_pyobject = bar.into_py(py); + let parsed_bar = Bar::from_pyobject(bar_pyobject.as_ref(py)).unwrap(); + assert_eq!(parsed_bar, bar); + }); + } + #[test] fn test_json_serialization() { let bar = create_stub_bar(); diff --git a/nautilus_core/model/src/data/bar_api.rs b/nautilus_core/model/src/data/bar_api.rs index 0bd4a67b230b..5291849433cb 100644 --- a/nautilus_core/model/src/data/bar_api.rs +++ b/nautilus_core/model/src/data/bar_api.rs @@ -30,7 +30,7 @@ use crate::{ #[no_mangle] pub extern "C" fn bar_specification_new( - step: u64, + step: usize, aggregation: u8, price_type: u8, ) -> BarSpecification { diff --git a/nautilus_core/model/src/data/delta.rs b/nautilus_core/model/src/data/delta.rs index 437fe340aa17..37ade20c8a34 100644 --- a/nautilus_core/model/src/data/delta.rs +++ b/nautilus_core/model/src/data/delta.rs @@ -17,14 +17,19 @@ use std::{ collections::{hash_map::DefaultHasher, HashMap}, fmt::{Display, Formatter}, hash::{Hash, Hasher}, + str::FromStr, }; use nautilus_core::{serialization::Serializable, time::UnixNanos}; use pyo3::{exceptions::PyValueError, prelude::*, pyclass::CompareOp, types::PyDict}; use serde::{Deserialize, Serialize}; -use super::order::BookOrder; -use crate::{enums::BookAction, identifiers::instrument_id::InstrumentId}; +use super::order::{BookOrder, NULL_ORDER}; +use crate::{ + enums::{BookAction, FromU8, OrderSide}, + identifiers::instrument_id::InstrumentId, + types::{price::Price, quantity::Quantity}, +}; /// Represents a single change/delta in an order book. #[repr(C)] @@ -71,6 +76,7 @@ impl OrderBookDelta { } } + /// Returns the metadata for the type, for use with serialization formats. pub fn get_metadata( instrument_id: &InstrumentId, price_precision: u8, @@ -82,6 +88,61 @@ impl OrderBookDelta { metadata.insert("size_precision".to_string(), size_precision.to_string()); metadata } + + /// Create a new [`OrderBookDelta`] extracted from the given [`PyAny`]. + pub fn from_pyobject(obj: &PyAny) -> PyResult { + let instrument_id_obj: &PyAny = obj.getattr("instrument_id")?.extract()?; + let instrument_id_str = instrument_id_obj.getattr("value")?.extract()?; + let instrument_id = InstrumentId::from_str(instrument_id_str) + .map_err(|e| PyValueError::new_err(format!("{}", e))) + .unwrap(); + + let action_obj: &PyAny = obj.getattr("action")?.extract()?; + let action_u8 = action_obj.getattr("value")?.extract()?; + let action = BookAction::from_u8(action_u8).unwrap(); + + let flags: u8 = obj.getattr("flags")?.extract()?; + let sequence: u64 = obj.getattr("sequence")?.extract()?; + let ts_event: UnixNanos = obj.getattr("ts_event")?.extract()?; + let ts_init: UnixNanos = obj.getattr("ts_init")?.extract()?; + + let order_pyobject = obj.getattr("order")?; + let order: BookOrder = if order_pyobject.is_none() { + NULL_ORDER + } else { + let side_obj: &PyAny = order_pyobject.getattr("side")?.extract()?; + let side_u8 = side_obj.getattr("value")?.extract()?; + let side = OrderSide::from_u8(side_u8).unwrap(); + + let price_py: &PyAny = order_pyobject.getattr("price")?; + let price_raw: i64 = price_py.getattr("raw")?.extract()?; + let price_prec: u8 = price_py.getattr("precision")?.extract()?; + let price = Price::from_raw(price_raw, price_prec); + + let size_py: &PyAny = order_pyobject.getattr("size")?; + let size_raw: u64 = size_py.getattr("raw")?.extract()?; + let size_prec: u8 = size_py.getattr("precision")?.extract()?; + let size = Quantity::from_raw(size_raw, size_prec); + + let order_id: u64 = order_pyobject.getattr("order_id")?.extract()?; + BookOrder { + side, + price, + size, + order_id, + } + }; + + Ok(Self::new( + instrument_id, + action, + order, + flags, + sequence, + ts_event, + ts_init, + )) + } } impl Serializable for OrderBookDelta {} @@ -188,9 +249,8 @@ impl OrderBookDelta { let json_str = serde_json::to_string(self).map_err(|e| PyValueError::new_err(e.to_string()))?; // Parse JSON into a Python dictionary - let py_dict: Py = PyModule::import(py, "msgspec")? - .getattr("json")? - .call_method("decode", (json_str,), None)? + let py_dict: Py = PyModule::import(py, "json")? + .call_method("loads", (json_str,), None)? .extract()?; Ok(py_dict) } @@ -198,13 +258,13 @@ impl OrderBookDelta { /// Return a new object from the given dictionary representation. #[staticmethod] pub fn from_dict(py: Python<'_>, values: Py) -> PyResult { - // Serialize to JSON bytes - let json_bytes: Vec = PyModule::import(py, "msgspec")? - .getattr("json")? - .call_method("encode", (values,), None)? + // Extract to JSON string + let json_str: String = PyModule::import(py, "json")? + .call_method("dumps", (values,), None)? .extract()?; + // Deserialize to object - let instance = serde_json::from_slice(&json_bytes) + let instance = serde_json::from_slice(&json_str.into_bytes()) .map_err(|e| PyValueError::new_err(e.to_string()))?; Ok(instance) } @@ -342,6 +402,18 @@ mod tests { }); } + #[test] + fn test_from_pyobject() { + pyo3::prepare_freethreaded_python(); + let delta = create_stub_delta(); + + Python::with_gil(|py| { + let delta_pyobject = delta.into_py(py); + let parsed_delta = OrderBookDelta::from_pyobject(delta_pyobject.as_ref(py)).unwrap(); + assert_eq!(parsed_delta, delta); + }); + } + #[test] fn test_json_serialization() { let delta = create_stub_delta(); diff --git a/nautilus_core/model/src/data/order.rs b/nautilus_core/model/src/data/order.rs index 2017cc834a37..f4276f1692bf 100644 --- a/nautilus_core/model/src/data/order.rs +++ b/nautilus_core/model/src/data/order.rs @@ -91,14 +91,17 @@ impl BookOrder { #[must_use] pub fn from_quote_tick(tick: &QuoteTick, side: OrderSide) -> Self { match side { - OrderSide::Buy => { - Self::new(OrderSide::Buy, tick.bid, tick.bid_size, tick.bid.raw as u64) - } + OrderSide::Buy => Self::new( + OrderSide::Buy, + tick.bid_price, + tick.bid_size, + tick.bid_price.raw as u64, + ), OrderSide::Sell => Self::new( OrderSide::Sell, - tick.ask, + tick.ask_price, tick.ask_size, - tick.ask.raw as u64, + tick.ask_price.raw as u64, ), _ => panic!("{}", BookIntegrityError::NoOrderSide), } @@ -210,9 +213,8 @@ impl BookOrder { let json_str = serde_json::to_string(self).map_err(|e| PyValueError::new_err(e.to_string()))?; // Parse JSON into a Python dictionary - let py_dict: Py = PyModule::import(py, "msgspec")? - .getattr("json")? - .call_method("decode", (json_str,), None)? + let py_dict: Py = PyModule::import(py, "json")? + .call_method("loads", (json_str,), None)? .extract()?; Ok(py_dict) } @@ -220,13 +222,13 @@ impl BookOrder { /// Return a new object from the given dictionary representation. #[staticmethod] pub fn from_dict(py: Python<'_>, values: Py) -> PyResult { - // Serialize to JSON bytes - let json_bytes: Vec = PyModule::import(py, "msgspec")? - .getattr("json")? - .call_method("encode", (values,), None)? + // Extract to JSON string + let json_str: String = PyModule::import(py, "json")? + .call_method("dumps", (values,), None)? .extract()?; + // Deserialize to object - let instance = serde_json::from_slice(&json_bytes) + let instance = serde_json::from_slice(&json_str.into_bytes()) .map_err(|e| PyValueError::new_err(e.to_string()))?; Ok(instance) } @@ -350,8 +352,10 @@ mod tests { assert_eq!(display, expected); } - #[rstest(side, case(OrderSide::Buy), case(OrderSide::Sell))] - fn test_from_quote_tick(side: OrderSide) { + #[rstest] + #[case(OrderSide::Buy)] + #[case(OrderSide::Sell)] + fn test_from_quote_tick(#[case] side: OrderSide) { let tick = QuoteTick::new( InstrumentId::from_str("ETHUSDT-PERP.BINANCE").unwrap(), Price::new(5000.0, 2), @@ -368,8 +372,8 @@ mod tests { assert_eq!( book_order.price, match side { - OrderSide::Buy => tick.bid, - OrderSide::Sell => tick.ask, + OrderSide::Buy => tick.bid_price, + OrderSide::Sell => tick.ask_price, _ => panic!("Invalid test"), } ); @@ -384,15 +388,17 @@ mod tests { assert_eq!( book_order.order_id, match side { - OrderSide::Buy => tick.bid.raw as u64, - OrderSide::Sell => tick.ask.raw as u64, + OrderSide::Buy => tick.bid_price.raw as u64, + OrderSide::Sell => tick.ask_price.raw as u64, _ => panic!("Invalid test"), } ); } - #[rstest(side, case(OrderSide::Buy), case(OrderSide::Sell))] - fn test_from_trade_tick(side: OrderSide) { + #[rstest] + #[case(OrderSide::Buy)] + #[case(OrderSide::Sell)] + fn test_from_trade_tick(#[case] side: OrderSide) { let tick = TradeTick::new( InstrumentId::from_str("ETHUSDT-PERP.BINANCE").unwrap(), Price::new(5000.0, 2), diff --git a/nautilus_core/model/src/data/quote.rs b/nautilus_core/model/src/data/quote.rs index f9569fa5beb1..be3cd8aa857f 100644 --- a/nautilus_core/model/src/data/quote.rs +++ b/nautilus_core/model/src/data/quote.rs @@ -18,6 +18,7 @@ use std::{ collections::{hash_map::DefaultHasher, HashMap}, fmt::{Display, Formatter}, hash::{Hash, Hasher}, + str::FromStr, }; use nautilus_core::{correctness, serialization::Serializable, time::UnixNanos}; @@ -38,9 +39,9 @@ pub struct QuoteTick { /// The quotes instrument ID. pub instrument_id: InstrumentId, /// The top of book bid price. - pub bid: Price, + pub bid_price: Price, /// The top of book ask price. - pub ask: Price, + pub ask_price: Price, /// The top of book bid size. pub bid_size: Quantity, /// The top of book ask size. @@ -55,18 +56,18 @@ impl QuoteTick { #[must_use] pub fn new( instrument_id: InstrumentId, - bid: Price, - ask: Price, + bid_price: Price, + ask_price: Price, bid_size: Quantity, ask_size: Quantity, ts_event: UnixNanos, ts_init: UnixNanos, ) -> Self { correctness::u8_equal( - bid.precision, - ask.precision, - "bid.precision", - "ask.precision", + bid_price.precision, + ask_price.precision, + "bid_price.precision", + "ask_price.precision", ); correctness::u8_equal( bid_size.precision, @@ -76,8 +77,8 @@ impl QuoteTick { ); Self { instrument_id, - bid, - ask, + bid_price, + ask_price, bid_size, ask_size, ts_event, @@ -85,6 +86,7 @@ impl QuoteTick { } } + /// Returns the metadata for the type, for use with serialization formats. pub fn get_metadata( instrument_id: &InstrumentId, price_precision: u8, @@ -97,14 +99,56 @@ impl QuoteTick { metadata } + /// Create a new [`Bar`] extracted from the given [`PyAny`]. + pub fn from_pyobject(obj: &PyAny) -> PyResult { + let instrument_id_obj: &PyAny = obj.getattr("instrument_id")?.extract()?; + let instrument_id_str = instrument_id_obj.getattr("value")?.extract()?; + let instrument_id = InstrumentId::from_str(instrument_id_str) + .map_err(|e| PyValueError::new_err(format!("{}", e))) + .unwrap(); + + let bid_price_py: &PyAny = obj.getattr("bid_price")?; + let bid_price_raw: i64 = bid_price_py.getattr("raw")?.extract()?; + let bid_price_prec: u8 = bid_price_py.getattr("precision")?.extract()?; + let bid_price = Price::from_raw(bid_price_raw, bid_price_prec); + + let ask_price_py: &PyAny = obj.getattr("ask_price")?; + let ask_price_raw: i64 = ask_price_py.getattr("raw")?.extract()?; + let ask_price_prec: u8 = ask_price_py.getattr("precision")?.extract()?; + let ask_price = Price::from_raw(ask_price_raw, ask_price_prec); + + let bid_size_py: &PyAny = obj.getattr("bid_size")?; + let bid_size_raw: u64 = bid_size_py.getattr("raw")?.extract()?; + let bid_size_prec: u8 = bid_size_py.getattr("precision")?.extract()?; + let bid_size = Quantity::from_raw(bid_size_raw, bid_size_prec); + + let ask_size_py: &PyAny = obj.getattr("ask_size")?; + let ask_size_raw: u64 = ask_size_py.getattr("raw")?.extract()?; + let ask_size_prec: u8 = ask_size_py.getattr("precision")?.extract()?; + let ask_size = Quantity::from_raw(ask_size_raw, ask_size_prec); + + let ts_event: UnixNanos = obj.getattr("ts_event")?.extract()?; + let ts_init: UnixNanos = obj.getattr("ts_init")?.extract()?; + + Ok(Self::new( + instrument_id, + bid_price, + ask_price, + bid_size, + ask_size, + ts_event, + ts_init, + )) + } + #[must_use] pub fn extract_price(&self, price_type: PriceType) -> Price { match price_type { - PriceType::Bid => self.bid, - PriceType::Ask => self.ask, + PriceType::Bid => self.bid_price, + PriceType::Ask => self.ask_price, PriceType::Mid => Price::from_raw( - (self.bid.raw + self.ask.raw) / 2, - cmp::min(self.bid.precision + 1, FIXED_PRECISION), + (self.bid_price.raw + self.ask_price.raw) / 2, + cmp::min(self.bid_price.precision + 1, FIXED_PRECISION), ), _ => panic!("Cannot extract with price type {price_type}"), } @@ -118,7 +162,12 @@ impl Display for QuoteTick { write!( f, "{},{},{},{},{},{}", - self.instrument_id, self.bid, self.ask, self.bid_size, self.ask_size, self.ts_event, + self.instrument_id, + self.bid_price, + self.ask_price, + self.bid_size, + self.ask_size, + self.ts_event, ) } } @@ -126,7 +175,7 @@ impl Display for QuoteTick { #[pymethods] impl QuoteTick { #[new] - fn new_py( + fn py_new( instrument_id: InstrumentId, bid_price: Price, ask_price: Price, @@ -175,12 +224,12 @@ impl QuoteTick { #[getter] fn bid_price(&self) -> Price { - self.bid + self.bid_price } #[getter] fn ask_price(&self) -> Price { - self.ask + self.ask_price } #[getter] @@ -213,9 +262,8 @@ impl QuoteTick { let json_str = serde_json::to_string(self).map_err(|e| PyValueError::new_err(e.to_string()))?; // Parse JSON into a Python dictionary - let py_dict: Py = PyModule::import(py, "msgspec")? - .getattr("json")? - .call_method("decode", (json_str,), None)? + let py_dict: Py = PyModule::import(py, "json")? + .call_method("loads", (json_str,), None)? .extract()?; Ok(py_dict) } @@ -223,13 +271,13 @@ impl QuoteTick { /// Return a new object from the given dictionary representation. #[staticmethod] pub fn from_dict(py: Python<'_>, values: Py) -> PyResult { - // Serialize to JSON bytes - let json_bytes: Vec = PyModule::import(py, "msgspec")? - .getattr("json")? - .call_method("encode", (values,), None)? + // Extract to JSON string + let json_str: String = PyModule::import(py, "json")? + .call_method("dumps", (values,), None)? .extract()?; + // Deserialize to object - let instance = serde_json::from_slice(&json_bytes) + let instance = serde_json::from_slice(&json_str.into_bytes()) .map_err(|e| PyValueError::new_err(e.to_string()))?; Ok(instance) } @@ -265,7 +313,7 @@ mod tests { use std::str::FromStr; use nautilus_core::serialization::Serializable; - use pyo3::Python; + use pyo3::{IntoPy, Python}; use rstest::rstest; use crate::{ @@ -278,8 +326,8 @@ mod tests { fn create_stub_quote_tick() -> QuoteTick { QuoteTick { instrument_id: InstrumentId::from_str("ETHUSDT-PERP.BINANCE").unwrap(), - bid: Price::new(10000.0, 4), - ask: Price::new(10001.0, 4), + bid_price: Price::new(10000.0, 4), + ask_price: Price::new(10001.0, 4), bid_size: Quantity::new(1.0, 8), ask_size: Quantity::new(1.0, 8), ts_event: 1, @@ -296,14 +344,11 @@ mod tests { ); } - #[rstest( - input, - expected, - case(PriceType::Bid, 10_000_000_000_000), - case(PriceType::Ask, 10_001_000_000_000), - case(PriceType::Mid, 10_000_500_000_000) - )] - fn test_extract_price(input: PriceType, expected: i64) { + #[rstest] + #[case(PriceType::Bid, 10_000_000_000_000)] + #[case(PriceType::Ask, 10_001_000_000_000)] + #[case(PriceType::Mid, 10_000_500_000_000)] + fn test_extract_price(#[case] input: PriceType, #[case] expected: i64) { let tick = create_stub_quote_tick(); let result = tick.extract_price(input).raw; assert_eq!(result, expected); @@ -317,7 +362,7 @@ mod tests { Python::with_gil(|py| { let dict_string = tick.as_dict(py).unwrap().to_string(); - let expected_string = r#"{'instrument_id': 'ETHUSDT-PERP.BINANCE', 'bid': '10000.0000', 'ask': '10001.0000', 'bid_size': '1.00000000', 'ask_size': '1.00000000', 'ts_event': 1, 'ts_init': 0}"#; + let expected_string = r#"{'instrument_id': 'ETHUSDT-PERP.BINANCE', 'bid_price': '10000.0000', 'ask_price': '10001.0000', 'bid_size': '1.00000000', 'ask_size': '1.00000000', 'ts_event': 1, 'ts_init': 0}"#; assert_eq!(dict_string, expected_string); }); } @@ -335,6 +380,18 @@ mod tests { }); } + #[test] + fn test_from_pyobject() { + pyo3::prepare_freethreaded_python(); + let tick = create_stub_quote_tick(); + + Python::with_gil(|py| { + let tick_pyobject = tick.into_py(py); + let parsed_tick = QuoteTick::from_pyobject(tick_pyobject.as_ref(py)).unwrap(); + assert_eq!(parsed_tick, tick); + }); + } + #[test] fn test_json_serialization() { let tick = create_stub_quote_tick(); diff --git a/nautilus_core/model/src/data/quote_api.rs b/nautilus_core/model/src/data/quote_api.rs index 3f4edb41ecaa..0ed3e72f0a59 100644 --- a/nautilus_core/model/src/data/quote_api.rs +++ b/nautilus_core/model/src/data/quote_api.rs @@ -54,9 +54,9 @@ pub extern "C" fn quote_tick_new( #[no_mangle] pub extern "C" fn quote_tick_eq(lhs: &QuoteTick, rhs: &QuoteTick) -> u8 { - assert_eq!(lhs.ask, rhs.ask); + assert_eq!(lhs.ask_price, rhs.ask_price); assert_eq!(lhs.ask_size, rhs.ask_size); - assert_eq!(lhs.bid, rhs.bid); + assert_eq!(lhs.bid_price, rhs.bid_price); assert_eq!(lhs.bid_size, rhs.bid_size); assert_eq!(lhs.ts_event, rhs.ts_event); assert_eq!(lhs.ts_init, rhs.ts_init); diff --git a/nautilus_core/model/src/data/ticker.rs b/nautilus_core/model/src/data/ticker.rs index 1f3b11edf5a4..e68f1dcbeed5 100644 --- a/nautilus_core/model/src/data/ticker.rs +++ b/nautilus_core/model/src/data/ticker.rs @@ -45,11 +45,7 @@ pub struct Ticker { impl Ticker { #[must_use] - pub fn new( - instrument_id: InstrumentId, - ts_event: UnixNanos, - ts_init: UnixNanos, - ) -> Self { + pub fn new(instrument_id: InstrumentId, ts_event: UnixNanos, ts_init: UnixNanos) -> Self { Self { instrument_id, ts_event, @@ -63,16 +59,8 @@ impl Serializable for Ticker {} #[pymethods] impl Ticker { #[new] - fn new_py( - instrument_id: InstrumentId, - ts_event: UnixNanos, - ts_init: UnixNanos, - ) -> Self { - Self::new( - instrument_id, - ts_event, - ts_init, - ) + fn py_new(instrument_id: InstrumentId, ts_event: UnixNanos, ts_init: UnixNanos) -> Self { + Self::new(instrument_id, ts_event, ts_init) } fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { @@ -115,11 +103,11 @@ impl Ticker { /// Return a dictionary representation of the object. pub fn as_dict(&self, py: Python<'_>) -> PyResult> { // Serialize object to JSON bytes - let json_str = serde_json::to_string(self).map_err(|e| PyValueError::new_err(e.to_string()))?; + let json_str = + serde_json::to_string(self).map_err(|e| PyValueError::new_err(e.to_string()))?; // Parse JSON into a Python dictionary - let py_dict: Py = PyModule::import(py, "msgspec")? - .getattr("json")? - .call_method("decode", (json_str,), None)? + let py_dict: Py = PyModule::import(py, "json")? + .call_method("loads", (json_str,), None)? .extract()?; Ok(py_dict) } @@ -127,13 +115,14 @@ impl Ticker { /// Return a new object from the given dictionary representation. #[staticmethod] pub fn from_dict(py: Python<'_>, values: Py) -> PyResult { - // Serialize to JSON bytes - let json_bytes: Vec = PyModule::import(py, "msgspec")? - .getattr("json")? - .call_method("encode", (values,), None)? + // Extract to JSON string + let json_str: String = PyModule::import(py, "json")? + .call_method("dumps", (values,), None)? .extract()?; + // Deserialize to object - let instance = serde_json::from_slice(&json_bytes).map_err(|e| PyValueError::new_err(e.to_string()))?; + let instance = serde_json::from_slice(&json_str.into_bytes()) + .map_err(|e| PyValueError::new_err(e.to_string()))?; Ok(instance) } @@ -154,7 +143,7 @@ impl Ticker { } /// Return MsgPack encoded bytes representation of the object. - fn as_msgpack(&self,py: Python<'_>) -> Py { + fn as_msgpack(&self, py: Python<'_>) -> Py { // Unwrapping is safe when serializing a valid object self.as_msgpack_bytes().unwrap().into_py(py) } diff --git a/nautilus_core/model/src/data/trade.rs b/nautilus_core/model/src/data/trade.rs index 52c82c3db803..3292b0c65693 100644 --- a/nautilus_core/model/src/data/trade.rs +++ b/nautilus_core/model/src/data/trade.rs @@ -17,6 +17,7 @@ use std::{ collections::{hash_map::DefaultHasher, HashMap}, fmt::{Display, Formatter}, hash::{Hash, Hasher}, + str::FromStr, }; use nautilus_core::{serialization::Serializable, time::UnixNanos}; @@ -24,7 +25,7 @@ use pyo3::{exceptions::PyValueError, prelude::*, pyclass::CompareOp, types::PyDi use serde::{Deserialize, Serialize}; use crate::{ - enums::AggressorSide, + enums::{AggressorSide, FromU8}, identifiers::{instrument_id::InstrumentId, trade_id::TradeId}, types::{price::Price, quantity::Quantity}, }; @@ -73,6 +74,7 @@ impl TradeTick { } } + /// Returns the metadata for the type, for use with serialization formats. pub fn get_metadata( instrument_id: &InstrumentId, price_precision: u8, @@ -84,6 +86,48 @@ impl TradeTick { metadata.insert("size_precision".to_string(), size_precision.to_string()); metadata } + + /// Create a new [`TradeTick`] extracted from the given [`PyAny`]. + pub fn from_pyobject(obj: &PyAny) -> PyResult { + let instrument_id_obj: &PyAny = obj.getattr("instrument_id")?.extract()?; + let instrument_id_str = instrument_id_obj.getattr("value")?.extract()?; + let instrument_id = InstrumentId::from_str(instrument_id_str) + .map_err(|e| PyValueError::new_err(format!("{}", e))) + .unwrap(); + + let price_py: &PyAny = obj.getattr("price")?; + let price_raw: i64 = price_py.getattr("raw")?.extract()?; + let price_prec: u8 = price_py.getattr("precision")?.extract()?; + let price = Price::from_raw(price_raw, price_prec); + + let size_py: &PyAny = obj.getattr("size")?; + let size_raw: u64 = size_py.getattr("raw")?.extract()?; + let size_prec: u8 = size_py.getattr("precision")?.extract()?; + let size = Quantity::from_raw(size_raw, size_prec); + + let aggressor_side_obj: &PyAny = obj.getattr("aggressor_side")?.extract()?; + let aggressor_side_u8 = aggressor_side_obj.getattr("value")?.extract()?; + let aggressor_side = AggressorSide::from_u8(aggressor_side_u8).unwrap(); + + let trade_id_obj: &PyAny = obj.getattr("trade_id")?.extract()?; + let trade_id_str = trade_id_obj.getattr("value")?.extract()?; + let trade_id = TradeId::from_str(trade_id_str) + .map_err(|e| PyValueError::new_err(e.to_string())) + .unwrap(); + + let ts_event: UnixNanos = obj.getattr("ts_event")?.extract()?; + let ts_init: UnixNanos = obj.getattr("ts_init")?.extract()?; + + Ok(Self::new( + instrument_id, + price, + size, + aggressor_side, + trade_id, + ts_event, + ts_init, + )) + } } impl Serializable for TradeTick {} @@ -189,9 +233,8 @@ impl TradeTick { let json_str = serde_json::to_string(self).map_err(|e| PyValueError::new_err(e.to_string()))?; // Parse JSON into a Python dictionary - let py_dict: Py = PyModule::import(py, "msgspec")? - .getattr("json")? - .call_method("decode", (json_str.as_bytes(),), None)? + let py_dict: Py = PyModule::import(py, "json")? + .call_method("loads", (json_str,), None)? .extract()?; Ok(py_dict) } @@ -199,13 +242,13 @@ impl TradeTick { /// Return a new object from the given dictionary representation. #[staticmethod] pub fn from_dict(py: Python<'_>, values: Py) -> PyResult { - // Serialize to JSON bytes - let json_bytes: Vec = PyModule::import(py, "msgspec")? - .getattr("json")? - .call_method("encode", (values,), None)? + // Extract to JSON string + let json_str: String = PyModule::import(py, "json")? + .call_method("dumps", (values,), None)? .extract()?; + // Deserialize to object - let instance = serde_json::from_slice(&json_bytes) + let instance = serde_json::from_slice(&json_str.into_bytes()) .map_err(|e| PyValueError::new_err(e.to_string()))?; Ok(instance) } @@ -241,7 +284,7 @@ mod tests { use std::str::FromStr; use nautilus_core::serialization::Serializable; - use pyo3::Python; + use pyo3::{IntoPy, Python}; use crate::{ data::trade::TradeTick, @@ -315,6 +358,18 @@ mod tests { }); } + #[test] + fn test_from_pyobject() { + pyo3::prepare_freethreaded_python(); + let tick = create_stub_trade_tick(); + + Python::with_gil(|py| { + let tick_pyobject = tick.into_py(py); + let parsed_tick = TradeTick::from_pyobject(tick_pyobject.as_ref(py)).unwrap(); + assert_eq!(parsed_tick, tick); + }); + } + #[test] fn test_json_serialization() { let tick = create_stub_trade_tick(); diff --git a/nautilus_core/model/src/enums.rs b/nautilus_core/model/src/enums.rs index a3dfe7c32b42..e14ab076a7a9 100644 --- a/nautilus_core/model/src/enums.rs +++ b/nautilus_core/model/src/enums.rs @@ -18,11 +18,11 @@ use std::{ffi::c_char, str::FromStr}; use nautilus_core::string::{cstr_to_string, str_to_cstr}; -use pyo3::prelude::*; +use pyo3::{exceptions::PyValueError, prelude::*, types::PyType, PyTypeInfo}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use strum::{AsRefStr, Display, EnumString, FromRepr}; +use strum::{AsRefStr, Display, EnumIter, EnumString, FromRepr}; -use crate::strum_serde; +use crate::{enum_for_python, enum_strum_serde, python::EnumIterator}; pub trait FromU8 { fn from_u8(value: u8) -> Option @@ -44,6 +44,7 @@ pub trait FromU8 { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -72,6 +73,7 @@ pub enum AccountType { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -98,6 +100,7 @@ pub enum AggregationSource { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -112,6 +115,17 @@ pub enum AggressorSide { Seller = 2, } +impl FromU8 for AggressorSide { + fn from_u8(value: u8) -> Option { + match value { + 0 => Some(AggressorSide::NoAggressor), + 1 => Some(AggressorSide::Buyer), + 2 => Some(AggressorSide::Seller), + _ => None, + } + } +} + /// A broad financial market asset class. #[repr(C)] #[derive( @@ -126,6 +140,7 @@ pub enum AggressorSide { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -167,6 +182,7 @@ pub enum AssetClass { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -203,6 +219,7 @@ pub enum AssetType { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -257,6 +274,7 @@ pub enum BarAggregation { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -299,6 +317,7 @@ impl FromU8 for BookAction { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -325,7 +344,7 @@ impl FromU8 for BookType { } } -/// The order contigency type which specifies the behaviour of linked orders. +/// The order contigency type which specifies the behavior of linked orders. /// /// [FIX 5.0 SP2 : ContingencyType <1385> field](https://www.onixs.biz/fix-dictionary/5.0.sp2/tagnum_1385.html). #[repr(C)] @@ -341,6 +360,7 @@ impl FromU8 for BookType { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -371,6 +391,7 @@ pub enum ContingencyType { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -397,6 +418,7 @@ pub enum CurrencyType { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -423,6 +445,7 @@ pub enum InstrumentCloseType { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -452,6 +475,7 @@ pub enum LiquiditySide { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -484,6 +508,7 @@ pub enum MarketStatus { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -514,6 +539,7 @@ pub enum OmsType { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -540,6 +566,7 @@ pub enum OptionKind { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -600,6 +627,7 @@ impl FromU8 for OrderSide { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -610,26 +638,30 @@ pub enum OrderStatus { Initialized = 1, /// The order was denied by the Nautilus system, either for being invalid, unprocessable or exceeding a risk limit. Denied = 2, - /// The order was submitted by the Nautilus system to the external service or trading venue (closed/done). - Submitted = 3, + /// The order became emulated by the Nautilus system in the `OrderEmulator` component. + Emulated = 3, + /// The order was released by the Nautilus system from the `OrderEmulator` component. + Released = 4, + /// The order was submitted by the Nautilus system to the external service or trading venue (awaiting acknowledgement). + Submitted = 5, /// The order was acknowledged by the trading venue as being received and valid (may now be working). - Accepted = 4, + Accepted = 6, /// The order was rejected by the trading venue. - Rejected = 5, + Rejected = 7, /// The order was canceled (closed/done). - Canceled = 6, + Canceled = 8, /// The order reached a GTD expiration (closed/done). - Expired = 7, - /// The order STOP price was triggered (closed/done). - Triggered = 8, - /// The order is currently pending a request to modify at the trading venue. - PendingUpdate = 9, - /// The order is currently pending a request to cancel at the trading venue. - PendingCancel = 10, - /// The order has been partially filled at the trading venue. - PartiallyFilled = 11, - /// The order has been completely filled at the trading venue (closed/done). - Filled = 12, + Expired = 9, + /// The order STOP price was triggered on a trading venue. + Triggered = 10, + /// The order is currently pending a request to modify on a trading venue. + PendingUpdate = 11, + /// The order is currently pending a request to cancel on a trading venue. + PendingCancel = 12, + /// The order has been partially filled on a trading venue. + PartiallyFilled = 13, + /// The order has been completely filled on a trading venue (closed/done). + Filled = 14, } /// The type of order. @@ -646,6 +678,7 @@ pub enum OrderStatus { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -686,6 +719,7 @@ pub enum OrderType { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -717,6 +751,7 @@ pub enum PositionSide { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -747,6 +782,7 @@ pub enum PriceType { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -783,6 +819,7 @@ pub enum TimeInForce { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -811,6 +848,7 @@ pub enum TradingState { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -843,6 +881,7 @@ pub enum TrailingOffsetType { Ord, AsRefStr, FromRepr, + EnumIter, EnumString, )] #[strum(ascii_case_insensitive)] @@ -871,30 +910,54 @@ pub enum TriggerType { IndexPrice = 9, } -strum_serde!(AccountType); -strum_serde!(AggregationSource); -strum_serde!(AggressorSide); -strum_serde!(AssetClass); -strum_serde!(AssetType); -strum_serde!(BarAggregation); -strum_serde!(BookAction); -strum_serde!(BookType); -strum_serde!(ContingencyType); -strum_serde!(CurrencyType); -strum_serde!(InstrumentCloseType); -strum_serde!(LiquiditySide); -strum_serde!(MarketStatus); -strum_serde!(OmsType); -strum_serde!(OptionKind); -strum_serde!(OrderSide); -strum_serde!(OrderStatus); -strum_serde!(OrderType); -strum_serde!(PositionSide); -strum_serde!(PriceType); -strum_serde!(TimeInForce); -strum_serde!(TradingState); -strum_serde!(TrailingOffsetType); -strum_serde!(TriggerType); +enum_strum_serde!(AccountType); +enum_strum_serde!(AggregationSource); +enum_strum_serde!(AggressorSide); +enum_strum_serde!(AssetClass); +enum_strum_serde!(AssetType); +enum_strum_serde!(BarAggregation); +enum_strum_serde!(BookAction); +enum_strum_serde!(BookType); +enum_strum_serde!(ContingencyType); +enum_strum_serde!(CurrencyType); +enum_strum_serde!(InstrumentCloseType); +enum_strum_serde!(LiquiditySide); +enum_strum_serde!(MarketStatus); +enum_strum_serde!(OmsType); +enum_strum_serde!(OptionKind); +enum_strum_serde!(OrderSide); +enum_strum_serde!(OrderStatus); +enum_strum_serde!(OrderType); +enum_strum_serde!(PositionSide); +enum_strum_serde!(PriceType); +enum_strum_serde!(TimeInForce); +enum_strum_serde!(TradingState); +enum_strum_serde!(TrailingOffsetType); +enum_strum_serde!(TriggerType); + +enum_for_python!(AccountType); +enum_for_python!(AggregationSource); +enum_for_python!(AggressorSide); +enum_for_python!(AssetClass); +enum_for_python!(BarAggregation); +enum_for_python!(BookAction); +enum_for_python!(BookType); +enum_for_python!(ContingencyType); +enum_for_python!(CurrencyType); +enum_for_python!(InstrumentCloseType); +enum_for_python!(LiquiditySide); +enum_for_python!(MarketStatus); +enum_for_python!(OmsType); +enum_for_python!(OptionKind); +enum_for_python!(OrderSide); +enum_for_python!(OrderStatus); +enum_for_python!(OrderType); +enum_for_python!(PositionSide); +enum_for_python!(PriceType); +enum_for_python!(TimeInForce); +enum_for_python!(TradingState); +enum_for_python!(TrailingOffsetType); +enum_for_python!(TriggerType); #[no_mangle] pub extern "C" fn account_type_to_cstr(value: AccountType) -> *const c_char { @@ -1282,3 +1345,22 @@ pub unsafe extern "C" fn trigger_type_from_cstr(ptr: *const c_char) -> TriggerTy TriggerType::from_str(&value) .unwrap_or_else(|_| panic!("invalid `TriggerType` enum string value, was '{value}'")) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_name() { + assert_eq!(OrderSide::NoOrderSide.name(), "NO_ORDER_SIDE"); + assert_eq!(OrderSide::Buy.name(), "BUY"); + assert_eq!(OrderSide::Sell.name(), "SELL"); + } + + #[test] + fn test_value() { + assert_eq!(OrderSide::NoOrderSide.value(), 0); + assert_eq!(OrderSide::Buy.value(), 1); + assert_eq!(OrderSide::Sell.value(), 2); + } +} diff --git a/nautilus_core/model/src/events/order.rs b/nautilus_core/model/src/events/order.rs index 1f707507b805..245711f8fb82 100644 --- a/nautilus_core/model/src/events/order.rs +++ b/nautilus_core/model/src/events/order.rs @@ -18,8 +18,10 @@ use std::collections::HashMap; use derive_builder::{self, Builder}; use nautilus_core::{time::UnixNanos, uuid::UUID4}; use serde::{Deserialize, Serialize}; +use ustr::Ustr; use crate::{ + currencies::USD, enums::{ ContingencyType, LiquiditySide, OrderSide, OrderType, TimeInForce, TrailingOffsetType, TriggerType, @@ -33,10 +35,12 @@ use crate::{ types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum OrderEvent { - OrderInitialized(OrderInitialized), + OrderInitialized(Box), OrderDenied(OrderDenied), + OrderEmulated(OrderEmulated), + OrderReleased(OrderReleased), OrderSubmitted(OrderSubmitted), OrderAccepted(OrderAccepted), OrderRejected(OrderRejected), @@ -52,6 +56,77 @@ pub enum OrderEvent { OrderFilled(OrderFilled), } +impl OrderEvent { + #[must_use] + pub fn client_order_id(&self) -> ClientOrderId { + match self { + Self::OrderInitialized(e) => e.client_order_id, + Self::OrderDenied(e) => e.client_order_id, + Self::OrderEmulated(e) => e.client_order_id, + Self::OrderReleased(e) => e.client_order_id, + Self::OrderSubmitted(e) => e.client_order_id, + Self::OrderAccepted(e) => e.client_order_id, + Self::OrderRejected(e) => e.client_order_id, + Self::OrderCanceled(e) => e.client_order_id, + Self::OrderExpired(e) => e.client_order_id, + Self::OrderTriggered(e) => e.client_order_id, + Self::OrderPendingUpdate(e) => e.client_order_id, + Self::OrderPendingCancel(e) => e.client_order_id, + Self::OrderModifyRejected(e) => e.client_order_id, + Self::OrderCancelRejected(e) => e.client_order_id, + Self::OrderUpdated(e) => e.client_order_id, + Self::OrderPartiallyFilled(e) => e.client_order_id, + Self::OrderFilled(e) => e.client_order_id, + } + } + + #[must_use] + pub fn strategy_id(&self) -> StrategyId { + match self { + Self::OrderInitialized(e) => e.strategy_id, + Self::OrderDenied(e) => e.strategy_id, + Self::OrderEmulated(e) => e.strategy_id, + Self::OrderReleased(e) => e.strategy_id, + Self::OrderSubmitted(e) => e.strategy_id, + Self::OrderAccepted(e) => e.strategy_id, + Self::OrderRejected(e) => e.strategy_id, + Self::OrderCanceled(e) => e.strategy_id, + Self::OrderExpired(e) => e.strategy_id, + Self::OrderTriggered(e) => e.strategy_id, + Self::OrderPendingUpdate(e) => e.strategy_id, + Self::OrderPendingCancel(e) => e.strategy_id, + Self::OrderModifyRejected(e) => e.strategy_id, + Self::OrderCancelRejected(e) => e.strategy_id, + Self::OrderUpdated(e) => e.strategy_id, + Self::OrderPartiallyFilled(e) => e.strategy_id, + Self::OrderFilled(e) => e.strategy_id, + } + } + + #[must_use] + pub fn ts_event(&self) -> UnixNanos { + match self { + Self::OrderInitialized(e) => e.ts_event, + Self::OrderDenied(e) => e.ts_event, + Self::OrderEmulated(e) => e.ts_event, + Self::OrderReleased(e) => e.ts_event, + Self::OrderSubmitted(e) => e.ts_event, + Self::OrderAccepted(e) => e.ts_event, + Self::OrderRejected(e) => e.ts_event, + Self::OrderCanceled(e) => e.ts_event, + Self::OrderExpired(e) => e.ts_event, + Self::OrderTriggered(e) => e.ts_event, + Self::OrderPendingUpdate(e) => e.ts_event, + Self::OrderPendingCancel(e) => e.ts_event, + Self::OrderModifyRejected(e) => e.ts_event, + Self::OrderCancelRejected(e) => e.ts_event, + Self::OrderUpdated(e) => e.ts_event, + Self::OrderPartiallyFilled(e) => e.ts_event, + Self::OrderFilled(e) => e.ts_event, + } + } +} + #[repr(C)] #[derive(Clone, PartialEq, Eq, Debug, Builder, Serialize, Deserialize)] #[builder(default)] @@ -67,24 +142,25 @@ pub struct OrderInitialized { pub price: Option, pub trigger_price: Option, pub trigger_type: Option, + pub limit_offset: Option, + pub trailing_offset: Option, + pub trailing_offset_type: Option, pub time_in_force: TimeInForce, pub expire_time: Option, pub post_only: bool, pub reduce_only: bool, pub quote_quantity: bool, pub display_qty: Option, - pub limit_offset: Option, - pub trailing_offset: Option, - pub trailing_offset_type: Option, pub emulation_trigger: Option, + pub trigger_instrument_id: Option, pub contingency_type: Option, pub order_list_id: Option, pub linked_order_ids: Option>, pub parent_order_id: Option, pub exec_algorithm_id: Option, - pub exec_algorithm_params: Option>, + pub exec_algorithm_params: Option>, pub exec_spawn_id: Option, - pub tags: Option, + pub tags: Option, pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, @@ -114,6 +190,7 @@ impl Default for OrderInitialized { trailing_offset: Default::default(), trailing_offset_type: Default::default(), emulation_trigger: Default::default(), + trigger_instrument_id: Default::default(), contingency_type: Default::default(), order_list_id: Default::default(), linked_order_ids: Default::default(), @@ -131,7 +208,7 @@ impl Default for OrderInitialized { } #[repr(C)] -#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] #[builder(default)] #[serde(tag = "type")] pub struct OrderDenied { @@ -139,7 +216,36 @@ pub struct OrderDenied { pub strategy_id: StrategyId, pub instrument_id: InstrumentId, pub client_order_id: ClientOrderId, - pub reason: Box, + pub reason: Ustr, + pub event_id: UUID4, + pub ts_event: UnixNanos, + pub ts_init: UnixNanos, +} + +#[repr(C)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[builder(default)] +#[serde(tag = "type")] +pub struct OrderEmulated { + pub trader_id: TraderId, + pub strategy_id: StrategyId, + pub instrument_id: InstrumentId, + pub client_order_id: ClientOrderId, + pub event_id: UUID4, + pub ts_event: UnixNanos, + pub ts_init: UnixNanos, +} + +#[repr(C)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize, Builder)] +#[builder(default)] +#[serde(tag = "type")] +pub struct OrderReleased { + pub trader_id: TraderId, + pub strategy_id: StrategyId, + pub instrument_id: InstrumentId, + pub client_order_id: ClientOrderId, + pub released_price: Price, pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, @@ -174,7 +280,7 @@ pub struct OrderAccepted { pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, - pub reconciliation: bool, + pub reconciliation: u8, } #[repr(C)] @@ -186,13 +292,12 @@ pub struct OrderRejected { pub strategy_id: StrategyId, pub instrument_id: InstrumentId, pub client_order_id: ClientOrderId, - pub venue_order_id: VenueOrderId, pub account_id: AccountId, - pub reason: String, + pub reason: Ustr, pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, - pub reconciliation: bool, + pub reconciliation: u8, } #[repr(C)] @@ -291,7 +396,7 @@ pub struct OrderModifyRejected { pub client_order_id: ClientOrderId, pub venue_order_id: Option, pub account_id: Option, - pub reason: Box, + pub reason: Ustr, pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, @@ -309,7 +414,7 @@ pub struct OrderCancelRejected { pub client_order_id: ClientOrderId, pub venue_order_id: Option, pub account_id: Option, - pub reason: Box, + pub reason: Ustr, pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, @@ -338,6 +443,7 @@ pub struct OrderUpdated { #[repr(C)] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Builder)] +#[builder(default)] #[serde(tag = "type")] pub struct OrderFilled { pub trader_id: TraderId, @@ -353,10 +459,36 @@ pub struct OrderFilled { pub last_qty: Quantity, pub last_px: Price, pub currency: Currency, - pub commission: Money, + pub commission: Option, pub liquidity_side: LiquiditySide, pub event_id: UUID4, pub ts_event: UnixNanos, pub ts_init: UnixNanos, pub reconciliation: bool, } + +impl Default for OrderFilled { + fn default() -> Self { + Self { + trader_id: TraderId::default(), + strategy_id: StrategyId::default(), + instrument_id: InstrumentId::default(), + client_order_id: ClientOrderId::default(), + venue_order_id: VenueOrderId::default(), + account_id: AccountId::default(), + trade_id: TradeId::default(), + position_id: None, + order_side: OrderSide::Buy, + order_type: OrderType::Market, + last_qty: Quantity::new(100_000.0, 0), + last_px: Price::new(1.0, 5), + currency: *USD, + commission: None, + liquidity_side: LiquiditySide::Taker, + event_id: Default::default(), + ts_event: Default::default(), + ts_init: Default::default(), + reconciliation: Default::default(), + } + } +} diff --git a/nautilus_core/model/src/events/order_api.rs b/nautilus_core/model/src/events/order_api.rs index 25c40663e316..67f07153c451 100644 --- a/nautilus_core/model/src/events/order_api.rs +++ b/nautilus_core/model/src/events/order_api.rs @@ -15,77 +15,151 @@ use std::ffi::c_char; -use nautilus_core::{ - string::{cstr_to_string, str_to_cstr}, - time::UnixNanos, - uuid::UUID4, -}; +use nautilus_core::{string::cstr_to_ustr, time::UnixNanos, uuid::UUID4}; -// use crate::types::price::Price; -// use crate::types::quantity::Quantity; -use super::order::OrderDenied; -// use crate::enums::{OrderSide, OrderType, TimeInForce, TriggerType}; -use crate::identifiers::client_order_id::ClientOrderId; -use crate::identifiers::{ - instrument_id::InstrumentId, strategy_id::StrategyId, trader_id::TraderId, +use super::order::{ + OrderAccepted, OrderDenied, OrderEmulated, OrderRejected, OrderReleased, OrderSubmitted, +}; +use crate::{ + identifiers::{ + account_id::AccountId, client_order_id::ClientOrderId, instrument_id::InstrumentId, + strategy_id::StrategyId, trader_id::TraderId, venue_order_id::VenueOrderId, + }, + types::price::Price, }; +/// # Safety +/// +/// - Assumes valid C string pointers. // #[no_mangle] +// #[allow(improper_ctypes_definitions)] // pub unsafe extern "C" fn order_initialized_new( -// trader_id: &TraderId, -// strategy_id: &StrategyId, -// instrument_id: &InstrumentId, -// client_order_id: &ClientOrderId, +// trader_id: TraderId, +// strategy_id: StrategyId, +// instrument_id: InstrumentId, +// client_order_id: ClientOrderId, // order_side: OrderSide, // order_type: OrderType, // quantity: Quantity, // price: *const Price, // trigger_price: *const Price, // trigger_type: TriggerType, +// limit_offset: *const Price, +// trailing_offset: *const Price, +// trailing_offset_type: TrailingOffsetType, // time_in_force: TimeInForce, // expire_time: *const UnixNanos, // post_only: u8, // reduce_only: u8, // quote_quantity: u8, // display_qty: *const Quantity, -// limit_offset: *const Price, -// trailing_offset: *const Price, -// trailing_offset_type: *const TriggerType, +// emulation_trigger: TriggerType, +// trigger_instrument_id: *const InstrumentId, +// contingency_type: ContingencyType, +// order_list_id: *const OrderListId, +// linked_order_ids: *const c_char, +// parent_order_id: *const ClientOrderId, +// exec_algorithm_id: *const ExecAlgorithmId, +// exec_algorithm_params: *const c_char, +// exec_spawn_id: *const ClientOrderId, +// tags: *const c_char, // event_id: UUID4, // ts_event: UnixNanos, // ts_init: UnixNanos, +// reconciliation: u8, // ) -> OrderInitialized { // OrderInitialized { -// trader_id: trader_id.clone(), -// strategy_id: strategy_id.clone(), -// instrument_id: instrument_id.clone(), -// client_order_id: client_order_id.clone(), +// trader_id, +// strategy_id, +// instrument_id, +// client_order_id, // order_side, // order_type, // quantity, -// price: if price.is_null() { +// price: if price.is_null() { None } else { Some(*price) }, +// trigger_price: if trigger_price.is_null() { // None // } else { -// Some(*price.clone()) +// Some(*trigger_price) // }, -// trigger_price: if trigger_price.is_null() { +// trigger_type: if trigger_type == TriggerType::NoTrigger { // None // } else { -// Some(*trigger_price.clone()) +// Some(trigger_type) +// }, +// limit_offset: if limit_offset.is_null() { +// None +// } else { +// Some(*limit_offset) +// }, +// trailing_offset: if trailing_offset.is_null() { +// None +// } else { +// Some(*trailing_offset) +// }, +// trailing_offset_type: if trailing_offset_type == TrailingOffsetType::NoTrailingOffset { +// None +// } else { +// Some(trailing_offset_type) // }, -// trigger_type, // time_in_force, // expire_time: if expire_time.is_null() { // None // } else { -// Some(*expire_time.clone()) +// Some(*expire_time) // }, -// post_only: post_only != 0, -// reduce_only: reduce_only != 0, -// quote_quantity: quote_quantity != 0, +// post_only, +// reduce_only, +// quote_quantity, +// display_qty: if display_qty.is_null() { +// None +// } else { +// Some(*display_qty) +// }, +// emulation_trigger: if emulation_trigger == TriggerType::NoTrigger { +// None +// } else { +// Some(emulation_trigger) +// }, +// trigger_instrument_id: if trigger_instrument_id.is_null() { +// None +// } else { +// Some(*trigger_instrument_id) +// }, +// contingency_type: if contingency_type == ContingencyType::NoContingency { +// None +// } else { +// Some(contingency_type) +// }, +// order_list_id: if order_list_id.is_null() { +// None +// } else { +// Some(*order_list_id) +// }, +// linked_order_ids: optional_ustr_to_vec_client_order_ids(optional_cstr_to_ustr( +// linked_order_ids, +// )), +// parent_order_id: if parent_order_id.is_null() { +// None +// } else { +// Some(*parent_order_id) +// }, +// exec_algorithm_id: if exec_algorithm_id.is_null() { +// None +// } else { +// Some(*exec_algorithm_id) +// }, +// exec_algorithm_params: optional_bytes_to_str_map(exec_algorithm_params), +// exec_spawn_id: if exec_spawn_id.is_null() { +// None +// } else { +// Some(*exec_spawn_id) +// }, +// tags: optional_cstr_to_ustr(tags).into(), // event_id, // ts_event, // ts_init, +// reconciliation, // } // } @@ -108,25 +182,160 @@ pub unsafe extern "C" fn order_denied_new( strategy_id, instrument_id, client_order_id, - reason: Box::new(cstr_to_string(reason_ptr)), + reason: cstr_to_ustr(reason_ptr), + event_id, + ts_event, + ts_init, + } +} + +#[no_mangle] +pub extern "C" fn order_emulated_new( + trader_id: TraderId, + strategy_id: StrategyId, + instrument_id: InstrumentId, + client_order_id: ClientOrderId, + event_id: UUID4, + ts_event: UnixNanos, + ts_init: UnixNanos, +) -> OrderEmulated { + OrderEmulated { + trader_id, + strategy_id, + instrument_id, + client_order_id, + event_id, + ts_event, + ts_init, + } +} + +#[no_mangle] +pub extern "C" fn order_released_new( + trader_id: TraderId, + strategy_id: StrategyId, + instrument_id: InstrumentId, + client_order_id: ClientOrderId, + released_price: Price, + event_id: UUID4, + ts_event: UnixNanos, + ts_init: UnixNanos, +) -> OrderReleased { + OrderReleased { + trader_id, + strategy_id, + instrument_id, + client_order_id, + released_price, event_id, ts_event, ts_init, } } -/// Frees the memory for the given `event` by dropping. #[no_mangle] -pub extern "C" fn order_denied_drop(event: OrderDenied) { - drop(event); // Memory freed here +pub extern "C" fn order_submitted_new( + trader_id: TraderId, + strategy_id: StrategyId, + instrument_id: InstrumentId, + client_order_id: ClientOrderId, + account_id: AccountId, + event_id: UUID4, + ts_event: UnixNanos, + ts_init: UnixNanos, +) -> OrderSubmitted { + OrderSubmitted { + trader_id, + strategy_id, + instrument_id, + client_order_id, + account_id, + event_id, + ts_event, + ts_init, + } } #[no_mangle] -pub extern "C" fn order_denied_clone(event: &OrderDenied) -> OrderDenied { - event.clone() +pub extern "C" fn order_accepted_new( + trader_id: TraderId, + strategy_id: StrategyId, + instrument_id: InstrumentId, + client_order_id: ClientOrderId, + venue_order_id: VenueOrderId, + account_id: AccountId, + event_id: UUID4, + ts_event: UnixNanos, + ts_init: UnixNanos, + reconciliation: u8, +) -> OrderAccepted { + OrderAccepted { + trader_id, + strategy_id, + instrument_id, + client_order_id, + venue_order_id, + account_id, + event_id, + ts_event, + ts_init, + reconciliation, + } } +/// # Safety +/// +/// - Assumes `reason_ptr` is a valid C string pointer. #[no_mangle] -pub extern "C" fn order_denied_reason_to_cstr(event: &OrderDenied) -> *const c_char { - str_to_cstr(&event.reason) +pub unsafe extern "C" fn order_rejected_new( + trader_id: TraderId, + strategy_id: StrategyId, + instrument_id: InstrumentId, + client_order_id: ClientOrderId, + account_id: AccountId, + reason_ptr: *const c_char, + event_id: UUID4, + ts_event: UnixNanos, + ts_init: UnixNanos, + reconciliation: u8, +) -> OrderRejected { + OrderRejected { + trader_id, + strategy_id, + instrument_id, + client_order_id, + account_id, + reason: cstr_to_ustr(reason_ptr), + event_id, + ts_event, + ts_init, + reconciliation, + } } + +// #[no_mangle] +// pub unsafe extern "C" fn order_canceled_new( +// trader_id: TraderId, +// strategy_id: StrategyId, +// instrument_id: InstrumentId, +// client_order_id: ClientOrderId, +// venue_order_id: VenueOrderId, +// account_id: AccountId, +// reconciliation: u8, +// event_id: UUID4, +// ts_event: UnixNanos, +// ts_init: UnixNanos, +// ) -> OrderCanceled { +// OrderCanceled { +// trader_id, +// strategy_id, +// instrument_id, +// client_order_id, +// venue_order_id, +// account_id, +// reconciliation, +// event_id, +// ts_event, +// ts_init, +// } +// } diff --git a/nautilus_core/model/src/identifiers/client_order_id.rs b/nautilus_core/model/src/identifiers/client_order_id.rs index 3d5ad986f455..b4e66ecfd320 100644 --- a/nautilus_core/model/src/identifiers/client_order_id.rs +++ b/nautilus_core/model/src/identifiers/client_order_id.rs @@ -61,6 +61,27 @@ impl Display for ClientOrderId { } } +pub fn optional_ustr_to_vec_client_order_ids(s: Option) -> Option> { + s.map(|ustr| { + let s_str = ustr.to_string(); + s_str + .split(',') + .map(ClientOrderId::new) + .collect::>() + }) +} + +pub fn optional_vec_client_order_ids_to_ustr(vec: Option>) -> Option { + vec.map(|client_order_ids| { + let s: String = client_order_ids + .into_iter() + .map(|id| id.value.to_string()) + .collect::>() + .join(","); + Ustr::from(&s) + }) +} + //////////////////////////////////////////////////////////////////////////////// // C API //////////////////////////////////////////////////////////////////////////////// @@ -85,7 +106,12 @@ pub extern "C" fn client_order_id_hash(id: &ClientOrderId) -> u64 { //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { + use ustr::Ustr; + use super::ClientOrderId; + use crate::identifiers::client_order_id::{ + optional_ustr_to_vec_client_order_ids, optional_vec_client_order_ids_to_ustr, + }; #[test] fn test_string_reprs() { @@ -93,4 +119,32 @@ mod tests { assert_eq!(id.to_string(), "O-20200814-102234-001-001-1"); assert_eq!(format!("{id}"), "O-20200814-102234-001-001-1"); } + + #[test] + fn test_optional_ustr_to_vec_client_order_ids() { + // Test with None + assert_eq!(optional_ustr_to_vec_client_order_ids(None), None); + + // Test with Some + let ustr = Ustr::from("id1,id2,id3"); + let client_order_ids = optional_ustr_to_vec_client_order_ids(Some(ustr)).unwrap(); + assert_eq!(client_order_ids[0].value.to_string(), "id1"); + assert_eq!(client_order_ids[1].value.to_string(), "id2"); + assert_eq!(client_order_ids[2].value.to_string(), "id3"); + } + + #[test] + fn test_optional_vec_client_order_ids_to_ustr() { + // Test with None + assert_eq!(optional_vec_client_order_ids_to_ustr(None), None); + + // Test with Some + let client_order_ids = vec![ + ClientOrderId::new("id1"), + ClientOrderId::new("id2"), + ClientOrderId::new("id3"), + ]; + let ustr = optional_vec_client_order_ids_to_ustr(Some(client_order_ids.into())).unwrap(); + assert_eq!(ustr.to_string(), "id1,id2,id3"); + } } diff --git a/nautilus_core/model/src/identifiers/instrument_id.rs b/nautilus_core/model/src/identifiers/instrument_id.rs index 0b7a3fd6df54..3984ab67808e 100644 --- a/nautilus_core/model/src/identifiers/instrument_id.rs +++ b/nautilus_core/model/src/identifiers/instrument_id.rs @@ -101,6 +101,14 @@ impl<'de> Deserialize<'de> for InstrumentId { } } +#[pymethods] +impl InstrumentId { + #[getter] + fn value(&self) -> String { + self.to_string() + } +} + //////////////////////////////////////////////////////////////////////////////// // C API //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/identifiers/position_id.rs b/nautilus_core/model/src/identifiers/position_id.rs index 607e53187055..292e8e24b696 100644 --- a/nautilus_core/model/src/identifiers/position_id.rs +++ b/nautilus_core/model/src/identifiers/position_id.rs @@ -41,6 +41,14 @@ impl PositionId { } } +impl Default for PositionId { + fn default() -> Self { + Self { + value: Ustr::from("P-001"), + } + } +} + impl Debug for PositionId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.value) diff --git a/nautilus_core/model/src/identifiers/trade_id.rs b/nautilus_core/model/src/identifiers/trade_id.rs index 3ca32e779601..f913f2b00c06 100644 --- a/nautilus_core/model/src/identifiers/trade_id.rs +++ b/nautilus_core/model/src/identifiers/trade_id.rs @@ -41,6 +41,14 @@ impl TradeId { } } +impl Default for TradeId { + fn default() -> Self { + Self { + value: Ustr::from("1"), + } + } +} + impl Debug for TradeId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.value) @@ -53,6 +61,14 @@ impl Display for TradeId { } } +#[pymethods] +impl TradeId { + #[getter] + fn value(&self) -> String { + self.value.to_string() + } +} + //////////////////////////////////////////////////////////////////////////////// // C API //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/model/src/lib.rs b/nautilus_core/model/src/lib.rs index fffae2091799..3608cf03efea 100644 --- a/nautilus_core/model/src/lib.rs +++ b/nautilus_core/model/src/lib.rs @@ -29,6 +29,7 @@ pub mod macros; pub mod orderbook; pub mod orders; pub mod position; +pub mod python; pub mod types; /// Loaded as nautilus_pyo3.model diff --git a/nautilus_core/model/src/macros.rs b/nautilus_core/model/src/macros.rs index b81baf514c7f..8d5c31f57ead 100644 --- a/nautilus_core/model/src/macros.rs +++ b/nautilus_core/model/src/macros.rs @@ -14,7 +14,7 @@ // ------------------------------------------------------------------------------------------------- #[macro_export] -macro_rules! strum_serde { +macro_rules! enum_strum_serde { ($type:ty) => { impl Serialize for $type { fn serialize(&self, serializer: S) -> Result @@ -36,3 +36,57 @@ macro_rules! strum_serde { } }; } + +#[macro_export] +macro_rules! enum_for_python { + ($type:ty) => { + #[pymethods] + impl $type { + #[new] + fn py_new(py: Python<'_>, value: &PyAny) -> PyResult { + let t = Self::type_object(py); + Self::py_from_str(t, value) + } + + fn __hash__(&self) -> isize { + *self as isize + } + + fn __str__(&self) -> String { + self.to_string() + } + + fn __repr__(&self) -> String { + format!( + "<{}.{}: '{}'>", + stringify!($type), + self.name(), + self.value(), + ) + } + + #[getter] + pub fn name(&self) -> String { + self.to_string() + } + + #[getter] + pub fn value(&self) -> u8 { + *self as u8 + } + + #[classmethod] + fn variants(_: &PyType, py: Python<'_>) -> EnumIterator { + EnumIterator::new::(py) + } + + #[classmethod] + #[pyo3(name = "from_str")] + fn py_from_str(_: &PyType, data: &PyAny) -> PyResult { + let data_str: &str = data.str().and_then(|s| s.extract())?; + let tokenized = data_str.to_uppercase(); + Self::from_str(&tokenized).map_err(|e| PyValueError::new_err(format!("{e:?}"))) + } + } + }; +} diff --git a/nautilus_core/model/src/orderbook/level_api.rs b/nautilus_core/model/src/orderbook/level_api.rs index da1233d145a0..cbf9fdb00d01 100644 --- a/nautilus_core/model/src/orderbook/level_api.rs +++ b/nautilus_core/model/src/orderbook/level_api.rs @@ -98,7 +98,7 @@ pub extern "C" fn level_exposure(level: &Level_API) -> f64 { #[no_mangle] pub extern "C" fn vec_levels_drop(v: CVec) { let CVec { ptr, len, cap } = v; - let data: Vec = unsafe { Vec::from_raw_parts(ptr as *mut Level, len, cap) }; + let data: Vec = unsafe { Vec::from_raw_parts(ptr as *mut Level_API, len, cap) }; drop(data); // Memory freed here } diff --git a/nautilus_core/model/src/orders/base.rs b/nautilus_core/model/src/orders/base.rs index 37a8ed907397..865aa4987e45 100644 --- a/nautilus_core/model/src/orders/base.rs +++ b/nautilus_core/model/src/orders/base.rs @@ -16,55 +16,84 @@ use std::collections::HashMap; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; use thiserror; +use ustr::Ustr; use crate::{ enums::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide, - TimeInForce, TriggerType, + TimeInForce, TrailingOffsetType, TriggerType, }, events::order::{ - OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEvent, OrderExpired, - OrderFilled, OrderModifyRejected, OrderPendingCancel, OrderPendingUpdate, OrderRejected, - OrderSubmitted, OrderTriggered, OrderUpdated, + OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated, OrderEvent, + OrderExpired, OrderFilled, OrderInitialized, OrderModifyRejected, OrderPendingCancel, + OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted, OrderTriggered, + OrderUpdated, }, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, - strategy_id::StrategyId, trade_id::TradeId, trader_id::TraderId, - venue_order_id::VenueOrderId, + strategy_id::StrategyId, symbol::Symbol, trade_id::TradeId, trader_id::TraderId, + venue::Venue, venue_order_id::VenueOrderId, }, - types::{price::Price, quantity::Quantity}, + types::{currency::Currency, money::Money, price::Price, quantity::Quantity}, }; #[derive(thiserror::Error, Debug)] pub enum OrderError { #[error("Invalid state transition")] InvalidStateTransition, + #[error("Invalid event for order type")] + InvalidOrderEvent, #[error("Unrecognized event")] UnrecognizedEvent, + #[error("No previous state")] + NoPreviousState, } +const VALID_STOP_ORDER_TYPES: &[OrderType] = &[ + OrderType::StopMarket, + OrderType::StopLimit, + OrderType::MarketIfTouched, + OrderType::LimitIfTouched, +]; + +const VALID_LIMIT_ORDER_TYPES: &[OrderType] = &[ + OrderType::Limit, + OrderType::StopLimit, + OrderType::LimitIfTouched, + OrderType::MarketIfTouched, +]; + impl OrderStatus { #[rustfmt::skip] pub fn transition(&mut self, event: &OrderEvent) -> Result { let new_state = match (self, event) { (OrderStatus::Initialized, OrderEvent::OrderDenied(_)) => OrderStatus::Denied, + (OrderStatus::Initialized, OrderEvent::OrderEmulated(_)) => OrderStatus::Emulated, // Emulated orders + (OrderStatus::Initialized, OrderEvent::OrderReleased(_)) => OrderStatus::Released, // Emulated orders (OrderStatus::Initialized, OrderEvent::OrderSubmitted(_)) => OrderStatus::Submitted, - (OrderStatus::Initialized, OrderEvent::OrderRejected(_)) => OrderStatus::Rejected, // Covers external orders - (OrderStatus::Initialized, OrderEvent::OrderAccepted(_)) => OrderStatus::Accepted, // Covers external orders - (OrderStatus::Initialized, OrderEvent::OrderCanceled(_)) => OrderStatus::Canceled, // Covers emulated and external orders - (OrderStatus::Initialized, OrderEvent::OrderExpired(_)) => OrderStatus::Expired, // Covers emulated and external orders - (OrderStatus::Initialized, OrderEvent::OrderTriggered(_)) => OrderStatus::Triggered, // Covers emulated and external orders + (OrderStatus::Initialized, OrderEvent::OrderRejected(_)) => OrderStatus::Rejected, // External orders + (OrderStatus::Initialized, OrderEvent::OrderAccepted(_)) => OrderStatus::Accepted, // External orders + (OrderStatus::Initialized, OrderEvent::OrderCanceled(_)) => OrderStatus::Canceled, // External orders + (OrderStatus::Initialized, OrderEvent::OrderExpired(_)) => OrderStatus::Expired, // External orders + (OrderStatus::Initialized, OrderEvent::OrderTriggered(_)) => OrderStatus::Triggered, // External orders + (OrderStatus::Emulated, OrderEvent::OrderCanceled(_)) => OrderStatus::Canceled, // Emulated orders + (OrderStatus::Emulated, OrderEvent::OrderExpired(_)) => OrderStatus::Expired, // Emulated orders + (OrderStatus::Emulated, OrderEvent::OrderReleased(_)) => OrderStatus::Released, // Emulated orders + (OrderStatus::Released, OrderEvent::OrderSubmitted(_)) => OrderStatus::Submitted, // Emulated orders + (OrderStatus::Released, OrderEvent::OrderDenied(_)) => OrderStatus::Denied, // Emulated orders + (OrderStatus::Released, OrderEvent::OrderCanceled(_)) => OrderStatus::Canceled, // Execution algo (OrderStatus::Submitted, OrderEvent::OrderPendingUpdate(_)) => OrderStatus::PendingUpdate, (OrderStatus::Submitted, OrderEvent::OrderPendingCancel(_)) => OrderStatus::PendingCancel, (OrderStatus::Submitted, OrderEvent::OrderRejected(_)) => OrderStatus::Rejected, - (OrderStatus::Submitted, OrderEvent::OrderCanceled(_)) => OrderStatus::Canceled, // Covers FOK and IOC cases + (OrderStatus::Submitted, OrderEvent::OrderCanceled(_)) => OrderStatus::Canceled, // FOK and IOC cases (OrderStatus::Submitted, OrderEvent::OrderAccepted(_)) => OrderStatus::Accepted, - (OrderStatus::Submitted, OrderEvent::OrderTriggered(_)) => OrderStatus::Triggered, // Covers emulated StopLimit order (OrderStatus::Submitted, OrderEvent::OrderPartiallyFilled(_)) => OrderStatus::PartiallyFilled, (OrderStatus::Submitted, OrderEvent::OrderFilled(_)) => OrderStatus::Filled, - (OrderStatus::Accepted, OrderEvent::OrderRejected(_)) => OrderStatus::Rejected, // Covers StopLimit order + (OrderStatus::Accepted, OrderEvent::OrderRejected(_)) => OrderStatus::Rejected, // StopLimit order (OrderStatus::Accepted, OrderEvent::OrderPendingUpdate(_)) => OrderStatus::PendingUpdate, (OrderStatus::Accepted, OrderEvent::OrderPendingCancel(_)) => OrderStatus::PendingCancel, (OrderStatus::Accepted, OrderEvent::OrderCanceled(_)) => OrderStatus::Canceled, @@ -114,6 +143,8 @@ pub trait Order { fn trader_id(&self) -> TraderId; fn strategy_id(&self) -> StrategyId; fn instrument_id(&self) -> InstrumentId; + fn symbol(&self) -> Symbol; + fn venue(&self) -> Venue; fn client_order_id(&self) -> ClientOrderId; fn venue_order_id(&self) -> Option; fn position_id(&self) -> Option; @@ -123,6 +154,7 @@ pub trait Order { fn order_type(&self) -> OrderType; fn quantity(&self) -> Quantity; fn time_in_force(&self) -> TimeInForce; + fn expire_time(&self) -> Option; fn price(&self) -> Option; fn trigger_price(&self) -> Option; fn trigger_type(&self) -> Option; @@ -130,15 +162,20 @@ pub trait Order { fn is_post_only(&self) -> bool; fn is_reduce_only(&self) -> bool; fn is_quote_quantity(&self) -> bool; + fn display_qty(&self) -> Option; + fn limit_offset(&self) -> Option; + fn trailing_offset(&self) -> Option; + fn trailing_offset_type(&self) -> Option; fn emulation_trigger(&self) -> Option; + fn trigger_instrument_id(&self) -> Option; fn contingency_type(&self) -> Option; fn order_list_id(&self) -> Option; fn linked_order_ids(&self) -> Option>; fn parent_order_id(&self) -> Option; fn exec_algorithm_id(&self) -> Option; - fn exec_algorithm_params(&self) -> Option>; + fn exec_algorithm_params(&self) -> Option>; fn exec_spawn_id(&self) -> Option; - fn tags(&self) -> Option; + fn tags(&self) -> Option; fn filled_qty(&self) -> Quantity; fn leaves_qty(&self) -> Quantity; fn avg_px(&self) -> Option; @@ -147,8 +184,10 @@ pub trait Order { fn ts_init(&self) -> UnixNanos; fn ts_last(&self) -> UnixNanos; - fn events(&self) -> Vec<&OrderEvent>; + fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError>; + fn update(&mut self, event: &OrderUpdated); + fn events(&self) -> Vec<&OrderEvent>; fn last_event(&self) -> &OrderEvent { // Safety: `Order` specification guarantees at least one event (`OrderInitialized`) self.events().last().unwrap() @@ -179,7 +218,26 @@ pub trait Order { } fn is_emulated(&self) -> bool { - self.emulation_trigger().is_some() + self.status() == OrderStatus::Emulated + } + + fn is_active_local(&self) -> bool { + matches!( + self.status(), + OrderStatus::Initialized | OrderStatus::Emulated | OrderStatus::Released + ) + } + + fn is_primary(&self) -> bool { + // TODO: Guarantee `exec_spawn_id` is some if `exec_algorithm_id` is some + self.exec_algorithm_id().is_some() + && self.client_order_id() == self.exec_spawn_id().unwrap() + } + + fn is_secondary(&self) -> bool { + // TODO: Guarantee `exec_spawn_id` is some if `exec_algorithm_id` is some + self.exec_algorithm_id().is_some() + && self.client_order_id() != self.exec_spawn_id().unwrap() } fn is_contingency(&self) -> bool { @@ -209,6 +267,10 @@ pub trait Order { ) } + fn is_canceled(&self) -> bool { + self.status() == OrderStatus::Canceled + } + fn is_closed(&self) -> bool { matches!( self.status(), @@ -237,13 +299,56 @@ pub trait Order { } } +impl From<&T> for OrderInitialized +where + T: Order, +{ + fn from(order: &T) -> Self { + Self { + trader_id: order.trader_id(), + strategy_id: order.strategy_id(), + instrument_id: order.instrument_id(), + client_order_id: order.client_order_id(), + order_side: order.side(), + order_type: order.order_type(), + quantity: order.quantity(), + price: order.price(), + trigger_price: order.trigger_price(), + trigger_type: order.trigger_type(), + time_in_force: order.time_in_force(), + expire_time: order.expire_time(), + post_only: order.is_post_only(), + reduce_only: order.is_reduce_only(), + quote_quantity: order.is_quote_quantity(), + display_qty: order.display_qty(), + limit_offset: order.limit_offset(), + trailing_offset: order.trailing_offset(), + trailing_offset_type: order.trailing_offset_type(), + emulation_trigger: order.emulation_trigger(), + trigger_instrument_id: order.trigger_instrument_id(), + contingency_type: order.contingency_type(), + order_list_id: order.order_list_id(), + linked_order_ids: order.linked_order_ids(), + parent_order_id: order.parent_order_id(), + exec_algorithm_id: order.exec_algorithm_id(), + exec_algorithm_params: order.exec_algorithm_params(), + exec_spawn_id: order.exec_spawn_id(), + tags: order.tags(), + event_id: order.init_id(), + ts_event: order.ts_init(), + ts_init: order.ts_init(), + reconciliation: false, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct OrderCore { pub events: Vec, + pub commissions: HashMap, pub venue_order_ids: Vec, pub trade_ids: Vec, pub previous_status: Option, - pub has_price: bool, - pub has_trigger_price: bool, pub status: OrderStatus, pub trader_id: TraderId, pub strategy_id: StrategyId, @@ -258,7 +363,6 @@ pub struct OrderCore { pub quantity: Quantity, pub time_in_force: TimeInForce, pub liquidity_side: Option, - pub is_post_only: bool, pub is_reduce_only: bool, pub is_quote_quantity: bool, pub emulation_trigger: Option, @@ -267,9 +371,9 @@ pub struct OrderCore { pub linked_order_ids: Option>, pub parent_order_id: Option, pub exec_algorithm_id: Option, - pub exec_algorithm_params: Option>, + pub exec_algorithm_params: Option>, pub exec_spawn_id: Option, - pub tags: Option, + pub tags: Option, pub filled_qty: Quantity, pub leaves_qty: Quantity, pub avg_px: Option, @@ -291,7 +395,6 @@ impl OrderCore { order_type: OrderType, quantity: Quantity, time_in_force: TimeInForce, - post_only: bool, reduce_only: bool, quote_quantity: bool, emulation_trigger: Option, @@ -300,19 +403,18 @@ impl OrderCore { linked_order_ids: Option>, parent_order_id: Option, exec_algorithm_id: Option, - exec_algorithm_params: Option>, + exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option, init_id: UUID4, ts_init: UnixNanos, ) -> Self { Self { events: Vec::new(), + commissions: HashMap::new(), venue_order_ids: Vec::new(), trade_ids: Vec::new(), previous_status: None, - has_price: true, // TODO - has_trigger_price: false, // TODO status: OrderStatus::Initialized, trader_id, strategy_id, @@ -327,7 +429,6 @@ impl OrderCore { quantity, time_in_force, liquidity_side: None, - is_post_only: post_only, is_reduce_only: reduce_only, is_quote_quantity: quote_quantity, emulation_trigger, @@ -349,13 +450,18 @@ impl OrderCore { } } - fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { + pub fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { + assert_eq!(self.client_order_id, event.client_order_id()); + assert_eq!(self.strategy_id, event.strategy_id()); + let new_status = self.status.transition(&event)?; self.previous_status = Some(self.status); self.status = new_status; match &event { OrderEvent::OrderDenied(event) => self.denied(event), + OrderEvent::OrderEmulated(event) => self.emulated(event), + OrderEvent::OrderReleased(event) => self.released(event), OrderEvent::OrderSubmitted(event) => self.submitted(event), OrderEvent::OrderRejected(event) => self.rejected(event), OrderEvent::OrderAccepted(event) => self.accepted(event), @@ -367,9 +473,11 @@ impl OrderCore { OrderEvent::OrderTriggered(event) => self.triggered(event), OrderEvent::OrderCanceled(event) => self.canceled(event), OrderEvent::OrderExpired(event) => self.expired(event), + OrderEvent::OrderFilled(event) => self.filled(event), _ => return Err(OrderError::UnrecognizedEvent), } + self.ts_last = event.ts_event(); self.events.push(event); Ok(()) } @@ -378,6 +486,14 @@ impl OrderCore { // Do nothing else } + fn emulated(&self, _event: &OrderEmulated) { + // Do nothing else + } + + fn released(&mut self, _event: &OrderReleased) { + self.emulation_trigger = None; + } + fn submitted(&mut self, event: &OrderSubmitted) { self.account_id = Some(event.account_id) } @@ -399,11 +515,15 @@ impl OrderCore { } fn modify_rejected(&mut self, _event: &OrderModifyRejected) { - self.status = self.previous_status.unwrap(); + self.status = self + .previous_status + .unwrap_or_else(|| panic!("{}", OrderError::NoPreviousState)); } fn cancel_rejected(&mut self, _event: &OrderCancelRejected) { - self.status = self.previous_status.unwrap(); + self.status = self + .previous_status + .unwrap_or_else(|| panic!("{}", OrderError::NoPreviousState)); } fn triggered(&mut self, _event: &OrderTriggered) {} @@ -413,37 +533,14 @@ impl OrderCore { fn expired(&mut self, _event: &OrderExpired) {} fn updated(&mut self, event: &OrderUpdated) { - match &event.venue_order_id { - Some(venue_order_id) => { - if self.venue_order_id.is_some() - && venue_order_id != self.venue_order_id.as_ref().unwrap() - { - self.venue_order_id = Some(*venue_order_id); - self.venue_order_ids.push(*venue_order_id); - } + if let Some(venue_order_id) = &event.venue_order_id { + if self.venue_order_id.is_none() + || venue_order_id != self.venue_order_id.as_ref().unwrap() + { + self.venue_order_id = Some(*venue_order_id); + self.venue_order_ids.push(*venue_order_id); } - None => {} } - - // TODO - // if let Some(price) = &event.price { - // if self.price.is_some() { - // self.price.replace(*price); - // } else { - // panic!("invalid update of `price` when None") - // } - // } - // - // if let Some(trigger_price) = &event.trigger_price { - // if self.trigger_price.is_some() { - // self.trigger_price.replace(*trigger_price); - // } else { - // panic!("invalid update of `trigger_price` when None") - // } - // } - - self.quantity = event.quantity; - self.leaves_qty = self.quantity - self.filled_qty; } fn filled(&mut self, event: &OrderFilled) { @@ -452,14 +549,13 @@ impl OrderCore { self.trade_ids.push(event.trade_id); self.last_trade_id = Some(event.trade_id); self.liquidity_side = Some(event.liquidity_side); - self.filled_qty += &event.last_qty; - self.leaves_qty -= &event.last_qty; + self.filled_qty += event.last_qty; + self.leaves_qty -= event.last_qty; self.ts_last = event.ts_event; - self.set_avg_px(&event.last_qty, &event.last_px); - // self.set_slippage(); // TODO + self.set_avg_px(event.last_qty, event.last_px); } - fn set_avg_px(&mut self, last_qty: &Quantity, last_px: &Price) { + fn set_avg_px(&mut self, last_qty: Quantity, last_px: Price) { if self.avg_px.is_none() { self.avg_px = Some(last_px.as_f64()); } @@ -475,21 +571,16 @@ impl OrderCore { self.avg_px = Some(avg_px); } - // TODO - // fn set_slippage(&mut self) { - // if self.has_price { - // self.slippage = self.avg_px.and_then(|avg_px| { - // self.price - // .as_ref() - // .map(|price| fixed_i64_to_f64(price.raw)) - // .and_then(|price| match self.side() { - // OrderSide::Buy if avg_px > price => Some(avg_px - price), - // OrderSide::Sell if avg_px < price => Some(price - avg_px), - // _ => None, - // }) - // }) - // } - // } + pub fn set_slippage(&mut self, price: Price) { + self.slippage = self.avg_px.and_then(|avg_px| { + let current_price = price.as_f64(); + match self.side { + OrderSide::Buy if avg_px > current_price => Some(avg_px - current_price), + OrderSide::Sell if avg_px < current_price => Some(current_price - avg_px), + _ => None, + } + }) + } fn opposite_side(&self, side: OrderSide) -> OrderSide { match side { @@ -508,6 +599,14 @@ impl OrderCore { } } + fn signed_decimal_qty(&self) -> Decimal { + match self.side { + OrderSide::Buy => self.quantity.as_decimal(), + OrderSide::Sell => -self.quantity.as_decimal(), + _ => panic!("invalid order side"), + } + } + fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool { if side == PositionSide::Flat { return false; @@ -521,6 +620,14 @@ impl OrderCore { _ => true, } } + + fn commission(&self, currency: &Currency) -> Option { + self.commissions.get(currency).copied() + } + + fn commissions(&self) -> HashMap { + self.commissions.clone() + } } //////////////////////////////////////////////////////////////////////////////// @@ -529,58 +636,79 @@ impl OrderCore { #[cfg(test)] mod tests { use rstest::rstest; + use rust_decimal_macros::dec; use super::*; use crate::{ + currencies::USD, enums::{OrderSide, OrderStatus, PositionSide}, - events::order::{OrderDeniedBuilder, OrderEvent, OrderInitializedBuilder}, + events::order::{ + OrderAcceptedBuilder, OrderDeniedBuilder, OrderEvent, OrderFilledBuilder, + OrderInitializedBuilder, OrderSubmittedBuilder, + }, orders::market::MarketOrder, }; - #[rstest( - order_side, - expected_side, - case(OrderSide::Buy, OrderSide::Sell), - case(OrderSide::Sell, OrderSide::Buy), - case(OrderSide::NoOrderSide, OrderSide::NoOrderSide) - )] - fn test_order_opposite_side(order_side: OrderSide, expected_side: OrderSide) { + fn test_initialize_market_order() { + let order = MarketOrder::default(); + assert_eq!(order.events().len(), 1); + assert_eq!( + stringify!(order.events().get(0)), + stringify!(OrderInitialized) + ); + } + + #[rstest] + #[case(OrderSide::Buy, OrderSide::Sell)] + #[case(OrderSide::Sell, OrderSide::Buy)] + #[case(OrderSide::NoOrderSide, OrderSide::NoOrderSide)] + fn test_order_opposite_side(#[case] order_side: OrderSide, #[case] expected_side: OrderSide) { let order = MarketOrder::default(); let result = order.opposite_side(order_side); assert_eq!(result, expected_side) } - #[rstest( - position_side, - expected_side, - case(PositionSide::Long, OrderSide::Sell), - case(PositionSide::Short, OrderSide::Buy), - case(PositionSide::NoPositionSide, OrderSide::NoOrderSide) - )] - fn test_closing_side(position_side: PositionSide, expected_side: OrderSide) { + #[rstest] + #[case(PositionSide::Long, OrderSide::Sell)] + #[case(PositionSide::Short, OrderSide::Buy)] + #[case(PositionSide::NoPositionSide, OrderSide::NoOrderSide)] + fn test_closing_side(#[case] position_side: PositionSide, #[case] expected_side: OrderSide) { let order = MarketOrder::default(); let result = order.closing_side(position_side); assert_eq!(result, expected_side) } + #[rstest] + #[case(OrderSide::Buy, dec!(10_000))] + #[case(OrderSide::Sell, dec!(-10_000))] + fn test_signed_decimal_qty(#[case] order_side: OrderSide, #[case] expected: Decimal) { + let order: MarketOrder = OrderInitializedBuilder::default() + .order_side(order_side) + .quantity(Quantity::new(10_000.0, 0)) + .build() + .unwrap() + .into(); + + let result = order.signed_decimal_qty(); + assert_eq!(result, expected) + } + #[rustfmt::skip] - #[rstest( - order_side, order_qty, position_side, position_qty, expected, - case(OrderSide::Buy, Quantity::from(100), PositionSide::Long, Quantity::from(50), false), - case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(50), true), - case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(100), true), - case(OrderSide::Buy, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false), - case(OrderSide::Sell, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false), - case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(50), true), - case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(100), true), - case(OrderSide::Sell, Quantity::from(100), PositionSide::Short, Quantity::from(50), false), - )] + #[rstest] + #[case(OrderSide::Buy, Quantity::from(100), PositionSide::Long, Quantity::from(50), false)] + #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(50), true)] + #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(100), true)] + #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false)] + #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false)] + #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(50), true)] + #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(100), true)] + #[case(OrderSide::Sell, Quantity::from(100), PositionSide::Short, Quantity::from(50), false)] fn test_would_reduce_only( - order_side: OrderSide, - order_qty: Quantity, - position_side: PositionSide, - position_qty: Quantity, - expected: bool, + #[case] order_side: OrderSide, + #[case] order_qty: Quantity, + #[case] position_side: PositionSide, + #[case] position_qty: Quantity, + #[case] expected: bool, ) { let order: MarketOrder = OrderInitializedBuilder::default() .order_side(order_side) @@ -597,12 +725,11 @@ mod tests { #[test] fn test_order_state_transition_denied() { - let init = OrderInitializedBuilder::default().build().unwrap(); + let mut order: MarketOrder = OrderInitializedBuilder::default().build().unwrap().into(); let denied = OrderDeniedBuilder::default().build().unwrap(); - let mut order: MarketOrder = init.into(); let event = OrderEvent::OrderDenied(denied); - let _ = order.apply(event.clone()); + order.apply(event.clone()).unwrap(); assert_eq!(order.status, OrderStatus::Denied); assert!(order.is_closed()); @@ -611,37 +738,26 @@ mod tests { assert_eq!(order.last_event(), &event); } - // #[test] - // fn test_buy_order_life_cycle_to_filled() { - // let init = OrderInitializedBuilder::default().build().unwrap(); - // let submitted = OrderSubmittedBuilder::default().build().unwrap(); - // let accepted = OrderAcceptedBuilder::default().build().unwrap(); - // - // // TODO: We should derive defaults for the below - // let filled = OrderFilledBuilder::default() - // .trader_id(TraderId::default()) - // .strategy_id(StrategyId::default()) - // .instrument_id(InstrumentId::default()) - // .account_id(AccountId::default()) - // .client_order_id(ClientOrderId::default()) - // .venue_order_id(VenueOrderId::default()) - // .position_id(None) - // .order_side(OrderSide::Buy) - // .order_type(OrderType::Market) - // .trade_id(TradeId::new("001")) - // .event_id(UUID4::default()) - // .ts_event(UnixNanos::default()) - // .ts_init(UnixNanos::default()) - // .reconciliation(false) - // .build() - // .unwrap(); - // - // let client_order_id = init.client_order_id; - // let mut order: MarketOrder = init.into(); - // let _ = order.apply(OrderEvent::OrderSubmitted(submitted)); - // let _ = order.apply(OrderEvent::OrderAccepted(accepted)); - // let _ = order.apply(OrderEvent::OrderFilled(filled)); - // - // assert_eq!(order.client_order_id, client_order_id); - // } + #[test] + fn test_order_life_cycle_to_filled() { + let init = OrderInitializedBuilder::default().build().unwrap(); + let submitted = OrderSubmittedBuilder::default().build().unwrap(); + let accepted = OrderAcceptedBuilder::default().build().unwrap(); + let filled = OrderFilledBuilder::default().build().unwrap(); + + let mut order: MarketOrder = init.clone().into(); + order.apply(OrderEvent::OrderSubmitted(submitted)).unwrap(); + order.apply(OrderEvent::OrderAccepted(accepted)).unwrap(); + order.apply(OrderEvent::OrderFilled(filled)).unwrap(); + + assert_eq!(order.client_order_id, init.client_order_id); + assert_eq!(order.status(), OrderStatus::Filled); + assert_eq!(order.filled_qty(), Quantity::from("100000")); + assert_eq!(order.leaves_qty(), Quantity::from("0")); + assert_eq!(order.avg_px(), Some(1.0)); + assert!(!order.is_open()); + assert!(order.is_closed()); + assert_eq!(order.commission(&*USD), None); + assert_eq!(order.commissions(), HashMap::new()); + } } diff --git a/nautilus_core/model/src/orders/limit.rs b/nautilus_core/model/src/orders/limit.rs index 78e28b396704..9fd1d31484c8 100644 --- a/nautilus_core/model/src/orders/limit.rs +++ b/nautilus_core/model/src/orders/limit.rs @@ -19,19 +19,22 @@ use std::{ }; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use ustr::Ustr; use super::base::{Order, OrderCore}; use crate::{ enums::{ - ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType, + ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, + TrailingOffsetType, TriggerType, }, - events::order::{OrderEvent, OrderInitialized}, + events::order::{OrderEvent, OrderInitialized, OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, - strategy_id::StrategyId, trade_id::TradeId, trader_id::TraderId, - venue_order_id::VenueOrderId, + strategy_id::StrategyId, symbol::Symbol, trade_id::TradeId, trader_id::TraderId, + venue::Venue, venue_order_id::VenueOrderId, }, + orders::base::OrderError, types::{price::Price, quantity::Quantity}, }; @@ -39,7 +42,9 @@ pub struct LimitOrder { core: OrderCore, pub price: Price, pub expire_time: Option, + pub is_post_only: bool, pub display_qty: Option, + pub trigger_instrument_id: Option, } impl LimitOrder { @@ -60,14 +65,15 @@ impl LimitOrder { quote_quantity: bool, display_qty: Option, emulation_trigger: Option, + trigger_instrument_id: Option, contingency_type: Option, order_list_id: Option, linked_order_ids: Option>, parent_order_id: Option, exec_algorithm_id: Option, - exec_algorithm_params: Option>, + exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option, init_id: UUID4, ts_init: UnixNanos, ) -> Self { @@ -81,7 +87,6 @@ impl LimitOrder { OrderType::Limit, quantity, time_in_force, - post_only, reduce_only, quote_quantity, emulation_trigger, @@ -98,7 +103,9 @@ impl LimitOrder { ), price, expire_time, + is_post_only: post_only, display_qty, + trigger_instrument_id, } } } @@ -129,6 +136,7 @@ impl Default for LimitOrder { None, None, None, + None, UUID4::default(), 0, ) @@ -166,6 +174,14 @@ impl Order for LimitOrder { self.instrument_id } + fn symbol(&self) -> Symbol { + self.instrument_id.symbol + } + + fn venue(&self) -> Venue { + self.instrument_id.venue + } + fn client_order_id(&self) -> ClientOrderId { self.client_order_id } @@ -202,6 +218,10 @@ impl Order for LimitOrder { self.time_in_force } + fn expire_time(&self) -> Option { + self.expire_time + } + fn price(&self) -> Option { Some(self.price) } @@ -230,10 +250,30 @@ impl Order for LimitOrder { self.is_quote_quantity } + fn display_qty(&self) -> Option { + self.display_qty + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + fn emulation_trigger(&self) -> Option { self.emulation_trigger } + fn trigger_instrument_id(&self) -> Option { + self.trigger_instrument_id + } + fn contingency_type(&self) -> Option { self.contingency_type } @@ -254,7 +294,7 @@ impl Order for LimitOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { + fn exec_algorithm_params(&self) -> Option> { self.exec_algorithm_params.clone() } @@ -262,8 +302,8 @@ impl Order for LimitOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags.clone() + fn tags(&self) -> Option { + self.tags } fn filled_qty(&self) -> Quantity { @@ -305,6 +345,34 @@ impl Order for LimitOrder { fn trade_ids(&self) -> Vec<&TradeId> { self.trade_ids.iter().collect() } + + fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { + if let OrderEvent::OrderUpdated(ref event) = event { + self.update(event); + }; + let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + + self.core.apply(event)?; + + if is_order_filled { + self.core.set_slippage(self.price) + }; + + Ok(()) + } + + fn update(&mut self, event: &OrderUpdated) { + if event.trigger_price.is_some() { + panic!("{}", OrderError::InvalidOrderEvent); + } + + if let Some(price) = event.price { + self.price = price; + } + + self.quantity = event.quantity; + self.leaves_qty = self.quantity - self.filled_qty; + } } impl From for LimitOrder { @@ -326,6 +394,7 @@ impl From for LimitOrder { event.quote_quantity, event.display_qty, event.emulation_trigger, + event.trigger_instrument_id, event.contingency_type, event.order_list_id, event.linked_order_ids, @@ -339,42 +408,3 @@ impl From for LimitOrder { ) } } - -impl From<&LimitOrder> for OrderInitialized { - fn from(order: &LimitOrder) -> Self { - Self { - trader_id: order.trader_id, - strategy_id: order.strategy_id, - instrument_id: order.instrument_id, - client_order_id: order.client_order_id, - order_side: order.side, - order_type: order.order_type, - quantity: order.quantity, - price: Some(order.price), - trigger_price: None, - trigger_type: None, - time_in_force: order.time_in_force, - expire_time: order.expire_time, - post_only: order.is_post_only, - reduce_only: order.is_reduce_only, - quote_quantity: order.is_quote_quantity, - display_qty: order.display_qty, - limit_offset: None, - trailing_offset: None, - trailing_offset_type: None, - emulation_trigger: order.emulation_trigger, - contingency_type: order.contingency_type, - order_list_id: order.order_list_id, - linked_order_ids: order.linked_order_ids.clone(), - parent_order_id: order.parent_order_id, - exec_algorithm_id: order.exec_algorithm_id, - exec_algorithm_params: order.exec_algorithm_params.clone(), - exec_spawn_id: order.exec_spawn_id, - tags: order.tags.clone(), - event_id: order.init_id, - ts_event: order.ts_init, - ts_init: order.ts_init, - reconciliation: false, - } - } -} diff --git a/nautilus_core/model/src/orders/limit_if_touched.rs b/nautilus_core/model/src/orders/limit_if_touched.rs index 7f32f8a2a251..77c0c139a24d 100644 --- a/nautilus_core/model/src/orders/limit_if_touched.rs +++ b/nautilus_core/model/src/orders/limit_if_touched.rs @@ -19,18 +19,20 @@ use std::{ }; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use ustr::Ustr; -use super::base::{Order, OrderCore}; +use super::base::{Order, OrderCore, OrderError}; use crate::{ enums::{ - ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType, + ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, + TrailingOffsetType, TriggerType, }, - events::order::{OrderEvent, OrderInitialized}, + events::order::{OrderEvent, OrderInitialized, OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, - strategy_id::StrategyId, trade_id::TradeId, trader_id::TraderId, - venue_order_id::VenueOrderId, + strategy_id::StrategyId, symbol::Symbol, trade_id::TradeId, trader_id::TraderId, + venue::Venue, venue_order_id::VenueOrderId, }, types::{price::Price, quantity::Quantity}, }; @@ -41,7 +43,9 @@ pub struct LimitIfTouchedOrder { pub trigger_price: Price, pub trigger_type: TriggerType, pub expire_time: Option, + pub is_post_only: bool, pub display_qty: Option, + pub trigger_instrument_id: Option, pub is_triggered: bool, pub ts_triggered: Option, } @@ -66,14 +70,15 @@ impl LimitIfTouchedOrder { quote_quantity: bool, display_qty: Option, emulation_trigger: Option, + trigger_instrument_id: Option, contingency_type: Option, order_list_id: Option, linked_order_ids: Option>, parent_order_id: Option, exec_algorithm_id: Option, - exec_algorithm_params: Option>, + exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option, init_id: UUID4, ts_init: UnixNanos, ) -> Self { @@ -87,7 +92,6 @@ impl LimitIfTouchedOrder { OrderType::LimitIfTouched, quantity, time_in_force, - post_only, reduce_only, quote_quantity, emulation_trigger, @@ -106,7 +110,9 @@ impl LimitIfTouchedOrder { trigger_price, trigger_type, expire_time, + is_post_only: post_only, display_qty, + trigger_instrument_id, is_triggered: false, ts_triggered: None, } @@ -141,6 +147,7 @@ impl Default for LimitIfTouchedOrder { None, None, None, + None, UUID4::default(), 0, ) @@ -178,6 +185,14 @@ impl Order for LimitIfTouchedOrder { self.instrument_id } + fn symbol(&self) -> Symbol { + self.instrument_id.symbol + } + + fn venue(&self) -> Venue { + self.instrument_id.venue + } + fn client_order_id(&self) -> ClientOrderId { self.client_order_id } @@ -214,6 +229,10 @@ impl Order for LimitIfTouchedOrder { self.time_in_force } + fn expire_time(&self) -> Option { + self.expire_time + } + fn price(&self) -> Option { Some(self.price) } @@ -242,10 +261,30 @@ impl Order for LimitIfTouchedOrder { self.is_quote_quantity } + fn display_qty(&self) -> Option { + self.display_qty + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + fn emulation_trigger(&self) -> Option { self.emulation_trigger } + fn trigger_instrument_id(&self) -> Option { + self.trigger_instrument_id + } + fn contingency_type(&self) -> Option { self.contingency_type } @@ -266,7 +305,7 @@ impl Order for LimitIfTouchedOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { + fn exec_algorithm_params(&self) -> Option> { self.exec_algorithm_params.clone() } @@ -274,8 +313,8 @@ impl Order for LimitIfTouchedOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags.clone() + fn tags(&self) -> Option { + self.tags } fn filled_qty(&self) -> Quantity { @@ -317,6 +356,34 @@ impl Order for LimitIfTouchedOrder { fn trade_ids(&self) -> Vec<&TradeId> { self.trade_ids.iter().collect() } + + fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { + if let OrderEvent::OrderUpdated(ref event) = event { + self.update(event); + }; + let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + + self.core.apply(event)?; + + if is_order_filled { + self.core.set_slippage(self.price) + }; + + Ok(()) + } + + fn update(&mut self, event: &OrderUpdated) { + if let Some(price) = event.price { + self.price = price; + } + + if let Some(trigger_price) = event.trigger_price { + self.trigger_price = trigger_price; + } + + self.quantity = event.quantity; + self.leaves_qty = self.quantity - self.filled_qty; + } } impl From for LimitIfTouchedOrder { @@ -346,6 +413,7 @@ impl From for LimitIfTouchedOrder { event.quote_quantity, event.display_qty, event.emulation_trigger, + event.trigger_instrument_id, event.contingency_type, event.order_list_id, event.linked_order_ids, @@ -359,42 +427,3 @@ impl From for LimitIfTouchedOrder { ) } } - -impl From<&LimitIfTouchedOrder> for OrderInitialized { - fn from(order: &LimitIfTouchedOrder) -> Self { - Self { - trader_id: order.trader_id, - strategy_id: order.strategy_id, - instrument_id: order.instrument_id, - client_order_id: order.client_order_id, - order_side: order.side, - order_type: order.order_type, - quantity: order.quantity, - price: Some(order.price), - trigger_price: Some(order.trigger_price), - trigger_type: Some(order.trigger_type), - time_in_force: order.time_in_force, - expire_time: order.expire_time, - post_only: order.is_post_only, - reduce_only: order.is_reduce_only, - quote_quantity: order.is_quote_quantity, - display_qty: order.display_qty, - limit_offset: None, - trailing_offset: None, - trailing_offset_type: None, - emulation_trigger: order.emulation_trigger, - contingency_type: order.contingency_type, - order_list_id: order.order_list_id, - linked_order_ids: order.linked_order_ids.clone(), - parent_order_id: order.parent_order_id, - exec_algorithm_id: order.exec_algorithm_id, - exec_algorithm_params: order.exec_algorithm_params.clone(), - exec_spawn_id: order.exec_spawn_id, - tags: order.tags.clone(), - event_id: order.init_id, - ts_event: order.ts_init, - ts_init: order.ts_init, - reconciliation: false, - } - } -} diff --git a/nautilus_core/model/src/orders/market.rs b/nautilus_core/model/src/orders/market.rs index 22cc9c880ede..e8ad52b97ac0 100644 --- a/nautilus_core/model/src/orders/market.rs +++ b/nautilus_core/model/src/orders/market.rs @@ -19,19 +19,22 @@ use std::{ }; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use ustr::Ustr; use super::base::{Order, OrderCore}; use crate::{ enums::{ - ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType, + ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, + TrailingOffsetType, TriggerType, }, - events::order::{OrderEvent, OrderInitialized}, + events::order::{OrderEvent, OrderInitialized, OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, - strategy_id::StrategyId, trade_id::TradeId, trader_id::TraderId, - venue_order_id::VenueOrderId, + strategy_id::StrategyId, symbol::Symbol, trade_id::TradeId, trader_id::TraderId, + venue::Venue, venue_order_id::VenueOrderId, }, + orders::base::OrderError, types::{price::Price, quantity::Quantity}, }; @@ -57,9 +60,9 @@ impl MarketOrder { linked_order_ids: Option>, parent_order_id: Option, exec_algorithm_id: Option, - exec_algorithm_params: Option>, + exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option, init_id: UUID4, ts_init: UnixNanos, ) -> Self { @@ -73,10 +76,9 @@ impl MarketOrder { OrderType::Market, quantity, time_in_force, - false, reduce_only, quote_quantity, - None, + None, // Emulation trigger contingency_type, order_list_id, linked_order_ids, @@ -150,6 +152,14 @@ impl Order for MarketOrder { self.instrument_id } + fn symbol(&self) -> Symbol { + self.instrument_id.symbol + } + + fn venue(&self) -> Venue { + self.instrument_id.venue + } + fn client_order_id(&self) -> ClientOrderId { self.client_order_id } @@ -186,6 +196,10 @@ impl Order for MarketOrder { self.time_in_force } + fn expire_time(&self) -> Option { + None + } + fn price(&self) -> Option { None } @@ -203,7 +217,7 @@ impl Order for MarketOrder { } fn is_post_only(&self) -> bool { - self.is_post_only + false } fn is_reduce_only(&self) -> bool { @@ -214,8 +228,28 @@ impl Order for MarketOrder { self.is_quote_quantity } + fn display_qty(&self) -> Option { + None + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + fn emulation_trigger(&self) -> Option { - self.emulation_trigger + None + } + + fn trigger_instrument_id(&self) -> Option { + None } fn contingency_type(&self) -> Option { @@ -238,7 +272,7 @@ impl Order for MarketOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { + fn exec_algorithm_params(&self) -> Option> { self.exec_algorithm_params.clone() } @@ -246,8 +280,8 @@ impl Order for MarketOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags.clone() + fn tags(&self) -> Option { + self.tags } fn filled_qty(&self) -> Quantity { @@ -289,6 +323,28 @@ impl Order for MarketOrder { fn trade_ids(&self) -> Vec<&TradeId> { self.trade_ids.iter().collect() } + + fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { + if let OrderEvent::OrderUpdated(ref event) = event { + self.update(event); + }; + + self.core.apply(event)?; + + Ok(()) + } + + fn update(&mut self, event: &OrderUpdated) { + if event.price.is_some() { + panic!("{}", OrderError::InvalidOrderEvent); + } + if event.trigger_price.is_some() { + panic!("{}", OrderError::InvalidOrderEvent); + } + + self.quantity = event.quantity; + self.leaves_qty = self.quantity - self.filled_qty; + } } impl From for MarketOrder { @@ -316,42 +372,3 @@ impl From for MarketOrder { ) } } - -impl From<&MarketOrder> for OrderInitialized { - fn from(order: &MarketOrder) -> Self { - Self { - trader_id: order.trader_id, - strategy_id: order.strategy_id, - instrument_id: order.instrument_id, - client_order_id: order.client_order_id, - order_side: order.side, - order_type: order.order_type, - quantity: order.quantity, - price: None, - trigger_price: None, - trigger_type: None, - time_in_force: order.time_in_force, - expire_time: None, - post_only: order.is_post_only, - reduce_only: order.is_reduce_only, - quote_quantity: order.is_quote_quantity, - display_qty: None, - limit_offset: None, - trailing_offset: None, - trailing_offset_type: None, - emulation_trigger: order.emulation_trigger, - contingency_type: order.contingency_type, - order_list_id: order.order_list_id, - linked_order_ids: order.linked_order_ids.clone(), - parent_order_id: order.parent_order_id, - exec_algorithm_id: order.exec_algorithm_id, - exec_algorithm_params: order.exec_algorithm_params.clone(), - exec_spawn_id: order.exec_spawn_id, - tags: order.tags.clone(), - event_id: order.init_id, - ts_event: order.ts_init, - ts_init: order.ts_init, - reconciliation: false, - } - } -} diff --git a/nautilus_core/model/src/orders/market_if_touched.rs b/nautilus_core/model/src/orders/market_if_touched.rs index b2e946634eb7..22ec37822e9e 100644 --- a/nautilus_core/model/src/orders/market_if_touched.rs +++ b/nautilus_core/model/src/orders/market_if_touched.rs @@ -19,18 +19,20 @@ use std::{ }; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use ustr::Ustr; -use super::base::{Order, OrderCore}; +use super::base::{Order, OrderCore, OrderError}; use crate::{ enums::{ - ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType, + ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, + TrailingOffsetType, TriggerType, }, - events::order::{OrderEvent, OrderInitialized}, + events::order::{OrderEvent, OrderInitialized, OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, - strategy_id::StrategyId, trade_id::TradeId, trader_id::TraderId, - venue_order_id::VenueOrderId, + strategy_id::StrategyId, symbol::Symbol, trade_id::TradeId, trader_id::TraderId, + venue::Venue, venue_order_id::VenueOrderId, }, types::{price::Price, quantity::Quantity}, }; @@ -41,6 +43,7 @@ pub struct MarketIfTouchedOrder { pub trigger_type: TriggerType, pub expire_time: Option, pub display_qty: Option, + pub trigger_instrument_id: Option, pub is_triggered: bool, pub ts_triggered: Option, } @@ -59,19 +62,19 @@ impl MarketIfTouchedOrder { trigger_type: TriggerType, time_in_force: TimeInForce, expire_time: Option, - post_only: bool, reduce_only: bool, quote_quantity: bool, display_qty: Option, emulation_trigger: Option, + trigger_instrument_id: Option, contingency_type: Option, order_list_id: Option, linked_order_ids: Option>, parent_order_id: Option, exec_algorithm_id: Option, - exec_algorithm_params: Option>, + exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option, init_id: UUID4, ts_init: UnixNanos, ) -> Self { @@ -85,7 +88,6 @@ impl MarketIfTouchedOrder { OrderType::MarketIfTouched, quantity, time_in_force, - post_only, reduce_only, quote_quantity, emulation_trigger, @@ -104,6 +106,7 @@ impl MarketIfTouchedOrder { trigger_type, expire_time, display_qty, + trigger_instrument_id, is_triggered: false, ts_triggered: None, } @@ -126,7 +129,7 @@ impl Default for MarketIfTouchedOrder { None, false, false, - false, + None, None, None, None, @@ -174,6 +177,14 @@ impl Order for MarketIfTouchedOrder { self.instrument_id } + fn symbol(&self) -> Symbol { + self.instrument_id.symbol + } + + fn venue(&self) -> Venue { + self.instrument_id.venue + } + fn client_order_id(&self) -> ClientOrderId { self.client_order_id } @@ -210,6 +221,10 @@ impl Order for MarketIfTouchedOrder { self.time_in_force } + fn expire_time(&self) -> Option { + self.expire_time + } + fn price(&self) -> Option { None } @@ -227,7 +242,7 @@ impl Order for MarketIfTouchedOrder { } fn is_post_only(&self) -> bool { - self.is_post_only + false } fn is_reduce_only(&self) -> bool { @@ -238,10 +253,30 @@ impl Order for MarketIfTouchedOrder { self.is_quote_quantity } + fn display_qty(&self) -> Option { + self.display_qty + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + fn emulation_trigger(&self) -> Option { self.emulation_trigger } + fn trigger_instrument_id(&self) -> Option { + self.trigger_instrument_id + } + fn contingency_type(&self) -> Option { self.contingency_type } @@ -262,7 +297,7 @@ impl Order for MarketIfTouchedOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { + fn exec_algorithm_params(&self) -> Option> { self.exec_algorithm_params.clone() } @@ -270,8 +305,8 @@ impl Order for MarketIfTouchedOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags.clone() + fn tags(&self) -> Option { + self.tags } fn filled_qty(&self) -> Quantity { @@ -313,6 +348,34 @@ impl Order for MarketIfTouchedOrder { fn trade_ids(&self) -> Vec<&TradeId> { self.trade_ids.iter().collect() } + + fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { + if let OrderEvent::OrderUpdated(ref event) = event { + self.update(event); + }; + let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + + self.core.apply(event)?; + + if is_order_filled { + self.core.set_slippage(self.trigger_price) + }; + + Ok(()) + } + + fn update(&mut self, event: &OrderUpdated) { + if event.price.is_some() { + panic!("{}", OrderError::InvalidOrderEvent); + } + + if let Some(trigger_price) = event.trigger_price { + self.trigger_price = trigger_price; + } + + self.quantity = event.quantity; + self.leaves_qty = self.quantity - self.filled_qty; + } } impl From for MarketIfTouchedOrder { @@ -334,11 +397,11 @@ impl From for MarketIfTouchedOrder { .expect("Error initializing order: `trigger_type` was `None` for `MarketIfTouchedOrder`"), event.time_in_force, event.expire_time, - event.post_only, event.reduce_only, event.quote_quantity, event.display_qty, event.emulation_trigger, + event.trigger_instrument_id, event.contingency_type, event.order_list_id, event.linked_order_ids, @@ -352,42 +415,3 @@ impl From for MarketIfTouchedOrder { ) } } - -impl From<&MarketIfTouchedOrder> for OrderInitialized { - fn from(order: &MarketIfTouchedOrder) -> Self { - Self { - trader_id: order.trader_id, - strategy_id: order.strategy_id, - instrument_id: order.instrument_id, - client_order_id: order.client_order_id, - order_side: order.side, - order_type: order.order_type, - quantity: order.quantity, - price: None, - trigger_price: Some(order.trigger_price), - trigger_type: Some(order.trigger_type), - time_in_force: order.time_in_force, - expire_time: order.expire_time, - post_only: order.is_post_only, - reduce_only: order.is_reduce_only, - quote_quantity: order.is_quote_quantity, - display_qty: order.display_qty, - limit_offset: None, - trailing_offset: None, - trailing_offset_type: None, - emulation_trigger: order.emulation_trigger, - contingency_type: order.contingency_type, - order_list_id: order.order_list_id, - linked_order_ids: order.linked_order_ids.clone(), - parent_order_id: order.parent_order_id, - exec_algorithm_id: order.exec_algorithm_id, - exec_algorithm_params: order.exec_algorithm_params.clone(), - exec_spawn_id: order.exec_spawn_id, - tags: order.tags.clone(), - event_id: order.init_id, - ts_event: order.ts_init, - ts_init: order.ts_init, - reconciliation: false, - } - } -} diff --git a/nautilus_core/model/src/orders/market_to_limit.rs b/nautilus_core/model/src/orders/market_to_limit.rs index bf58c6b7ee0e..98d278aae06a 100644 --- a/nautilus_core/model/src/orders/market_to_limit.rs +++ b/nautilus_core/model/src/orders/market_to_limit.rs @@ -19,19 +19,22 @@ use std::{ }; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use ustr::Ustr; use super::base::{Order, OrderCore}; use crate::{ enums::{ - ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType, + ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, + TrailingOffsetType, TriggerType, }, - events::order::{OrderEvent, OrderInitialized}, + events::order::{OrderEvent, OrderInitialized, OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, - strategy_id::StrategyId, trade_id::TradeId, trader_id::TraderId, - venue_order_id::VenueOrderId, + strategy_id::StrategyId, symbol::Symbol, trade_id::TradeId, trader_id::TraderId, + venue::Venue, venue_order_id::VenueOrderId, }, + orders::base::OrderError, types::{price::Price, quantity::Quantity}, }; @@ -39,6 +42,7 @@ pub struct MarketToLimitOrder { core: OrderCore, pub price: Option, pub expire_time: Option, + pub is_post_only: bool, pub display_qty: Option, } @@ -58,15 +62,14 @@ impl MarketToLimitOrder { reduce_only: bool, quote_quantity: bool, display_qty: Option, - emulation_trigger: Option, contingency_type: Option, order_list_id: Option, linked_order_ids: Option>, parent_order_id: Option, exec_algorithm_id: Option, - exec_algorithm_params: Option>, + exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option, init_id: UUID4, ts_init: UnixNanos, ) -> Self { @@ -80,10 +83,9 @@ impl MarketToLimitOrder { OrderType::MarketToLimit, quantity, time_in_force, - post_only, reduce_only, quote_quantity, - emulation_trigger, + None, // Emulation trigger contingency_type, order_list_id, linked_order_ids, @@ -97,6 +99,7 @@ impl MarketToLimitOrder { ), price: None, // Price will be determined on fill expire_time, + is_post_only: post_only, display_qty, } } @@ -126,7 +129,6 @@ impl Default for MarketToLimitOrder { None, None, None, - None, UUID4::default(), 0, ) @@ -164,6 +166,14 @@ impl Order for MarketToLimitOrder { self.instrument_id } + fn symbol(&self) -> Symbol { + self.instrument_id.symbol + } + + fn venue(&self) -> Venue { + self.instrument_id.venue + } + fn client_order_id(&self) -> ClientOrderId { self.client_order_id } @@ -200,6 +210,10 @@ impl Order for MarketToLimitOrder { self.time_in_force } + fn expire_time(&self) -> Option { + self.expire_time + } + fn price(&self) -> Option { self.price } @@ -228,8 +242,28 @@ impl Order for MarketToLimitOrder { self.is_quote_quantity } + fn display_qty(&self) -> Option { + self.display_qty + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + fn emulation_trigger(&self) -> Option { - self.emulation_trigger + None + } + + fn trigger_instrument_id(&self) -> Option { + None } fn contingency_type(&self) -> Option { @@ -252,7 +286,7 @@ impl Order for MarketToLimitOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { + fn exec_algorithm_params(&self) -> Option> { self.exec_algorithm_params.clone() } @@ -260,8 +294,8 @@ impl Order for MarketToLimitOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags.clone() + fn tags(&self) -> Option { + self.tags } fn filled_qty(&self) -> Quantity { @@ -303,6 +337,34 @@ impl Order for MarketToLimitOrder { fn trade_ids(&self) -> Vec<&TradeId> { self.trade_ids.iter().collect() } + + fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { + if let OrderEvent::OrderUpdated(ref event) = event { + self.update(event); + }; + let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + + self.core.apply(event)?; + + if is_order_filled && self.price.is_some() { + self.core.set_slippage(self.price.unwrap()) + }; + + Ok(()) + } + + fn update(&mut self, event: &OrderUpdated) { + if event.trigger_price.is_some() { + panic!("{}", OrderError::InvalidOrderEvent); + } + + if let Some(price) = event.price { + self.price = Some(price); + } + + self.quantity = event.quantity; + self.leaves_qty = self.quantity - self.filled_qty; + } } impl From for MarketToLimitOrder { @@ -320,7 +382,6 @@ impl From for MarketToLimitOrder { event.reduce_only, event.quote_quantity, event.display_qty, - event.emulation_trigger, event.contingency_type, event.order_list_id, event.linked_order_ids, @@ -334,42 +395,3 @@ impl From for MarketToLimitOrder { ) } } - -impl From<&MarketToLimitOrder> for OrderInitialized { - fn from(order: &MarketToLimitOrder) -> Self { - Self { - trader_id: order.trader_id, - strategy_id: order.strategy_id, - instrument_id: order.instrument_id, - client_order_id: order.client_order_id, - order_side: order.side, - order_type: order.order_type, - quantity: order.quantity, - price: None, - trigger_price: None, - trigger_type: None, - time_in_force: order.time_in_force, - expire_time: order.expire_time, - post_only: order.is_post_only, - reduce_only: order.is_reduce_only, - quote_quantity: order.is_quote_quantity, - display_qty: order.display_qty, - limit_offset: None, - trailing_offset: None, - trailing_offset_type: None, - emulation_trigger: order.emulation_trigger, - contingency_type: order.contingency_type, - order_list_id: order.order_list_id, - linked_order_ids: order.linked_order_ids.clone(), - parent_order_id: order.parent_order_id, - exec_algorithm_id: order.exec_algorithm_id, - exec_algorithm_params: order.exec_algorithm_params.clone(), - exec_spawn_id: order.exec_spawn_id, - tags: order.tags.clone(), - event_id: order.init_id, - ts_event: order.ts_init, - ts_init: order.ts_init, - reconciliation: false, - } - } -} diff --git a/nautilus_core/model/src/orders/mod.rs b/nautilus_core/model/src/orders/mod.rs index a6ccfc1f9365..95eeefdff047 100644 --- a/nautilus_core/model/src/orders/mod.rs +++ b/nautilus_core/model/src/orders/mod.rs @@ -22,5 +22,6 @@ pub mod market; pub mod market_if_touched; pub mod market_to_limit; pub mod stop_limit; +pub mod stop_market; pub mod trailing_stop_limit; pub mod trailing_stop_market; diff --git a/nautilus_core/model/src/orders/stop_limit.rs b/nautilus_core/model/src/orders/stop_limit.rs index 37e86e8d3ccf..aa5838b99dc9 100644 --- a/nautilus_core/model/src/orders/stop_limit.rs +++ b/nautilus_core/model/src/orders/stop_limit.rs @@ -19,18 +19,20 @@ use std::{ }; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use ustr::Ustr; -use super::base::{Order, OrderCore}; +use super::base::{Order, OrderCore, OrderError}; use crate::{ enums::{ - ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType, + ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, + TrailingOffsetType, TriggerType, }, - events::order::{OrderEvent, OrderInitialized}, + events::order::{OrderEvent, OrderInitialized, OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, - strategy_id::StrategyId, trade_id::TradeId, trader_id::TraderId, - venue_order_id::VenueOrderId, + strategy_id::StrategyId, symbol::Symbol, trade_id::TradeId, trader_id::TraderId, + venue::Venue, venue_order_id::VenueOrderId, }, types::{price::Price, quantity::Quantity}, }; @@ -41,7 +43,9 @@ pub struct StopLimitOrder { pub trigger_price: Price, pub trigger_type: TriggerType, pub expire_time: Option, + pub is_post_only: bool, pub display_qty: Option, + pub trigger_instrument_id: Option, pub is_triggered: bool, pub ts_triggered: Option, } @@ -66,14 +70,15 @@ impl StopLimitOrder { quote_quantity: bool, display_qty: Option, emulation_trigger: Option, + trigger_instrument_id: Option, contingency_type: Option, order_list_id: Option, linked_order_ids: Option>, parent_order_id: Option, exec_algorithm_id: Option, - exec_algorithm_params: Option>, + exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option, init_id: UUID4, ts_init: UnixNanos, ) -> Self { @@ -87,7 +92,6 @@ impl StopLimitOrder { OrderType::LimitIfTouched, quantity, time_in_force, - post_only, reduce_only, quote_quantity, emulation_trigger, @@ -106,7 +110,9 @@ impl StopLimitOrder { trigger_price, trigger_type, expire_time, + is_post_only: post_only, display_qty, + trigger_instrument_id, is_triggered: false, ts_triggered: None, } @@ -141,6 +147,7 @@ impl Default for StopLimitOrder { None, None, None, + None, UUID4::default(), 0, ) @@ -178,6 +185,14 @@ impl Order for StopLimitOrder { self.instrument_id } + fn symbol(&self) -> Symbol { + self.instrument_id.symbol + } + + fn venue(&self) -> Venue { + self.instrument_id.venue + } + fn client_order_id(&self) -> ClientOrderId { self.client_order_id } @@ -214,6 +229,10 @@ impl Order for StopLimitOrder { self.time_in_force } + fn expire_time(&self) -> Option { + self.expire_time + } + fn price(&self) -> Option { Some(self.price) } @@ -242,10 +261,30 @@ impl Order for StopLimitOrder { self.is_quote_quantity } + fn display_qty(&self) -> Option { + self.display_qty + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + fn emulation_trigger(&self) -> Option { self.emulation_trigger } + fn trigger_instrument_id(&self) -> Option { + self.trigger_instrument_id + } + fn contingency_type(&self) -> Option { self.contingency_type } @@ -266,7 +305,7 @@ impl Order for StopLimitOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { + fn exec_algorithm_params(&self) -> Option> { self.exec_algorithm_params.clone() } @@ -274,8 +313,8 @@ impl Order for StopLimitOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags.clone() + fn tags(&self) -> Option { + self.tags } fn filled_qty(&self) -> Quantity { @@ -317,6 +356,36 @@ impl Order for StopLimitOrder { fn trade_ids(&self) -> Vec<&TradeId> { self.trade_ids.iter().collect() } + + fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { + if let OrderEvent::OrderUpdated(ref event) = event { + self.update(event); + }; + let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + + self.core.apply(event)?; + + if is_order_filled { + self.core.set_slippage(self.price) + }; + + Ok(()) + } + + fn update(&mut self, event: &OrderUpdated) { + self.quantity = event.quantity; + + if let Some(price) = event.price { + self.price = price; + } + + if let Some(trigger_price) = event.trigger_price { + self.trigger_price = trigger_price; + } + + self.quantity = event.quantity; + self.leaves_qty = self.quantity - self.filled_qty; + } } impl From for StopLimitOrder { @@ -344,6 +413,7 @@ impl From for StopLimitOrder { event.quote_quantity, event.display_qty, event.emulation_trigger, + event.trigger_instrument_id, event.contingency_type, event.order_list_id, event.linked_order_ids, @@ -357,42 +427,3 @@ impl From for StopLimitOrder { ) } } - -impl From<&StopLimitOrder> for OrderInitialized { - fn from(order: &StopLimitOrder) -> Self { - Self { - trader_id: order.trader_id, - strategy_id: order.strategy_id, - instrument_id: order.instrument_id, - client_order_id: order.client_order_id, - order_side: order.side, - order_type: order.order_type, - quantity: order.quantity, - price: Some(order.price), - trigger_price: Some(order.trigger_price), - trigger_type: Some(order.trigger_type), - time_in_force: order.time_in_force, - expire_time: order.expire_time, - post_only: order.is_post_only, - reduce_only: order.is_reduce_only, - quote_quantity: order.is_quote_quantity, - display_qty: order.display_qty, - limit_offset: None, - trailing_offset: None, - trailing_offset_type: None, - emulation_trigger: order.emulation_trigger, - contingency_type: order.contingency_type, - order_list_id: order.order_list_id, - linked_order_ids: order.linked_order_ids.clone(), - parent_order_id: order.parent_order_id, - exec_algorithm_id: order.exec_algorithm_id, - exec_algorithm_params: order.exec_algorithm_params.clone(), - exec_spawn_id: order.exec_spawn_id, - tags: order.tags.clone(), - event_id: order.init_id, - ts_event: order.ts_init, - ts_init: order.ts_init, - reconciliation: false, - } - } -} diff --git a/nautilus_core/model/src/orders/stop_market.rs b/nautilus_core/model/src/orders/stop_market.rs index 74375a9e94e6..c72c5dc625e0 100644 --- a/nautilus_core/model/src/orders/stop_market.rs +++ b/nautilus_core/model/src/orders/stop_market.rs @@ -19,19 +19,22 @@ use std::{ }; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use ustr::Ustr; use super::base::{Order, OrderCore}; use crate::{ enums::{ - ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType, + ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, + TrailingOffsetType, TriggerType, }, - events::order::{OrderEvent, OrderInitialized}, + events::order::{OrderEvent, OrderInitialized, OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, - strategy_id::StrategyId, trade_id::TradeId, trader_id::TraderId, - venue_order_id::VenueOrderId, + strategy_id::StrategyId, symbol::Symbol, trade_id::TradeId, trader_id::TraderId, + venue::Venue, venue_order_id::VenueOrderId, }, + orders::base::OrderError, types::{price::Price, quantity::Quantity}, }; @@ -41,6 +44,7 @@ pub struct StopMarketOrder { pub trigger_type: TriggerType, pub expire_time: Option, pub display_qty: Option, + pub trigger_instrument_id: Option, pub is_triggered: bool, pub ts_triggered: Option, } @@ -59,19 +63,19 @@ impl StopMarketOrder { trigger_type: TriggerType, time_in_force: TimeInForce, expire_time: Option, - post_only: bool, reduce_only: bool, quote_quantity: bool, display_qty: Option, emulation_trigger: Option, + trigger_instrument_id: Option, contingency_type: Option, order_list_id: Option, linked_order_ids: Option>, parent_order_id: Option, exec_algorithm_id: Option, - exec_algorithm_params: Option>, + exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option, init_id: UUID4, ts_init: UnixNanos, ) -> Self { @@ -85,7 +89,6 @@ impl StopMarketOrder { OrderType::StopMarket, quantity, time_in_force, - post_only, reduce_only, quote_quantity, emulation_trigger, @@ -104,6 +107,7 @@ impl StopMarketOrder { trigger_type, expire_time, display_qty, + trigger_instrument_id, is_triggered: false, ts_triggered: None, } @@ -113,7 +117,7 @@ impl StopMarketOrder { /// Provides a default [`StopMarketOrder`] used for testing. impl Default for StopMarketOrder { fn default() -> Self { - StopLimitOrder::new( + StopMarketOrder::new( TraderId::default(), StrategyId::default(), InstrumentId::default(), @@ -126,7 +130,7 @@ impl Default for StopMarketOrder { None, false, false, - false, + None, None, None, None, @@ -174,6 +178,14 @@ impl Order for StopMarketOrder { self.instrument_id } + fn symbol(&self) -> Symbol { + self.instrument_id.symbol + } + + fn venue(&self) -> Venue { + self.instrument_id.venue + } + fn client_order_id(&self) -> ClientOrderId { self.client_order_id } @@ -210,8 +222,12 @@ impl Order for StopMarketOrder { self.time_in_force } + fn expire_time(&self) -> Option { + self.expire_time + } + fn price(&self) -> Option { - Some(self.price) + None } fn trigger_price(&self) -> Option { @@ -227,7 +243,7 @@ impl Order for StopMarketOrder { } fn is_post_only(&self) -> bool { - self.is_post_only + false } fn is_reduce_only(&self) -> bool { @@ -238,10 +254,30 @@ impl Order for StopMarketOrder { self.is_quote_quantity } + fn display_qty(&self) -> Option { + self.display_qty + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + None + } + + fn trailing_offset_type(&self) -> Option { + None + } + fn emulation_trigger(&self) -> Option { self.emulation_trigger } + fn trigger_instrument_id(&self) -> Option { + self.trigger_instrument_id + } + fn contingency_type(&self) -> Option { self.contingency_type } @@ -262,7 +298,7 @@ impl Order for StopMarketOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { + fn exec_algorithm_params(&self) -> Option> { self.exec_algorithm_params.clone() } @@ -270,8 +306,8 @@ impl Order for StopMarketOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags.clone() + fn tags(&self) -> Option { + self.tags } fn filled_qty(&self) -> Quantity { @@ -313,11 +349,39 @@ impl Order for StopMarketOrder { fn trade_ids(&self) -> Vec<&TradeId> { self.trade_ids.iter().collect() } + + fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { + if let OrderEvent::OrderUpdated(ref event) = event { + self.update(event); + }; + let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + + self.core.apply(event)?; + + if is_order_filled { + self.core.set_slippage(self.trigger_price) + }; + + Ok(()) + } + + fn update(&mut self, event: &OrderUpdated) { + if event.price.is_some() { + panic!("{}", OrderError::InvalidOrderEvent); + } + + if let Some(trigger_price) = event.trigger_price { + self.trigger_price = trigger_price; + } + + self.quantity = event.quantity; + self.leaves_qty = self.quantity - self.filled_qty; + } } impl From for StopMarketOrder { fn from(event: OrderInitialized) -> Self { - StopLimitOrder::new( + StopMarketOrder::new( event.trader_id, event.strategy_id, event.instrument_id, @@ -334,11 +398,11 @@ impl From for StopMarketOrder { ), event.time_in_force, event.expire_time, - event.post_only, event.reduce_only, event.quote_quantity, event.display_qty, event.emulation_trigger, + event.trigger_instrument_id, event.contingency_type, event.order_list_id, event.linked_order_ids, @@ -352,42 +416,3 @@ impl From for StopMarketOrder { ) } } - -impl From<&StopLimitOrder> for OrderInitialized { - fn from(order: &StopLimitOrder) -> Self { - Self { - trader_id: order.trader_id, - strategy_id: order.strategy_id, - instrument_id: order.instrument_id, - client_order_id: order.client_order_id, - order_side: order.side, - order_type: order.order_type, - quantity: order.quantity, - price: None, - trigger_price: Some(order.trigger_price), - trigger_type: Some(order.trigger_type), - time_in_force: order.time_in_force, - expire_time: order.expire_time, - post_only: order.is_post_only, - reduce_only: order.is_reduce_only, - quote_quantity: order.is_quote_quantity, - display_qty: order.display_qty, - limit_offset: None, - trailing_offset: None, - trailing_offset_type: None, - emulation_trigger: order.emulation_trigger, - contingency_type: order.contingency_type, - order_list_id: order.order_list_id, - linked_order_ids: order.linked_order_ids.clone(), - parent_order_id: order.parent_order_id, - exec_algorithm_id: order.exec_algorithm_id, - exec_algorithm_params: order.exec_algorithm_params.clone(), - exec_spawn_id: order.exec_spawn_id, - tags: order.tags.clone(), - event_id: order.init_id, - ts_event: order.ts_init, - ts_init: order.ts_init, - reconciliation: false, - } - } -} diff --git a/nautilus_core/model/src/orders/trailing_stop_limit.rs b/nautilus_core/model/src/orders/trailing_stop_limit.rs index 64449fcd09bd..58c35f2643d6 100644 --- a/nautilus_core/model/src/orders/trailing_stop_limit.rs +++ b/nautilus_core/model/src/orders/trailing_stop_limit.rs @@ -19,19 +19,20 @@ use std::{ }; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use ustr::Ustr; -use super::base::{Order, OrderCore}; +use super::base::{Order, OrderCore, OrderError}; use crate::{ enums::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType, TriggerType, }, - events::order::{OrderEvent, OrderInitialized}, + events::order::{OrderEvent, OrderInitialized, OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, - strategy_id::StrategyId, trade_id::TradeId, trader_id::TraderId, - venue_order_id::VenueOrderId, + strategy_id::StrategyId, symbol::Symbol, trade_id::TradeId, trader_id::TraderId, + venue::Venue, venue_order_id::VenueOrderId, }, types::{price::Price, quantity::Quantity}, }; @@ -45,7 +46,9 @@ pub struct TrailingStopLimitOrder { pub trailing_offset: Price, pub trailing_offset_type: TrailingOffsetType, pub expire_time: Option, + pub is_post_only: bool, pub display_qty: Option, + pub trigger_instrument_id: Option, pub is_triggered: bool, pub ts_triggered: Option, } @@ -73,14 +76,15 @@ impl TrailingStopLimitOrder { quote_quantity: bool, display_qty: Option, emulation_trigger: Option, + trigger_instrument_id: Option, contingency_type: Option, order_list_id: Option, linked_order_ids: Option>, parent_order_id: Option, exec_algorithm_id: Option, - exec_algorithm_params: Option>, + exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option, init_id: UUID4, ts_init: UnixNanos, ) -> Self { @@ -94,7 +98,6 @@ impl TrailingStopLimitOrder { OrderType::TrailingStopLimit, quantity, time_in_force, - post_only, reduce_only, quote_quantity, emulation_trigger, @@ -116,7 +119,9 @@ impl TrailingStopLimitOrder { trailing_offset, trailing_offset_type, expire_time, + is_post_only: post_only, display_qty, + trigger_instrument_id, is_triggered: false, ts_triggered: None, } @@ -154,6 +159,7 @@ impl Default for TrailingStopLimitOrder { None, None, None, + None, UUID4::default(), 0, ) @@ -191,6 +197,14 @@ impl Order for TrailingStopLimitOrder { self.instrument_id } + fn symbol(&self) -> Symbol { + self.instrument_id.symbol + } + + fn venue(&self) -> Venue { + self.instrument_id.venue + } + fn client_order_id(&self) -> ClientOrderId { self.client_order_id } @@ -227,6 +241,10 @@ impl Order for TrailingStopLimitOrder { self.time_in_force } + fn expire_time(&self) -> Option { + self.expire_time + } + fn price(&self) -> Option { Some(self.price) } @@ -255,10 +273,30 @@ impl Order for TrailingStopLimitOrder { self.is_quote_quantity } + fn display_qty(&self) -> Option { + self.display_qty + } + + fn limit_offset(&self) -> Option { + Some(self.limit_offset) + } + + fn trailing_offset(&self) -> Option { + Some(self.trailing_offset) + } + + fn trailing_offset_type(&self) -> Option { + Some(self.trailing_offset_type) + } + fn emulation_trigger(&self) -> Option { self.emulation_trigger } + fn trigger_instrument_id(&self) -> Option { + self.trigger_instrument_id + } + fn contingency_type(&self) -> Option { self.contingency_type } @@ -279,7 +317,7 @@ impl Order for TrailingStopLimitOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { + fn exec_algorithm_params(&self) -> Option> { self.exec_algorithm_params.clone() } @@ -287,8 +325,8 @@ impl Order for TrailingStopLimitOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags.clone() + fn tags(&self) -> Option { + self.tags } fn filled_qty(&self) -> Quantity { @@ -330,6 +368,34 @@ impl Order for TrailingStopLimitOrder { fn trade_ids(&self) -> Vec<&TradeId> { self.trade_ids.iter().collect() } + + fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { + if let OrderEvent::OrderUpdated(ref event) = event { + self.update(event); + }; + let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + + self.core.apply(event)?; + + if is_order_filled { + self.core.set_slippage(self.price) + }; + + Ok(()) + } + + fn update(&mut self, event: &OrderUpdated) { + if let Some(price) = event.price { + self.price = price; + } + + if let Some(trigger_price) = event.trigger_price { + self.trigger_price = trigger_price; + } + + self.quantity = event.quantity; + self.leaves_qty = self.quantity - self.filled_qty; + } } impl From for TrailingStopLimitOrder { @@ -362,6 +428,7 @@ impl From for TrailingStopLimitOrder { event.quote_quantity, event.display_qty, event.emulation_trigger, + event.trigger_instrument_id, event.contingency_type, event.order_list_id, event.linked_order_ids, @@ -375,42 +442,3 @@ impl From for TrailingStopLimitOrder { ) } } - -impl From<&TrailingStopLimitOrder> for OrderInitialized { - fn from(order: &TrailingStopLimitOrder) -> Self { - Self { - trader_id: order.trader_id, - strategy_id: order.strategy_id, - instrument_id: order.instrument_id, - client_order_id: order.client_order_id, - order_side: order.side, - order_type: order.order_type, - quantity: order.quantity, - price: Some(order.price), - trigger_price: Some(order.trigger_price), - trigger_type: Some(order.trigger_type), - time_in_force: order.time_in_force, - expire_time: order.expire_time, - post_only: order.is_post_only, - reduce_only: order.is_reduce_only, - quote_quantity: order.is_quote_quantity, - display_qty: order.display_qty, - limit_offset: Some(order.limit_offset), - trailing_offset: Some(order.trailing_offset), - trailing_offset_type: Some(order.trailing_offset_type), - emulation_trigger: order.emulation_trigger, - contingency_type: order.contingency_type, - order_list_id: order.order_list_id, - linked_order_ids: order.linked_order_ids.clone(), - parent_order_id: order.parent_order_id, - exec_algorithm_id: order.exec_algorithm_id, - exec_algorithm_params: order.exec_algorithm_params.clone(), - exec_spawn_id: order.exec_spawn_id, - tags: order.tags.clone(), - event_id: order.init_id, - ts_event: order.ts_init, - ts_init: order.ts_init, - reconciliation: false, - } - } -} diff --git a/nautilus_core/model/src/orders/trailing_stop_market.rs b/nautilus_core/model/src/orders/trailing_stop_market.rs index f2fc4bcc8658..76bb619999b2 100644 --- a/nautilus_core/model/src/orders/trailing_stop_market.rs +++ b/nautilus_core/model/src/orders/trailing_stop_market.rs @@ -19,6 +19,7 @@ use std::{ }; use nautilus_core::{time::UnixNanos, uuid::UUID4}; +use ustr::Ustr; use super::base::{Order, OrderCore}; use crate::{ @@ -26,13 +27,14 @@ use crate::{ ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce, TrailingOffsetType, TriggerType, }, - events::order::{OrderEvent, OrderInitialized}, + events::order::{OrderEvent, OrderInitialized, OrderUpdated}, identifiers::{ account_id::AccountId, client_order_id::ClientOrderId, exec_algorithm_id::ExecAlgorithmId, instrument_id::InstrumentId, order_list_id::OrderListId, position_id::PositionId, - strategy_id::StrategyId, trade_id::TradeId, trader_id::TraderId, - venue_order_id::VenueOrderId, + strategy_id::StrategyId, symbol::Symbol, trade_id::TradeId, trader_id::TraderId, + venue::Venue, venue_order_id::VenueOrderId, }, + orders::base::OrderError, types::{price::Price, quantity::Quantity}, }; @@ -44,6 +46,7 @@ pub struct TrailingStopMarketOrder { pub trailing_offset_type: TrailingOffsetType, pub expire_time: Option, pub display_qty: Option, + pub trigger_instrument_id: Option, pub is_triggered: bool, pub ts_triggered: Option, } @@ -68,14 +71,15 @@ impl TrailingStopMarketOrder { quote_quantity: bool, display_qty: Option, emulation_trigger: Option, + trigger_instrument_id: Option, contingency_type: Option, order_list_id: Option, linked_order_ids: Option>, parent_order_id: Option, exec_algorithm_id: Option, - exec_algorithm_params: Option>, + exec_algorithm_params: Option>, exec_spawn_id: Option, - tags: Option, + tags: Option, init_id: UUID4, ts_init: UnixNanos, ) -> Self { @@ -89,7 +93,6 @@ impl TrailingStopMarketOrder { OrderType::TrailingStopMarket, quantity, time_in_force, - false, reduce_only, quote_quantity, emulation_trigger, @@ -110,6 +113,7 @@ impl TrailingStopMarketOrder { trailing_offset_type, expire_time, display_qty, + trigger_instrument_id, is_triggered: false, ts_triggered: None, } @@ -144,6 +148,7 @@ impl Default for TrailingStopMarketOrder { None, None, None, + None, UUID4::default(), 0, ) @@ -181,6 +186,14 @@ impl Order for TrailingStopMarketOrder { self.instrument_id } + fn symbol(&self) -> Symbol { + self.instrument_id.symbol + } + + fn venue(&self) -> Venue { + self.instrument_id.venue + } + fn client_order_id(&self) -> ClientOrderId { self.client_order_id } @@ -217,6 +230,10 @@ impl Order for TrailingStopMarketOrder { self.time_in_force } + fn expire_time(&self) -> Option { + self.expire_time + } + fn price(&self) -> Option { None } @@ -234,7 +251,7 @@ impl Order for TrailingStopMarketOrder { } fn is_post_only(&self) -> bool { - self.is_post_only + false } fn is_reduce_only(&self) -> bool { @@ -245,10 +262,30 @@ impl Order for TrailingStopMarketOrder { self.is_quote_quantity } + fn display_qty(&self) -> Option { + self.display_qty + } + + fn limit_offset(&self) -> Option { + None + } + + fn trailing_offset(&self) -> Option { + Some(self.trailing_offset) + } + + fn trailing_offset_type(&self) -> Option { + Some(self.trailing_offset_type) + } + fn emulation_trigger(&self) -> Option { self.emulation_trigger } + fn trigger_instrument_id(&self) -> Option { + self.trigger_instrument_id + } + fn contingency_type(&self) -> Option { self.contingency_type } @@ -269,7 +306,7 @@ impl Order for TrailingStopMarketOrder { self.exec_algorithm_id } - fn exec_algorithm_params(&self) -> Option> { + fn exec_algorithm_params(&self) -> Option> { self.exec_algorithm_params.clone() } @@ -277,8 +314,8 @@ impl Order for TrailingStopMarketOrder { self.exec_spawn_id } - fn tags(&self) -> Option { - self.tags.clone() + fn tags(&self) -> Option { + self.tags } fn filled_qty(&self) -> Quantity { @@ -320,6 +357,34 @@ impl Order for TrailingStopMarketOrder { fn trade_ids(&self) -> Vec<&TradeId> { self.trade_ids.iter().collect() } + + fn apply(&mut self, event: OrderEvent) -> Result<(), OrderError> { + if let OrderEvent::OrderUpdated(ref event) = event { + self.update(event); + }; + let is_order_filled = matches!(event, OrderEvent::OrderFilled(_)); + + self.core.apply(event)?; + + if is_order_filled { + self.core.set_slippage(self.trigger_price) + }; + + Ok(()) + } + + fn update(&mut self, event: &OrderUpdated) { + if event.price.is_some() { + panic!("{}", OrderError::InvalidOrderEvent); + } + + if let Some(trigger_price) = event.trigger_price { + self.trigger_price = trigger_price; + } + + self.quantity = event.quantity; + self.leaves_qty = self.quantity - self.filled_qty; + } } impl From for TrailingStopMarketOrder { @@ -347,6 +412,7 @@ impl From for TrailingStopMarketOrder { event.quote_quantity, event.display_qty, event.emulation_trigger, + event.trigger_instrument_id, event.contingency_type, event.order_list_id, event.linked_order_ids, @@ -360,42 +426,3 @@ impl From for TrailingStopMarketOrder { ) } } - -impl From<&TrailingStopMarketOrder> for OrderInitialized { - fn from(order: &TrailingStopMarketOrder) -> Self { - Self { - trader_id: order.trader_id, - strategy_id: order.strategy_id, - instrument_id: order.instrument_id, - client_order_id: order.client_order_id, - order_side: order.side, - order_type: order.order_type, - quantity: order.quantity, - price: None, - trigger_price: Some(order.trigger_price), - trigger_type: Some(order.trigger_type), - time_in_force: order.time_in_force, - expire_time: order.expire_time, - post_only: order.is_post_only, - reduce_only: order.is_reduce_only, - quote_quantity: order.is_quote_quantity, - display_qty: order.display_qty, - limit_offset: None, - trailing_offset: Some(order.trailing_offset), - trailing_offset_type: Some(order.trailing_offset_type), - emulation_trigger: order.emulation_trigger, - contingency_type: order.contingency_type, - order_list_id: order.order_list_id, - linked_order_ids: order.linked_order_ids.clone(), - parent_order_id: order.parent_order_id, - exec_algorithm_id: order.exec_algorithm_id, - exec_algorithm_params: order.exec_algorithm_params.clone(), - exec_spawn_id: order.exec_spawn_id, - tags: order.tags.clone(), - event_id: order.init_id, - ts_event: order.ts_init, - ts_init: order.ts_init, - reconciliation: false, - } - } -} diff --git a/nautilus_core/model/src/position.rs b/nautilus_core/model/src/position.rs index ba6d6be6f13c..457690b6354e 100644 --- a/nautilus_core/model/src/position.rs +++ b/nautilus_core/model/src/position.rs @@ -148,14 +148,14 @@ impl Position { self.trade_ids.push(fill.trade_id); // Calculate cumulative commissions - let commission_currency = fill.commission.currency; - let commission_clone = fill.commission; - - if let Some(existing_commission) = self.commissions.get_mut(&commission_currency) { - *existing_commission += commission_clone; - } else { - self.commissions - .insert(commission_currency, fill.commission); + if let Some(commission_value) = fill.commission { + let commission_currency = commission_value.currency; + if let Some(existing_commission) = self.commissions.get_mut(&commission_currency) { + *existing_commission += commission_value; + } else { + self.commissions + .insert(commission_currency, commission_value); + } } // Calculate avg prices, points, return, PnL diff --git a/nautilus_core/model/src/python.rs b/nautilus_core/model/src/python.rs new file mode 100644 index 000000000000..b4821357b9fe --- /dev/null +++ b/nautilus_core/model/src/python.rs @@ -0,0 +1,89 @@ +use pyo3::{ + exceptions::PyValueError, + prelude::*, + types::{PyDict, PyList}, +}; +use serde_json::Value; +use strum::IntoEnumIterator; + +/// Python iterator over the variants of an enum. +#[pyclass] +pub struct EnumIterator { + // Type erasure for code reuse. Generic types can't be exposed to Python. + iter: Box + Send>, +} + +#[pymethods] +impl EnumIterator { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + + fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { + slf.iter.next() + } +} + +impl EnumIterator { + pub fn new(py: Python<'_>) -> Self + where + E: strum::IntoEnumIterator + IntoPy>, + ::Iterator: Send, + { + Self { + iter: Box::new( + E::iter() + .map(|var| var.into_py(py)) + // Force eager evaluation because `py` isn't `Send` + .collect::>() + .into_iter(), + ), + } + } +} + +pub fn value_to_pydict(py: Python<'_>, val: &Value) -> PyResult> { + let dict = PyDict::new(py); + + match val { + Value::Object(map) => { + for (key, value) in map.iter() { + let py_value = value_to_pyobject(py, value)?; + dict.set_item(key, py_value)?; + } + } + // This shouldn't be reached in this function, but we include it for completeness + _ => return Err(PyValueError::new_err("Expected JSON object")), + } + + Ok(dict.into_py(py)) +} + +pub fn value_to_pyobject(py: Python<'_>, val: &Value) -> PyResult { + match val { + Value::Null => Ok(py.None()), + Value::Bool(b) => Ok(b.into_py(py)), + Value::String(s) => Ok(s.into_py(py)), + Value::Number(n) => { + if n.is_i64() { + Ok(n.as_i64().unwrap().into_py(py)) + } else if n.is_f64() { + Ok(n.as_f64().unwrap().into_py(py)) + } else { + Err(PyValueError::new_err("Unsupported JSON number type")) + } + } + Value::Array(arr) => { + let py_list = PyList::new(py, &[] as &[PyObject]); + for item in arr.iter() { + let py_item = value_to_pyobject(py, item)?; + py_list.append(py_item)?; + } + Ok(py_list.into()) + } + Value::Object(_) => { + let py_dict = value_to_pydict(py, val)?; + Ok(py_dict.into()) + } + } +} diff --git a/nautilus_core/model/src/types/currency.rs b/nautilus_core/model/src/types/currency.rs index a11c0fc1a378..1527f0a5bf9b 100644 --- a/nautilus_core/model/src/types/currency.rs +++ b/nautilus_core/model/src/types/currency.rs @@ -197,7 +197,26 @@ pub unsafe extern "C" fn currency_from_cstr(code_ptr: *const c_char) -> Currency //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { - use crate::{enums::CurrencyType, types::currency::Currency}; + use nautilus_core::string::str_to_cstr; + + use super::currency_register; + use crate::{ + enums::CurrencyType, + types::currency::{currency_exists, Currency}, + }; + + #[test] + #[should_panic(expected = "`Currency` code")] + fn test_invalid_currency_code() { + let _ = Currency::new("", 2, 840, "United States dollar", CurrencyType::Fiat); + } + + #[test] + #[should_panic(expected = "`Currency` precision")] + fn test_invalid_currency_precision() { + // Precision out of range for fixed + let _ = Currency::new("USD", 10, 840, "United States dollar", CurrencyType::Fiat); + } #[test] fn test_currency_new_for_fiat() { @@ -220,4 +239,28 @@ mod tests { assert_eq!(currency.name.as_str(), "Ether"); assert_eq!(currency.currency_type, CurrencyType::Crypto); } + + #[test] + fn test_currency_equality() { + let currency1 = Currency::new("USD", 2, 840, "United States dollar", CurrencyType::Fiat); + let currency2 = Currency::new("USD", 2, 840, "United States dollar", CurrencyType::Fiat); + assert_eq!(currency1, currency2); + } + + #[test] + fn test_currency_serialization_deserialization() { + let currency = Currency::new("USD", 2, 840, "United States dollar", CurrencyType::Fiat); + let serialized = serde_json::to_string(¤cy).unwrap(); + let deserialized: Currency = serde_json::from_str(&serialized).unwrap(); + assert_eq!(currency, deserialized); + } + + #[test] + fn test_currency_registration() { + let currency = Currency::new("MYC", 4, 0, "My Currency", CurrencyType::Crypto); + currency_register(currency); + unsafe { + assert_eq!(currency_exists(str_to_cstr("MYC")), 1); + } + } } diff --git a/nautilus_core/model/src/types/fixed.rs b/nautilus_core/model/src/types/fixed.rs index f9d614bd2dd6..356b7a537be0 100644 --- a/nautilus_core/model/src/types/fixed.rs +++ b/nautilus_core/model/src/types/fixed.rs @@ -53,96 +53,87 @@ mod tests { use super::*; - #[rstest(precision, value, - case(0, 0.0), - case(1, 1.0), - case(1, 1.1), - case(9, 0.000_000_001), - case(0, -0.0), - case(1, -1.0), - case(1, -1.1), - case(9, -0.000_000_001), - )] - fn test_f64_to_fixed_i64_to_fixed(precision: u8, value: f64) { + #[rstest] + #[case(0, 0.0)] + #[case(1, 1.0)] + #[case(1, 1.1)] + #[case(9, 0.000_000_001)] + #[case(0, -0.0)] + #[case(1, -1.0)] + #[case(1, -1.1)] + #[case(9, -0.000_000_001)] + fn test_f64_to_fixed_i64_to_fixed(#[case] precision: u8, #[case] value: f64) { let fixed = f64_to_fixed_i64(value, precision); let result = fixed_i64_to_f64(fixed); assert_eq!(result, value); } - #[rstest( - precision, - value, - case(0, 0.0), - case(1, 1.0), - case(1, 1.1), - case(9, 0.000_000_001) - )] - fn test_f64_to_fixed_u64_to_fixed(precision: u8, value: f64) { + #[rstest] + #[case(0, 0.0)] + #[case(1, 1.0)] + #[case(1, 1.1)] + #[case(9, 0.000_000_001)] + fn test_f64_to_fixed_u64_to_fixed(#[case] precision: u8, #[case] value: f64) { let fixed = f64_to_fixed_u64(value, precision); let result = fixed_u64_to_f64(fixed); assert_eq!(result, value); } - #[rstest( - precision, - value, - expected, - case(0, 123_456.0, 123_456_000_000_000), - case(0, 123_456.7, 123_457_000_000_000), - case(0, 123_456.4, 123_456_000_000_000), - case(1, 123_456.0, 123_456_000_000_000), - case(1, 123_456.7, 123_456_700_000_000), - case(1, 123_456.4, 123_456_400_000_000), - case(2, 123_456.0, 123_456_000_000_000), - case(2, 123_456.7, 123_456_700_000_000), - case(2, 123_456.4, 123_456_400_000_000) - )] - fn test_f64_to_fixed_i64_with_precision(precision: u8, value: f64, expected: i64) { + #[rstest] + #[case(0, 123_456.0, 123_456_000_000_000)] + #[case(0, 123_456.7, 123_457_000_000_000)] + #[case(0, 123_456.4, 123_456_000_000_000)] + #[case(1, 123_456.0, 123_456_000_000_000)] + #[case(1, 123_456.7, 123_456_700_000_000)] + #[case(1, 123_456.4, 123_456_400_000_000)] + #[case(2, 123_456.0, 123_456_000_000_000)] + #[case(2, 123_456.7, 123_456_700_000_000)] + #[case(2, 123_456.4, 123_456_400_000_000)] + fn test_f64_to_fixed_i64_with_precision( + #[case] precision: u8, + #[case] value: f64, + #[case] expected: i64, + ) { assert_eq!(f64_to_fixed_i64(value, precision), expected); } - #[rstest(precision, value, expected, - case(0, 5.5, 6_000_000_000), - case(1, 5.55, 5_600_000_000), - case(2, 5.555, 5_560_000_000), - case(3, 5.5555, 5_556_000_000), - case(4, 5.55555, 5_555_600_000), - case(5, 5.555_555, 5_555_560_000), - case(6, 5.555_555_5, 5_555_556_000), - case(7, 5.555_555_55, 5_555_555_600), - case(8, 5.555_555_555, 5_555_555_560), - case(9, 5.555_555_555_5, 5_555_555_556), - case(0, -5.5, -6_000_000_000), - case(1, -5.55, -5_600_000_000), - case(2, -5.555, -5_560_000_000), - case(3, -5.5555, -5_556_000_000), - case(4, -5.55555, -5_555_600_000), - case(5, -5.555_555, -5_555_560_000), - case(6, -5.555_555_5, -5_555_556_000), - case(7, -5.555_555_55, -5_555_555_600), - case(8, -5.555_555_555, -5_555_555_560), - case(9, -5.555_555_555_5, -5_555_555_556), - )] - fn test_f64_to_fixed_i64(precision: u8, value: f64, expected: i64) { + #[rstest] + #[case(0, 5.5, 6_000_000_000)] + #[case(1, 5.55, 5_600_000_000)] + #[case(2, 5.555, 5_560_000_000)] + #[case(3, 5.5555, 5_556_000_000)] + #[case(4, 5.55555, 5_555_600_000)] + #[case(5, 5.555_555, 5_555_560_000)] + #[case(6, 5.555_555_5, 5_555_556_000)] + #[case(7, 5.555_555_55, 5_555_555_600)] + #[case(8, 5.555_555_555, 5_555_555_560)] + #[case(9, 5.555_555_555_5, 5_555_555_556)] + #[case(0, -5.5, -6_000_000_000)] + #[case(1, -5.55, -5_600_000_000)] + #[case(2, -5.555, -5_560_000_000)] + #[case(3, -5.5555, -5_556_000_000)] + #[case(4, -5.55555, -5_555_600_000)] + #[case(5, -5.555_555, -5_555_560_000)] + #[case(6, -5.555_555_5, -5_555_556_000)] + #[case(7, -5.555_555_55, -5_555_555_600)] + #[case(8, -5.555_555_555, -5_555_555_560)] + #[case(9, -5.555_555_555_5, -5_555_555_556)] + fn test_f64_to_fixed_i64(#[case] precision: u8, #[case] value: f64, #[case] expected: i64) { assert_eq!(f64_to_fixed_i64(value, precision), expected); } - #[rstest( - precision, - value, - expected, - case(0, 5.5, 6_000_000_000), - case(1, 5.55, 5_600_000_000), - case(2, 5.555, 5_560_000_000), - case(3, 5.5555, 5_556_000_000), - case(4, 5.55555, 5_555_600_000), - case(5, 5.555_555, 5_555_560_000), - case(6, 5.555_555_5, 5_555_556_000), - case(7, 5.555_555_55, 5_555_555_600), - case(8, 5.555_555_555, 5_555_555_560), - case(9, 5.555_555_555_5, 5_555_555_556) - )] - fn test_f64_to_fixed_u64(precision: u8, value: f64, expected: u64) { + #[rstest] + #[case(0, 5.5, 6_000_000_000)] + #[case(1, 5.55, 5_600_000_000)] + #[case(2, 5.555, 5_560_000_000)] + #[case(3, 5.5555, 5_556_000_000)] + #[case(4, 5.55555, 5_555_600_000)] + #[case(5, 5.555_555, 5_555_560_000)] + #[case(6, 5.555_555_5, 5_555_556_000)] + #[case(7, 5.555_555_55, 5_555_555_600)] + #[case(8, 5.555_555_555, 5_555_555_560)] + #[case(9, 5.555_555_555_5, 5_555_555_556)] + fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64, #[case] expected: u64) { assert_eq!(f64_to_fixed_u64(value, precision), expected); } diff --git a/nautilus_core/model/src/types/money.rs b/nautilus_core/model/src/types/money.rs index 2d5b246efd0c..fc3aa911d805 100644 --- a/nautilus_core/model/src/types/money.rs +++ b/nautilus_core/model/src/types/money.rs @@ -17,14 +17,16 @@ use std::{ cmp::Ordering, fmt::{Display, Formatter}, hash::{Hash, Hasher}, - ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}, + ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign}, str::FromStr, }; use nautilus_core::correctness; use pyo3::prelude::*; +use rust_decimal::Decimal; use serde::{Deserialize, Deserializer, Serialize}; +use super::fixed::FIXED_PRECISION; use crate::types::{ currency::Currency, fixed::{f64_to_fixed_i64, fixed_i64_to_f64}, @@ -37,7 +39,7 @@ pub const MONEY_MIN: f64 = -9_223_372_036.0; #[derive(Clone, Copy, Debug, Eq)] #[pyclass] pub struct Money { - raw: i64, + pub raw: i64, pub currency: Currency, } @@ -61,10 +63,31 @@ impl Money { pub fn is_zero(&self) -> bool { self.raw == 0 } + #[must_use] pub fn as_f64(&self) -> f64 { fixed_i64_to_f64(self.raw) } + + #[must_use] + pub fn as_decimal(&self) -> Decimal { + // Scale down the raw value to match the precision + let precision = self.currency.precision; + let rescaled_raw = self.raw / i64::pow(10, (FIXED_PRECISION - precision) as u32); + Decimal::from_i128_with_scale(rescaled_raw as i128, precision as u32) + } +} + +impl From for f64 { + fn from(money: Money) -> Self { + money.as_f64() + } +} + +impl From<&Money> for f64 { + fn from(money: &Money) -> Self { + money.as_f64() + } } impl Hash for Money { @@ -145,17 +168,6 @@ impl Sub for Money { } } -impl Mul for Money { - type Output = Self; - fn mul(self, rhs: Self) -> Self { - assert_eq!(self.currency, rhs.currency); - Self { - raw: self.raw * rhs.raw, - currency: self.currency, - } - } -} - impl AddAssign for Money { fn add_assign(&mut self, other: Self) { assert_eq!(self.currency, other.currency); @@ -170,12 +182,6 @@ impl SubAssign for Money { } } -impl MulAssign for Money { - fn mul_assign(&mut self, multiplier: Self) { - self.raw *= multiplier.raw; - } -} - impl Add for Money { type Output = f64; fn add(self, rhs: f64) -> Self::Output { @@ -244,6 +250,29 @@ impl<'de> Deserialize<'de> for Money { } } +#[pymethods] +impl Money { + #[getter] + fn raw(&self) -> i64 { + self.raw + } + + #[getter] + fn currency(&self) -> Currency { + self.currency + } + + #[pyo3(name = "as_double")] + fn py_as_double(&self) -> f64 { + fixed_i64_to_f64(self.raw) + } + + // #[pyo3(name = "as_decimal")] + // fn py_as_decimal(&self) -> Decimal { + // self.as_decimal() + // } +} + //////////////////////////////////////////////////////////////////////////////// // C API //////////////////////////////////////////////////////////////////////////////// @@ -277,15 +306,51 @@ pub extern "C" fn money_sub_assign(mut a: Money, b: Money) { //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { + use float_cmp::approx_eq; + use rust_decimal_macros::dec; + use super::*; use crate::currencies::{BTC, USD}; + #[test] + #[should_panic] + fn test_money_different_currency_addition() { + let usd = Money::new(1000.0, USD.clone()); + let btc = Money::new(1.0, BTC.clone()); + let _result = usd + btc; // This should panic since currencies are different + } + + #[test] + fn test_money_min_max_values() { + let min_money = Money::new(MONEY_MIN, USD.clone()); + let max_money = Money::new(MONEY_MAX, USD.clone()); + assert_eq!(min_money.raw, f64_to_fixed_i64(MONEY_MIN, USD.precision)); + assert_eq!(max_money.raw, f64_to_fixed_i64(MONEY_MAX, USD.precision)); + } + + #[test] + fn test_money_addition_f64() { + let money = Money::new(1000.0, USD.clone()); + let result = money + 500.0; + assert_eq!(result, 1500.0); + } + + #[test] + fn test_money_negation() { + let money = Money::new(100.0, USD.clone()); + let result = -money; + assert_eq!(result.as_f64(), -100.0); + assert_eq!(result.currency, USD.clone()); + } + #[test] fn test_money_new_usd() { let money = Money::new(1000.0, USD.clone()); assert_eq!(money.currency.code.as_str(), "USD"); assert_eq!(money.currency.precision, 2); assert_eq!(money.to_string(), "1000.00 USD"); + assert_eq!(money.as_decimal(), dec!(1000.00)); + assert!(approx_eq!(f64, money.as_f64(), 1000.0, epsilon = 0.001)); } #[test] @@ -296,29 +361,11 @@ mod tests { assert_eq!(money.to_string(), "10.30000000 BTC"); } - // #[test] - // fn test_account_balance() { - // let usd = Currency { - // code: String::from("USD"), - // precision: 2, - // currency_type: CurrencyType::Fiat, - // }; - // let balance = AccountBalance { - // currency: usd, - // total: Money { - // amount: Decimal::new(103, 1), - // currency: usd, - // }, - // locked: Money { - // amount: Decimal::new(0, 0), - // currency: usd, - // }, - // free: Money { - // amount: Decimal::new(103, 1), - // currency: usd, - // }, - // }; - // - // assert_eq!(balance.to_string(), "10.30000000 BTC"); - // } + #[test] + fn test_money_serialization_deserialization() { + let money = Money::new(123.45, USD.clone()); + let serialized = serde_json::to_string(&money).unwrap(); + let deserialized: Money = serde_json::from_str(&serialized).unwrap(); + assert_eq!(money, deserialized); + } } diff --git a/nautilus_core/model/src/types/price.rs b/nautilus_core/model/src/types/price.rs index 7ea090efb746..0b4d0492783f 100644 --- a/nautilus_core/model/src/types/price.rs +++ b/nautilus_core/model/src/types/price.rs @@ -17,15 +17,16 @@ use std::{ cmp::Ordering, fmt::{Debug, Display, Formatter}, hash::{Hash, Hasher}, - ops::{Add, AddAssign, Deref, Mul, MulAssign, Neg, Sub, SubAssign}, + ops::{Add, AddAssign, Deref, Mul, Neg, Sub, SubAssign}, str::FromStr, }; use nautilus_core::{correctness, parsing::precision_from_str}; use pyo3::prelude::*; +use rust_decimal::Decimal; use serde::{Deserialize, Deserializer, Serialize}; -use super::fixed::FIXED_SCALAR; +use super::fixed::{FIXED_PRECISION, FIXED_SCALAR}; use crate::types::fixed::{f64_to_fixed_i64, fixed_i64_to_f64}; pub const PRICE_MAX: f64 = 9_223_372_036.0; @@ -91,6 +92,13 @@ impl Price { pub fn as_f64(&self) -> f64 { fixed_i64_to_f64(self.raw) } + + #[must_use] + pub fn as_decimal(&self) -> Decimal { + // Scale down the raw value to match the precision + let rescaled_raw = self.raw / i64::pow(10, (FIXED_PRECISION - self.precision) as u32); + Decimal::from_i128_with_scale(rescaled_raw as i128, self.precision as u32) + } } impl FromStr for Price { @@ -112,14 +120,14 @@ impl From<&str> for Price { } impl From for f64 { - fn from(value: Price) -> Self { - value.as_f64() + fn from(price: Price) -> Self { + price.as_f64() } } impl From<&Price> for f64 { - fn from(value: &Price) -> Self { - value.as_f64() + fn from(price: &Price) -> Self { + price.as_f64() } } @@ -201,16 +209,6 @@ impl Sub for Price { } } -impl Mul for Price { - type Output = Self; - fn mul(self, rhs: Self) -> Self { - Self { - raw: (self.raw * rhs.raw) / (FIXED_SCALAR as i64), - precision: self.precision, - } - } -} - impl AddAssign for Price { fn add_assign(&mut self, other: Self) { self.raw += other.raw; @@ -223,12 +221,6 @@ impl SubAssign for Price { } } -impl MulAssign for Price { - fn mul_assign(&mut self, multiplier: Self) { - self.raw *= multiplier.raw; - } -} - impl Add for Price { type Output = f64; fn add(self, rhs: f64) -> Self::Output { @@ -282,6 +274,29 @@ impl<'de> Deserialize<'de> for Price { } } +#[pymethods] +impl Price { + #[getter] + fn raw(&self) -> i64 { + self.raw + } + + #[getter] + fn precision(&self) -> u8 { + self.precision + } + + #[pyo3(name = "as_double")] + fn py_as_double(&self) -> f64 { + fixed_i64_to_f64(self.raw) + } + + // #[pyo3(name = "as_decimal")] + // fn py_as_decimal(&self) -> Decimal { + // self.as_decimal() + // } +} + //////////////////////////////////////////////////////////////////////////////// // C API //////////////////////////////////////////////////////////////////////////////// @@ -317,6 +332,9 @@ pub extern "C" fn price_sub_assign(mut a: Price, b: Price) { mod tests { use std::str::FromStr; + use float_cmp::approx_eq; + use rust_decimal_macros::dec; + use super::*; #[test] @@ -328,6 +346,8 @@ mod tests { assert_eq!(price.as_f64(), 0.00812); assert_eq!(price.to_string(), "0.00812000"); assert!(!price.is_zero()); + assert_eq!(price.as_decimal(), dec!(0.00812000)); + assert!(approx_eq!(f64, price.as_f64(), 0.00812, epsilon = 0.000001)); } #[test] @@ -465,20 +485,12 @@ mod tests { fn test_mul() { let price1 = Price::new(1.000, 3); let price2 = Price::new(1.011, 3); - let price3 = price1 * price2; - assert_eq!(price3.raw, 1_011_000_000); + let result = price1 * price2.into(); + assert!(approx_eq!(f64, result, 1.011, epsilon = 0.000001)); } #[test] - fn test_mul_assign() { - let mut price1 = Price::new(1.000, 3); - let price2 = Price::new(1.011, 3); - price1 *= price2; - assert_eq!(price1.raw, 1_011_000_000_000_000_000); - } - - #[test] - fn test_display_works() { + fn test_display() { use std::fmt::Write as FmtWrite; let input_string = "44.12"; let price = Price::from_str(input_string).unwrap(); @@ -486,14 +498,4 @@ mod tests { write!(&mut res, "{price}").unwrap(); assert_eq!(res, input_string); } - - #[test] - fn test_display() { - let input_string = "44.123456"; - let price = Price::from_str(input_string).unwrap(); - assert_eq!(price.raw, 44_123_456_000); - assert_eq!(price.precision, 6); - assert_eq!(price.as_f64(), 44.123_456_000_000_004); - assert_eq!(price.to_string(), input_string); - } } diff --git a/nautilus_core/model/src/types/quantity.rs b/nautilus_core/model/src/types/quantity.rs index de5555877ab7..f948b0b7b724 100644 --- a/nautilus_core/model/src/types/quantity.rs +++ b/nautilus_core/model/src/types/quantity.rs @@ -23,9 +23,10 @@ use std::{ use nautilus_core::{correctness, parsing::precision_from_str}; use pyo3::prelude::*; +use rust_decimal::Decimal; use serde::{Deserialize, Deserializer, Serialize}; -use super::fixed::FIXED_SCALAR; +use super::fixed::{FIXED_PRECISION, FIXED_SCALAR}; use crate::types::fixed::{f64_to_fixed_u64, fixed_u64_to_f64}; pub const QUANTITY_MAX: f64 = 18_446_744_073.0; @@ -74,17 +75,24 @@ impl Quantity { pub fn as_f64(&self) -> f64 { fixed_u64_to_f64(self.raw) } + + #[must_use] + pub fn as_decimal(&self) -> Decimal { + // Scale down the raw value to match the precision + let rescaled_raw = self.raw / u64::pow(10, (FIXED_PRECISION - self.precision) as u32); + Decimal::from_i128_with_scale(rescaled_raw as i128, self.precision as u32) + } } impl From for f64 { - fn from(value: Quantity) -> Self { - value.as_f64() + fn from(qty: Quantity) -> Self { + qty.as_f64() } } impl From<&Quantity> for f64 { - fn from(value: &Quantity) -> Self { - value.as_f64() + fn from(qty: &Quantity) -> Self { + qty.as_f64() } } @@ -190,6 +198,13 @@ impl Mul for Quantity { } } +impl Mul for Quantity { + type Output = f64; + fn mul(self, rhs: f64) -> Self::Output { + self.as_f64() * rhs + } +} + impl From for u64 { fn from(value: Quantity) -> Self { value.raw @@ -252,6 +267,29 @@ impl<'de> Deserialize<'de> for Quantity { } } +#[pymethods] +impl Quantity { + #[getter] + fn raw(&self) -> u64 { + self.raw + } + + #[getter] + fn precision(&self) -> u8 { + self.precision + } + + #[pyo3(name = "as_double")] + fn py_as_double(&self) -> f64 { + self.as_f64() + } + + // #[pyo3(name = "as_decimal")] + // fn py_as_decimal(&self, py: Python<'py>) -> Decimal { + // self.as_decimal().into_py(py) + // } +} + //////////////////////////////////////////////////////////////////////////////// // C API //////////////////////////////////////////////////////////////////////////////// @@ -297,6 +335,9 @@ pub extern "C" fn quantity_sub_assign_u64(mut a: Quantity, b: u64) { mod tests { use std::str::FromStr; + use float_cmp::approx_eq; + use rust_decimal_macros::dec; + use super::*; #[test] @@ -309,6 +350,8 @@ mod tests { assert_eq!(qty.to_string(), "0.00812000"); assert!(!qty.is_zero()); assert!(qty.is_positive()); + assert_eq!(qty.as_decimal(), dec!(0.00812000)); + assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000001)); } #[test] @@ -432,14 +475,6 @@ mod tests { assert_eq!(quantity3.raw, 4_000_000_000); } - #[test] - fn test_mul_assign() { - let mut q = Quantity::from_raw(100, 0); - q *= 2u64; - - assert_eq!(q.raw, 200); - } - #[test] fn test_equality() { assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1)); diff --git a/nautilus_core/network/benches/test_client.rs b/nautilus_core/network/benches/test_client.rs index 7b9881811c62..2525fd0e9b6a 100644 --- a/nautilus_core/network/benches/test_client.rs +++ b/nautilus_core/network/benches/test_client.rs @@ -23,7 +23,7 @@ const TOTAL: usize = 1_000_000; #[tokio::main] async fn main() { - let client = HttpClient::new(Vec::new()); + let client = HttpClient::py_new(Vec::new()); let mut reqs = Vec::new(); for _ in 0..(TOTAL / CONCURRENCY) { for _ in 0..CONCURRENCY { diff --git a/nautilus_core/network/src/http.rs b/nautilus_core/network/src/http.rs index 7f64c88cdbb0..99509add490a 100644 --- a/nautilus_core/network/src/http.rs +++ b/nautilus_core/network/src/http.rs @@ -68,8 +68,7 @@ impl HttpResponse { impl HttpClient { #[new] #[pyo3(signature=(header_keys=[].to_vec()))] - #[must_use] - pub fn new(header_keys: Vec) -> Self { + pub fn py_new(header_keys: Vec) -> Self { let https = HttpsConnector::new(); let client = Client::builder().build::<_, hyper::Body>(https); diff --git a/nautilus_core/network/src/socket.rs b/nautilus_core/network/src/socket.rs index 21061b793186..fff6f9b355f2 100644 --- a/nautilus_core/network/src/socket.rs +++ b/nautilus_core/network/src/socket.rs @@ -355,11 +355,11 @@ mod tests { class Counter: def __init__(self): self.count = 0 - + def handler(self, bytes): if bytes.decode().rstrip() == 'ping': self.count = self.count + 1 - + def get_count(self): return self.count diff --git a/nautilus_core/network/src/websocket.rs b/nautilus_core/network/src/websocket.rs index d9f98e1df466..934705a8239d 100644 --- a/nautilus_core/network/src/websocket.rs +++ b/nautilus_core/network/src/websocket.rs @@ -259,7 +259,7 @@ impl WebSocketClient { Python::with_gil(|py| match handler.call0(py) { Ok(_) => debug!("Called post_connection handler"), Err(err) => error!("post_connection handler failed because: {}", err), - }) + }); } Ok(Self { @@ -319,9 +319,9 @@ impl WebSocketClient { Python::with_gil(|py| match handler.call0(py) { Ok(_) => debug!("Called post_reconnection handler"), Err(err) => { - error!("post_reconnection handler failed because: {}", err) + error!("post_reconnection handler failed because: {}", err); } - }) + }); } } Err(err) => { @@ -336,9 +336,9 @@ impl WebSocketClient { Python::with_gil(|py| match handler.call0(py) { Ok(_) => debug!("Called post_reconnection handler"), Err(err) => { - error!("post_reconnection handler failed because: {}", err) + error!("post_reconnection handler failed because: {}", err); } - }) + }); } break; } @@ -385,6 +385,20 @@ impl WebSocketClient { }) } + /// Send text data to the connection. + /// + /// # Safety + /// - Throws an Exception if it is not able to send data + fn send_text<'py>(slf: PyRef<'_, Self>, data: String, py: Python<'py>) -> PyResult<&'py PyAny> { + let writer = slf.writer.clone(); + pyo3_asyncio::tokio::future_into_py(py, async move { + let mut guard = writer.lock().await; + guard.send(Message::Text(data)).await.map_err(|err| { + PyException::new_err(format!("Unable to send data because of error: {}", err)) + }) + }) + } + /// Send bytes data to the connection. /// /// # Safety @@ -510,11 +524,11 @@ mod tests { class Counter: def __init__(self): self.count = 0 - + def handler(self, bytes): if bytes.decode() == 'ping': self.count = self.count + 1 - + def get_count(self): return self.count diff --git a/nautilus_core/network/tokio-tungstenite/src/tls.rs b/nautilus_core/network/tokio-tungstenite/src/tls.rs index 3fd20c12e889..d7e7702f37cf 100644 --- a/nautilus_core/network/tokio-tungstenite/src/tls.rs +++ b/nautilus_core/network/tokio-tungstenite/src/tls.rs @@ -113,7 +113,7 @@ pub mod encryption { } #[cfg(feature = "rustls-tls-webpki-roots")] { - root_store.add_server_trust_anchors( + root_store.add_trust_anchors( webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| { rustls::OwnedTrustAnchor::from_subject_spki_name_constraints( ta.subject, diff --git a/nautilus_core/persistence/benches/bench_persistence.rs b/nautilus_core/persistence/benches/bench_persistence.rs index 3d76a4115262..6c0ee5173335 100644 --- a/nautilus_core/persistence/benches/bench_persistence.rs +++ b/nautilus_core/persistence/benches/bench_persistence.rs @@ -28,7 +28,7 @@ fn single_stream_bench(c: &mut Criterion) { let file_path = "../../bench_data/quotes_0005.parquet"; group.bench_function("persistence v2", |b| { - b.iter_batched( + b.iter_batched_ref( || { let rt = get_runtime(); let mut catalog = DataBackendSession::new(chunk_size); @@ -56,7 +56,7 @@ fn multi_stream_bench(c: &mut Criterion) { let dir_path = "../../bench_data/multi_stream_data"; group.bench_function("persistence v2", |b| { - b.iter_batched( + b.iter_batched_ref( || { let rt = get_runtime(); let mut catalog = DataBackendSession::new(chunk_size); diff --git a/nautilus_core/persistence/src/arrow/bar.rs b/nautilus_core/persistence/src/arrow/bar.rs index 5e861e19adcf..d858126450cc 100644 --- a/nautilus_core/persistence/src/arrow/bar.rs +++ b/nautilus_core/persistence/src/arrow/bar.rs @@ -17,7 +17,7 @@ use std::{collections::HashMap, str::FromStr, sync::Arc}; use datafusion::arrow::{ array::{Array, Int64Array, UInt64Array}, - datatypes::{DataType, Field, Schema, SchemaRef}, + datatypes::{DataType, Field, Schema}, record_batch::RecordBatch, }; use nautilus_model::{ @@ -29,7 +29,7 @@ use super::DecodeDataFromRecordBatch; use crate::arrow::{ArrowSchemaProvider, Data, DecodeFromRecordBatch, EncodeToRecordBatch}; impl ArrowSchemaProvider for Bar { - fn get_schema(metadata: std::collections::HashMap) -> SchemaRef { + fn get_schema(metadata: Option>) -> Schema { let fields = vec![ Field::new("open", DataType::Int64, false), Field::new("high", DataType::Int64, false), @@ -40,7 +40,10 @@ impl ArrowSchemaProvider for Bar { Field::new("ts_init", DataType::UInt64, false), ]; - Schema::new_with_metadata(fields, metadata).into() + match metadata { + Some(metadata) => Schema::new_with_metadata(fields, metadata), + None => Schema::new(fields), + } } } @@ -93,7 +96,7 @@ impl EncodeToRecordBatch for Bar { // Build record batch RecordBatch::try_new( - Self::get_schema(metadata.clone()), + Self::get_schema(Some(metadata.clone())).into(), vec![ Arc::new(open_array), Arc::new(high_array), @@ -174,7 +177,7 @@ mod tests { fn test_get_schema() { let bar_type = BarType::from_str("AAPL.NASDAQ-1-MINUTE-LAST-INTERNAL").unwrap(); let metadata = Bar::get_metadata(&bar_type, 2, 0); - let schema = Bar::get_schema(metadata.clone()); + let schema = Bar::get_schema(Some(metadata.clone())); let expected_fields = vec![ Field::new("open", DataType::Int64, false), Field::new("high", DataType::Int64, false), @@ -184,10 +187,24 @@ mod tests { Field::new("ts_event", DataType::UInt64, false), Field::new("ts_init", DataType::UInt64, false), ]; - let expected_schema = Schema::new_with_metadata(expected_fields, metadata).into(); + let expected_schema = Schema::new_with_metadata(expected_fields, metadata); assert_eq!(schema, expected_schema); } + #[test] + fn test_get_schema_map() { + let schema_map = Bar::get_schema_map(); + let mut expected_map = HashMap::new(); + expected_map.insert("open".to_string(), "Int64".to_string()); + expected_map.insert("high".to_string(), "Int64".to_string()); + expected_map.insert("low".to_string(), "Int64".to_string()); + expected_map.insert("close".to_string(), "Int64".to_string()); + expected_map.insert("volume".to_string(), "UInt64".to_string()); + expected_map.insert("ts_event".to_string(), "UInt64".to_string()); + expected_map.insert("ts_init".to_string(), "UInt64".to_string()); + assert_eq!(schema_map, expected_map); + } + #[test] fn test_encode_batch() { let bar_type = BarType::from_str("AAPL.NASDAQ-1-MINUTE-LAST-INTERNAL").unwrap(); @@ -264,7 +281,7 @@ mod tests { let ts_init = UInt64Array::from(vec![3, 4]); let record_batch = RecordBatch::try_new( - Bar::get_schema(metadata.clone()), + Bar::get_schema(Some(metadata.clone())).into(), vec![ Arc::new(open), Arc::new(high), diff --git a/nautilus_core/persistence/src/arrow/delta.rs b/nautilus_core/persistence/src/arrow/delta.rs index 560de148e15e..6c36f1c34b6c 100644 --- a/nautilus_core/persistence/src/arrow/delta.rs +++ b/nautilus_core/persistence/src/arrow/delta.rs @@ -17,7 +17,7 @@ use std::{collections::HashMap, str::FromStr, sync::Arc}; use datafusion::arrow::{ array::{Array, Int64Array, UInt64Array, UInt8Array}, - datatypes::{DataType, Field, Schema, SchemaRef}, + datatypes::{DataType, Field, Schema}, record_batch::RecordBatch, }; use nautilus_model::{ @@ -31,7 +31,7 @@ use super::DecodeDataFromRecordBatch; use crate::arrow::{ArrowSchemaProvider, Data, DecodeFromRecordBatch, EncodeToRecordBatch}; impl ArrowSchemaProvider for OrderBookDelta { - fn get_schema(metadata: HashMap) -> SchemaRef { + fn get_schema(metadata: Option>) -> Schema { let fields = vec![ Field::new("action", DataType::UInt8, false), Field::new("side", DataType::UInt8, false), @@ -44,7 +44,10 @@ impl ArrowSchemaProvider for OrderBookDelta { Field::new("ts_init", DataType::UInt64, false), ]; - Schema::new_with_metadata(fields, metadata).into() + match metadata { + Some(metadata) => Schema::new_with_metadata(fields, metadata), + None => Schema::new(fields), + } } } @@ -104,7 +107,7 @@ impl EncodeToRecordBatch for OrderBookDelta { // Build record batch RecordBatch::try_new( - Self::get_schema(metadata.clone()), + Self::get_schema(Some(metadata.clone())).into(), vec![ Arc::new(action_array), Arc::new(side_array), @@ -200,7 +203,7 @@ mod tests { fn test_get_schema() { let instrument_id = InstrumentId::from_str("AAPL.NASDAQ").unwrap(); let metadata = OrderBookDelta::get_metadata(&instrument_id, 2, 0); - let schema = OrderBookDelta::get_schema(metadata.clone()); + let schema = OrderBookDelta::get_schema(Some(metadata.clone())); let expected_fields = vec![ Field::new("action", DataType::UInt8, false), Field::new("side", DataType::UInt8, false), @@ -212,17 +215,33 @@ mod tests { Field::new("ts_event", DataType::UInt64, false), Field::new("ts_init", DataType::UInt64, false), ]; - let expected_schema = Schema::new_with_metadata(expected_fields, metadata).into(); + let expected_schema = Schema::new_with_metadata(expected_fields, metadata); assert_eq!(schema, expected_schema); } + #[test] + fn test_get_schema_map() { + let schema_map = OrderBookDelta::get_schema_map(); + let mut expected_map = HashMap::new(); + expected_map.insert("action".to_string(), "UInt8".to_string()); + expected_map.insert("side".to_string(), "UInt8".to_string()); + expected_map.insert("price".to_string(), "Int64".to_string()); + expected_map.insert("size".to_string(), "UInt64".to_string()); + expected_map.insert("order_id".to_string(), "UInt64".to_string()); + expected_map.insert("flags".to_string(), "UInt8".to_string()); + expected_map.insert("sequence".to_string(), "UInt64".to_string()); + expected_map.insert("ts_event".to_string(), "UInt64".to_string()); + expected_map.insert("ts_init".to_string(), "UInt64".to_string()); + assert_eq!(schema_map, expected_map); + } + #[test] fn test_encode_batch() { let instrument_id = InstrumentId::from_str("AAPL.NASDAQ").unwrap(); let metadata = OrderBookDelta::get_metadata(&instrument_id, 2, 0); let delta1 = OrderBookDelta { - instrument_id: instrument_id, + instrument_id, action: BookAction::Add, order: BookOrder { side: OrderSide::Buy, @@ -311,7 +330,7 @@ mod tests { let ts_init = UInt64Array::from(vec![3, 4]); let record_batch = RecordBatch::try_new( - OrderBookDelta::get_schema(metadata.clone()), + OrderBookDelta::get_schema(Some(metadata.clone())).into(), vec![ Arc::new(action), Arc::new(side), diff --git a/nautilus_core/persistence/src/arrow/mod.rs b/nautilus_core/persistence/src/arrow/mod.rs index 1c5ed37073e3..251421cf0eed 100644 --- a/nautilus_core/persistence/src/arrow/mod.rs +++ b/nautilus_core/persistence/src/arrow/mod.rs @@ -23,9 +23,7 @@ use std::{ io::{self, Write}, }; -use datafusion::arrow::{ - datatypes::SchemaRef, ipc::writer::StreamWriter, record_batch::RecordBatch, -}; +use datafusion::arrow::{datatypes::Schema, ipc::writer::StreamWriter, record_batch::RecordBatch}; use nautilus_model::data::Data; use pyo3::prelude::*; use thiserror; @@ -52,7 +50,17 @@ pub enum DataStreamingError { } pub trait ArrowSchemaProvider { - fn get_schema(metadata: HashMap) -> SchemaRef; + fn get_schema(metadata: Option>) -> Schema; + fn get_schema_map() -> HashMap { + let schema = Self::get_schema(None); + let mut map = HashMap::new(); + for field in schema.fields() { + let name = field.name().to_string(); + let data_type = format!("{:?}", field.data_type()); + map.insert(name, data_type); + } + map + } } pub trait EncodeToRecordBatch diff --git a/nautilus_core/persistence/src/arrow/quote.rs b/nautilus_core/persistence/src/arrow/quote.rs index 4aa878aed659..c15b40621b6f 100644 --- a/nautilus_core/persistence/src/arrow/quote.rs +++ b/nautilus_core/persistence/src/arrow/quote.rs @@ -17,7 +17,7 @@ use std::{collections::HashMap, str::FromStr, sync::Arc}; use datafusion::arrow::{ array::{Array, Int64Array, UInt64Array}, - datatypes::{DataType, Field, Schema, SchemaRef}, + datatypes::{DataType, Field, Schema}, record_batch::RecordBatch, }; use nautilus_model::{ @@ -30,17 +30,20 @@ use super::DecodeDataFromRecordBatch; use crate::arrow::{ArrowSchemaProvider, Data, DecodeFromRecordBatch, EncodeToRecordBatch}; impl ArrowSchemaProvider for QuoteTick { - fn get_schema(metadata: std::collections::HashMap) -> SchemaRef { + fn get_schema(metadata: Option>) -> Schema { let fields = vec![ - Field::new("bid", DataType::Int64, false), - Field::new("ask", DataType::Int64, false), + Field::new("bid_price", DataType::Int64, false), + Field::new("ask_price", DataType::Int64, false), Field::new("bid_size", DataType::UInt64, false), Field::new("ask_size", DataType::UInt64, false), Field::new("ts_event", DataType::UInt64, false), Field::new("ts_init", DataType::UInt64, false), ]; - Schema::new_with_metadata(fields, metadata).into() + match metadata { + Some(metadata) => Schema::new_with_metadata(fields, metadata), + None => Schema::new(fields), + } } } @@ -64,8 +67,8 @@ fn parse_metadata(metadata: &HashMap) -> (InstrumentId, u8, u8) impl EncodeToRecordBatch for QuoteTick { fn encode_batch(metadata: &HashMap, data: &[Self]) -> RecordBatch { // Create array builders - let mut bid_builder = Int64Array::builder(data.len()); - let mut ask_builder = Int64Array::builder(data.len()); + let mut bid_price_builder = Int64Array::builder(data.len()); + let mut ask_price_builder = Int64Array::builder(data.len()); let mut bid_size_builder = UInt64Array::builder(data.len()); let mut ask_size_builder = UInt64Array::builder(data.len()); let mut ts_event_builder = UInt64Array::builder(data.len()); @@ -73,8 +76,8 @@ impl EncodeToRecordBatch for QuoteTick { // Iterate over data for tick in data { - bid_builder.append_value(tick.bid.raw); - ask_builder.append_value(tick.ask.raw); + bid_price_builder.append_value(tick.bid_price.raw); + ask_price_builder.append_value(tick.ask_price.raw); bid_size_builder.append_value(tick.bid_size.raw); ask_size_builder.append_value(tick.ask_size.raw); ts_event_builder.append_value(tick.ts_event); @@ -82,8 +85,8 @@ impl EncodeToRecordBatch for QuoteTick { } // Build arrays - let bid_array = bid_builder.finish(); - let ask_array = ask_builder.finish(); + let bid_price_array = bid_price_builder.finish(); + let ask_price_array = ask_price_builder.finish(); let bid_size_array = bid_size_builder.finish(); let ask_size_array = ask_size_builder.finish(); let ts_event_array = ts_event_builder.finish(); @@ -91,10 +94,10 @@ impl EncodeToRecordBatch for QuoteTick { // Build record batch RecordBatch::try_new( - Self::get_schema(metadata.clone()), + Self::get_schema(Some(metadata.clone())).into(), vec![ - Arc::new(bid_array), - Arc::new(ask_array), + Arc::new(bid_price_array), + Arc::new(ask_price_array), Arc::new(bid_size_array), Arc::new(ask_size_array), Arc::new(ts_event_array), @@ -112,26 +115,26 @@ impl DecodeFromRecordBatch for QuoteTick { // Extract field value arrays let cols = record_batch.columns(); - let bid_values = cols[0].as_any().downcast_ref::().unwrap(); - let ask_values = cols[1].as_any().downcast_ref::().unwrap(); + let bid_price_values = cols[0].as_any().downcast_ref::().unwrap(); + let ask_price_values = cols[1].as_any().downcast_ref::().unwrap(); let ask_size_values = cols[2].as_any().downcast_ref::().unwrap(); let bid_size_values = cols[3].as_any().downcast_ref::().unwrap(); let ts_event_values = cols[4].as_any().downcast_ref::().unwrap(); let ts_init_values = cols[5].as_any().downcast_ref::().unwrap(); // Construct iterator of values from arrays - let values = bid_values + let values = bid_price_values .into_iter() - .zip(ask_values.iter()) + .zip(ask_price_values.iter()) .zip(ask_size_values.iter()) .zip(bid_size_values.iter()) .zip(ts_event_values.iter()) .zip(ts_init_values.iter()) .map( - |(((((bid, ask), ask_size), bid_size), ts_event), ts_init)| Self { + |(((((bid_price, ask_price), ask_size), bid_size), ts_event), ts_init)| Self { instrument_id, - bid: Price::from_raw(bid.unwrap(), price_precision), - ask: Price::from_raw(ask.unwrap(), price_precision), + bid_price: Price::from_raw(bid_price.unwrap(), price_precision), + ask_price: Price::from_raw(ask_price.unwrap(), price_precision), bid_size: Quantity::from_raw(bid_size.unwrap(), size_precision), ask_size: Quantity::from_raw(ask_size.unwrap(), size_precision), ts_event: ts_event.unwrap(), @@ -168,27 +171,40 @@ mod tests { fn test_get_schema() { let instrument_id = InstrumentId::from_str("AAPL.NASDAQ").unwrap(); let metadata = QuoteTick::get_metadata(&instrument_id, 2, 0); - let schema = QuoteTick::get_schema(metadata.clone()); + let schema = QuoteTick::get_schema(Some(metadata.clone())); let expected_fields = vec![ - Field::new("bid", DataType::Int64, false), - Field::new("ask", DataType::Int64, false), + Field::new("bid_price", DataType::Int64, false), + Field::new("ask_price", DataType::Int64, false), Field::new("bid_size", DataType::UInt64, false), Field::new("ask_size", DataType::UInt64, false), Field::new("ts_event", DataType::UInt64, false), Field::new("ts_init", DataType::UInt64, false), ]; - let expected_schema = Schema::new_with_metadata(expected_fields, metadata).into(); + let expected_schema = Schema::new_with_metadata(expected_fields, metadata); assert_eq!(schema, expected_schema); } + #[test] + fn test_get_schema_map() { + let arrow_schema = QuoteTick::get_schema_map(); + let mut expected_map = HashMap::new(); + expected_map.insert("bid_price".to_string(), "Int64".to_string()); + expected_map.insert("ask_price".to_string(), "Int64".to_string()); + expected_map.insert("bid_size".to_string(), "UInt64".to_string()); + expected_map.insert("ask_size".to_string(), "UInt64".to_string()); + expected_map.insert("ts_event".to_string(), "UInt64".to_string()); + expected_map.insert("ts_init".to_string(), "UInt64".to_string()); + assert_eq!(arrow_schema, expected_map); + } + #[test] fn test_encode_quote_tick() { // Create test data let instrument_id = InstrumentId::from_str("AAPL.NASDAQ").unwrap(); let tick1 = QuoteTick { - instrument_id: instrument_id, - bid: Price::new(100.10, 2), - ask: Price::new(101.50, 2), + instrument_id, + bid_price: Price::new(100.10, 2), + ask_price: Price::new(101.50, 2), bid_size: Quantity::new(1000.0, 0), ask_size: Quantity::new(500.0, 0), ts_event: 1, @@ -197,8 +213,8 @@ mod tests { let tick2 = QuoteTick { instrument_id, - bid: Price::new(100.75, 2), - ask: Price::new(100.20, 2), + bid_price: Price::new(100.75, 2), + ask_price: Price::new(100.20, 2), bid_size: Quantity::new(750.0, 0), ask_size: Quantity::new(300.0, 0), ts_event: 2, @@ -211,20 +227,20 @@ mod tests { // Verify the encoded data let columns = record_batch.columns(); - let bid_values = columns[0].as_any().downcast_ref::().unwrap(); - let ask_values = columns[1].as_any().downcast_ref::().unwrap(); + let bid_price_values = columns[0].as_any().downcast_ref::().unwrap(); + let ask_price_values = columns[1].as_any().downcast_ref::().unwrap(); let bid_size_values = columns[2].as_any().downcast_ref::().unwrap(); let ask_size_values = columns[3].as_any().downcast_ref::().unwrap(); let ts_event_values = columns[4].as_any().downcast_ref::().unwrap(); let ts_init_values = columns[5].as_any().downcast_ref::().unwrap(); assert_eq!(columns.len(), 6); - assert_eq!(bid_values.len(), 2); - assert_eq!(bid_values.value(0), 100_100_000_000); - assert_eq!(bid_values.value(1), 100_750_000_000); - assert_eq!(ask_values.len(), 2); - assert_eq!(ask_values.value(0), 101_500_000_000); - assert_eq!(ask_values.value(1), 100_200_000_000); + assert_eq!(bid_price_values.len(), 2); + assert_eq!(bid_price_values.value(0), 100_100_000_000); + assert_eq!(bid_price_values.value(1), 100_750_000_000); + assert_eq!(ask_price_values.len(), 2); + assert_eq!(ask_price_values.value(0), 101_500_000_000); + assert_eq!(ask_price_values.value(1), 100_200_000_000); assert_eq!(bid_size_values.len(), 2); assert_eq!(bid_size_values.value(0), 1_000_000_000_000); assert_eq!(bid_size_values.value(1), 750_000_000_000); @@ -244,18 +260,18 @@ mod tests { let instrument_id = InstrumentId::from_str("AAPL.NASDAQ").unwrap(); let metadata = QuoteTick::get_metadata(&instrument_id, 2, 0); - let bid = Int64Array::from(vec![10000, 9900]); - let ask = Int64Array::from(vec![10100, 10000]); + let bid_price = Int64Array::from(vec![10000, 9900]); + let ask_price = Int64Array::from(vec![10100, 10000]); let bid_size = UInt64Array::from(vec![100, 90]); let ask_size = UInt64Array::from(vec![110, 100]); let ts_event = UInt64Array::from(vec![1, 2]); let ts_init = UInt64Array::from(vec![3, 4]); let record_batch = RecordBatch::try_new( - QuoteTick::get_schema(metadata.clone()), + QuoteTick::get_schema(Some(metadata.clone())).into(), vec![ - Arc::new(bid), - Arc::new(ask), + Arc::new(bid_price), + Arc::new(ask_price), Arc::new(bid_size), Arc::new(ask_size), Arc::new(ts_event), diff --git a/nautilus_core/persistence/src/arrow/trade.rs b/nautilus_core/persistence/src/arrow/trade.rs index 107d43a996c4..d02f8148083a 100644 --- a/nautilus_core/persistence/src/arrow/trade.rs +++ b/nautilus_core/persistence/src/arrow/trade.rs @@ -17,7 +17,7 @@ use std::{collections::HashMap, str::FromStr, sync::Arc}; use datafusion::arrow::{ array::{Array, Int64Array, StringArray, StringBuilder, UInt64Array, UInt8Array}, - datatypes::{DataType, Field, Schema, SchemaRef}, + datatypes::{DataType, Field, Schema}, record_batch::RecordBatch, }; use nautilus_model::{ @@ -31,7 +31,7 @@ use super::DecodeDataFromRecordBatch; use crate::arrow::{ArrowSchemaProvider, Data, DecodeFromRecordBatch, EncodeToRecordBatch}; impl ArrowSchemaProvider for TradeTick { - fn get_schema(metadata: std::collections::HashMap) -> SchemaRef { + fn get_schema(metadata: Option>) -> Schema { let fields = vec![ Field::new("price", DataType::Int64, false), Field::new("size", DataType::UInt64, false), @@ -41,7 +41,10 @@ impl ArrowSchemaProvider for TradeTick { Field::new("ts_init", DataType::UInt64, false), ]; - Schema::new_with_metadata(fields, metadata).into() + match metadata { + Some(metadata) => Schema::new_with_metadata(fields, metadata), + None => Schema::new(fields), + } } } @@ -92,7 +95,7 @@ impl EncodeToRecordBatch for TradeTick { // Build record batch RecordBatch::try_new( - Self::get_schema(metadata.clone()), + Self::get_schema(Some(metadata.clone())).into(), vec![ Arc::new(price_array), Arc::new(size_array), @@ -173,7 +176,7 @@ mod tests { fn test_get_schema() { let instrument_id = InstrumentId::from_str("AAPL.NASDAQ").unwrap(); let metadata = TradeTick::get_metadata(&instrument_id, 2, 0); - let schema = TradeTick::get_schema(metadata.clone()); + let schema = TradeTick::get_schema(Some(metadata.clone())); let expected_fields = vec![ Field::new("price", DataType::Int64, false), Field::new("size", DataType::UInt64, false), @@ -182,10 +185,23 @@ mod tests { Field::new("ts_event", DataType::UInt64, false), Field::new("ts_init", DataType::UInt64, false), ]; - let expected_schema = Schema::new_with_metadata(expected_fields, metadata).into(); + let expected_schema = Schema::new_with_metadata(expected_fields, metadata); assert_eq!(schema, expected_schema); } + #[test] + fn test_get_schema_map() { + let schema_map = TradeTick::get_schema_map(); + let mut expected_map = HashMap::new(); + expected_map.insert("price".to_string(), "Int64".to_string()); + expected_map.insert("size".to_string(), "UInt64".to_string()); + expected_map.insert("aggressor_side".to_string(), "UInt8".to_string()); + expected_map.insert("trade_id".to_string(), "Utf8".to_string()); + expected_map.insert("ts_event".to_string(), "UInt64".to_string()); + expected_map.insert("ts_init".to_string(), "UInt64".to_string()); + assert_eq!(schema_map, expected_map); + } + #[test] fn test_encode_trade_tick() { // Create test data @@ -193,7 +209,7 @@ mod tests { let metadata = TradeTick::get_metadata(&instrument_id, 2, 0); let tick1 = TradeTick { - instrument_id: instrument_id, + instrument_id, price: Price::new(100.10, 2), size: Quantity::new(1000.0, 0), aggressor_side: AggressorSide::Buyer, @@ -258,7 +274,7 @@ mod tests { let ts_init = UInt64Array::from(vec![3, 4]); let record_batch = RecordBatch::try_new( - TradeTick::get_schema(metadata.clone()), + TradeTick::get_schema(Some(metadata.clone())).into(), vec![ Arc::new(price), Arc::new(size), diff --git a/nautilus_core/persistence/src/backend/session.rs b/nautilus_core/persistence/src/backend/session.rs index b842328a1ae2..0a455f568f9a 100644 --- a/nautilus_core/persistence/src/backend/session.rs +++ b/nautilus_core/persistence/src/backend/session.rs @@ -196,14 +196,13 @@ unsafe impl Send for DataBackendSession {} impl DataBackendSession { #[new] #[pyo3(signature=(chunk_size=5000))] - #[must_use] - pub fn new_session(chunk_size: usize) -> Self { + fn new_session(chunk_size: usize) -> Self { // Initialize runtime here get_runtime(); Self::new(chunk_size) } - pub fn add_file( + fn add_file( mut slf: PyRefMut<'_, Self>, table_name: &str, file_path: &str, @@ -241,7 +240,7 @@ impl DataBackendSession { } } - pub fn add_file_with_query( + fn add_file_with_query( mut slf: PyRefMut<'_, Self>, table_name: &str, file_path: &str, @@ -289,8 +288,7 @@ impl DataBackendSession { } } - #[must_use] - pub fn to_query_result(mut slf: PyRefMut<'_, Self>) -> DataQueryResult { + fn to_query_result(mut slf: PyRefMut<'_, Self>) -> DataQueryResult { let rt = get_runtime(); let query_result = rt.block_on(slf.get_query_result()); DataQueryResult::new(query_result) diff --git a/nautilus_core/persistence/src/backend/transformer.rs b/nautilus_core/persistence/src/backend/transformer.rs index 3dc4b2467240..69c898b488a4 100644 --- a/nautilus_core/persistence/src/backend/transformer.rs +++ b/nautilus_core/persistence/src/backend/transformer.rs @@ -15,14 +15,12 @@ use std::io::Cursor; -use datafusion::arrow::{ - datatypes::SchemaRef, ipc::writer::StreamWriter, record_batch::RecordBatch, -}; +use datafusion::arrow::{datatypes::Schema, ipc::writer::StreamWriter, record_batch::RecordBatch}; use nautilus_model::data::{bar::Bar, delta::OrderBookDelta, quote::QuoteTick, trade::TradeTick}; use pyo3::{ - exceptions::{PyKeyError, PyRuntimeError, PyValueError}, + exceptions::{PyRuntimeError, PyTypeError, PyValueError}, prelude::*, - types::{PyBytes, PyDict}, + types::{IntoPyDict, PyBytes, PyDict, PyType}, }; use crate::arrow::{ArrowSchemaProvider, EncodeToRecordBatch}; @@ -33,60 +31,54 @@ const ERROR_EMPTY_DATA: &str = "`data` was empty"; pub struct DataTransformer {} impl DataTransformer { - /// Transforms the given `data_dicts` into a vector of [`OrderBookDelta`] objects. - fn pydicts_to_order_book_deltas( + /// Transforms the given Python objects `data` into a vector of [`OrderBookDelta`] objects. + fn pyobjects_to_order_book_deltas( py: Python<'_>, - data_dicts: Vec>, + data: Vec, ) -> PyResult> { - let deltas: Vec = data_dicts + let deltas: Vec = data .into_iter() - .map(|dict| OrderBookDelta::from_dict(py, dict)) + .map(|obj| OrderBookDelta::from_pyobject(obj.as_ref(py))) .collect::>>()?; Ok(deltas) } - /// Transforms the given `data_dicts` into a vector of [`QuoteTick`] objects. - fn pydicts_to_quote_ticks( - py: Python<'_>, - data_dicts: Vec>, - ) -> PyResult> { - let ticks: Vec = data_dicts + /// Transforms the given Python objects `data` into a vector of [`QuoteTick`] objects. + fn pyobjects_to_quote_ticks(py: Python<'_>, data: Vec) -> PyResult> { + let ticks: Vec = data .into_iter() - .map(|dict| QuoteTick::from_dict(py, dict)) + .map(|obj| QuoteTick::from_pyobject(obj.as_ref(py))) .collect::>>()?; Ok(ticks) } - /// Transforms the given `data_dicts` into a vector of [`TradeTick`] objects. - fn pydicts_to_trade_ticks( - py: Python<'_>, - data_dicts: Vec>, - ) -> PyResult> { - let ticks: Vec = data_dicts + /// Transforms the given Python objects `data` into a vector of [`TradeTick`] objects. + fn pyobjects_to_trade_ticks(py: Python<'_>, data: Vec) -> PyResult> { + let ticks: Vec = data .into_iter() - .map(|dict| TradeTick::from_dict(py, dict)) + .map(|obj| TradeTick::from_pyobject(obj.as_ref(py))) .collect::>>()?; Ok(ticks) } - /// Transforms the given `data_dicts` into a vector of [`Bar`] objects. - fn pydicts_to_bars(py: Python<'_>, data_dicts: Vec>) -> PyResult> { - let bars: Vec = data_dicts + /// Transforms the given Python objects `data` into a vector of [`Bar`] objects. + fn pyobjects_to_bars(py: Python<'_>, data: Vec) -> PyResult> { + let bars: Vec = data .into_iter() - .map(|dict| Bar::from_dict(py, dict)) + .map(|obj| Bar::from_pyobject(obj.as_ref(py))) .collect::>>()?; Ok(bars) } - /// Transforms the given `batches` into Python `bytes`. + /// Transforms the given record `batches` into Python `bytes`. fn record_batches_to_pybytes( py: Python<'_>, batches: Vec, - schema: SchemaRef, + schema: Schema, ) -> PyResult> { // Create a cursor to write to a byte array in memory let mut cursor = Cursor::new(Vec::new()); @@ -113,6 +105,24 @@ impl DataTransformer { #[pymethods] impl DataTransformer { + #[staticmethod] + pub fn get_schema_map(py: Python<'_>, cls: &PyType) -> PyResult> { + let cls_str: &str = cls.getattr("__name__")?.extract()?; + let result_map = match cls_str { + stringify!(OrderBookDelta) => OrderBookDelta::get_schema_map(), + stringify!(QuoteTick) => QuoteTick::get_schema_map(), + stringify!(TradeTick) => TradeTick::get_schema_map(), + stringify!(Bar) => Bar::get_schema_map(), + _ => { + return Err(PyTypeError::new_err(format!( + "Arrow schema for `{cls_str}` is not currently implemented in Rust." + ))); + } + }; + + Ok(result_map.into_py_dict(py).into()) + } + /// Return Python `bytes` from the given list of 'legacy' data objects, which can be passed /// to `pa.ipc.open_stream` to create a `RecordBatchReader`. #[staticmethod] @@ -124,38 +134,29 @@ impl DataTransformer { return Err(PyValueError::new_err(ERROR_EMPTY_DATA)); } - // Iterate over all objects calling the legacy 'to_dict' method - let mut data_dicts: Vec> = vec![]; - for obj in data { - let dict: Py = obj - .call_method1(py, "to_dict", (obj.clone(),))? - .extract(py)?; - data_dicts.push(dict); - } - - let data_type: String = data_dicts + let data_type: String = data .first() .unwrap() // Safety: already checked that `data` not empty above .as_ref(py) - .get_item("type") - .ok_or_else(|| PyKeyError::new_err("'type' key not found in dict."))? + .getattr("__class__")? + .getattr("__name__")? .extract()?; match data_type.as_str() { stringify!(OrderBookDelta) => { - let deltas = Self::pydicts_to_order_book_deltas(py, data_dicts)?; + let deltas = Self::pyobjects_to_order_book_deltas(py, data)?; Self::pyo3_order_book_deltas_to_batches_bytes(py, deltas) } stringify!(QuoteTick) => { - let ticks = Self::pydicts_to_quote_ticks(py, data_dicts)?; + let ticks = Self::pyobjects_to_quote_ticks(py, data)?; Self::pyo3_quote_ticks_to_batches_bytes(py, ticks) } stringify!(TradeTick) => { - let ticks = Self::pydicts_to_trade_ticks(py, data_dicts)?; + let ticks = Self::pyobjects_to_trade_ticks(py, data)?; Self::pyo3_trade_ticks_to_batches_bytes(py, ticks) } stringify!(Bar) => { - let bars = Self::pydicts_to_bars(py, data_dicts)?; + let bars = Self::pyobjects_to_bars(py, data)?; Self::pyo3_bars_to_batches_bytes(py, bars) } _ => Err(PyValueError::new_err(format!( @@ -187,7 +188,7 @@ impl DataTransformer { .map(|delta| OrderBookDelta::encode_batch(&metadata, &[delta])) .collect(); - let schema = OrderBookDelta::get_schema(metadata); + let schema = OrderBookDelta::get_schema(Some(metadata)); Self::record_batches_to_pybytes(py, batches, schema) } @@ -204,7 +205,7 @@ impl DataTransformer { let first = data.first().unwrap(); let metadata = QuoteTick::get_metadata( &first.instrument_id, - first.bid.precision, + first.bid_price.precision, first.bid_size.precision, ); @@ -214,7 +215,7 @@ impl DataTransformer { .map(|quote| QuoteTick::encode_batch(&metadata, &[quote])) .collect(); - let schema = QuoteTick::get_schema(metadata); + let schema = QuoteTick::get_schema(Some(metadata)); Self::record_batches_to_pybytes(py, batches, schema) } @@ -241,7 +242,7 @@ impl DataTransformer { .map(|trade| TradeTick::encode_batch(&metadata, &[trade])) .collect(); - let schema = TradeTick::get_schema(metadata); + let schema = TradeTick::get_schema(Some(metadata)); Self::record_batches_to_pybytes(py, batches, schema) } @@ -265,7 +266,7 @@ impl DataTransformer { .map(|bar| Bar::encode_batch(&metadata, &[bar])) .collect(); - let schema = TradeTick::get_schema(metadata); + let schema = TradeTick::get_schema(Some(metadata)); Self::record_batches_to_pybytes(py, batches, schema) } } diff --git a/nautilus_core/pyo3/Cargo.toml b/nautilus_core/pyo3/Cargo.toml index 02e0a0fbe018..75d6c46bb889 100644 --- a/nautilus_core/pyo3/Cargo.toml +++ b/nautilus_core/pyo3/Cargo.toml @@ -16,6 +16,9 @@ nautilus-model = { path = "../model" } nautilus-persistence = { path = "../persistence" } nautilus-network = { path = "../network" } pyo3.workspace = true +tracing-appender = "0.2.2" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +tracing.workspace = true [features] extension-module = [ diff --git a/nautilus_core/pyo3/src/lib.rs b/nautilus_core/pyo3/src/lib.rs index 37a17d2f272c..ea77b925c26d 100644 --- a/nautilus_core/pyo3/src/lib.rs +++ b/nautilus_core/pyo3/src/lib.rs @@ -13,7 +13,84 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +use std::str::FromStr; + use pyo3::{prelude::*, types::PyDict}; +use tracing::Level; +use tracing_appender::{ + non_blocking::WorkerGuard, + rolling::{RollingFileAppender, Rotation}, +}; +use tracing_subscriber::{fmt::Layer, prelude::*, EnvFilter, Registry}; + +/// Guards the log collector and flushes it when dropped +/// +/// This struct must be dropped when the application has completed operation +/// it ensures that the any pending log lines are flushed before the application +/// closes. +#[pyclass] +pub struct LogGuard { + #[allow(dead_code)] + guards: Vec, +} + +/// Sets the global log collector +/// +/// stdout_level: Set the level for the stdout writer +/// stderr_level: Set the level for the stderr writer +/// file_level: Set the level, the directory and the prefix for the file writer +/// +/// It also configures a top level filter based on module/component name. +/// The format for the string is component1=info,component2=debug. +/// For e.g. network=error,kernel=info +/// +/// # Safety +/// Should only be called once during an applications run, ideally at the +/// beginning of the run. +#[pyfunction] +pub fn set_global_log_collector( + stdout_level: Option, + stderr_level: Option, + file_level: Option<(String, String, String)>, +) -> LogGuard { + let mut guards = Vec::new(); + let stdout_sub_builder = stdout_level.map(|stdout_level| { + let stdout_level = Level::from_str(&stdout_level).unwrap(); + let (non_blocking, guard) = tracing_appender::non_blocking(std::io::stdout()); + guards.push(guard); + Layer::default().with_writer(non_blocking.with_max_level(stdout_level)) + }); + let stderr_sub_builder = stderr_level.map(|stderr_level| { + let stderr_level = Level::from_str(&stderr_level).unwrap(); + let (non_blocking, guard) = tracing_appender::non_blocking(std::io::stdout()); + guards.push(guard); + Layer::default().with_writer(non_blocking.with_max_level(stderr_level)) + }); + let file_sub_builder = file_level.map(|(dir_path, file_prefix, file_level)| { + let file_level = Level::from_str(&file_level).unwrap(); + let rolling_log = RollingFileAppender::new(Rotation::NEVER, dir_path, file_prefix); + let (non_blocking, guard) = tracing_appender::non_blocking(rolling_log); + guards.push(guard); + Layer::default() + .with_ansi(false) // turn off unicode colors when writing to file + .with_writer(non_blocking.with_max_level(file_level)) + }); + + if let Err(err) = Registry::default() + .with(stderr_sub_builder) + .with(stdout_sub_builder) + .with(file_sub_builder) + .with(EnvFilter::from_default_env()) + .try_init() + { + println!( + "Failed to set global default dispatcher because of error: {}", + err + ); + }; + + LogGuard { guards } +} /// Need to modify sys modules so that submodule can be loaded directly as /// import supermodule.submodule @@ -21,11 +98,12 @@ use pyo3::{prelude::*, types::PyDict}; /// refer: https://github.com/PyO3/pyo3/issues/2644 #[pymodule] fn nautilus_pyo3(py: Python<'_>, m: &PyModule) -> PyResult<()> { + let sys = PyModule::import(py, "sys")?; + let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; + // Indicators let submodule = pyo3::wrap_pymodule!(nautilus_indicators::indicators); m.add_wrapped(submodule)?; - let sys = PyModule::import(py, "sys")?; - let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; sys_modules.set_item( "nautilus_trader.core.nautilus_pyo3.indicators", m.getattr("indicators")?, @@ -34,8 +112,6 @@ fn nautilus_pyo3(py: Python<'_>, m: &PyModule) -> PyResult<()> { // Model let submodule = pyo3::wrap_pymodule!(nautilus_model::model); m.add_wrapped(submodule)?; - let sys = PyModule::import(py, "sys")?; - let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; sys_modules.set_item( "nautilus_trader.core.nautilus_pyo3.model", m.getattr("model")?, @@ -44,8 +120,6 @@ fn nautilus_pyo3(py: Python<'_>, m: &PyModule) -> PyResult<()> { // Network let submodule = pyo3::wrap_pymodule!(nautilus_network::network); m.add_wrapped(submodule)?; - let sys = PyModule::import(py, "sys")?; - let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; sys_modules.set_item( "nautilus_trader.core.nautilus_pyo3.network", m.getattr("network")?, @@ -54,12 +128,13 @@ fn nautilus_pyo3(py: Python<'_>, m: &PyModule) -> PyResult<()> { // Persistence let submodule = pyo3::wrap_pymodule!(nautilus_persistence::persistence); m.add_wrapped(submodule)?; - let sys = PyModule::import(py, "sys")?; - let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; sys_modules.set_item( "nautilus_trader.core.nautilus_pyo3.persistence", m.getattr("persistence")?, )?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(set_global_log_collector, m)?)?; + Ok(()) } diff --git a/nautilus_core/rust-toolchain.toml b/nautilus_core/rust-toolchain.toml index 9517d79e96ee..317cf3df7cd4 100644 --- a/nautilus_core/rust-toolchain.toml +++ b/nautilus_core/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -version = "1.71.0" +version = "1.71.1" channel = "nightly" diff --git a/nautilus_trader/adapters/betfair/sockets.py b/nautilus_trader/adapters/betfair/sockets.py index c21535e08deb..6161a132c171 100644 --- a/nautilus_trader/adapters/betfair/sockets.py +++ b/nautilus_trader/adapters/betfair/sockets.py @@ -96,7 +96,7 @@ async def post_disconnection(self) -> None: """ Actions to be performed post disconnection. """ - # Override to implement additional disconnection related behaviour + # Override to implement additional disconnection related behavior # (canceling ping tasks etc.). async def reconnect(self): diff --git a/nautilus_trader/adapters/binance/common/execution.py b/nautilus_trader/adapters/binance/common/execution.py index cd05b7069fa4..0ad599fc472f 100644 --- a/nautilus_trader/adapters/binance/common/execution.py +++ b/nautilus_trader/adapters/binance/common/execution.py @@ -751,9 +751,9 @@ async def _submit_trailing_stop_market_order(self, order: TrailingStopMarketOrde trade = self._cache.trade_tick(order.instrument_id) if quote: if order.side == OrderSide.BUY: - activation_price = quote.ask + activation_price = quote.ask_price elif order.side == OrderSide.SELL: - activation_price = quote.bid + activation_price = quote.bid_price elif trade: activation_price = trade.price else: @@ -917,13 +917,17 @@ async def _cancel_order_single( orig_client_order_id=client_order_id.value if client_order_id else None, ) except BinanceError as e: - self._log.exception( - f"Cannot cancel order " - f"{client_order_id!r}, " - f"{venue_order_id!r}: " - f"{e.message}", - e, - ) + error_code = BinanceErrorCode(e.message["code"]) + if error_code == BinanceErrorCode.CANCEL_REJECTED: + self._log.warning(f"Cancel rejected: {e.message}.") + else: + self._log.exception( + f"Cannot cancel order " + f"{client_order_id!r}, " + f"{venue_order_id!r}: " + f"{e.message}", + e, + ) # -- WEBSOCKET EVENT HANDLERS -------------------------------------------------------------------- diff --git a/nautilus_trader/adapters/binance/common/schemas/market.py b/nautilus_trader/adapters/binance/common/schemas/market.py index a2659f1c106c..5ae0a79759c3 100644 --- a/nautilus_trader/adapters/binance/common/schemas/market.py +++ b/nautilus_trader/adapters/binance/common/schemas/market.py @@ -480,8 +480,8 @@ def parse_to_quote_tick( ) -> QuoteTick: return QuoteTick( instrument_id=instrument_id, - bid=Price.from_str(self.b), - ask=Price.from_str(self.a), + bid_price=Price.from_str(self.b), + ask_price=Price.from_str(self.a), bid_size=Quantity.from_str(self.B), ask_size=Quantity.from_str(self.A), ts_event=ts_init, diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index 7d900c2a1625..8102109b6ea8 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -543,12 +543,15 @@ async def _socket_connect(self): serverVersion=self._client.serverVersion(), ) fields = [] + connection_retries_remaining = 5 # sometimes I get news before the server version, thus the loop while len(fields) != 2: + connection_retries_remaining -= 1 self._client.decoder.interpret(fields) + await asyncio.sleep(1) buf = await self._loop.run_in_executor(None, self._client.conn.recvMsg) - if not self._client.conn.isConnected() or not (len(buf) > 0): + if not self._client.conn.isConnected() or connection_retries_remaining <= 0: # recvMsg() triggers disconnect() where there's a socket.error or 0 length buffer # if we don't then drop out of the while loop it infinitely loops self._log.warning("Disconnected; resetting connection") @@ -561,6 +564,9 @@ async def _socket_connect(self): fields = comm.read_fields(msg) self._log.debug(f"fields {fields}") else: + self._log.debug( + f"Received empty buffer from socket (retries_remaining={connection_retries_remaining})", + ) fields = [] (server_version, conn_time) = fields @@ -732,9 +738,9 @@ def tickByTickBidAsk( # : Override the EWrapper ts_event = pd.Timestamp.fromtimestamp(time, "UTC").value quote_tick = QuoteTick( instrument_id=instrument_id, - bid=instrument.make_price(bid_price), + bid_price=instrument.make_price(bid_price), + ask_price=instrument.make_price(ask_price), bid_size=instrument.make_qty(bid_size), - ask=instrument.make_price(ask_price), ask_size=instrument.make_qty(ask_size), ts_event=ts_event, ts_init=max( @@ -1283,9 +1289,9 @@ def historicalTicksBidAsk(self, req_id: int, ticks: list, done: bool): ts_event = pd.Timestamp.fromtimestamp(tick.time, "UTC").value quote_tick = QuoteTick( instrument_id=instrument_id, - bid=instrument.make_price(tick.priceBid), + bid_price=instrument.make_price(tick.priceBid), + ask_price=instrument.make_price(tick.priceAsk), bid_size=instrument.make_qty(tick.sizeBid), - ask=instrument.make_price(tick.priceAsk), ask_size=instrument.make_qty(tick.sizeAsk), ts_event=ts_event, ts_init=ts_event, diff --git a/nautilus_trader/adapters/interactive_brokers/client/common.py b/nautilus_trader/adapters/interactive_brokers/client/common.py index de9637349d0e..a4f1c5dc3b49 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/common.py +++ b/nautilus_trader/adapters/interactive_brokers/client/common.py @@ -14,8 +14,7 @@ # ------------------------------------------------------------------------------------------------- import asyncio -from collections import namedtuple -from typing import Annotated, Any, Callable, Optional, Union +from typing import Annotated, Any, Callable, NamedTuple, Optional, Union import msgspec @@ -26,7 +25,12 @@ # fmt: on -IBPosition = namedtuple("IBPosition", ["account", "contract", "quantity", "avg_cost"]) + +class IBPosition(NamedTuple): + account: Any # TODO: More specific type + contract: Any # TODO: More specific type + quantity: Any # TODO: More specific type + avg_cost: Any # TODO: More specific type class Request(msgspec.Struct, frozen=True): diff --git a/nautilus_trader/adapters/interactive_brokers/historic.py b/nautilus_trader/adapters/interactive_brokers/historic.py index 988a25e437cf..4c5839e78bc4 100644 --- a/nautilus_trader/adapters/interactive_brokers/historic.py +++ b/nautilus_trader/adapters/interactive_brokers/historic.py @@ -341,9 +341,9 @@ def parse_historic_quote_ticks( ts_init = dt_to_unix_nanos(tick.time) quote_tick = QuoteTick( instrument_id=instrument.id, - bid=Price(value=tick.priceBid, precision=instrument.price_precision), + bid_price=Price(value=tick.priceBid, precision=instrument.price_precision), + ask_price=Price(value=tick.priceAsk, precision=instrument.price_precision), bid_size=Quantity(value=tick.sizeBid, precision=instrument.size_precision), - ask=Price(value=tick.priceAsk, precision=instrument.price_precision), ask_size=Quantity(value=tick.sizeAsk, precision=instrument.size_precision), ts_init=ts_init, ts_event=ts_init, diff --git a/nautilus_trader/adapters/interactive_brokers/web.py b/nautilus_trader/adapters/interactive_brokers/web.py index 17325873faa4..fb1cf5a5f257 100644 --- a/nautilus_trader/adapters/interactive_brokers/web.py +++ b/nautilus_trader/adapters/interactive_brokers/web.py @@ -14,7 +14,7 @@ # ------------------------------------------------------------------------------------------------- import enum -from collections import namedtuple +from typing import Any, NamedTuple import requests from lxml.html import fromstring @@ -121,11 +121,16 @@ class Exchange(enum.Enum): WSE = "wse" -class Product(namedtuple("Product", "ib_symbol, description, native_symbol, currency")): +class Product(NamedTuple): """ Interactive Brokers Web Product. """ + ib_symbol: Any # TODO: More specific type + description: Any # TODO: More specific type + native_symbol: Any # TODO: More specific type + currency: Any # TODO: More specific type + def _parse_products(table): for row in table.xpath(".//tr")[1:]: diff --git a/nautilus_trader/analysis/reporter.py b/nautilus_trader/analysis/reporter.py index 8a625950751d..b8ff2fd7ba10 100644 --- a/nautilus_trader/analysis/reporter.py +++ b/nautilus_trader/analysis/reporter.py @@ -112,7 +112,8 @@ def generate_positions_report(positions: list[Position]) -> pd.DataFrame: del report["settlement_currency"] report["ts_opened"] = [unix_nanos_to_dt(ts_opened) for ts_opened in report["ts_opened"]] report["ts_closed"] = [ - unix_nanos_to_dt(ts_closed or 0) for ts_closed in report["ts_closed"] + unix_nanos_to_dt(ts_closed) if not pd.isna(ts_closed) else pd.NA + for ts_closed in report["ts_closed"] ] return report diff --git a/nautilus_trader/backtest/engine.pyx b/nautilus_trader/backtest/engine.pyx index 8bcb66883ef1..191e0af313f9 100644 --- a/nautilus_trader/backtest/engine.pyx +++ b/nautilus_trader/backtest/engine.pyx @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -import asyncio import pickle from decimal import Decimal from typing import Optional, Union @@ -361,6 +360,7 @@ cdef class BacktestEngine: bar_execution: bool = True, reject_stop_orders: bool = True, support_gtd_orders: bool = True, + use_position_ids: bool = True, use_random_ids: bool = False, use_reduce_only: bool = True, ) -> None: @@ -402,8 +402,10 @@ cdef class BacktestEngine: If stop orders are rejected on submission if trigger price is in the market. support_gtd_orders : bool, default True If orders with GTD time in force will be supported by the venue. + use_position_ids : bool, default True + If venue position IDs will be generated on order fills. use_random_ids : bool, default False - If venue order and position IDs will be randomly generated UUID4s. + If all venue generated identifiers will be random UUID4's. use_reduce_only : bool, default True If the `reduce_only` execution instruction on orders will be honored. @@ -451,7 +453,9 @@ cdef class BacktestEngine: bar_execution=bar_execution, reject_stop_orders=reject_stop_orders, support_gtd_orders=support_gtd_orders, + use_position_ids=use_position_ids, use_random_ids=use_random_ids, + use_reduce_only=use_reduce_only, ) self._venues[venue] = exchange @@ -998,7 +1002,7 @@ cdef class BacktestEngine: ################################################################################### # Common kernel start-up sequence - asyncio.run(self._kernel.start()) + self._kernel.start() # Change logger clock for the run self._kernel.logger.change_clock(self.kernel.clock) diff --git a/nautilus_trader/backtest/exchange.pxd b/nautilus_trader/backtest/exchange.pxd index 280017a0f23f..b53902584b2a 100644 --- a/nautilus_trader/backtest/exchange.pxd +++ b/nautilus_trader/backtest/exchange.pxd @@ -23,7 +23,6 @@ from nautilus_trader.backtest.models cimport LatencyModel from nautilus_trader.cache.cache cimport Cache from nautilus_trader.common.clock cimport Clock from nautilus_trader.common.logging cimport LoggerAdapter -from nautilus_trader.common.queue cimport Queue from nautilus_trader.core.data cimport Data from nautilus_trader.execution.messages cimport TradingCommand from nautilus_trader.model.currency cimport Currency @@ -85,6 +84,8 @@ cdef class SimulatedExchange: """If stop orders are rejected on submission if in the market.\n\n:returns: `bool`""" cdef readonly bint support_gtd_orders """If orders with GTD time in force will be supported by the venue.\n\n:returns: `bool`""" + cdef readonly bint use_position_ids + """If venue position IDs will be generated on order fills.\n\n:returns: `bool`""" cdef readonly bint use_random_ids """If venue order and position IDs will be randomly generated UUID4s.\n\n:returns: `bool`""" cdef readonly bint use_reduce_only @@ -95,7 +96,7 @@ cdef class SimulatedExchange: """The exchange instruments.\n\n:returns: `dict[InstrumentId, Instrument]`""" cdef dict _matching_engines - cdef Queue _message_queue + cdef object _message_queue cdef list _inflight_queue cdef dict _inflight_counter diff --git a/nautilus_trader/backtest/exchange.pyx b/nautilus_trader/backtest/exchange.pyx index 2fb7ec528921..7cd6bd4a2db3 100644 --- a/nautilus_trader/backtest/exchange.pyx +++ b/nautilus_trader/backtest/exchange.pyx @@ -13,6 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from collections import deque from decimal import Decimal from heapq import heappush from typing import Optional @@ -30,7 +31,6 @@ from nautilus_trader.backtest.modules cimport SimulationModule from nautilus_trader.cache.base cimport CacheFacade from nautilus_trader.common.clock cimport TestClock from nautilus_trader.common.logging cimport Logger -from nautilus_trader.common.queue cimport Queue from nautilus_trader.core.correctness cimport Condition from nautilus_trader.execution.messages cimport CancelAllOrders from nautilus_trader.execution.messages cimport CancelOrder @@ -101,8 +101,10 @@ cdef class SimulatedExchange: 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. + use_position_ids : bool, default True + If venue position IDs will be generated on order fills. use_random_ids : bool, default False - If venue order and position IDs will be randomly generated UUID4s. + If all venue generated identifiers will be random UUID4's. use_reduce_only : bool, default True If the `reduce_only` execution instruction on orders will be honored. @@ -144,6 +146,7 @@ cdef class SimulatedExchange: bint bar_execution = True, bint reject_stop_orders = True, bint support_gtd_orders = True, + bint use_position_ids = True, bint use_random_ids = False, bint use_reduce_only = True, ): @@ -183,6 +186,7 @@ cdef class SimulatedExchange: self.bar_execution = bar_execution self.reject_stop_orders = reject_stop_orders self.support_gtd_orders = support_gtd_orders + self.use_position_ids = use_position_ids self.use_random_ids = use_random_ids self.use_reduce_only = use_reduce_only self.fill_model = fill_model @@ -210,7 +214,7 @@ cdef class SimulatedExchange: for instrument in instruments: self.add_instrument(instrument) - self._message_queue = Queue() + self._message_queue = deque() self._inflight_queue: list[tuple[(uint64_t, uint64_t), TradingCommand]] = [] self._inflight_counter: dict[uint64_t, int] = {} @@ -330,6 +334,7 @@ cdef class SimulatedExchange: bar_execution=self.bar_execution, reject_stop_orders=self.reject_stop_orders, support_gtd_orders=self.support_gtd_orders, + use_position_ids=self.use_position_ids, use_random_ids=self.use_random_ids, use_reduce_only=self.use_reduce_only, ) @@ -606,7 +611,7 @@ cdef class SimulatedExchange: Condition.not_none(command, "command") if self.latency_model is None: - self._message_queue.put_nowait(command) + self._message_queue.appendleft(command) else: heappush(self._inflight_queue, self.generate_inflight_command(command)) @@ -777,7 +782,7 @@ cdef class SimulatedExchange: ts = self._inflight_queue[0][0][0] if ts <= ts_now: # Place message on queue to be processed - self._message_queue.put_nowait(self._inflight_queue.pop(0)[1]) + self._message_queue.appendleft(self._inflight_queue.pop(0)[1]) self._inflight_counter.pop(ts, None) else: break @@ -786,8 +791,8 @@ cdef class SimulatedExchange: TradingCommand command Order order list orders - while self._message_queue.count > 0: - command = self._message_queue.get_nowait() + 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): @@ -821,7 +826,7 @@ cdef class SimulatedExchange: for matching_engine in self._matching_engines.values(): matching_engine.reset() - self._message_queue = Queue() + self._message_queue = deque() self._inflight_queue.clear() self._inflight_counter.clear() diff --git a/nautilus_trader/backtest/matching_engine.pxd b/nautilus_trader/backtest/matching_engine.pxd index f1d28d3e142f..835dc4af2840 100644 --- a/nautilus_trader/backtest/matching_engine.pxd +++ b/nautilus_trader/backtest/matching_engine.pxd @@ -78,6 +78,7 @@ cdef class OrderMatchingEngine: cdef bint _bar_execution cdef bint _reject_stop_orders cdef bint _support_gtd_orders + cdef bint _use_position_ids cdef bint _use_random_ids cdef bint _use_reduce_only cdef dict _account_ids diff --git a/nautilus_trader/backtest/matching_engine.pyx b/nautilus_trader/backtest/matching_engine.pyx index 1cffc9095e79..216bdd624bad 100644 --- a/nautilus_trader/backtest/matching_engine.pyx +++ b/nautilus_trader/backtest/matching_engine.pyx @@ -121,8 +121,10 @@ cdef class OrderMatchingEngine: If stop orders are rejected if already in the market on submitting. support_gtd_orders : bool, default True If orders with GTD time in force will be supported by the venue. + use_position_ids : bool, default True + If venue position IDs will be generated on order fills. use_random_ids : bool, default False - If venue order and position IDs will be randomly generated UUID4s. + If all venue generated identifiers will be random UUID4's. use_reduce_only : bool, default True If the `reduce_only` execution instruction on orders will be honored. auction_match_algo : Callable[[Ladder, Ladder], Tuple[List, List], optional @@ -143,6 +145,7 @@ cdef class OrderMatchingEngine: bint bar_execution = True, bint reject_stop_orders = True, bint support_gtd_orders = True, + bint use_position_ids = True, bint use_random_ids = False, bint use_reduce_only = True, # auction_match_algo = default_auction_match @@ -165,6 +168,7 @@ cdef class OrderMatchingEngine: self._bar_execution = bar_execution self._reject_stop_orders = reject_stop_orders self._support_gtd_orders = support_gtd_orders + self._use_position_ids = use_position_ids self._use_random_ids = use_random_ids self._use_reduce_only = use_reduce_only # self._auction_match_algo = auction_match_algo @@ -600,20 +604,20 @@ cdef class OrderMatchingEngine: self.iterate(tick.ts_init) # High - tick._mem.bid = self._last_bid_bar._mem.high # Direct memory assignment - tick._mem.ask = self._last_ask_bar._mem.high # Direct memory assignment + tick._mem.bid_price = self._last_bid_bar._mem.high # Direct memory assignment + tick._mem.ask_price = self._last_ask_bar._mem.high # Direct memory assignment self._book.update_quote_tick(tick) self.iterate(tick.ts_init) # Low - tick._mem.bid = self._last_bid_bar._mem.low # Assigning memory directly - tick._mem.ask = self._last_ask_bar._mem.low # Assigning memory directly + tick._mem.bid_price = self._last_bid_bar._mem.low # Assigning memory directly + tick._mem.ask_price = self._last_ask_bar._mem.low # Assigning memory directly self._book.update_quote_tick(tick) self.iterate(tick.ts_init) # Close - tick._mem.bid = self._last_bid_bar._mem.close # Assigning memory directly - tick._mem.ask = self._last_ask_bar._mem.close # Assigning memory directly + tick._mem.bid_price = self._last_bid_bar._mem.close # Assigning memory directly + tick._mem.ask_price = self._last_ask_bar._mem.close # Assigning memory directly self._book.update_quote_tick(tick) self.iterate(tick.ts_init) @@ -1163,6 +1167,12 @@ cdef class OrderMatchingEngine: self._core.set_last_raw(self._target_last) self._has_targets = False + # Reset any targets after iteration + self._target_bid = 0 + self._target_ask = 0 + self._target_last = 0 + self._has_targets = False + cpdef list determine_limit_price_and_volume(self, Order order): """ Return the projected fills for the given *limit* order filling passively @@ -1665,6 +1675,10 @@ cdef class OrderMatchingEngine: for client_order_id in order.linked_order_ids: child_order = self.cache.order(client_order_id) assert child_order is not None, "OTO child order not found" + if child_order.is_closed_c(): + continue + if child_order.is_active_local_c(): + continue # Order is not on the exchange yet if child_order.position_id is None and order.position_id is not None: self.cache.add_position_id( position_id=order.position_id, @@ -1676,7 +1690,7 @@ cdef class OrderMatchingEngine: f"Indexed {repr(order.position_id)} " f"for {repr(child_order.client_order_id)}", ) - if not child_order.is_open_c() or (child_order.status == OrderStatus.PENDING_UPDATE and child_order._previous_status == OrderStatus.SUBMITTED): + if not child_order.is_open_c() or (child_order.status_c() == OrderStatus.PENDING_UPDATE and child_order._previous_status == OrderStatus.SUBMITTED): self.process_order( order=child_order, account_id=order.account_id or self._account_ids[order.trader_id], @@ -1685,11 +1699,17 @@ cdef class OrderMatchingEngine: for client_order_id in order.linked_order_ids: oco_order = self.cache.order(client_order_id) assert oco_order is not None, "OCO order not found" + if oco_order.is_closed_c(): + continue + if oco_order.is_active_local_c(): + continue # Order is not on the exchange yet self.cancel_order(oco_order) elif order.contingency_type == ContingencyType.OUO: for client_order_id in order.linked_order_ids: ouo_order = self.cache.order(client_order_id) assert ouo_order is not None, "OUO order not found" + if ouo_order.is_active_local_c(): + continue # Order is not on the exchange yet if order.is_closed_c() and ouo_order.is_open_c(): self.cancel_order(ouo_order) elif order.leaves_qty._mem.raw != 0 and order.leaves_qty._mem.raw != ouo_order.leaves_qty._mem.raw: @@ -1746,6 +1766,9 @@ cdef class OrderMatchingEngine: return None cdef PositionId _generate_venue_position_id(self): + if not self._use_position_ids: + return None + self._position_count += 1 if self._use_random_ids: return PositionId(str(uuid.uuid4())) @@ -1772,6 +1795,9 @@ cdef class OrderMatchingEngine: # -- EVENT HANDLING ------------------------------------------------------------------------------- cpdef void accept_order(self, Order order): + if order.is_closed_c(): + return # Temporary guard to prevent invalid processing + # Check if order already accepted (being added back into the matching engine) if not order.status_c() == OrderStatus.ACCEPTED: self._generate_order_accepted(order) @@ -1792,6 +1818,12 @@ cdef class OrderMatchingEngine: self._generate_order_expired(order) cpdef void cancel_order(self, Order order, bint cancel_contingencies=True): + if order.is_active_local_c(): + self._log.error( + f"Cannot cancel an order with {order.status_string_c()} from the matching engine.", + ) + return + if order.venue_order_id is None: order.venue_order_id = self._generate_venue_order_id() @@ -1899,7 +1931,13 @@ cdef class OrderMatchingEngine: for client_order_id in order.linked_order_ids: ouo_order = self.cache.order(client_order_id) assert ouo_order is not None, "OUO order not found" - if ouo_order.order_type != OrderType.MARKET and ouo_order.leaves_qty._mem.raw != order.leaves_qty._mem.raw: + if ouo_order.is_active_local_c(): + continue # Order is not on the exchange yet + if ouo_order.order_type == OrderType.MARKET or ouo_order.is_closed_c(): + continue + if order.leaves_qty._mem.raw == 0: + self.cancel_order(ouo_order) + elif ouo_order.leaves_qty._mem.raw != order.leaves_qty._mem.raw: self.update_order( ouo_order, order.leaves_qty, @@ -1915,6 +1953,8 @@ cdef class OrderMatchingEngine: for client_order_id in order.linked_order_ids: contingent_order = self.cache.order(client_order_id) assert contingent_order is not None, "Contingency order not found" + if contingent_order.is_active_local_c(): + continue # Order is not on the exchange yet if not contingent_order.is_closed_c(): self.cancel_order(contingent_order, cancel_contingencies=False) diff --git a/nautilus_trader/backtest/node.py b/nautilus_trader/backtest/node.py index 7a2180d27e83..05adba8d9c97 100644 --- a/nautilus_trader/backtest/node.py +++ b/nautilus_trader/backtest/node.py @@ -186,6 +186,7 @@ def _create_engine( frozen_account=config.frozen_account, reject_stop_orders=config.reject_stop_orders, support_gtd_orders=config.support_gtd_orders, + use_position_ids=config.use_position_ids, use_random_ids=config.use_random_ids, use_reduce_only=config.use_reduce_only, ) diff --git a/nautilus_trader/cache/base.pxd b/nautilus_trader/cache/base.pxd index ba6ddf0b0acd..d268eb10e8f6 100644 --- a/nautilus_trader/cache/base.pxd +++ b/nautilus_trader/cache/base.pxd @@ -36,6 +36,7 @@ from nautilus_trader.model.identifiers cimport VenueOrderId 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.orderbook.book cimport OrderBook from nautilus_trader.model.orders.base cimport Order from nautilus_trader.model.orders.list cimport OrderList @@ -86,7 +87,7 @@ cdef class CacheFacade: cpdef list instrument_ids(self, Venue venue=*) cpdef list instruments(self, Venue venue=*) -# -- SYNTHETIC QUERIES --------------------------------------------------------------------------- +# -- SYNTHETIC QUERIES ---------------------------------------------------------------------------- cpdef SyntheticInstrument synthetic(self, InstrumentId instrument_id) cpdef list synthetic_ids(self) @@ -126,8 +127,6 @@ cdef class CacheFacade: cpdef list orders_emulated(self, Venue venue=*, InstrumentId instrument_id=*, StrategyId strategy_id=*, OrderSide side=*) cpdef list orders_inflight(self, Venue venue=*, InstrumentId instrument_id=*, StrategyId strategy_id=*, OrderSide side=*) cpdef list orders_for_position(self, PositionId position_id) - cpdef list orders_for_exec_algorithm(self, ExecAlgorithmId exec_algorithm_id, Venue venue=*, InstrumentId instrument_id=*, StrategyId strategy_id=*, OrderSide side=*) - cpdef list orders_for_exec_spawn(self, ClientOrderId client_order_id) cpdef bint order_exists(self, ClientOrderId client_order_id) cpdef bint is_order_open(self, ClientOrderId client_order_id) cpdef bint is_order_closed(self, ClientOrderId client_order_id) @@ -139,12 +138,20 @@ cdef class CacheFacade: cpdef int orders_inflight_count(self, Venue venue=*, InstrumentId instrument_id=*, StrategyId strategy_id=*, OrderSide side=*) cpdef int orders_total_count(self, Venue venue=*, InstrumentId instrument_id=*, StrategyId strategy_id=*, OrderSide side=*) -# -- ORDER LIST QUERIES -------------------------------------------------------------------------------- +# -- ORDER LIST QUERIES --------------------------------------------------------------------------- cpdef OrderList order_list(self, OrderListId order_list_id) cpdef list order_lists(self, Venue venue=*, InstrumentId instrument_id=*, StrategyId strategy_id=*) cpdef bint order_list_exists(self, OrderListId order_list_id) +# -- EXEC ALGORITHM QUERIES ----------------------------------------------------------------------- + + cpdef list orders_for_exec_algorithm(self, ExecAlgorithmId exec_algorithm_id, Venue venue=*, InstrumentId instrument_id=*, StrategyId strategy_id=*, OrderSide side=*) + cpdef list orders_for_exec_spawn(self, ClientOrderId exec_spawn_id) + cpdef Quantity exec_spawn_total_quantity(self, ClientOrderId exec_spawn_id, bint active_only=*) + cpdef Quantity exec_spawn_total_filled_qty(self, ClientOrderId exec_spawn_id, bint active_only=*) + cpdef Quantity exec_spawn_total_leaves_qty(self, ClientOrderId exec_spawn_id, bint active_only=*) + # -- POSITION QUERIES ----------------------------------------------------------------------------- cpdef Position position(self, PositionId position_id) diff --git a/nautilus_trader/cache/base.pyx b/nautilus_trader/cache/base.pyx index 4c7fe091eddd..ed2b7b3cf726 100644 --- a/nautilus_trader/cache/base.pyx +++ b/nautilus_trader/cache/base.pyx @@ -32,6 +32,7 @@ from nautilus_trader.model.identifiers cimport Venue from nautilus_trader.model.identifiers cimport VenueOrderId from nautilus_trader.model.instruments.base cimport Instrument from nautilus_trader.model.instruments.synthetic cimport SyntheticInstrument +from nautilus_trader.model.objects cimport Quantity cdef class CacheFacade: @@ -155,7 +156,7 @@ cdef class CacheFacade: """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover -# -- SYNTHETIC QUERIES --------------------------------------------------------------------------- +# -- SYNTHETIC QUERIES ---------------------------------------------------------------------------- cpdef SyntheticInstrument synthetic(self, InstrumentId instrument_id): """Abstract method (implement in subclass).""" @@ -279,14 +280,6 @@ cdef class CacheFacade: """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover - cpdef list orders_for_exec_algorithm(self, ExecAlgorithmId exec_algorithm_id, Venue venue = None, InstrumentId instrument_id = None, StrategyId strategy_id = None, OrderSide side = OrderSide.NO_ORDER_SIDE): - """Abstract method (implement in subclass).""" - raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover - - cpdef list orders_for_exec_spawn(self, ClientOrderId client_order_id): - """Abstract method (implement in subclass).""" - raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover - cpdef bint order_exists(self, ClientOrderId client_order_id): """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover @@ -327,7 +320,7 @@ cdef class CacheFacade: """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover -# -- ORDER_LIST_QUERIES -------------------------------------------------------------------------------- +# -- ORDER_LIST_QUERIES --------------------------------------------------------------------------- cpdef OrderList order_list(self, OrderListId order_list_id): """Abstract method (implement in subclass).""" @@ -341,6 +334,28 @@ cdef class CacheFacade: """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover +# -- EXEC ALGORITHM QUERIES ----------------------------------------------------------------------- + + cpdef list orders_for_exec_algorithm(self, ExecAlgorithmId exec_algorithm_id, Venue venue = None, InstrumentId instrument_id = None, StrategyId strategy_id = None, OrderSide side = OrderSide.NO_ORDER_SIDE): + """Abstract method (implement in subclass).""" + raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + + cpdef list orders_for_exec_spawn(self, ClientOrderId exec_spawn_id): + """Abstract method (implement in subclass).""" + raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + + cpdef Quantity exec_spawn_total_quantity(self, ClientOrderId exec_spawn_id, bint active_only=False): + """Abstract method (implement in subclass).""" + raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + + cpdef Quantity exec_spawn_total_filled_qty(self, ClientOrderId exec_spawn_id, bint active_only=False): + """Abstract method (implement in subclass).""" + raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + + cpdef Quantity exec_spawn_total_leaves_qty(self, ClientOrderId exec_spawn_id, bint active_only=False): + """Abstract method (implement in subclass).""" + raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + # -- POSITION QUERIES ----------------------------------------------------------------------------- cpdef Position position(self, PositionId position_id): diff --git a/nautilus_trader/cache/cache.pxd b/nautilus_trader/cache/cache.pxd index 6add83477068..01c019778aa9 100644 --- a/nautilus_trader/cache/cache.pxd +++ b/nautilus_trader/cache/cache.pxd @@ -14,6 +14,7 @@ # ------------------------------------------------------------------------------------------------- from cpython.datetime cimport datetime +from libc.stdint cimport uint64_t from nautilus_trader.accounting.accounts.base cimport Account from nautilus_trader.accounting.calculators cimport ExchangeRateCalculator @@ -42,6 +43,7 @@ from nautilus_trader.model.identifiers cimport Venue from nautilus_trader.model.instruments.base cimport Instrument from nautilus_trader.model.instruments.synthetic cimport SyntheticInstrument from nautilus_trader.model.objects cimport Money +from nautilus_trader.model.objects cimport Quantity from nautilus_trader.model.orderbook.book cimport OrderBook from nautilus_trader.model.orders.base cimport Order from nautilus_trader.model.orders.list cimport OrderList @@ -160,7 +162,7 @@ cdef class Cache(CacheFacade): cpdef void add_position_id(self, PositionId position_id, Venue venue, ClientOrderId client_order_id, StrategyId strategy_id) cpdef void add_position(self, Position position, OmsType oms_type) cpdef void snapshot_position(self, Position position) - cpdef void snapshot_position_state(self, Position position) + cpdef void snapshot_position_state(self, Position position, uint64_t ts_snapshot) cpdef void snapshot_order_state(self, Order order) cpdef void update_account(self, Account account) diff --git a/nautilus_trader/cache/cache.pyx b/nautilus_trader/cache/cache.pyx index 81455d32cf3d..d951988e90ad 100644 --- a/nautilus_trader/cache/cache.pyx +++ b/nautilus_trader/cache/cache.pyx @@ -23,6 +23,7 @@ from typing import Optional from nautilus_trader.config import CacheConfig from cpython.datetime cimport datetime +from libc.stdint cimport uint8_t from libc.stdint cimport uint64_t from nautilus_trader.accounting.accounts.base cimport Account @@ -63,6 +64,7 @@ from nautilus_trader.model.instruments.currency_pair cimport CurrencyPair from nautilus_trader.model.instruments.synthetic cimport SyntheticInstrument from nautilus_trader.model.objects cimport Money from nautilus_trader.model.objects cimport Price +from nautilus_trader.model.objects cimport Quantity from nautilus_trader.model.orders.base cimport Order from nautilus_trader.model.orders.list cimport OrderList from nautilus_trader.trading.strategy cimport Strategy @@ -794,14 +796,9 @@ cdef class Cache(CacheFacade): # 8: Build _index_exec_spawn_orders -> {ClientOrderId, {ClientOrderId}} if order.exec_algorithm_id is not None: - if order.exec_spawn_id is None: - if order.client_order_id not in self._index_exec_spawn_orders: - self._index_exec_spawn_orders[order.client_order_id] = set() - self._index_exec_spawn_orders[order.client_order_id].add(order.client_order_id) - else: - if order.exec_spawn_id not in self._index_exec_spawn_orders: - self._index_exec_spawn_orders[order.exec_spawn_id] = set() - self._index_exec_spawn_orders[order.exec_spawn_id].add(order.client_order_id) + if order.exec_spawn_id not in self._index_exec_spawn_orders: + self._index_exec_spawn_orders[order.exec_spawn_id] = set() + self._index_exec_spawn_orders[order.exec_spawn_id].add(order.client_order_id) # 9: Build _index_orders -> {ClientOrderId} self._index_orders.add(client_order_id) @@ -907,9 +904,9 @@ cdef class Cache(CacheFacade): if position.side == PositionSide.FLAT: return Money(0.0, position.settlement_currency) elif position.side == PositionSide.LONG: - last = quote.ask + last = quote.ask_price else: - last = quote.bid + last = quote.bid_price return position.unrealized_pnl(last) @@ -1486,20 +1483,11 @@ cdef class Cache(CacheFacade): exec_algorithm_orders.add(order.client_order_id) # Set exec_spawn_id index - if order.exec_spawn_id is None: - # Primary order - exec_spawn_orders = self._index_exec_spawn_orders.get(order.client_order_id) - if not exec_spawn_orders: - self._index_exec_spawn_orders[order.client_order_id] = {order.client_order_id} - else: - self._index_exec_spawn_orders[order.client_order_id].add(order.client_order_id) + exec_spawn_orders = self._index_exec_spawn_orders.get(order.exec_spawn_id) + if not exec_spawn_orders: + self._index_exec_spawn_orders[order.exec_spawn_id] = {order.client_order_id} else: - # Secondary order - exec_spawn_orders = self._index_exec_spawn_orders.get(order.exec_spawn_id) - if not exec_spawn_orders: - self._index_exec_spawn_orders[order.exec_spawn_id] = {order.client_order_id} - else: - self._index_exec_spawn_orders[order.exec_spawn_id].add(order.client_order_id) + self._index_exec_spawn_orders[order.exec_spawn_id].add(order.client_order_id) # Update emulation if order.emulation_trigger == TriggerType.NO_TRIGGER: @@ -1668,6 +1656,7 @@ cdef class Cache(CacheFacade): if self.snapshot_positions: self._database.snapshot_position_state( position, + position.ts_last, self._calculate_unrealized_pnl(position), ) @@ -1698,7 +1687,7 @@ cdef class Cache(CacheFacade): self._log.debug(f"Snapshot {repr(copied_position)}.") - cpdef void snapshot_position_state(self, Position position): + cpdef void snapshot_position_state(self, Position position, uint64_t ts_snapshot): """ Snapshot the state dictionary for the given `position`. @@ -1708,6 +1697,8 @@ cdef class Cache(CacheFacade): ---------- position : Position The position to snapshot the state for. + ts_snapshot : uint64_t + The UNIX timestamp (nanoseconds) when the snapshot was taken. """ Condition.not_none(position, "position") @@ -1720,6 +1711,7 @@ cdef class Cache(CacheFacade): self._database.snapshot_position_state( position, + ts_snapshot, self._calculate_unrealized_pnl(position), ) @@ -1792,7 +1784,7 @@ cdef class Cache(CacheFacade): self._index_orders_closed.add(order.client_order_id) # Update emulation - if order.emulation_trigger == TriggerType.NO_TRIGGER: + if order.is_closed_c() or order.emulation_trigger == TriggerType.NO_TRIGGER: self._index_orders_emulated.discard(order.client_order_id) else: self._index_orders_emulated.add(order.client_order_id) @@ -1832,6 +1824,7 @@ cdef class Cache(CacheFacade): if self.snapshot_positions: self._database.snapshot_position_state( position, + position.ts_last, self._calculate_unrealized_pnl(position), ) @@ -2442,8 +2435,8 @@ cdef class Cache(CacheFacade): cdef: InstrumentId instrument_id str base_quote - Price bid - Price ask + Price bid_price + Price ask_price Bar bid_bar Bar ask_bar for instrument_id, base_quote in self._xrate_symbols.items(): @@ -2452,19 +2445,19 @@ cdef class Cache(CacheFacade): ticks = self._quote_ticks.get(instrument_id) if ticks: - bid = ticks[0].bid - ask = ticks[0].ask + bid_price = ticks[0].bid_price + ask_price = ticks[0].ask_price else: # No quotes for instrument_id bid_bar = self._bars_bid.get(instrument_id) ask_bar = self._bars_ask.get(instrument_id) if bid_bar is None or ask_bar is None: continue # No prices for instrument_id - bid = bid_bar.close - ask = ask_bar.close + bid_price = bid_bar.close + ask_price = ask_bar.close - bid_quotes[base_quote] = bid.as_f64_c() - ask_quotes[base_quote] = ask.as_f64_c() + bid_quotes[base_quote] = bid_price.as_f64_c() + ask_quotes[base_quote] = ask_price.as_f64_c() return bid_quotes, ask_quotes @@ -2520,7 +2513,7 @@ cdef class Cache(CacheFacade): """ return [x for x in self._instruments.values() if venue is None or venue == x.id.venue] -# -- SYNTHETIC QUERIES --------------------------------------------------------------------------- +# -- SYNTHETIC QUERIES ---------------------------------------------------------------------------- cpdef SyntheticInstrument synthetic(self, InstrumentId instrument_id): """ @@ -3254,66 +3247,6 @@ cdef class Cache(CacheFacade): return [self._orders[client_order_id] for client_order_id in client_order_ids] - cpdef list orders_for_exec_algorithm( - self, - ExecAlgorithmId exec_algorithm_id, - Venue venue = None, - InstrumentId instrument_id = None, - StrategyId strategy_id = None, - OrderSide side = OrderSide.NO_ORDER_SIDE, - ): - """ - Return all execution algorithm orders for the given query filters. - - Parameters - ---------- - exec_algorithm_id : ExecAlgorithmId - The execution algorithm ID. - venue : Venue, optional - The venue ID query filter. - instrument_id : InstrumentId, optional - The instrument ID query filter. - strategy_id : StrategyId, optional - The strategy ID query filter. - side : OrderSide, default ``NO_ORDER_SIDE`` (no filter) - The order side query filter. - - Returns - ------- - list[Order] - - """ - Condition.not_none(exec_algorithm_id, "exec_algorithm_id") - - cdef set query = self._build_order_query_filter_set(venue, instrument_id, strategy_id) - - cdef set exec_algorithm_order_ids = self._index_exec_algorithm_orders.get(exec_algorithm_id) - - if query is not None and exec_algorithm_order_ids is not None: - exec_algorithm_order_ids = query.intersection(exec_algorithm_order_ids) - - return self._get_orders_for_ids(exec_algorithm_order_ids, side) - - cpdef list orders_for_exec_spawn(self, ClientOrderId client_order_id): - """ - Return all orders for the given execution spawn ID (if found). - - Will also include the primary (original) order. - - Parameters - ---------- - client_order_id : ClientOrderId - The execution algorithm spawning primary (original) client order ID. - - Returns - ------- - list[Order] - - """ - Condition.not_none(client_order_id, "client_order_id") - - return self._get_orders_for_ids(self._index_exec_spawn_orders.get(client_order_id), OrderSide.NO_ORDER_SIDE) - cpdef bint order_exists(self, ClientOrderId client_order_id): """ Return a value indicating whether an order with the given ID exists. @@ -3544,7 +3477,7 @@ cdef class Cache(CacheFacade): """ return len(self.orders(venue, instrument_id, strategy_id, side)) -# -- ORDER LIST QUERIES -------------------------------------------------------------------------------- +# -- ORDER LIST QUERIES --------------------------------------------------------------------------- cpdef OrderList order_list(self, OrderListId order_list_id): """ @@ -3607,6 +3540,188 @@ cdef class Cache(CacheFacade): return order_list_id in self._order_lists +# -- EXEC ALGORITHM QUERIES ----------------------------------------------------------------------- + + cpdef list orders_for_exec_algorithm( + self, + ExecAlgorithmId exec_algorithm_id, + Venue venue = None, + InstrumentId instrument_id = None, + StrategyId strategy_id = None, + OrderSide side = OrderSide.NO_ORDER_SIDE, + ): + """ + Return all execution algorithm orders for the given query filters. + + Parameters + ---------- + exec_algorithm_id : ExecAlgorithmId + The execution algorithm ID. + venue : Venue, optional + The venue ID query filter. + instrument_id : InstrumentId, optional + The instrument ID query filter. + strategy_id : StrategyId, optional + The strategy ID query filter. + side : OrderSide, default ``NO_ORDER_SIDE`` (no filter) + The order side query filter. + + Returns + ------- + list[Order] + + """ + Condition.not_none(exec_algorithm_id, "exec_algorithm_id") + + cdef set query = self._build_order_query_filter_set(venue, instrument_id, strategy_id) + + cdef set exec_algorithm_order_ids = self._index_exec_algorithm_orders.get(exec_algorithm_id) + + if query is not None and exec_algorithm_order_ids is not None: + exec_algorithm_order_ids = query.intersection(exec_algorithm_order_ids) + + return self._get_orders_for_ids(exec_algorithm_order_ids, side) + + cpdef list orders_for_exec_spawn(self, ClientOrderId exec_spawn_id): + """ + Return all orders for the given execution spawn ID (if found). + + Will also include the primary (original) order. + + Parameters + ---------- + exec_spawn_id : ClientOrderId + The execution algorithm spawning primary (original) client order ID. + + Returns + ------- + list[Order] + + """ + Condition.not_none(exec_spawn_id, "exec_spawn_id") + + return self._get_orders_for_ids(self._index_exec_spawn_orders.get(exec_spawn_id), OrderSide.NO_ORDER_SIDE) + + cpdef Quantity exec_spawn_total_quantity(self, ClientOrderId exec_spawn_id, bint active_only=False): + """ + Return the total quantity for the given execution spawn ID (if found). + + If no execution spawn ID matches then returns ``None``. + + Parameters + ---------- + exec_spawn_id : ClientOrderId + The execution algorithm spawning primary (original) client order ID. + active_only : bool, default False + The flag to filter for active execution spawn orders only. + + Returns + ------- + Quantity or ``None`` + + Notes + ----- + An "active" order is defined as one which is *not closed*. + + """ + Condition.not_none(exec_spawn_id, "exec_spawn_id") + + cdef list exec_spawn_orders = self.orders_for_exec_spawn(exec_spawn_id) + + if not exec_spawn_orders: + return None + + cdef: + Order spawn_order + uint8_t precision = 0 + uint64_t raw_total_quantity = 0 + for spawn_order in exec_spawn_orders: + precision = spawn_order.quantity._mem.precision + if not active_only or not spawn_order.is_closed_c(): + raw_total_quantity += spawn_order.quantity._mem.raw + + return Quantity.from_raw_c(raw_total_quantity, precision) + + cpdef Quantity exec_spawn_total_filled_qty(self, ClientOrderId exec_spawn_id, bint active_only=False): + """ + Return the total filled quantity for the given execution spawn ID (if found). + + If no execution spawn ID matches then returns ``None``. + + Parameters + ---------- + exec_spawn_id : ClientOrderId + The execution algorithm spawning primary (original) client order ID. + active_only : bool, default False + The flag to filter for active execution spawn orders only. + + Returns + ------- + Quantity or ``None`` + + Notes + ----- + An "active" order is defined as one which is *not closed*. + + """ + Condition.not_none(exec_spawn_id, "exec_spawn_id") + + cdef list exec_spawn_orders = self.orders_for_exec_spawn(exec_spawn_id) + + if not exec_spawn_orders: + return None + + cdef: + Order spawn_order + uint8_t precision = 0 + uint64_t raw_filled_qty = 0 + for spawn_order in exec_spawn_orders: + precision = spawn_order.filled_qty._mem.precision + if not active_only or not spawn_order.is_closed_c(): + raw_filled_qty += spawn_order.filled_qty._mem.raw + + return Quantity.from_raw_c(raw_filled_qty, precision) + + cpdef Quantity exec_spawn_total_leaves_qty(self, ClientOrderId exec_spawn_id, bint active_only=False): + """ + Return the total leaves quantity for the given execution spawn ID (if found). + + If no execution spawn ID matches then returns ``None``. + + Parameters + ---------- + exec_spawn_id : ClientOrderId + The execution algorithm spawning primary (original) client order ID. + active_only : bool, default False + The flag to filter for active execution spawn orders only. + + Returns + ------- + Quantity or ``None`` + + Notes + ----- + An "active" order is defined as one which is *not closed*. + + """ + Condition.not_none(exec_spawn_id, "exec_spawn_id") + + cdef list exec_spawn_orders = self.orders_for_exec_spawn(exec_spawn_id) + + if not exec_spawn_orders: + return None + + cdef: + Order spawn_order + uint8_t precision = 0 + uint64_t raw_leaves_qty = 0 + for spawn_order in exec_spawn_orders: + precision = spawn_order.leaves_qty._mem.precision + if not active_only or not spawn_order.is_closed_c(): + raw_leaves_qty += spawn_order.leaves_qty._mem.raw + + return Quantity.from_raw_c(raw_leaves_qty, precision) + # -- POSITION QUERIES ----------------------------------------------------------------------------- cpdef Position position(self, PositionId position_id): diff --git a/nautilus_trader/cache/database.pxd b/nautilus_trader/cache/database.pxd index 3316ad4712a0..b8f7f71b31bd 100644 --- a/nautilus_trader/cache/database.pxd +++ b/nautilus_trader/cache/database.pxd @@ -14,6 +14,7 @@ # ------------------------------------------------------------------------------------------------- from cpython.datetime cimport datetime +from libc.stdint cimport uint64_t from nautilus_trader.accounting.accounts.base cimport Account from nautilus_trader.common.actor cimport Actor @@ -29,6 +30,7 @@ from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.identifiers cimport OrderListId from nautilus_trader.model.identifiers cimport PositionId from nautilus_trader.model.identifiers cimport StrategyId +from nautilus_trader.model.identifiers cimport VenueOrderId from nautilus_trader.model.instruments.base cimport Instrument from nautilus_trader.model.instruments.synthetic cimport SyntheticInstrument from nautilus_trader.model.objects cimport Money @@ -69,6 +71,7 @@ cdef class CacheDatabase: cpdef void add_order(self, Order order, PositionId position_id=*, ClientId client_id=*) cpdef void add_position(self, Position position) + cpdef void index_venue_order_id(self, ClientOrderId client_order_id, VenueOrderId venue_order_id) cpdef void index_order_position(self, ClientOrderId client_order_id, PositionId position_id) cpdef void update_account(self, Account account) @@ -78,6 +81,6 @@ cdef class CacheDatabase: cpdef void update_strategy(self, Strategy strategy) cpdef void snapshot_order_state(self, Order order) - cpdef void snapshot_position_state(self, Position position, Money unrealized_pnl=*) + cpdef void snapshot_position_state(self, Position position, uint64_t ts_snapshot, Money unrealized_pnl=*) cpdef void heartbeat(self, datetime timestamp) diff --git a/nautilus_trader/cache/database.pyx b/nautilus_trader/cache/database.pyx index 01db4625b45d..b4e83a429148 100644 --- a/nautilus_trader/cache/database.pyx +++ b/nautilus_trader/cache/database.pyx @@ -13,10 +13,11 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from cpython.datetime cimport datetime - from nautilus_trader.config import CacheDatabaseConfig +from cpython.datetime cimport datetime +from libc.stdint cimport uint64_t + from nautilus_trader.accounting.accounts.base cimport Account from nautilus_trader.common.actor cimport Actor from nautilus_trader.common.logging cimport Logger @@ -29,6 +30,7 @@ from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.identifiers cimport OrderListId from nautilus_trader.model.identifiers cimport PositionId from nautilus_trader.model.identifiers cimport StrategyId +from nautilus_trader.model.identifiers cimport VenueOrderId from nautilus_trader.model.instruments.base cimport Instrument from nautilus_trader.model.instruments.synthetic cimport SyntheticInstrument from nautilus_trader.model.objects cimport Money @@ -168,6 +170,10 @@ cdef class CacheDatabase: """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + cpdef void index_venue_order_id(self, ClientOrderId client_order_id, VenueOrderId venue_order_id): + """Abstract method (implement in subclass).""" + raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + cpdef void index_order_position(self, ClientOrderId client_order_id, PositionId position_id): """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover @@ -196,7 +202,7 @@ cdef class CacheDatabase: """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover - cpdef void snapshot_position_state(self, Position position, Money unrealized_pnl = None): + cpdef void snapshot_position_state(self, Position position, uint64_t ts_snapshot, Money unrealized_pnl = None): """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover diff --git a/nautilus_trader/common/actor.pxd b/nautilus_trader/common/actor.pxd index 36c7827c5707..2002b78dc40c 100644 --- a/nautilus_trader/common/actor.pxd +++ b/nautilus_trader/common/actor.pxd @@ -50,6 +50,7 @@ from nautilus_trader.msgbus.bus cimport MessageBus cdef class Actor(Component): + cdef object _executor cdef set _warning_events cdef dict _signal_classes cdef dict _pending_requests @@ -100,6 +101,7 @@ cdef class Actor(Component): Logger logger, ) + cpdef void register_executor(self, loop, executor) cpdef void register_warning_event(self, type event) cpdef void deregister_warning_event(self, type event) @@ -109,6 +111,15 @@ cdef class Actor(Component): cpdef void load(self, dict state) cpdef void add_synthetic(self, SyntheticInstrument synthetic) cpdef void update_synthetic(self, SyntheticInstrument synthetic) + cpdef queue_for_executor(self, func, tuple args=*, dict kwargs=*) + cpdef run_in_executor(self, func, tuple args=*, dict kwargs=*) + cpdef list queued_task_ids(self) + cpdef list active_task_ids(self) + cpdef bint has_queued_tasks(self) + cpdef bint has_active_tasks(self) + cpdef bint has_any_tasks(self) + cpdef void cancel_task(self, task_id) + cpdef void cancel_all_tasks(self) # -- SUBSCRIPTIONS -------------------------------------------------------------------------------- diff --git a/nautilus_trader/common/actor.pyx b/nautilus_trader/common/actor.pyx index 65d75e56aa9b..3d64a20320b6 100644 --- a/nautilus_trader/common/actor.pyx +++ b/nautilus_trader/common/actor.pyx @@ -24,10 +24,14 @@ attempts to operate without a managing `Trader` instance. """ +import asyncio +from concurrent.futures import Executor from typing import Optional import cython +from nautilus_trader.common.executor import ActorExecutor +from nautilus_trader.common.executor import TaskId from nautilus_trader.config import ActorConfig from nautilus_trader.config import ImportableActorConfig from nautilus_trader.persistence.streaming.writer import generate_signal_class @@ -542,6 +546,33 @@ cdef class Actor(Component): self.clock = self._clock self.log = self._log + cpdef void register_executor( + self, + loop: asyncio.AbstractEventLoop, + executor: Executor, + ): + """ + Register the given `Executor` for the actor. + + Parameters + ---------- + loop : asyncio.AsbtractEventLoop + The event loop of the application. + executor : concurrent.futures.Executor + The executor to register. + + Raises + ------ + TypeError + If `executor` is not of type `concurrent.futures.Executor` + + """ + Condition.type(executor, Executor, "executor") + + self._executor = ActorExecutor(loop, executor, logger=self._log) + + self._log.debug(f"Registered {executor}.") + cpdef void register_warning_event(self, type event): """ Register the given event type for warning log levels. @@ -691,6 +722,213 @@ cdef class Actor(Component): # This will replace the previous synthetic self.cache.add_synthetic(synthetic) + cpdef queue_for_executor( + self, + func: Callable[..., Any], + tuple args=None, + dict kwargs=None, + ): + """ + Queues the callable `func` to be executed as `fn(*args, **kwargs)` sequentially. + + Parameters + ---------- + func : Callable + The function to be executed. + args : positional arguments + The positional arguments for the call to `func`. + kwargs : arbitrary keyword arguments + The keyword arguments for the call to `func`. + + Raises + ------ + TypeError + If `func` is not of type `Callable`. + + Notes + ----- + For backtesting the `func` is immediately executed, as there's no need for a `Future` + object that can be awaited. In a backtesting scenario, the execution is not in real time, + and so the results of `func` are 'immediately' available after it's called. + + """ + Condition.callable(func, "func") + + if args is None: + args = () + if kwargs is None: + kwargs = {} + + if self._executor is None: + func(*args, **kwargs) + task_id = TaskId.create() + else: + task_id = self._executor.queue_for_executor( + func, + *args, + **kwargs, + ) + + self._log.info( + f"Executor: Queued {task_id}: {func.__name__}({args=}, {kwargs=}).", LogColor.BLUE, + ) + return task_id + + cpdef run_in_executor( + self, + func: Callable[..., Any], + tuple args=None, + dict kwargs=None, + ): + """ + Schedules the callable `func` to be executed as `fn(*args, **kwargs)`. + + Parameters + ---------- + func : Callable + The function to be executed. + args : positional arguments + The positional arguments for the call to `func`. + kwargs : arbitrary keyword arguments + The keyword arguments for the call to `func`. + + Returns + ------- + TaskId + The unique task identifier for the execution. + This also corresponds to any future objects memory address. + + Raises + ------ + TypeError + If `func` is not of type `Callable`. + + Notes + ----- + For backtesting the `func` is immediately executed, as there's no need for a `Future` + object that can be awaited. In a backtesting scenario, the execution is not in real time, + and so the results of `func` are 'immediately' available after it's called. + + """ + Condition.callable(func, "func") + + if args is None: + args = () + if kwargs is None: + kwargs = {} + + if self._executor is None: + func(*args, **kwargs) + task_id = TaskId.create() + else: + task_id = self._executor.run_in_executor( + func, + *args, + **kwargs, + ) + + self._log.info( + f"Executor: Submitted {task_id}: {func.__name__}({args=}, {kwargs=}).", LogColor.BLUE, + ) + return task_id + + cpdef list queued_task_ids(self): + """ + Return the queued task identifiers. + + Returns + ------- + list[TaskId] + + """ + if self._executor is None: + return [] # Tasks are immediately executed + + return self._executor.queued_task_ids() + + cpdef list active_task_ids(self): + """ + Return the active task identifiers. + + Returns + ------- + list[TaskId] + + """ + if self._executor is None: + return [] # Tasks are immediately executed + + return self._executor.active_task_ids() + + cpdef bint has_queued_tasks(self): + """ + Return a value indicating whether there are any queued tasks. + + Returns + ------- + bool + + """ + if self._executor is None: + return False + + return self._executor.has_queued_tasks() + + cpdef bint has_active_tasks(self): + """ + Return a value indicating whether there are any active tasks. + + Returns + ------- + bool + + """ + if self._executor is None: + return False + + return self._executor.has_active_tasks() + + cpdef bint has_any_tasks(self): + """ + Return a value indicating whether there are any queued or active tasks. + + Returns + ------- + bool + + """ + if self._executor is None: + return False + + return self._executor.has_queued_tasks() and self._executor.has_active_tasks() + + cpdef void cancel_task(self, task_id: TaskId): + """ + Cancel the task with the given `task_id` (if queued or active). + + If the task is not found then a warning is logged. + + Parameters + ---------- + task_id : TaskId + The task identifier. + + """ + if self._executor is None: + self._log.warning(f"Executor: {task_id} not found.") + return + + self._executor.cancel_task(task_id) + + cpdef void cancel_all_tasks(self): + """ + Cancel all queued and active tasks. + """ + if self._executor is None: + return + + self._executor.cancel_all_tasks() + # -- ACTION IMPLEMENTATIONS ----------------------------------------------------------------------- cpdef void _start(self): @@ -703,7 +941,11 @@ cdef class Actor(Component): cdef str name for name in timer_names: - self._log.info(f"Cancelled Timer(name={name}).") + self._log.info(f"Canceled Timer(name={name}).") + + if self._executor is not None: + self._log.info(f"Canceling executor tasks...") + self._executor.cancel_all_tasks() self.on_stop() diff --git a/nautilus_trader/common/executor.py b/nautilus_trader/common/executor.py new file mode 100644 index 000000000000..0a8bebb0fb08 --- /dev/null +++ b/nautilus_trader/common/executor.py @@ -0,0 +1,353 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2023 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from __future__ import annotations + +import asyncio +import functools +from asyncio import Future +from asyncio import Queue +from collections.abc import Callable +from concurrent.futures import Executor +from dataclasses import dataclass +from typing import Any + +from nautilus_trader.common.logging import LoggerAdapter +from nautilus_trader.core.uuid import UUID4 + + +@dataclass(frozen=True) +class TaskId: + """ + Represents a unique identifier for a task managed by the ActorExecutor. + + This ID can be associated with a task that is either queued for execution or + actively executing as an `asyncio.Future`. + + """ + + value: str + + def __repr__(self) -> str: + return f"{self.__class__.__name__}('{self.value}')" + + @classmethod + def create(cls) -> TaskId: + """ + Create and return a new task identifier with a UUID v4 value. + + Returns + ------- + TaskId + + """ + return TaskId(str(UUID4())) + + +class ActorExecutor: + """ + Provides an executor for `Actor` and `Strategy` classes. + + Provides an executor designed to handle asynchronous tasks for `Actor` and `Strategy` classes. + This custom executor queues and executes tasks within a given event loop and is tailored for + single-threaded applications. + + The `ActorExecutor` maintains its internal state to manage both queued and active tasks, + providing facilities for scheduling, cancellation, and monitoring. It can be used to create + more controlled execution flows within complex asynchronous systems. + + Parameters + ---------- + loop : AbstractEventLoop + The event loop for the application. + executor : Executor + The inner executor to register. + logger : LoggerAdatper + The logger for the executor. + + Warnings + -------- + This executor is not thread-safe and must be invoked from the same thread in which + it was created. Special care should be taken to ensure thread consistency when integrating + with multi-threaded applications. + + """ + + def __init__( + self, + loop: asyncio.AbstractEventLoop, + executor: Executor, + logger: LoggerAdapter, + ): + self._loop = loop + self._executor: Executor = executor + self._log: LoggerAdapter = logger + + self._active_tasks: dict[TaskId, Future[Any]] = {} + self._future_index: dict[Future[Any], TaskId] = {} + self._queued_tasks: set[TaskId] = set() + + self._queue: Queue = Queue() + self._worker_task = self._loop.create_task(self._worker()) + + def reset(self) -> None: + """ + Reset the executor. + + This will cancel all queued and active tasks, and drain the internal queue + without executing those tasks. + + """ + self.cancel_all_tasks() + self._drain_queue() + + self._active_tasks.clear() + self._future_index.clear() + self._queued_tasks.clear() + + def get_future(self, task_id: TaskId) -> Future | None: + """ + Return the executing `Future` with the given task_id (if found). + + Parameters + ---------- + task_id : TaskId + The task identifier for the future. + + Returns + ------- + asyncio.Future or ``None`` + + """ + return self._active_tasks.get(task_id) + + async def shutdown(self) -> None: + """ + Shutdown the executor in an async context. + + This will cancel the inner worker task. + + """ + self._worker_task.cancel() + try: + await asyncio.wait_for(self._worker_task, timeout=2.0) + except asyncio.CancelledError: + pass # Ignore the exception since we intentionally cancelled the task + except asyncio.TimeoutError: + self._log.error("Executor: TimeoutError shutting down worker.") + + def _drain_queue(self) -> None: + # Drain the internal task queue (this will not execute the tasks) + while not self._queue.empty(): + task_id, _, _, _ = self._queue.get_nowait() + self._log.info(f"Executor: Dequeued {task_id} prior to execution.") + self._queued_tasks.clear() + + def _add_active_task(self, task_id: TaskId, task: Future[Any]) -> None: + self._active_tasks[task_id] = task + self._future_index[task] = task_id + + async def _worker(self) -> None: + try: + while True: + task_id, func, args, kwargs = await self._queue.get() + if task_id not in self._queued_tasks: + continue # Already canceled + + task = self._submit_to_executor(func, *args, **kwargs) + + self._add_active_task(task_id, task) + self._log.debug(f"Executor: Scheduled {task_id}, {task} ...") + + # Sequentially execute tasks + await asyncio.wrap_future(self._active_tasks[task_id]) + self._queue.task_done() + except asyncio.CancelledError: + self._log.debug("Executor: Canceled inner worker task.") + + def _remove_done_task(self, task: Future[Any]) -> None: + task_id = self._future_index.pop(task, None) + if not task_id: + self._log.error(f"Executor: {task} not found on done callback.") + return + + self._active_tasks.pop(task_id, None) + + if task.done(): + try: + if task.exception() is not None: + self._log.error(f"Executor: Exception in {task_id}: {task.exception()}") + return + except asyncio.CancelledError: + # Make this a warning level for now + self._log.warning(f"Executor: Canceled {task_id}.") + return + self._log.info(f"Executor: Completed {task_id}.") + + def _submit_to_executor( + self, + func: Callable[..., Any], + *args: Any, + **kwargs: Any, + ) -> Future[Any]: + partial_func = functools.partial(func, *args, **kwargs) + task: Future[Any] = self._loop.run_in_executor(self._executor, partial_func) + task.add_done_callback(self._remove_done_task) + return task + + def queue_for_executor( + self, + func: Callable[..., Any], + *args: Any, + **kwargs: Any, + ) -> TaskId: + """ + Enqueue the given `func` to be executed sequentially. + + Parameters + ---------- + func : Callable + The function to be executed. + args : positional arguments + The positional arguments for the call to `func`. + kwargs : arbitrary keyword arguments + The keyword arguments for the call to `func`. + + Returns + ------- + TaskId + + """ + task_id = TaskId.create() + self._queue.put_nowait((task_id, func, args, kwargs)) + self._queued_tasks.add(task_id) + + return task_id + + def run_in_executor( + self, + func: Callable[..., Any], + *args: Any, + **kwargs: Any, + ) -> TaskId: + """ + Arrange for the given `func` to be called in the executor. + + Parameters + ---------- + func : Callable + The function to be executed. + args : positional arguments + The positional arguments for the call to `func`. + kwargs : arbitrary keyword arguments + The keyword arguments for the call to `func`. + + Returns + ------- + TaskId + + """ + self._log.info(f"Executor: {type(func).__name__}({args=}, {kwargs=})") + task: Future = self._submit_to_executor(func, *args, **kwargs) + + task_id = TaskId.create() + self._active_tasks[task_id] = task + self._future_index[task] = task_id + self._log.debug(f"Executor: Scheduled {task_id}, {task} ...") + + return task_id + + def queued_task_ids(self) -> list[TaskId]: + """ + Return the queued task identifiers. + + Returns + ------- + list[TaskId] + + """ + return list(self._queued_tasks) + + def active_task_ids(self) -> list[TaskId]: + """ + Return the active task identifiers. + + Returns + ------- + list[TaskId] + + """ + return list(self._active_tasks.keys()) + + def has_queued_tasks(self) -> bool: + """ + Return a value indicating whether there are any queued tasks. + + Returns + ------- + bool + + """ + return bool(self._queued_tasks) + + def has_active_tasks(self) -> bool: + """ + Return a value indicating whether there are any active tasks. + + Returns + ------- + bool + + """ + return bool(self._active_tasks) + + def cancel_task(self, task_id: TaskId) -> None: + """ + Cancel the task with the given `task_id` (if queued or active). + + If the task is not found then a warning is logged. + + Parameters + ---------- + task_id : TaskId + The active task identifier. + + """ + if task_id in self._queued_tasks: + self._queued_tasks.discard(task_id) + self._log.info(f"Executor: Canceled {task_id} prior to execution.") + return + + task: Future | None = self._active_tasks.pop(task_id, None) + if not task: + self._log.warning(f"Executor: {task_id} not found.") + return + + self._future_index.pop(task, None) + + result = task.cancel() + self._log.info(f"Executor: Canceled {task_id} with result {result}.") + + def cancel_all_tasks(self) -> None: + """ + Cancel all active and queued tasks. + """ + self._drain_queue() + + if self._worker_task is not None: + self._worker_task.cancel() + + for task_id in self._active_tasks.copy(): + self.cancel_task(task_id) diff --git a/nautilus_trader/common/factories.pxd b/nautilus_trader/common/factories.pxd index 9c41e800732a..fba3dbf29f8a 100644 --- a/nautilus_trader/common/factories.pxd +++ b/nautilus_trader/common/factories.pxd @@ -261,4 +261,7 @@ cdef class OrderFactory: dict entry_exec_algorithm_params=*, dict tp_exec_algorithm_params=*, dict sl_exec_algorithm_params=*, + str entry_tags=*, + str tp_tags=*, + str sl_tags=*, ) diff --git a/nautilus_trader/common/factories.pyx b/nautilus_trader/common/factories.pyx index 1e572d020c38..e0d2352ed224 100644 --- a/nautilus_trader/common/factories.pyx +++ b/nautilus_trader/common/factories.pyx @@ -259,11 +259,12 @@ cdef class OrderFactory: If `time_in_force` is ``GTD``. """ + cdef ClientOrderId client_order_id = self._order_id_generator.generate() return MarketOrder( trader_id=self.trader_id, strategy_id=self.strategy_id, instrument_id=instrument_id, - client_order_id=self._order_id_generator.generate(), + client_order_id=client_order_id, order_side=order_side, quantity=quantity, time_in_force=time_in_force, @@ -277,6 +278,7 @@ cdef class OrderFactory: parent_order_id=None, exec_algorithm_id=exec_algorithm_id, exec_algorithm_params=exec_algorithm_params, + exec_spawn_id=client_order_id if exec_algorithm_id is not None else None, tags=tags, ) @@ -349,11 +351,12 @@ cdef class OrderFactory: If `display_qty` is negative (< 0) or greater than `quantity`. """ + cdef ClientOrderId client_order_id = self._order_id_generator.generate() return LimitOrder( trader_id=self.trader_id, strategy_id=self.strategy_id, instrument_id=instrument_id, - client_order_id=self._order_id_generator.generate(), + client_order_id=client_order_id, order_side=order_side, quantity=quantity, price=price, @@ -373,6 +376,7 @@ cdef class OrderFactory: parent_order_id=None, exec_algorithm_id=exec_algorithm_id, exec_algorithm_params=exec_algorithm_params, + exec_spawn_id=client_order_id if exec_algorithm_id is not None else None, tags=tags, ) @@ -444,11 +448,12 @@ cdef class OrderFactory: If `time_in_force` is ``GTD`` and `expire_time` <= UNIX epoch. """ + cdef ClientOrderId client_order_id = self._order_id_generator.generate() return StopMarketOrder( trader_id=self.trader_id, strategy_id=self.strategy_id, instrument_id=instrument_id, - client_order_id=self._order_id_generator.generate(), + client_order_id=client_order_id, order_side=order_side, quantity=quantity, trigger_price=trigger_price, @@ -467,6 +472,7 @@ cdef class OrderFactory: parent_order_id=None, exec_algorithm_id=exec_algorithm_id, exec_algorithm_params=exec_algorithm_params, + exec_spawn_id=client_order_id if exec_algorithm_id is not None else None, tags=tags, ) @@ -549,11 +555,12 @@ cdef class OrderFactory: If `display_qty` is negative (< 0) or greater than `quantity`. """ + cdef ClientOrderId client_order_id = self._order_id_generator.generate() return StopLimitOrder( trader_id=self.trader_id, strategy_id=self.strategy_id, instrument_id=instrument_id, - client_order_id=self._order_id_generator.generate(), + client_order_id=client_order_id, order_side=order_side, quantity=quantity, price=price, @@ -575,6 +582,7 @@ cdef class OrderFactory: parent_order_id=None, exec_algorithm_id=exec_algorithm_id, exec_algorithm_params=exec_algorithm_params, + exec_spawn_id=client_order_id if exec_algorithm_id is not None else None, tags=tags, ) @@ -633,11 +641,12 @@ cdef class OrderFactory: If `time_in_force` is ``AT_THE_OPEN`` or ``AT_THE_CLOSE``. """ + cdef ClientOrderId client_order_id = self._order_id_generator.generate() return MarketToLimitOrder( trader_id=self.trader_id, strategy_id=self.strategy_id, instrument_id=instrument_id, - client_order_id=self._order_id_generator.generate(), + client_order_id=client_order_id, order_side=order_side, quantity=quantity, reduce_only=reduce_only, @@ -652,6 +661,7 @@ cdef class OrderFactory: parent_order_id=None, exec_algorithm_id=exec_algorithm_id, exec_algorithm_params=exec_algorithm_params, + exec_spawn_id=client_order_id if exec_algorithm_id is not None else None, tags=tags, ) @@ -723,11 +733,12 @@ cdef class OrderFactory: If `time_in_force` is ``GTD`` and `expire_time` <= UNIX epoch. """ + cdef ClientOrderId client_order_id = self._order_id_generator.generate() return MarketIfTouchedOrder( trader_id=self.trader_id, strategy_id=self.strategy_id, instrument_id=instrument_id, - client_order_id=self._order_id_generator.generate(), + client_order_id=client_order_id, order_side=order_side, quantity=quantity, trigger_price=trigger_price, @@ -746,6 +757,7 @@ cdef class OrderFactory: parent_order_id=None, exec_algorithm_id=exec_algorithm_id, exec_algorithm_params=exec_algorithm_params, + exec_spawn_id=client_order_id if exec_algorithm_id is not None else None, tags=tags, ) @@ -828,11 +840,12 @@ cdef class OrderFactory: If `display_qty` is negative (< 0) or greater than `quantity`. """ + cdef ClientOrderId client_order_id = self._order_id_generator.generate() return LimitIfTouchedOrder( trader_id=self.trader_id, strategy_id=self.strategy_id, instrument_id=instrument_id, - client_order_id=self._order_id_generator.generate(), + client_order_id=client_order_id, order_side=order_side, quantity=quantity, price=price, @@ -854,6 +867,7 @@ cdef class OrderFactory: parent_order_id=None, exec_algorithm_id=exec_algorithm_id, exec_algorithm_params=exec_algorithm_params, + exec_spawn_id=client_order_id if exec_algorithm_id is not None else None, tags=tags, ) @@ -932,11 +946,12 @@ cdef class OrderFactory: If `time_in_force` is ``GTD`` and `expire_time` <= UNIX epoch. """ + cdef ClientOrderId client_order_id = self._order_id_generator.generate() return TrailingStopMarketOrder( trader_id=self.trader_id, strategy_id=self.strategy_id, instrument_id=instrument_id, - client_order_id=self._order_id_generator.generate(), + client_order_id=client_order_id, order_side=order_side, quantity=quantity, trigger_price=trigger_price, @@ -957,6 +972,7 @@ cdef class OrderFactory: parent_order_id=None, exec_algorithm_id=exec_algorithm_id, exec_algorithm_params=exec_algorithm_params, + exec_spawn_id=client_order_id if exec_algorithm_id is not None else None, tags=tags, ) @@ -1052,11 +1068,12 @@ cdef class OrderFactory: If `display_qty` is negative (< 0) or greater than `quantity`. """ + cdef ClientOrderId client_order_id = self._order_id_generator.generate() return TrailingStopLimitOrder( trader_id=self.trader_id, strategy_id=self.strategy_id, instrument_id=instrument_id, - client_order_id=self._order_id_generator.generate(), + client_order_id=client_order_id, order_side=order_side, quantity=quantity, price=price, @@ -1081,6 +1098,7 @@ cdef class OrderFactory: parent_order_id=None, exec_algorithm_id=exec_algorithm_id, exec_algorithm_params=exec_algorithm_params, + exec_spawn_id=client_order_id if exec_algorithm_id is not None else None, tags=tags, ) @@ -1110,6 +1128,9 @@ cdef class OrderFactory: dict entry_exec_algorithm_params = None, dict tp_exec_algorithm_params = None, dict sl_exec_algorithm_params = None, + str entry_tags = "ENTRY", + str tp_tags = "TAKE_PROFIT", + str sl_tags = "STOP_LOSS", ): """ Create a bracket order with optional entry of take-profit order types. @@ -1168,6 +1189,15 @@ cdef class OrderFactory: The execution algorithm parameters for the order. sl_exec_algorithm_params : dict[str, Any], optional The execution algorithm parameters for the order. + entry_tags : str, default "ENTRY" + The custom user tags for the entry order. These are optional and can + contain any arbitrary delimiter if required. + tp_tags : str, default "TAKE_PROFIT" + The custom user tags for the take-profit order. These are optional and can + contain any arbitrary delimiter if required. + sl_tags : str, default "STOP_LOSS" + The custom user tags for the stop-loss order. These are optional and can + contain any arbitrary delimiter if required. Returns ------- @@ -1200,7 +1230,8 @@ cdef class OrderFactory: parent_order_id=None, exec_algorithm_id=entry_exec_algorithm_id, exec_algorithm_params=entry_exec_algorithm_params, - tags="ENTRY", + exec_spawn_id=entry_client_order_id if entry_exec_algorithm_id is not None else None, + tags=entry_tags, ) elif entry_order_type == OrderType.LIMIT: entry_order = LimitOrder( @@ -1225,7 +1256,8 @@ cdef class OrderFactory: parent_order_id=None, exec_algorithm_id=entry_exec_algorithm_id, exec_algorithm_params=entry_exec_algorithm_params, - tags="ENTRY", + exec_spawn_id=entry_client_order_id if entry_exec_algorithm_id is not None else None, + tags=entry_tags, ) elif entry_order_type == OrderType.MARKET_IF_TOUCHED: entry_order = MarketIfTouchedOrder( @@ -1250,7 +1282,8 @@ cdef class OrderFactory: parent_order_id=None, exec_algorithm_id=entry_exec_algorithm_id, exec_algorithm_params=entry_exec_algorithm_params, - tags="ENTRY", + exec_spawn_id=entry_client_order_id if entry_exec_algorithm_id is not None else None, + tags=entry_tags, ) elif entry_order_type == OrderType.LIMIT_IF_TOUCHED: entry_order = LimitIfTouchedOrder( @@ -1277,7 +1310,8 @@ cdef class OrderFactory: parent_order_id=None, exec_algorithm_id=entry_exec_algorithm_id, exec_algorithm_params=entry_exec_algorithm_params, - tags="ENTRY", + exec_spawn_id=entry_client_order_id if entry_exec_algorithm_id is not None else None, + tags=entry_tags, ) else: raise ValueError(f"invalid `entry_order_type`, was {order_type_to_str(entry_order_type)}") @@ -1309,7 +1343,8 @@ cdef class OrderFactory: parent_order_id=entry_client_order_id, exec_algorithm_id=tp_exec_algorithm_id, exec_algorithm_params=tp_exec_algorithm_params, - tags="TAKE_PROFIT", + exec_spawn_id=tp_client_order_id if tp_exec_algorithm_id is not None else None, + tags=tp_tags, ) elif tp_order_type == OrderType.LIMIT_IF_TOUCHED: tp_order = LimitIfTouchedOrder( @@ -1337,7 +1372,8 @@ cdef class OrderFactory: parent_order_id=entry_client_order_id, exec_algorithm_id=tp_exec_algorithm_id, exec_algorithm_params=tp_exec_algorithm_params, - tags="TAKE_PROFIT", + exec_spawn_id=tp_client_order_id if tp_exec_algorithm_id is not None else None, + tags=tp_tags, ) elif tp_order_type == OrderType.MARKET_IF_TOUCHED: tp_order = MarketIfTouchedOrder( @@ -1362,7 +1398,8 @@ cdef class OrderFactory: parent_order_id=entry_client_order_id, exec_algorithm_id=tp_exec_algorithm_id, exec_algorithm_params=tp_exec_algorithm_params, - tags="TAKE_PROFIT", + exec_spawn_id=tp_client_order_id if tp_exec_algorithm_id is not None else None, + tags=tp_tags, ) else: raise ValueError(f"invalid `tp_order_type`, was {order_type_to_str(entry_order_type)}") @@ -1392,7 +1429,8 @@ cdef class OrderFactory: parent_order_id=entry_client_order_id, exec_algorithm_id=sl_exec_algorithm_id, exec_algorithm_params=sl_exec_algorithm_params, - tags="STOP_LOSS", + exec_spawn_id=sl_client_order_id if sl_exec_algorithm_id is not None else None, + tags=sl_tags, ) return OrderList( diff --git a/nautilus_trader/common/messages.pxd b/nautilus_trader/common/messages.pxd index 8e4cee202c4c..f7e16d04a192 100644 --- a/nautilus_trader/common/messages.pxd +++ b/nautilus_trader/common/messages.pxd @@ -13,8 +13,11 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from libc.stdint cimport uint64_t + from nautilus_trader.common.enums_c cimport ComponentState from nautilus_trader.core.message cimport Event +from nautilus_trader.core.uuid cimport UUID4 from nautilus_trader.model.enums_c cimport TradingState from nautilus_trader.model.identifiers cimport ComponentId from nautilus_trader.model.identifiers cimport Identifier @@ -22,6 +25,10 @@ from nautilus_trader.model.identifiers cimport TraderId cdef class ComponentStateChanged(Event): + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init + cdef readonly TraderId trader_id """The trader ID associated with the event.\n\n:returns: `TraderId`""" cdef readonly Identifier component_id @@ -41,6 +48,10 @@ cdef class ComponentStateChanged(Event): cdef class RiskEvent(Event): + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init + cdef readonly TraderId trader_id """The trader ID associated with the event.\n\n:returns: `TraderId`""" diff --git a/nautilus_trader/common/messages.pyx b/nautilus_trader/common/messages.pyx index 20149a7c94ae..307d5ce1581a 100644 --- a/nautilus_trader/common/messages.pyx +++ b/nautilus_trader/common/messages.pyx @@ -68,13 +68,20 @@ cdef class ComponentStateChanged(Event): uint64_t ts_event, uint64_t ts_init, ): - super().__init__(event_id, ts_event, ts_init) - self.trader_id = trader_id self.component_id = component_id self.component_type = component_type self.state = state self.config = config + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init + + def __eq__(self, Event other) -> bool: + return self._event_id == other.id + + def __hash__(self) -> int: + return hash(self._event_id) def __str__(self) -> str: return ( @@ -84,7 +91,7 @@ cdef class ComponentStateChanged(Event): f"component_type={self.component_type}, " f"state={component_state_to_str(self.state)}, " f"config={self.config}, " - f"event_id={self.id.to_str()})" + f"event_id={self._event_id.to_str()})" ) def __repr__(self) -> str: @@ -95,10 +102,46 @@ cdef class ComponentStateChanged(Event): f"component_type={self.component_type}, " f"state={component_state_to_str(self.state)}, " f"config={self.config}, " - f"event_id={self.id.to_str()}, " - f"ts_init={self.ts_init})" + f"event_id={self._event_id.to_str()}, " + f"ts_init={self._ts_init})" ) + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init + @staticmethod cdef ComponentStateChanged from_dict_c(dict values): Condition.not_none(values, "values") @@ -141,9 +184,9 @@ cdef class ComponentStateChanged(Event): "component_type": obj.component_type, "state": component_state_to_str(obj.state), "config": config_bytes, - "event_id": obj.id.to_str(), - "ts_event": obj.ts_event, - "ts_init": obj.ts_init, + "event_id": obj._event_id.to_str(), + "ts_event": obj._ts_event, + "ts_init": obj._ts_init, } @staticmethod @@ -199,9 +242,52 @@ cdef class RiskEvent(Event): uint64_t ts_event, uint64_t ts_init, ): - super().__init__(event_id, ts_event, ts_init) - self.trader_id = trader_id + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init + + def __eq__(self, Event other) -> bool: + return self._event_id == other.id + + def __hash__(self) -> int: + return hash(self._event_id) + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init cdef class TradingStateChanged(RiskEvent): @@ -233,10 +319,12 @@ cdef class TradingStateChanged(RiskEvent): uint64_t ts_event, uint64_t ts_init, ): - super().__init__(trader_id, event_id, ts_event, ts_init) - + self.trader_id = trader_id self.state = state self.config = config + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init def __str__(self) -> str: return ( @@ -244,7 +332,7 @@ cdef class TradingStateChanged(RiskEvent): f"trader_id={self.trader_id.to_str()}, " f"state={trading_state_to_str(self.state)}, " f"config={self.config}, " - f"event_id={self.id.to_str()})" + f"event_id={self._event_id.to_str()})" ) def __repr__(self) -> str: @@ -253,10 +341,46 @@ cdef class TradingStateChanged(RiskEvent): f"trader_id={self.trader_id.to_str()}, " f"state={trading_state_to_str(self.state)}, " f"config={self.config}, " - f"event_id={self.id.to_str()}, " - f"ts_init={self.ts_init})" + f"event_id={self._event_id.to_str()}, " + f"ts_init={self._ts_init})" ) + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init + @staticmethod cdef TradingStateChanged from_dict_c(dict values): Condition.not_none(values, "values") @@ -291,9 +415,9 @@ cdef class TradingStateChanged(RiskEvent): "trader_id": obj.trader_id.to_str(), "state": trading_state_to_str(obj.state), "config": config_bytes, - "event_id": obj.id.to_str(), - "ts_event": obj.ts_event, - "ts_init": obj.ts_init, + "event_id": obj._event_id.to_str(), + "ts_event": obj._ts_event, + "ts_init": obj._ts_init, } @staticmethod diff --git a/nautilus_trader/common/queue.pyx b/nautilus_trader/common/queue.pyx deleted file mode 100644 index 448188accfcc..000000000000 --- a/nautilus_trader/common/queue.pyx +++ /dev/null @@ -1,230 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# Copyright (C) 2015-2023 Nautech Systems Pty Ltd. All rights reserved. -# https://nautechsystems.io -# -# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ------------------------------------------------------------------------------------------------- - -import asyncio -import collections - -from nautilus_trader.core.asynchronous import sleep0 - - -cdef class Queue: - """ - Provides a high-performance stripped back queue for use with coroutines - and an event loop. - - If `maxsize` is less than or equal to zero, the queue size is infinite. If it - is an integer greater than 0, then "await put()" will block when the - queue reaches maxsize, until an item is removed by get(). - - Unlike the standard library `Queue`, you can reliably know this Queue's size - with qsize(), since your single-threaded asyncio application won't be - interrupted between calling qsize() and doing an operation on the `Queue`. - - Parameters - ---------- - maxsize : int - The maximum capacity of the queue before blocking. - - Warnings - -------- - This queue is not thread-safe and must be called from the same thread as the - event loop. - """ - - def __init__(self, int maxsize=0): - self.maxsize = maxsize - self.count = 0 - - self._queue = collections.deque() - - cpdef int qsize(self): - """ - Return the number of items in the queue. - - Returns - ------- - int - - """ - return self._qsize() - - cpdef bint empty(self): - """ - Return a value indicating whether the queue is empty. - - Returns - ------- - bool - True if the queue is empty, False otherwise. - - """ - return self._empty() - - cpdef bint full(self): - """ - Return a value indicating whether the queue is full. - - Returns - ------- - bool - True if there are maxsize items in the queue. - - Notes - ----- - If the Queue was initialized with maxsize=0 (the default), - then full() is never True. - - """ - return self._full() - - async def put(self, item): - """ - Put `item` onto the queue. - - If the queue is full, wait until a free slot is available before adding - item. - - Parameters - --------- - item : object - The item to add to the queue. - - """ - while self._full(): - # Wait for free slot - await sleep0() - continue - - self._put_nowait(item) - - cpdef void put_nowait(self, item): - """ - Put `item` onto the queue *without* blocking. - - Raises - ------ - QueueFull - If no free slot is immediately available. - - """ - self._put_nowait(item) - - async def get(self): - """ - Remove and return the next item from the queue. - - If the queue is empty, wait until an item is available. - - Returns - ------- - object - - """ - while self._empty(): - # Wait for item to become available - await sleep0() - continue - - return self._get_nowait() - - cpdef object get_nowait(self): - """ - Remove and return an item from the queue. - - Raises - ------ - QueueEmpty - If an item is not immediately available. - - """ - return self._get_nowait() - - cpdef object peek_back(self): - """ - Return the item at the back of the queue without popping (if not empty). - - Returns - ------- - object or ``None`` - - """ - if self.count == 0: - return None - return self._queue[0] - - cpdef object peek_front(self): - """ - Return the item at the front of the queue without popping (if not empty). - - Returns - ------- - object or ``None`` - - """ - if self.count == 0: - return None - return self._queue[-1] - - cpdef object peek_index(self, int index): - """ - Return the item at the given `index` without popping (if in range). - - Returns - ------- - object - - Raises - ------ - IndexError - If `index` is out of range. - - """ - return self._queue[index] - - cpdef list to_list(self): - """ - Return a copy of the items in the queue. - - Returns - ------- - list[Any] - - """ - return list(self._queue) - - cdef int _qsize(self): - return self.count - - cdef bint _empty(self): - return self.count == 0 - - cdef bint _full(self): - if self.maxsize <= 0: - return False - else: - return self.count >= self.maxsize - - cdef void _put_nowait(self, item): - if self._full(): - raise asyncio.QueueFull() - self._queue.appendleft(item) - self.count += 1 - - cdef object _get_nowait(self): - if self._empty(): - raise asyncio.QueueEmpty() - item = self._queue.pop() - self.count -= 1 - return item diff --git a/nautilus_trader/common/throttler.pxd b/nautilus_trader/common/throttler.pxd index 5c9dd504e16a..1b81b5acf8e8 100644 --- a/nautilus_trader/common/throttler.pxd +++ b/nautilus_trader/common/throttler.pxd @@ -21,7 +21,6 @@ from libc.stdint cimport uint64_t from nautilus_trader.common.clock cimport Clock from nautilus_trader.common.logging cimport LoggerAdapter -from nautilus_trader.common.queue cimport Queue from nautilus_trader.common.timer cimport TimeEvent @@ -29,7 +28,7 @@ cdef class Throttler: cdef Clock _clock cdef LoggerAdapter _log cdef uint64_t _interval_ns - cdef Queue _buffer + cdef object _buffer cdef str _timer_name cdef object _timestamps cdef object _output_send diff --git a/nautilus_trader/common/throttler.pyx b/nautilus_trader/common/throttler.pyx index ced42aa3d0a9..abdb7b6a27fb 100644 --- a/nautilus_trader/common/throttler.pyx +++ b/nautilus_trader/common/throttler.pyx @@ -22,7 +22,6 @@ from collections import deque from nautilus_trader.common.clock cimport Clock from nautilus_trader.common.logging cimport Logger -from nautilus_trader.common.queue cimport Queue from nautilus_trader.common.timer cimport TimeEvent from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.rust.core cimport secs_to_nanos @@ -96,7 +95,7 @@ cdef class Throttler: self._clock = clock self._log = LoggerAdapter(component_name=f"Throttler-{name}", logger=logger) self._interval_ns = secs_to_nanos(interval.total_seconds()) - self._buffer = Queue() + self._buffer = deque() self._timer_name = f"{name}-DEQUE" self._timestamps = deque(maxlen=limit) self._output_send = output_send @@ -122,7 +121,7 @@ cdef class Throttler: int """ - return self._buffer.qsize() + return len(self._buffer) cpdef double used(self): """ @@ -184,7 +183,7 @@ cdef class Throttler: cdef void _limit_msg(self, msg): if self._output_drop is None: # Buffer - self._buffer.put_nowait(msg) + self._buffer.appendleft(msg) timer_target = self._process self._log.warning(f"Buffering {msg}.") else: @@ -198,6 +197,10 @@ cdef class Throttler: self.is_limiting = True cdef void _set_timer(self, handler: Callable[[TimeEvent], None]): + # Cancel any existing timer + if self._timer_name in self._clock.timer_names: + self._clock.cancel_timer(self._timer_name) + self._clock.set_time_alert_ns( name=self._timer_name, alert_time_ns=self._clock.timestamp_ns() + self._delta_next(), @@ -206,13 +209,14 @@ cdef class Throttler: cpdef void _process(self, TimeEvent event): # Send next msg on buffer - msg = self._buffer.get_nowait() + msg = self._buffer.pop() self._send_msg(msg) # Send remaining messages if within rate cdef int64_t delta_next - while not self._buffer.empty(): + while self._buffer: delta_next = self._delta_next() + msg = self._buffer.pop() if delta_next <= 0: self._send_msg(msg) else: diff --git a/nautilus_trader/common/timer.pyx b/nautilus_trader/common/timer.pyx index b0592c815179..b3fa5b4de554 100644 --- a/nautilus_trader/common/timer.pyx +++ b/nautilus_trader/common/timer.pyx @@ -21,15 +21,13 @@ from libc.stdint cimport uint64_t from nautilus_trader.common.timer cimport TimeEvent from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.message cimport Event -from nautilus_trader.core.rust.common cimport time_event_clone -from nautilus_trader.core.rust.common cimport time_event_drop -from nautilus_trader.core.rust.common cimport time_event_name_to_cstr from nautilus_trader.core.rust.common cimport time_event_new from nautilus_trader.core.rust.common cimport time_event_to_cstr from nautilus_trader.core.rust.core cimport nanos_to_secs from nautilus_trader.core.rust.core cimport uuid4_from_cstr from nautilus_trader.core.string cimport cstr_to_pystr from nautilus_trader.core.string cimport pystr_to_cstr +from nautilus_trader.core.string cimport ustr_to_pystr from nautilus_trader.core.uuid cimport UUID4 @@ -57,8 +55,6 @@ cdef class TimeEvent(Event): uint64_t ts_init, ): # Precondition: `name` validated in Rust - super().__init__(event_id, ts_event, ts_init) - self._mem = time_event_new( pystr_to_cstr(name), event_id._mem, @@ -66,21 +62,15 @@ cdef class TimeEvent(Event): ts_init, ) - def __del__(self) -> None: - if self._mem.name != NULL: - time_event_drop(self._mem) # `self._mem` moved to Rust (then dropped) - def __getstate__(self): return ( self.to_str(), - self.id.to_str(), + self.id.value, self.ts_event, self.ts_init, ) def __setstate__(self, state): - self.ts_event = state[2] - self.ts_init = state[3] self._mem = time_event_new( pystr_to_cstr(state[0]), uuid4_from_cstr(pystr_to_cstr(state[1])), @@ -89,7 +79,7 @@ cdef class TimeEvent(Event): ) cdef str to_str(self): - return cstr_to_pystr(time_event_name_to_cstr(&self._mem)) + return ustr_to_pystr(self._mem.name) def __eq__(self, TimeEvent other) -> bool: return self.to_str() == other.to_str() @@ -113,15 +103,50 @@ cdef class TimeEvent(Event): str """ - return cstr_to_pystr(time_event_name_to_cstr((&self._mem))) + return ustr_to_pystr(self._mem.name) + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + cdef UUID4 uuid4 = UUID4.__new__(UUID4) + uuid4._mem = self._mem.event_id + return uuid4 + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._mem.ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._mem.ts_init @staticmethod cdef TimeEvent from_mem_c(TimeEvent_t mem): cdef TimeEvent event = TimeEvent.__new__(TimeEvent) - event._mem = time_event_clone(&mem) - event.id = UUID4.from_mem_c(mem.event_id) - event.ts_event = mem.ts_event - event.ts_init = mem.ts_init + event._mem = mem return event diff --git a/nautilus_trader/config/backtest.py b/nautilus_trader/config/backtest.py index 709a43213eaa..db6fb86a9316 100644 --- a/nautilus_trader/config/backtest.py +++ b/nautilus_trader/config/backtest.py @@ -52,6 +52,7 @@ class BacktestVenueConfig(NautilusConfig, frozen=True): bar_execution: bool = True reject_stop_orders: bool = True support_gtd_orders: bool = True + use_position_ids: bool = True use_random_ids: bool = False use_reduce_only: bool = True # fill_model: Optional[FillModel] = None # TODO(cs): Implement diff --git a/nautilus_trader/config/common.py b/nautilus_trader/config/common.py index 4fb86431bfda..39b7139d9874 100644 --- a/nautilus_trader/config/common.py +++ b/nautilus_trader/config/common.py @@ -288,8 +288,16 @@ class ExecEngineConfig(NautilusConfig, frozen=True): class OrderEmulatorConfig(NautilusConfig, frozen=True): """ Configuration for ``OrderEmulator`` instances. + + Parameters + ---------- + debug : bool, default False + If debug mode is active (will provide extra debug logging). + """ + debug: bool = False + class StreamingConfig(NautilusConfig, frozen=True): """ @@ -561,6 +569,35 @@ def create(config: ImportableExecAlgorithmConfig): return exec_algorithm_cls(config=config_cls(**config.config)) +class TracingConfig(NautilusConfig, frozen=True): + """ + Configuration for standard output and file logging for Rust tracing statements for a + ``NautilusKernel`` instance. + + Parameters + ---------- + stdout_level : str, optional + The minimum log level to write to stdout. Possible options are "debug", + "info", "warn", "error". Setting it None means no logs are written to + stdout. + stderr_level : str, optional + The minimum log level to write to stderr. Possible options are "debug", + "info", "warn", "error". Setting it None means no logs are written to + stderr. + file_config : tuple[str, str, str], optional + The minimum log level to write to log file. Possible options are "debug", + "info", "warn", "error". Setting it None means no logs are written to + the log file. + The second str is the prefix name of the log file and the third str is + the name of the directory. + + """ + + stdout_level: Optional[str] = None + stderr_level: Optional[str] = None + file_level: Optional[tuple[str, str, str]] = None + + class LoggingConfig(NautilusConfig, frozen=True): """ Configuration for standard output and file logging for a ``NautilusKernel`` @@ -619,6 +656,8 @@ class NautilusKernelConfig(NautilusConfig, frozen=True): The live risk engine configuration. exec_engine : ExecEngineConfig, optional The live execution engine configuration. + emulator : OrderEmulatorConfig, optional + The order emulator configuration. streaming : StreamingConfig, optional The configuration for streaming to feather files. catalog : DataCatalogConfig, optional @@ -654,6 +693,7 @@ class NautilusKernelConfig(NautilusConfig, frozen=True): data_engine: Optional[DataEngineConfig] = None risk_engine: Optional[RiskEngineConfig] = None exec_engine: Optional[ExecEngineConfig] = None + emulator: Optional[OrderEmulatorConfig] = None streaming: Optional[StreamingConfig] = None catalog: Optional[DataCatalogConfig] = None actors: list[ImportableActorConfig] = [] @@ -663,6 +703,7 @@ class NautilusKernelConfig(NautilusConfig, frozen=True): save_state: bool = False loop_debug: bool = False logging: Optional[LoggingConfig] = None + tracing: Optional[TracingConfig] = None timeout_connection: PositiveFloat = 10.0 timeout_reconciliation: PositiveFloat = 10.0 timeout_portfolio: PositiveFloat = 10.0 diff --git a/nautilus_trader/core/correctness.pyx b/nautilus_trader/core/correctness.pyx index db53e8507ab7..a7983a778011 100644 --- a/nautilus_trader/core/correctness.pyx +++ b/nautilus_trader/core/correctness.pyx @@ -26,7 +26,7 @@ cdef class Condition: Provides checking of function or method conditions. A condition is a predicate which must be true just prior to the execution of - some section of code - for correct behaviour as per the design specification. + some section of code - for correct behavior as per the design specification. If a check fails, then an Exception is thrown with a descriptive message. """ diff --git a/nautilus_trader/core/includes/common.h b/nautilus_trader/core/includes/common.h index 6a568094f3bd..81e81713b0e5 100644 --- a/nautilus_trader/core/includes/common.h +++ b/nautilus_trader/core/includes/common.h @@ -203,8 +203,6 @@ typedef struct LiveClock LiveClock; */ typedef struct Logger_t Logger_t; -typedef struct Rc_String Rc_String; - typedef struct TestClock TestClock; /** @@ -257,7 +255,7 @@ typedef struct TimeEvent_t { /** * The event name. */ - struct Rc_String *name; + char* name; /** * The event ID. */ @@ -469,12 +467,6 @@ struct TimeEvent_t time_event_new(const char *name_ptr, uint64_t ts_event, uint64_t ts_init); -struct TimeEvent_t time_event_clone(const struct TimeEvent_t *event); - -void time_event_drop(struct TimeEvent_t event); - -const char *time_event_name_to_cstr(const struct TimeEvent_t *event); - /** * Returns a [`TimeEvent`] as a C string pointer. */ diff --git a/nautilus_trader/core/includes/core.h b/nautilus_trader/core/includes/core.h index bb63abaf10e3..c8da10f4e57d 100644 --- a/nautilus_trader/core/includes/core.h +++ b/nautilus_trader/core/includes/core.h @@ -9,7 +9,7 @@ * `CVec` is a C compatible struct that stores an opaque pointer to a block of * memory, it's length and the capacity of the vector it was allocated from. * - * NOTE: Changing the values here may lead to undefined behaviour when the + * NOTE: Changing the values here may lead to undefined behavior when the * memory is dropped. */ typedef struct CVec { diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index 193af58e3f6a..7d140e5820f5 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -188,7 +188,7 @@ typedef enum BookType { } BookType; /** - * The order contigency type which specifies the behaviour of linked orders. + * The order contigency type which specifies the behavior of linked orders. * * [FIX 5.0 SP2 : ContingencyType <1385> field](https://www.onixs.biz/fix-dictionary/5.0.sp2/tagnum_1385.html). */ @@ -367,45 +367,53 @@ typedef enum OrderStatus { */ DENIED = 2, /** - * The order was submitted by the Nautilus system to the external service or trading venue (closed/done). + * The order became emulated by the Nautilus system in the `OrderEmulator` component. */ - SUBMITTED = 3, + EMULATED = 3, + /** + * The order was released by the Nautilus system from the `OrderEmulator` component. + */ + RELEASED = 4, + /** + * The order was submitted by the Nautilus system to the external service or trading venue (awaiting acknowledgement). + */ + SUBMITTED = 5, /** * The order was acknowledged by the trading venue as being received and valid (may now be working). */ - ACCEPTED = 4, + ACCEPTED = 6, /** * The order was rejected by the trading venue. */ - REJECTED = 5, + REJECTED = 7, /** * The order was canceled (closed/done). */ - CANCELED = 6, + CANCELED = 8, /** * The order reached a GTD expiration (closed/done). */ - EXPIRED = 7, + EXPIRED = 9, /** - * The order STOP price was triggered (closed/done). + * The order STOP price was triggered on a trading venue. */ - TRIGGERED = 8, + TRIGGERED = 10, /** - * The order is currently pending a request to modify at the trading venue. + * The order is currently pending a request to modify on a trading venue. */ - PENDING_UPDATE = 9, + PENDING_UPDATE = 11, /** - * The order is currently pending a request to cancel at the trading venue. + * The order is currently pending a request to cancel on a trading venue. */ - PENDING_CANCEL = 10, + PENDING_CANCEL = 12, /** - * The order has been partially filled at the trading venue. + * The order has been partially filled on a trading venue. */ - PARTIALLY_FILLED = 11, + PARTIALLY_FILLED = 13, /** - * The order has been completely filled at the trading venue (closed/done). + * The order has been completely filled on a trading venue (closed/done). */ - FILLED = 12, + FILLED = 14, } OrderStatus; /** @@ -622,8 +630,6 @@ typedef struct Level Level; typedef struct OrderBook OrderBook; -typedef struct String String; - /** * Represents a synthetic instrument with prices derived from component instruments using a * formula. @@ -720,11 +726,11 @@ typedef struct QuoteTick_t { /** * The top of book bid price. */ - struct Price_t bid; + struct Price_t bid_price; /** * The top of book ask price. */ - struct Price_t ask; + struct Price_t ask_price; /** * The top of book bid size. */ @@ -789,7 +795,7 @@ typedef struct BarSpecification_t { /** * The step for binning samples for bar aggregation. */ - uint64_t step; + uintptr_t step; /** * The type of bar aggregation. */ @@ -899,16 +905,78 @@ typedef struct OrderDenied_t { struct StrategyId_t strategy_id; struct InstrumentId_t instrument_id; struct ClientOrderId_t client_order_id; - struct String *reason; + char* reason; UUID4_t event_id; uint64_t ts_event; uint64_t ts_init; } OrderDenied_t; +typedef struct OrderEmulated_t { + struct TraderId_t trader_id; + struct StrategyId_t strategy_id; + struct InstrumentId_t instrument_id; + struct ClientOrderId_t client_order_id; + UUID4_t event_id; + uint64_t ts_event; + uint64_t ts_init; +} OrderEmulated_t; + +typedef struct OrderReleased_t { + struct TraderId_t trader_id; + struct StrategyId_t strategy_id; + struct InstrumentId_t instrument_id; + struct ClientOrderId_t client_order_id; + struct Price_t released_price; + UUID4_t event_id; + uint64_t ts_event; + uint64_t ts_init; +} OrderReleased_t; + typedef struct AccountId_t { char* value; } AccountId_t; +typedef struct OrderSubmitted_t { + struct TraderId_t trader_id; + struct StrategyId_t strategy_id; + struct InstrumentId_t instrument_id; + struct ClientOrderId_t client_order_id; + struct AccountId_t account_id; + UUID4_t event_id; + uint64_t ts_event; + uint64_t ts_init; +} OrderSubmitted_t; + +typedef struct VenueOrderId_t { + char* value; +} VenueOrderId_t; + +typedef struct OrderAccepted_t { + struct TraderId_t trader_id; + struct StrategyId_t strategy_id; + struct InstrumentId_t instrument_id; + struct ClientOrderId_t client_order_id; + struct VenueOrderId_t venue_order_id; + struct AccountId_t account_id; + UUID4_t event_id; + uint64_t ts_event; + uint64_t ts_init; + uint8_t reconciliation; +} OrderAccepted_t; + +typedef struct OrderRejected_t { + struct TraderId_t trader_id; + struct StrategyId_t strategy_id; + struct InstrumentId_t instrument_id; + struct ClientOrderId_t client_order_id; + struct AccountId_t account_id; + char* reason; + UUID4_t event_id; + uint64_t ts_event; + uint64_t ts_init; + uint8_t reconciliation; +} OrderRejected_t; + typedef struct ClientId_t { char* value; } ClientId_t; @@ -929,10 +997,6 @@ typedef struct PositionId_t { char* value; } PositionId_t; -typedef struct VenueOrderId_t { - char* value; -} VenueOrderId_t; - /** * Provides a C compatible Foreign Function Interface (FFI) for an underlying * [`SyntheticInstrument`]. @@ -998,7 +1062,7 @@ typedef struct Money_t { struct Data_t data_clone(const struct Data_t *data); -struct BarSpecification_t bar_specification_new(uint64_t step, +struct BarSpecification_t bar_specification_new(uintptr_t step, uint8_t aggregation, uint8_t price_type); @@ -1392,6 +1456,9 @@ const char *trigger_type_to_cstr(enum TriggerType value); enum TriggerType trigger_type_from_cstr(const char *ptr); /** + * # Safety + * + * - Assumes valid C string pointers. * # Safety * * - Assumes `reason_ptr` is a valid C string pointer. @@ -1405,14 +1472,58 @@ struct OrderDenied_t order_denied_new(struct TraderId_t trader_id, uint64_t ts_event, uint64_t ts_init); -/** - * Frees the memory for the given `event` by dropping. - */ -void order_denied_drop(struct OrderDenied_t event); +struct OrderEmulated_t order_emulated_new(struct TraderId_t trader_id, + struct StrategyId_t strategy_id, + struct InstrumentId_t instrument_id, + struct ClientOrderId_t client_order_id, + UUID4_t event_id, + uint64_t ts_event, + uint64_t ts_init); + +struct OrderReleased_t order_released_new(struct TraderId_t trader_id, + struct StrategyId_t strategy_id, + struct InstrumentId_t instrument_id, + struct ClientOrderId_t client_order_id, + struct Price_t released_price, + UUID4_t event_id, + uint64_t ts_event, + uint64_t ts_init); + +struct OrderSubmitted_t order_submitted_new(struct TraderId_t trader_id, + struct StrategyId_t strategy_id, + struct InstrumentId_t instrument_id, + struct ClientOrderId_t client_order_id, + struct AccountId_t account_id, + UUID4_t event_id, + uint64_t ts_event, + uint64_t ts_init); -struct OrderDenied_t order_denied_clone(const struct OrderDenied_t *event); +struct OrderAccepted_t order_accepted_new(struct TraderId_t trader_id, + struct StrategyId_t strategy_id, + struct InstrumentId_t instrument_id, + struct ClientOrderId_t client_order_id, + struct VenueOrderId_t venue_order_id, + struct AccountId_t account_id, + UUID4_t event_id, + uint64_t ts_event, + uint64_t ts_init, + uint8_t reconciliation); -const char *order_denied_reason_to_cstr(const struct OrderDenied_t *event); +/** + * # Safety + * + * - Assumes `reason_ptr` is a valid C string pointer. + */ +struct OrderRejected_t order_rejected_new(struct TraderId_t trader_id, + struct StrategyId_t strategy_id, + struct InstrumentId_t instrument_id, + struct ClientOrderId_t client_order_id, + struct AccountId_t account_id, + const char *reason_ptr, + UUID4_t event_id, + uint64_t ts_event, + uint64_t ts_init, + uint8_t reconciliation); void interned_string_stats(void); diff --git a/nautilus_trader/core/inspect.py b/nautilus_trader/core/inspect.py index dcb4395a8ebd..e41395f9b11a 100644 --- a/nautilus_trader/core/inspect.py +++ b/nautilus_trader/core/inspect.py @@ -15,6 +15,7 @@ import gc import sys +from typing import Any def is_nautilus_class(cls: type) -> bool: @@ -28,7 +29,7 @@ def is_nautilus_class(cls: type) -> bool: return bool(any(base.__module__.startswith("nautilus_trader.model") for base in cls.__bases__)) -def get_size_of(obj) -> int: +def get_size_of(obj: Any) -> int: """ Return the bytes size in memory of the given object. diff --git a/nautilus_trader/core/message.pxd b/nautilus_trader/core/message.pxd index ed869adde09f..b2f37babb3bf 100644 --- a/nautilus_trader/core/message.pxd +++ b/nautilus_trader/core/message.pxd @@ -33,12 +33,7 @@ cdef class Document: cdef class Event: - cdef readonly UUID4 id - """The event message ID.\n\n:returns: `UUID4`""" - cdef readonly uint64_t ts_init - """The UNIX timestamp (nanoseconds) when the object was initialized.\n\n:returns: `uint64_t`""" - cdef readonly uint64_t ts_event - """The UNIX timestamp (nanoseconds) when the event occurred.\n\n:returns: `uint64_t`""" + pass cdef class Request: diff --git a/nautilus_trader/core/message.pyx b/nautilus_trader/core/message.pyx index b804aad10a39..3e56bbc5b487 100644 --- a/nautilus_trader/core/message.pyx +++ b/nautilus_trader/core/message.pyx @@ -15,6 +15,8 @@ from typing import Any, Callable +import cython + from nautilus_trader.core.uuid cimport UUID4 @@ -106,54 +108,51 @@ cdef class Document: return f"{type(self).__name__}(id={self.id}, ts_init={self.ts_init})" +@cython.auto_pickle(False) cdef class Event: """ - The base class for all event messages. - - Parameters - ---------- - event_id : UUID4 - The event ID. - ts_event : uint64_t - The UNIX timestamp (nanoseconds) when the event occurred. - ts_init : uint64_t - The UNIX timestamp (nanoseconds) when the object was initialized. + The abstract base class for all event messages. Warnings -------- This class should not be used directly, but through a concrete subclass. """ - def __init__( - self, - UUID4 event_id not None, - uint64_t ts_event, - uint64_t ts_init, - ): - self.id = event_id - self.ts_event = ts_event - self.ts_init = ts_init + @property + def id(self) -> UUID4: + """ + The event message identifier. - def __getstate__(self): - return ( - self.id.to_str(), - self.ts_event, - self.ts_init, - ) + Returns + ------- + UUID4 - def __setstate__(self, state): - self.id = UUID4(state[0]) - self.ts_event = state[1] - self.ts_init = state[2] + """ + raise NotImplementedError("abstract property must be implemented") - def __eq__(self, Event other) -> bool: - return self.id == other.id + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. - def __hash__(self) -> int: - return hash(self.id) + Returns + ------- + int - def __repr__(self) -> str: - return f"{type(self).__name__}(id={self.id}, ts_event={self.ts_event}, ts_init={self.ts_init})" + """ + raise NotImplementedError("abstract property must be implemented") + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + raise NotImplementedError("abstract property must be implemented") cdef class Request: diff --git a/nautilus_trader/core/rust/common.pxd b/nautilus_trader/core/rust/common.pxd index a8f4ae5f3f61..d2c8c4d505b8 100644 --- a/nautilus_trader/core/rust/common.pxd +++ b/nautilus_trader/core/rust/common.pxd @@ -110,9 +110,6 @@ cdef extern from "../includes/common.h": cdef struct Logger_t: pass - cdef struct Rc_String: - pass - cdef struct TestClock: pass @@ -153,7 +150,7 @@ cdef extern from "../includes/common.h": # Represents a time event occurring at the event timestamp. cdef struct TimeEvent_t: # The event name. - Rc_String *name; + char* name; # The event ID. UUID4_t event_id; # The message category @@ -325,11 +322,5 @@ cdef extern from "../includes/common.h": uint64_t ts_event, uint64_t ts_init); - TimeEvent_t time_event_clone(const TimeEvent_t *event); - - void time_event_drop(TimeEvent_t event); - - const char *time_event_name_to_cstr(const TimeEvent_t *event); - # Returns a [`TimeEvent`] as a C string pointer. const char *time_event_to_cstr(const TimeEvent_t *event); diff --git a/nautilus_trader/core/rust/core.pxd b/nautilus_trader/core/rust/core.pxd index 06ac18eeaaf2..f6885c65c212 100644 --- a/nautilus_trader/core/rust/core.pxd +++ b/nautilus_trader/core/rust/core.pxd @@ -7,7 +7,7 @@ cdef extern from "../includes/core.h": # `CVec` is a C compatible struct that stores an opaque pointer to a block of # memory, it's length and the capacity of the vector it was allocated from. # - # NOTE: Changing the values here may lead to undefined behaviour when the + # NOTE: Changing the values here may lead to undefined behavior when the # memory is dropped. cdef struct CVec: # Opaque pointer to block of memory storing elements to access the diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index 52423dae21e8..610ecb767133 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -104,7 +104,7 @@ cdef extern from "../includes/model.h": # Market by order, multiple orders per level (full granularity). L3_MBO # = 3, - # The order contigency type which specifies the behaviour of linked orders. + # The order contigency type which specifies the behavior of linked orders. # # [FIX 5.0 SP2 : ContingencyType <1385> field](https://www.onixs.biz/fix-dictionary/5.0.sp2/tagnum_1385.html). cpdef enum ContingencyType: @@ -205,26 +205,30 @@ cdef extern from "../includes/model.h": INITIALIZED # = 1, # The order was denied by the Nautilus system, either for being invalid, unprocessable or exceeding a risk limit. DENIED # = 2, - # The order was submitted by the Nautilus system to the external service or trading venue (closed/done). - SUBMITTED # = 3, + # The order became emulated by the Nautilus system in the `OrderEmulator` component. + EMULATED # = 3, + # The order was released by the Nautilus system from the `OrderEmulator` component. + RELEASED # = 4, + # The order was submitted by the Nautilus system to the external service or trading venue (awaiting acknowledgement). + SUBMITTED # = 5, # The order was acknowledged by the trading venue as being received and valid (may now be working). - ACCEPTED # = 4, + ACCEPTED # = 6, # The order was rejected by the trading venue. - REJECTED # = 5, + REJECTED # = 7, # The order was canceled (closed/done). - CANCELED # = 6, + CANCELED # = 8, # The order reached a GTD expiration (closed/done). - EXPIRED # = 7, - # The order STOP price was triggered (closed/done). - TRIGGERED # = 8, - # The order is currently pending a request to modify at the trading venue. - PENDING_UPDATE # = 9, - # The order is currently pending a request to cancel at the trading venue. - PENDING_CANCEL # = 10, - # The order has been partially filled at the trading venue. - PARTIALLY_FILLED # = 11, - # The order has been completely filled at the trading venue (closed/done). - FILLED # = 12, + EXPIRED # = 9, + # The order STOP price was triggered on a trading venue. + TRIGGERED # = 10, + # The order is currently pending a request to modify on a trading venue. + PENDING_UPDATE # = 11, + # The order is currently pending a request to cancel on a trading venue. + PENDING_CANCEL # = 12, + # The order has been partially filled on a trading venue. + PARTIALLY_FILLED # = 13, + # The order has been completely filled on a trading venue (closed/done). + FILLED # = 14, # The type of order. cpdef enum OrderType: @@ -337,9 +341,6 @@ cdef extern from "../includes/model.h": cdef struct OrderBook: pass - cdef struct String: - pass - # Represents a synthetic instrument with prices derived from component instruments using a # formula. cdef struct SyntheticInstrument: @@ -396,9 +397,9 @@ cdef extern from "../includes/model.h": # The quotes instrument ID. InstrumentId_t instrument_id; # The top of book bid price. - Price_t bid; + Price_t bid_price; # The top of book ask price. - Price_t ask; + Price_t ask_price; # The top of book bid size. Quantity_t bid_size; # The top of book ask size. @@ -432,7 +433,7 @@ cdef extern from "../includes/model.h": # method/rule and price type. cdef struct BarSpecification_t: # The step for binning samples for bar aggregation. - uint64_t step; + uintptr_t step; # The type of bar aggregation. uint8_t aggregation; # The price type to use for aggregation. @@ -494,7 +495,26 @@ cdef extern from "../includes/model.h": StrategyId_t strategy_id; InstrumentId_t instrument_id; ClientOrderId_t client_order_id; - String *reason; + char* reason; + UUID4_t event_id; + uint64_t ts_event; + uint64_t ts_init; + + cdef struct OrderEmulated_t: + TraderId_t trader_id; + StrategyId_t strategy_id; + InstrumentId_t instrument_id; + ClientOrderId_t client_order_id; + UUID4_t event_id; + uint64_t ts_event; + uint64_t ts_init; + + cdef struct OrderReleased_t: + TraderId_t trader_id; + StrategyId_t strategy_id; + InstrumentId_t instrument_id; + ClientOrderId_t client_order_id; + Price_t released_price; UUID4_t event_id; uint64_t ts_event; uint64_t ts_init; @@ -502,6 +522,43 @@ cdef extern from "../includes/model.h": cdef struct AccountId_t: char* value; + cdef struct OrderSubmitted_t: + TraderId_t trader_id; + StrategyId_t strategy_id; + InstrumentId_t instrument_id; + ClientOrderId_t client_order_id; + AccountId_t account_id; + UUID4_t event_id; + uint64_t ts_event; + uint64_t ts_init; + + cdef struct VenueOrderId_t: + char* value; + + cdef struct OrderAccepted_t: + TraderId_t trader_id; + StrategyId_t strategy_id; + InstrumentId_t instrument_id; + ClientOrderId_t client_order_id; + VenueOrderId_t venue_order_id; + AccountId_t account_id; + UUID4_t event_id; + uint64_t ts_event; + uint64_t ts_init; + uint8_t reconciliation; + + cdef struct OrderRejected_t: + TraderId_t trader_id; + StrategyId_t strategy_id; + InstrumentId_t instrument_id; + ClientOrderId_t client_order_id; + AccountId_t account_id; + char* reason; + UUID4_t event_id; + uint64_t ts_event; + uint64_t ts_init; + uint8_t reconciliation; + cdef struct ClientId_t: char* value; @@ -517,9 +574,6 @@ cdef extern from "../includes/model.h": cdef struct PositionId_t: char* value; - cdef struct VenueOrderId_t: - char* value; - # Provides a C compatible Foreign Function Interface (FFI) for an underlying # [`SyntheticInstrument`]. # @@ -572,7 +626,7 @@ cdef extern from "../includes/model.h": Data_t data_clone(const Data_t *data); - BarSpecification_t bar_specification_new(uint64_t step, + BarSpecification_t bar_specification_new(uintptr_t step, uint8_t aggregation, uint8_t price_type); @@ -898,6 +952,9 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is a valid C string pointer. TriggerType trigger_type_from_cstr(const char *ptr); + # # Safety + # + # - Assumes valid C string pointers. # # Safety # # - Assumes `reason_ptr` is a valid C string pointer. @@ -910,12 +967,56 @@ cdef extern from "../includes/model.h": uint64_t ts_event, uint64_t ts_init); - # Frees the memory for the given `event` by dropping. - void order_denied_drop(OrderDenied_t event); + OrderEmulated_t order_emulated_new(TraderId_t trader_id, + StrategyId_t strategy_id, + InstrumentId_t instrument_id, + ClientOrderId_t client_order_id, + UUID4_t event_id, + uint64_t ts_event, + uint64_t ts_init); + + OrderReleased_t order_released_new(TraderId_t trader_id, + StrategyId_t strategy_id, + InstrumentId_t instrument_id, + ClientOrderId_t client_order_id, + Price_t released_price, + UUID4_t event_id, + uint64_t ts_event, + uint64_t ts_init); + + OrderSubmitted_t order_submitted_new(TraderId_t trader_id, + StrategyId_t strategy_id, + InstrumentId_t instrument_id, + ClientOrderId_t client_order_id, + AccountId_t account_id, + UUID4_t event_id, + uint64_t ts_event, + uint64_t ts_init); - OrderDenied_t order_denied_clone(const OrderDenied_t *event); + OrderAccepted_t order_accepted_new(TraderId_t trader_id, + StrategyId_t strategy_id, + InstrumentId_t instrument_id, + ClientOrderId_t client_order_id, + VenueOrderId_t venue_order_id, + AccountId_t account_id, + UUID4_t event_id, + uint64_t ts_event, + uint64_t ts_init, + uint8_t reconciliation); - const char *order_denied_reason_to_cstr(const OrderDenied_t *event); + # # Safety + # + # - Assumes `reason_ptr` is a valid C string pointer. + OrderRejected_t order_rejected_new(TraderId_t trader_id, + StrategyId_t strategy_id, + InstrumentId_t instrument_id, + ClientOrderId_t client_order_id, + AccountId_t account_id, + const char *reason_ptr, + UUID4_t event_id, + uint64_t ts_event, + uint64_t ts_init, + uint8_t reconciliation); void interned_string_stats(); diff --git a/nautilus_trader/core/string.pxd b/nautilus_trader/core/string.pxd index 67898bdc4ca6..c788eab08e77 100644 --- a/nautilus_trader/core/string.pxd +++ b/nautilus_trader/core/string.pxd @@ -61,7 +61,7 @@ cdef inline str cstr_to_pystr(const char* ptr, bint drop = True): cdef str obj = PyUnicode_FromString(ptr) # Assumes `ptr` was created from Rust `CString::from_raw`, - # otherwise will lead to undefined behaviour when passed to `cstr_drop`. + # otherwise will lead to undefined behavior when passed to `cstr_drop`. if drop: cstr_drop(ptr) return obj @@ -76,7 +76,7 @@ cdef inline bytes cstr_to_pybytes(const char* ptr): cdef bytes obj = PyBytes_FromString(ptr) # Assumes `ptr` was created from Rust `CString::from_raw`, - # otherwise will lead to undefined behaviour when passed to `cstr_drop`. + # otherwise will lead to undefined behavior when passed to `cstr_drop`. cstr_drop(ptr) return obj diff --git a/nautilus_trader/data/engine.pyx b/nautilus_trader/data/engine.pyx index eb808c7c860d..a5d76db73f4e 100644 --- a/nautilus_trader/data/engine.pyx +++ b/nautilus_trader/data/engine.pyx @@ -1734,8 +1734,8 @@ cdef class DataEngine(Component): Price update_ask for instrument_id in components: if instrument_id == update.instrument_id: - update_bid = update.bid - update_ask = update.ask + update_bid = update.bid_price + update_ask = update.ask_price inputs_bid.append(update_bid.as_f64_c()) inputs_ask.append(update_ask.as_f64_c()) continue @@ -1746,20 +1746,20 @@ cdef class DataEngine(Component): f"no quotes for {instrument_id} yet...", ) return - update_bid = component_quote.bid - update_ask = component_quote.ask + update_bid = component_quote.bid_price + update_ask = component_quote.ask_price inputs_bid.append(update_bid.as_f64_c()) inputs_ask.append(update_ask.as_f64_c()) - cdef Price bid = synthetic.calculate(inputs_bid) - cdef Price ask = synthetic.calculate(inputs_ask) + cdef Price bid_price = synthetic.calculate(inputs_bid) + cdef Price ask_price = synthetic.calculate(inputs_ask) cdef Quantity size_one = Quantity(1, 0) # Placeholder for now cdef InstrumentId synthetic_instrument_id = synthetic.id cdef QuoteTick synthetic_quote = QuoteTick( synthetic_instrument_id, - bid, - ask, + bid_price, + ask_price, size_one, size_one, update.ts_event, diff --git a/nautilus_trader/examples/strategies/ema_cross.py b/nautilus_trader/examples/strategies/ema_cross.py index bde7b8499c2b..e43496efb490 100644 --- a/nautilus_trader/examples/strategies/ema_cross.py +++ b/nautilus_trader/examples/strategies/ema_cross.py @@ -235,7 +235,7 @@ def on_bar(self, bar: Bar) -> None: # Check if indicators ready if not self.indicators_initialized(): self.log.info( - f"Waiting for indicators to warm up " f"[{self.cache.bar_count(self.bar_type)}]...", + f"Waiting for indicators to warm up [{self.cache.bar_count(self.bar_type)}]...", color=LogColor.BLUE, ) return # Wait for indicators to warm up... diff --git a/nautilus_trader/examples/strategies/ema_cross_bracket.py b/nautilus_trader/examples/strategies/ema_cross_bracket.py index 8da1715ff9d0..b9111bc828d1 100644 --- a/nautilus_trader/examples/strategies/ema_cross_bracket.py +++ b/nautilus_trader/examples/strategies/ema_cross_bracket.py @@ -178,7 +178,7 @@ def on_bar(self, bar: Bar) -> None: # Check if indicators ready if not self.indicators_initialized(): self.log.info( - f"Waiting for indicators to warm up " f"[{self.cache.bar_count(self.bar_type)}]...", + f"Waiting for indicators to warm up [{self.cache.bar_count(self.bar_type)}]...", color=LogColor.BLUE, ) return # Wait for indicators to warm up... diff --git a/nautilus_trader/examples/strategies/ema_cross_bracket_algo.py b/nautilus_trader/examples/strategies/ema_cross_bracket_algo.py index 63d5cc8605c5..6d5360eb114d 100644 --- a/nautilus_trader/examples/strategies/ema_cross_bracket_algo.py +++ b/nautilus_trader/examples/strategies/ema_cross_bracket_algo.py @@ -224,7 +224,7 @@ def on_bar(self, bar: Bar) -> None: # Check if indicators ready if not self.indicators_initialized(): self.log.info( - f"Waiting for indicators to warm up " f"[{self.cache.bar_count(self.bar_type)}]...", + f"Waiting for indicators to warm up [{self.cache.bar_count(self.bar_type)}]...", color=LogColor.BLUE, ) return # Wait for indicators to warm up... diff --git a/nautilus_trader/examples/strategies/ema_cross_cython.pyx b/nautilus_trader/examples/strategies/ema_cross_cython.pyx index ccf0dd787d74..4d5eb19181e7 100644 --- a/nautilus_trader/examples/strategies/ema_cross_cython.pyx +++ b/nautilus_trader/examples/strategies/ema_cross_cython.pyx @@ -206,8 +206,7 @@ cdef class EMACross(Strategy): # Check if indicators ready if not self.indicators_initialized(): self.log.info( - f"Waiting for indicators to warm up " - f"[{self.cache.bar_count(self.bar_type)}]...", + f"Waiting for indicators to warm up [{self.cache.bar_count(self.bar_type)}]...", color=LogColor.BLUE, ) return # Wait for indicators to warm up... diff --git a/nautilus_trader/examples/strategies/ema_cross_stop_entry.py b/nautilus_trader/examples/strategies/ema_cross_stop_entry.py index 1563ff9437dd..76a9afa80eac 100644 --- a/nautilus_trader/examples/strategies/ema_cross_stop_entry.py +++ b/nautilus_trader/examples/strategies/ema_cross_stop_entry.py @@ -235,7 +235,7 @@ def on_bar(self, bar: Bar) -> None: # Check if indicators ready if not self.indicators_initialized(): self.log.info( - f"Waiting for indicators to warm up " f"[{self.cache.bar_count(self.bar_type)}]...", + f"Waiting for indicators to warm up [{self.cache.bar_count(self.bar_type)}]...", color=LogColor.BLUE, ) return # Wait for indicators to warm up... diff --git a/nautilus_trader/examples/strategies/ema_cross_trailing_stop.py b/nautilus_trader/examples/strategies/ema_cross_trailing_stop.py index debaecbea4ea..e3e0d0b6df17 100644 --- a/nautilus_trader/examples/strategies/ema_cross_trailing_stop.py +++ b/nautilus_trader/examples/strategies/ema_cross_trailing_stop.py @@ -253,7 +253,7 @@ def on_bar(self, bar: Bar) -> None: # Check if indicators ready if not self.indicators_initialized(): self.log.info( - f"Waiting for indicators to warm up " f"[{self.cache.bar_count(self.bar_type)}]...", + f"Waiting for indicators to warm up [{self.cache.bar_count(self.bar_type)}]...", color=LogColor.BLUE, ) return # Wait for indicators to warm up... diff --git a/nautilus_trader/examples/strategies/ema_cross_twap.py b/nautilus_trader/examples/strategies/ema_cross_twap.py index e8903abcf5f1..ca28d783d74e 100644 --- a/nautilus_trader/examples/strategies/ema_cross_twap.py +++ b/nautilus_trader/examples/strategies/ema_cross_twap.py @@ -252,7 +252,7 @@ def on_bar(self, bar: Bar) -> None: # Check if indicators ready if not self.indicators_initialized(): self.log.info( - f"Waiting for indicators to warm up " f"[{self.cache.bar_count(self.bar_type)}]...", + f"Waiting for indicators to warm up [{self.cache.bar_count(self.bar_type)}]...", color=LogColor.BLUE, ) return # Wait for indicators to warm up... diff --git a/nautilus_trader/examples/strategies/orderbook_imbalance.py b/nautilus_trader/examples/strategies/orderbook_imbalance.py index 1d29ed14bfc7..ab01dd8f2c22 100644 --- a/nautilus_trader/examples/strategies/orderbook_imbalance.py +++ b/nautilus_trader/examples/strategies/orderbook_imbalance.py @@ -138,12 +138,12 @@ def on_quote_tick(self, tick: QuoteTick) -> None: Actions to be performed when a delta is received. """ bid = BookOrder( - price=tick.bid.as_double(), + price=tick.bid_price.as_double(), size=tick.bid_size.as_double(), side=OrderSide.BUY, ) ask = BookOrder( - price=tick.ask.as_double(), + price=tick.ask_price.as_double(), size=tick.ask_size.as_double(), side=OrderSide.SELL, ) diff --git a/nautilus_trader/examples/strategies/volatility_market_maker.py b/nautilus_trader/examples/strategies/volatility_market_maker.py index 068f03a4e656..139d18fdc73b 100644 --- a/nautilus_trader/examples/strategies/volatility_market_maker.py +++ b/nautilus_trader/examples/strategies/volatility_market_maker.py @@ -257,7 +257,7 @@ def on_bar(self, bar: Bar) -> None: # Check if indicators ready if not self.indicators_initialized(): self.log.info( - f"Waiting for indicators to warm up " f"[{self.cache.bar_count(self.bar_type)}]...", + f"Waiting for indicators to warm up [{self.cache.bar_count(self.bar_type)}]...", color=LogColor.BLUE, ) return # Wait for indicators to warm up... @@ -269,7 +269,7 @@ def on_bar(self, bar: Bar) -> None: # Maintain buy orders if self.buy_order and (self.buy_order.is_emulated or self.buy_order.is_open): - # price: Decimal = last.bid - (self.atr.value * self.atr_multiple) + # price: Decimal = last.bid_price - (self.atr.value * self.atr_multiple) # self.modify_order( # order=self.buy_order, # price=self.instrument.make_price(price), @@ -279,7 +279,7 @@ def on_bar(self, bar: Bar) -> None: # Maintain sell orders if self.sell_order and (self.sell_order.is_emulated or self.sell_order.is_open): - # price = last.ask + (self.atr.value * self.atr_multiple) + # price = last.ask_price + (self.atr.value * self.atr_multiple) # self.modify_order( # order=self.sell_order, # price=self.instrument.make_price(price), @@ -295,7 +295,7 @@ def create_buy_order(self, last: QuoteTick) -> None: self.log.error("No instrument loaded.") return - price: Decimal = last.bid - (self.atr.value * self.atr_multiple) + price: Decimal = last.bid_price - (self.atr.value * self.atr_multiple) order: LimitOrder = self.order_factory.limit( instrument_id=self.instrument_id, order_side=OrderSide.BUY, @@ -318,7 +318,7 @@ def create_sell_order(self, last: QuoteTick) -> None: self.log.error("No instrument loaded.") return - price: Decimal = last.ask + (self.atr.value * self.atr_multiple) + price: Decimal = last.ask_price + (self.atr.value * self.atr_multiple) order: LimitOrder = self.order_factory.limit( instrument_id=self.instrument_id, order_side=OrderSide.SELL, diff --git a/nautilus_trader/execution/algorithm.pxd b/nautilus_trader/execution/algorithm.pxd index 987bbac109f5..bcef659b1d3f 100644 --- a/nautilus_trader/execution/algorithm.pxd +++ b/nautilus_trader/execution/algorithm.pxd @@ -29,6 +29,7 @@ from nautilus_trader.execution.messages cimport TradingCommand from nautilus_trader.model.enums_c cimport ContingencyType from nautilus_trader.model.enums_c cimport TimeInForce from nautilus_trader.model.enums_c cimport TriggerType +from nautilus_trader.model.events.order cimport OrderCanceled from nautilus_trader.model.events.order cimport OrderEvent from nautilus_trader.model.events.order cimport OrderPendingCancel from nautilus_trader.model.events.order cimport OrderPendingUpdate @@ -75,8 +76,9 @@ cdef class ExecAlgorithm(Actor): # -- COMMANDS ------------------------------------------------------------------------------------- cpdef void execute(self, TradingCommand command) - cdef _handle_submit_order(self, SubmitOrder command) - cdef _handle_submit_order_list(self, SubmitOrderList command) + cdef void _handle_submit_order(self, SubmitOrder command) + cdef void _handle_submit_order_list(self, SubmitOrderList command) + cdef void _handle_cancel_order(self, CancelOrder command) # -- EVENT HANDLERS ------------------------------------------------------------------------------- @@ -147,7 +149,9 @@ cdef class ExecAlgorithm(Actor): cdef OrderPendingUpdate _generate_order_pending_update(self, Order order) cdef OrderPendingCancel _generate_order_pending_cancel(self, Order order) + cdef OrderCanceled _generate_order_canceled(self, Order order) # -- EGRESS --------------------------------------------------------------------------------------- + cdef void _send_emulator_command(self, TradingCommand command) cdef void _send_risk_command(self, TradingCommand command) diff --git a/nautilus_trader/execution/algorithm.pyx b/nautilus_trader/execution/algorithm.pyx index 450a191f4e29..d9c70b554f57 100644 --- a/nautilus_trader/execution/algorithm.pyx +++ b/nautilus_trader/execution/algorithm.pyx @@ -44,10 +44,15 @@ from nautilus_trader.model.enums_c cimport ContingencyType from nautilus_trader.model.enums_c cimport OrderStatus from nautilus_trader.model.enums_c cimport TimeInForce from nautilus_trader.model.enums_c cimport TriggerType +from nautilus_trader.model.events.order cimport OrderCanceled from nautilus_trader.model.events.order cimport OrderEvent +from nautilus_trader.model.events.order cimport OrderExpired +from nautilus_trader.model.events.order cimport OrderFilled from nautilus_trader.model.events.order cimport OrderPendingCancel from nautilus_trader.model.events.order cimport OrderPendingUpdate +from nautilus_trader.model.events.order cimport OrderRejected from nautilus_trader.model.events.order cimport OrderUpdated +from nautilus_trader.model.events.position cimport PositionEvent from nautilus_trader.model.identifiers cimport ClientId from nautilus_trader.model.identifiers cimport ClientOrderId from nautilus_trader.model.identifiers cimport ExecAlgorithmId @@ -193,13 +198,12 @@ cdef class ExecAlgorithm(Actor): return ClientOrderId(f"{primary.client_order_id.to_str()}-E{spawn_sequence}") cdef void _reduce_primary_order(self, Order primary, Quantity spawn_qty): - cdef uint8_t size_precision = primary.quantity._mem.precision - cdef uint64_t new_raw = primary.quantity._mem.raw - spawn_qty._mem.raw - if new_raw <= 0: - self._log.error("Cannot reduce primary order to non-positive quantity.") - return + Condition.true(primary.quantity >= spawn_qty, "Spawn order quantity was greater than or equal to primary order") - cdef Quantity new_qty = Quantity.from_raw_c(new_raw, size_precision) + cdef Quantity new_qty = Quantity.from_raw_c( + primary.quantity._mem.raw - spawn_qty._mem.raw, + primary.quantity._mem.precision, + ) # Generate event cdef uint64_t ts_now = self._clock.timestamp_ns() @@ -240,7 +244,7 @@ cdef class ExecAlgorithm(Actor): """ Condition.not_none(command, "command") - Condition.equal(command.exec_algorithm_id, self.id, "command.exec_algorithm_id", "self.id") + # Condition.equal(command.exec_algorithm_id, self.id, "command.exec_algorithm_id", "self.id") self._log.debug(f"{RECV}{CMD} {command}.", LogColor.MAGENTA) @@ -251,6 +255,8 @@ cdef class ExecAlgorithm(Actor): self._handle_submit_order(command) elif isinstance(command, SubmitOrderList): self._handle_submit_order_list(command) + elif isinstance(command, CancelOrder): + self._handle_cancel_order(command) else: self._log.error(f"Cannot handle command: unrecognized {command}.") @@ -261,24 +267,49 @@ cdef class ExecAlgorithm(Actor): self._msgbus.subscribe(topic=f"events.order.{command.strategy_id.to_str()}", handler=self._handle_order_event) self._subscribed_strategies.add(command.strategy_id) - cdef _handle_submit_order(self, SubmitOrder command): + cdef void _handle_submit_order(self, SubmitOrder command): try: self.on_order(command.order) - except Exception as e: + except Exception as e: # pragma: no cover self.log.exception(f"Error on handling {repr(command.order)}", e) raise - cdef _handle_submit_order_list(self, SubmitOrderList command): + cdef void _handle_submit_order_list(self, SubmitOrderList command): cdef Order order for order in command.order_list.orders: if order.exec_algorithm_id is not None: Condition.equal(order.exec_algorithm_id, self.id, "order.exec_algorithm_id", "self.id") try: self.on_order_list(command.order_list) - except Exception as e: + except Exception as e: # pragma: no cover self.log.exception(f"Error on handling {repr(command.order_list)}", e) raise + cdef void _handle_cancel_order(self, CancelOrder command): + cdef Order order = self.cache.order(command.client_order_id) + if order is None: # pragma: no cover (design-time error) + self._log.error( + f"Cannot cancel order: {repr(command.client_order_id)} not found.", + ) + return + + # Generate event + cdef OrderCanceled event = self._generate_order_canceled(order) + + try: + order.apply(event) + except InvalidStateTrigger as e: # pragma: no cover + self._log.warning(f"InvalidStateTrigger: {e}, did not apply {event}") + return + + self.cache.update_order(order) + + # Publish canceled event + self._msgbus.publish_c( + topic=f"events.order.{order.strategy_id.to_str()}", + msg=event, + ) + # -- EVENT HANDLERS ------------------------------------------------------------------------------- cdef void _handle_order_event(self, OrderEvent event): @@ -293,7 +324,7 @@ cdef class ExecAlgorithm(Actor): try: self.on_order_event(event) - except Exception as e: + except Exception as e: # pragma: no cover self.log.exception(f"Error on handling {repr(event)}", e) raise @@ -608,9 +639,9 @@ cdef class ExecAlgorithm(Actor): Raises ------ ValueError - If `order.status` is not ``INITIALIZED``. + If `order.status` is not ``INITIALIZED`` or ``RELEASED``. ValueError - If `order.emulation_trigger` is not ``None``. + If `order.emulation_trigger` is not ``NO_TRIGGER``. Warning ------- @@ -618,26 +649,30 @@ cdef class ExecAlgorithm(Actor): position opened by the order will have this position ID assigned. This may not be what you intended. - Emulated orders cannot be sent from execution algorithms (intentioally constraining complexity). + Emulated orders cannot be sent from execution algorithms (intentionally constraining complexity). """ Condition.true(self.trader_id is not None, "The execution algorithm has not been registered") Condition.not_none(order, "order") - Condition.equal(order.status, OrderStatus.INITIALIZED, "order", "order_status") Condition.equal(order.emulation_trigger, TriggerType.NO_TRIGGER, "order.emulation_trigger", "NO_TRIGGER") + Condition.true( + order.status_c() in (OrderStatus.INITIALIZED, OrderStatus.RELEASED), + "order", + "order status was not either ``INITIALIZED`` or ``RELEASED``", + ) cdef Order primary = None cdef PositionId position_id = None cdef ClientId client_id = None cdef SubmitOrder command = None - if order.exec_spawn_id is not None: + if order.is_spawned_c(): # Handle new spawned order primary = self.cache.order(order.exec_spawn_id) Condition.equal(order.strategy_id, primary.strategy_id, "order.strategy_id", "primary.strategy_id") if primary is None: self._log.error( - "Cannot submit order: cannot find primary order for {repr(order.exec_spawn_id)}." + f"Cannot submit order: cannot find primary order for {order.exec_spawn_id!r}." ) return @@ -646,7 +681,7 @@ cdef class ExecAlgorithm(Actor): if self.cache.order_exists(order.client_order_id): self._log.error( - f"Cannot submit order: order already exists for {repr(order.client_order_id)}.", + f"Cannot submit order: order already exists for {order.client_order_id!r}.", ) return @@ -673,6 +708,7 @@ cdef class ExecAlgorithm(Actor): # Handle primary (original) order position_id = self.cache.position_id(order.client_order_id) + client_id = self.cache.client_id(order.client_order_id) cdef Order cached_order = self.cache.order(order.client_order_id) if cached_order.order_type != order.order_type: self.cache.add_order(order, position_id, client_id, override=True) @@ -684,7 +720,7 @@ cdef class ExecAlgorithm(Actor): command_id=UUID4(), ts_init=self.clock.timestamp_ns(), position_id=position_id, - client_id=None, # Not yet supported + client_id=client_id, ) self._send_risk_command(command) @@ -780,13 +816,13 @@ cdef class ExecAlgorithm(Actor): return # Cannot send command cdef OrderPendingUpdate event - if order.status != OrderStatus.INITIALIZED and not order.is_emulated_c(): + if not order.is_active_local_c(): # Generate and apply event event = self._generate_order_pending_update(order) try: order.apply(event) self.cache.update_order(order) - except InvalidStateTrigger as e: + except InvalidStateTrigger as e: # pragma: no cover self._log.warning(f"InvalidStateTrigger: {e}, did not apply {event}") return @@ -838,7 +874,7 @@ cdef class ExecAlgorithm(Actor): Raises ------ ValueError - If `order.status` is not ``INITIALIZED``. + If `order.status` is not ``INITIALIZED`` or ``RELEASED``. ValueError If `price` is not ``None`` and order does not have a `price`. ValueError @@ -856,7 +892,11 @@ cdef class ExecAlgorithm(Actor): """ Condition.true(self.trader_id is not None, "The strategy has not been registered") Condition.not_none(order, "order") - Condition.equal(order.status, OrderStatus.INITIALIZED, "order", "order_status") + Condition.true( + order.status_c() in (OrderStatus.INITIALIZED, OrderStatus.RELEASED), + "order", + "order status was not either ``INITIALIZED`` or ``RELEASED``", + ) cdef bint updating = False # Set validation flag (must become true) @@ -943,13 +983,13 @@ cdef class ExecAlgorithm(Actor): return # Cannot send command cdef OrderPendingCancel event - if order.status != OrderStatus.INITIALIZED and not order.is_emulated_c(): + if not order.is_active_local_c(): # Generate and apply event event = self._generate_order_pending_cancel(order) try: order.apply(event) self.cache.update_order(order) - except InvalidStateTrigger as e: + except InvalidStateTrigger as e: # pragma: no cover self._log.warning(f"InvalidStateTrigger: {e}, did not apply {event}") return @@ -970,7 +1010,7 @@ cdef class ExecAlgorithm(Actor): client_id=client_id, ) - if order.is_emulated_c(): + if order.is_emulated_c() or order.status_c() == OrderStatus.RELEASED: self._send_emulator_command(command) else: self._send_risk_command(command) @@ -1005,8 +1045,27 @@ cdef class ExecAlgorithm(Actor): ts_init=ts_now, ) + cdef OrderCanceled _generate_order_canceled(self, Order order): + cdef uint64_t ts_now = self._clock.timestamp_ns() + return OrderCanceled( + trader_id=order.trader_id, + strategy_id=order.strategy_id, + instrument_id=order.instrument_id, + client_order_id=order.client_order_id, + venue_order_id=order.venue_order_id, + account_id=order.account_id, + event_id=UUID4(), + ts_event=ts_now, + ts_init=ts_now, + ) + # -- EGRESS --------------------------------------------------------------------------------------- + cdef void _send_emulator_command(self, TradingCommand command): + if not self.log.is_bypassed: + self.log.info(f"{CMD}{SENT} {command}.") + self._msgbus.send(endpoint="OrderEmulator.execute", msg=command) + cdef void _send_risk_command(self, TradingCommand command): if not self.log.is_bypassed: self.log.info(f"{CMD}{SENT} {command}.") diff --git a/nautilus_trader/execution/emulator.pxd b/nautilus_trader/execution/emulator.pxd index b3e9e1139a49..5bcfd002e42c 100644 --- a/nautilus_trader/execution/emulator.pxd +++ b/nautilus_trader/execution/emulator.pxd @@ -14,6 +14,7 @@ # ------------------------------------------------------------------------------------------------- from nautilus_trader.common.actor cimport Actor +from nautilus_trader.execution.manager cimport OrderManager from nautilus_trader.execution.matching_core cimport MatchingCore from nautilus_trader.execution.messages cimport CancelAllOrders from nautilus_trader.execution.messages cimport CancelOrder @@ -38,6 +39,7 @@ from nautilus_trader.model.orders.base cimport Order cdef class OrderEmulator(Actor): + cdef OrderManager _manager cdef dict _matching_cores cdef dict _commands_submit_order @@ -46,6 +48,13 @@ cdef class OrderEmulator(Actor): cdef set _subscribed_strategies cdef set _monitored_positions + cdef readonly bint debug + """If debug mode is active (will provide extra debug logging).\n\n:returns: `bool`""" + cdef readonly int command_count + """The total count of commands received by the emulator.\n\n:returns: `int`""" + cdef readonly int event_count + """The total count of events received by the emulator.\n\n:returns: `int`""" + cpdef void execute(self, TradingCommand command) cpdef MatchingCore create_matching_core(self, InstrumentId instrument_id, Price price_increment) cdef void _handle_submit_order(self, SubmitOrder command) @@ -54,20 +63,8 @@ cdef class OrderEmulator(Actor): cdef void _handle_cancel_order(self, CancelOrder command) cdef void _handle_cancel_all_orders(self, CancelAllOrders command) - cdef void _check_monitoring(self, StrategyId strategy_id, PositionId position_id) - cdef void _create_new_submit_order(self, Order order, PositionId position_id, ClientId client_id) - cdef void _cancel_order(self, MatchingCore matching_core, Order order) - -# -- EVENT HANDLERS ------------------------------------------------------------------------------- - - cdef void _handle_order_rejected(self, OrderRejected rejected) - cdef void _handle_order_canceled(self, OrderCanceled canceled) - cdef void _handle_order_expired(self, OrderExpired expired) - cdef void _handle_order_updated(self, OrderUpdated updated) - cdef void _handle_order_filled(self, OrderFilled filled) - cdef void _handle_position_event(self, PositionEvent event) - cdef void _handle_contingencies(self, Order order) - cdef void _update_order_quantity(self, Order order, Quantity new_quantity) + cpdef void _check_monitoring(self, StrategyId strategy_id, PositionId position_id) + cpdef void _cancel_order(self, Order order) # ------------------------------------------------------------------------------------------------- @@ -77,11 +74,3 @@ cdef class OrderEmulator(Actor): cdef void _iterate_orders(self, MatchingCore matching_core) cdef void _update_trailing_stop_order(self, MatchingCore matching_core, Order order) - -# -- EGRESS --------------------------------------------------------------------------------------- - - cdef void _send_algo_command(self, TradingCommand command) - cdef void _send_risk_command(self, TradingCommand command) - cdef void _send_exec_command(self, TradingCommand command) - cdef void _send_risk_event(self, OrderEvent event) - cdef void _send_exec_event(self, OrderEvent event) diff --git a/nautilus_trader/execution/emulator.pyx b/nautilus_trader/execution/emulator.pyx index 2098c3ec4c5f..da52ca1e57e8 100644 --- a/nautilus_trader/execution/emulator.pyx +++ b/nautilus_trader/execution/emulator.pyx @@ -17,6 +17,7 @@ from typing import Optional from nautilus_trader.config.common import OrderEmulatorConfig +from libc.stdint cimport uint8_t from libc.stdint cimport uint64_t from nautilus_trader.cache.cache cimport Cache @@ -31,6 +32,7 @@ from nautilus_trader.common.logging cimport Logger from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.message cimport Event from nautilus_trader.core.uuid cimport UUID4 +from nautilus_trader.execution.manager cimport OrderManager from nautilus_trader.execution.matching_core cimport MatchingCore from nautilus_trader.execution.messages cimport CancelAllOrders from nautilus_trader.execution.messages cimport CancelOrder @@ -48,10 +50,12 @@ from nautilus_trader.model.enums_c cimport TimeInForce from nautilus_trader.model.enums_c cimport TriggerType from nautilus_trader.model.enums_c cimport trigger_type_to_str from nautilus_trader.model.events.order cimport OrderCanceled +from nautilus_trader.model.events.order cimport OrderEmulated from nautilus_trader.model.events.order cimport OrderEvent from nautilus_trader.model.events.order cimport OrderExpired from nautilus_trader.model.events.order cimport OrderFilled from nautilus_trader.model.events.order cimport OrderRejected +from nautilus_trader.model.events.order cimport OrderReleased from nautilus_trader.model.events.order cimport OrderTriggered from nautilus_trader.model.events.order cimport OrderUpdated from nautilus_trader.model.identifiers cimport ClientId @@ -76,6 +80,22 @@ cdef tuple SUPPORTED_TRIGGERS = (TriggerType.DEFAULT, TriggerType.BID_ASK, Trigg cdef class OrderEmulator(Actor): """ Provides order emulation for specified trigger types. + + Parameters + ---------- + trader_id : TraderId + The trader ID for the order emulator. + msgbus : MessageBus + The message bus for the order emulator. + cache : Cache + The cache for the order emulator. + clock : Clock + The clock for the order emulator. + logger : Logger + The logger for the order emulator. + config : OrderEmulatorConfig, optional + The configuration for the order emulator. + """ def __init__( @@ -87,6 +107,9 @@ cdef class OrderEmulator(Actor): Logger logger not None, config: Optional[OrderEmulatorConfig] = None, ): + if config is None: + config = OrderEmulatorConfig() + Condition.type(config, OrderEmulatorConfig, "config") super().__init__() self.register_base( @@ -96,14 +119,31 @@ cdef class OrderEmulator(Actor): logger=logger, ) + self._manager = OrderManager( + clock=clock, + logger=logger, + msgbus=msgbus, + cache=cache, + component_name=type(self).__name__, + submit_order_handler=self._handle_submit_order, + cancel_order_handler=self._cancel_order, + debug=config.debug, + ) + self._matching_cores: dict[InstrumentId, MatchingCore] = {} - self._commands_submit_order: dict[ClientOrderId, SubmitOrder] = {} self._subscribed_quotes: set[InstrumentId] = set() self._subscribed_trades: set[InstrumentId] = set() self._subscribed_strategies: set[StrategyId] = set() self._monitored_positions: set[PositionId] = set() + # Settings + self.debug: bool = config.debug + + # Counters + self.command_count: int = 0 + self.event_count: int = 0 + # Register endpoints self._msgbus.register(endpoint="OrderEmulator.execute", handler=self.execute) @@ -140,7 +180,7 @@ cdef class OrderEmulator(Actor): dict[ClientOrderId, SubmitOrder] """ - return self._commands_submit_order.copy() + return self._manager.get_submit_order_commands() def get_matching_core(self, InstrumentId instrument_id) -> Optional[MatchingCore]: """ @@ -167,7 +207,7 @@ cdef class OrderEmulator(Actor): PositionId position_id ClientId client_id for order in emulated_orders: - if order.status != OrderStatus.INITIALIZED: + if order.status_c() not in (OrderStatus.INITIALIZED, OrderStatus.EMULATED): continue # No longer emulated position_id = self.cache.position_id(order.client_order_id) @@ -185,27 +225,57 @@ cdef class OrderEmulator(Actor): self._handle_submit_order(command) cpdef void on_event(self, Event event): - self._log.info(f"Received {event}.", LogColor.MAGENTA) + """ + Handle the given `event`. + + Parameters + ---------- + event : Event + The received event to handle. + + """ + Condition.not_none(event, "event") + + if self.debug: + self._log.info(f"{RECV}{EVT} {event}.", LogColor.MAGENTA) + self.event_count += 1 + if isinstance(event, OrderRejected): - self._handle_order_rejected(event) + self._manager.handle_order_rejected(event) elif isinstance(event, OrderCanceled): - self._handle_order_canceled(event) + self._manager.handle_order_canceled(event) elif isinstance(event, OrderExpired): - self._handle_order_expired(event) + self._manager.handle_order_expired(event) elif isinstance(event, OrderUpdated): - self._handle_order_updated(event) + self._manager.handle_order_updated(event) elif isinstance(event, OrderFilled): - self._handle_order_filled(event) + self._manager.handle_order_filled(event) elif isinstance(event, PositionEvent): - self._handle_position_event(event) + self._manager.handle_position_event(event) + + if not isinstance(event, OrderEvent): + return + + cdef Order order = self.cache.order(event.client_order_id) + if order is None: + return # Order not in cache yet + + cdef MatchingCore matching_core = None + if order.is_closed_c(): + matching_core = self._matching_cores.get(order.instrument_id) + if matching_core is not None: + matching_core.delete_order(order) cpdef void on_stop(self): pass cpdef void on_reset(self): - self._commands_submit_order.clear() + self._manager.reset() self._matching_cores.clear() + self.command_count = 0 + self.event_count = 0 + cpdef void on_dispose(self): pass @@ -223,7 +293,9 @@ cdef class OrderEmulator(Actor): """ Condition.not_none(command, "command") - self._log.debug(f"{RECV}{CMD} {command}.", LogColor.MAGENTA) + if self.debug: + self._log.info(f"{RECV}{CMD} {command}.", LogColor.MAGENTA) + self.command_count += 1 if isinstance(command, SubmitOrder): self._handle_submit_order(command) @@ -259,12 +331,11 @@ cdef class OrderEmulator(Actor): Raises ------ - RuntimeError + KeyError If a matching core for the given `instrument_id` already exists. """ - if instrument_id in self._matching_cores: - raise RuntimeError(f"A matching core already exists for {instrument_id}.") + Condition.not_in(instrument_id, self._matching_cores, "instrument_id", "self._matching_cores") matching_core = MatchingCore( instrument_id=instrument_id, @@ -275,7 +346,9 @@ cdef class OrderEmulator(Actor): ) self._matching_cores[instrument_id] = matching_core - self._log.debug(f"Created matching core for {instrument_id}.") + + if self.debug: + self._log.info(f"Created matching core for {instrument_id}.", LogColor.MAGENTA) return matching_core @@ -283,12 +356,12 @@ cdef class OrderEmulator(Actor): cdef Order order = command.order cdef TriggerType emulation_trigger = command.order.emulation_trigger Condition.not_equal(emulation_trigger, TriggerType.NO_TRIGGER, "command.order.emulation_trigger", "TriggerType.NO_TRIGGER") - Condition.not_in(command.order.client_order_id, self._commands_submit_order, "command.order.client_order_id", "self._commands_submit_order") + Condition.not_in(command.order.client_order_id, self._manager.get_submit_order_commands(), "command.order.client_order_id", "self._commands_submit_order") if emulation_trigger not in SUPPORTED_TRIGGERS: self._log.error( f"Cannot emulate order: `TriggerType` {trigger_type_to_str(emulation_trigger)} not supported.") - self._cancel_order(matching_core=None, order=order) + self._manager.cancel_order(order=order) return self._check_monitoring(command.strategy_id, command.position_id) @@ -302,7 +375,7 @@ cdef class OrderEmulator(Actor): self._log.error( f"Cannot emulate order: no synthetic instrument {trigger_instrument_id} for trigger.", ) - self._cancel_order(matching_core=None, order=order) + self._manager.cancel_order(order=order) return matching_core = self.create_matching_core(synthetic.id, synthetic.price_increment) else: @@ -311,7 +384,7 @@ cdef class OrderEmulator(Actor): self._log.error( f"Cannot emulate order: no instrument {trigger_instrument_id} for trigger.", ) - self._cancel_order(matching_core=None, order=order) + self._manager.cancel_order(order=order) return matching_core = self.create_matching_core(instrument.id, instrument.price_increment) @@ -322,11 +395,11 @@ cdef class OrderEmulator(Actor): self.log.error( "Cannot handle trailing stop order with no `trigger_price` and no market updates.", ) - self._cancel_order(None, order) + self._manager.cancel_order(order) return # Cache command - self._commands_submit_order[order.client_order_id] = command + self._manager.cache_submit_order_command(command) # Check if immediately marketable (initial match) matching_core.match_order(order, initial=True) @@ -345,12 +418,34 @@ cdef class OrderEmulator(Actor): f"invalid `TriggerType`, was {emulation_trigger}", # pragma: no cover (design-time error) ) - if order.client_order_id not in self._commands_submit_order: + if order.client_order_id not in self._manager.get_submit_order_commands(): return # Already released # Hold in matching core matching_core.add_order(order) + cdef OrderEmulated event + if order.status_c() == OrderStatus.INITIALIZED: + # Generate event + event = OrderEmulated( + trader_id=order.trader_id, + strategy_id=order.strategy_id, + instrument_id=order.instrument_id, + client_order_id=order.client_order_id, + event_id=UUID4(), + ts_init=self._clock.timestamp_ns(), + ) + order.apply(event) + self.cache.update_order(order) + + self._manager.send_risk_event(event) + + # Publish event + self._msgbus.publish_c( + topic=f"events.order.{order.strategy_id.to_str()}", + msg=event, + ) + self.log.info(f"Emulating {command.order}.", LogColor.MAGENTA) cdef void _handle_submit_order_list(self, SubmitOrderList command): @@ -363,7 +458,7 @@ cdef class OrderEmulator(Actor): assert parent_order, f"Parent order for {repr(order.client_order_id)} not found" if parent_order.contingency_type == ContingencyType.OTO: continue # Process contingency order later once triggered - self._create_new_submit_order( + self._manager.create_new_submit_order( order=order, position_id=command.position_id, client_id=command.client_id, @@ -401,18 +496,23 @@ cdef class OrderEmulator(Actor): ts_event=ts_now, ts_init=ts_now, ) - self.msgbus.send(endpoint="ExecEngine.process", msg=event) + self._manager.send_exec_event(event) cdef InstrumentId trigger_instrument_id = order.instrument_id if order.trigger_instrument_id is None else order.trigger_instrument_id cdef MatchingCore matching_core = self._matching_cores.get(trigger_instrument_id) if matching_core is None: - raise RuntimeError(f"Cannot handle `ModifyOrder`: no matching core for trigger instrument {trigger_instrument_id}.") # pragma: no cover (design-time error) + self._log.error( + f"Cannot handle `ModifyOrder`: no matching core for trigger instrument {trigger_instrument_id}.", + ) + return matching_core.match_order(order) if order.side == OrderSide.BUY: matching_core.sort_bid_orders() elif order.side == OrderSide.SELL: matching_core.sort_ask_orders() + else: + raise RuntimeError("invalid `OrderSide`") # pragma: no cover (design-time error) cdef void _handle_cancel_order(self, CancelOrder command): cdef Order order = self.cache.order(command.client_order_id) @@ -425,13 +525,16 @@ cdef class OrderEmulator(Actor): cdef InstrumentId trigger_instrument_id = order.instrument_id if order.trigger_instrument_id is None else order.trigger_instrument_id cdef MatchingCore matching_core = self._matching_cores.get(trigger_instrument_id) if matching_core is None: - raise RuntimeError(f"Cannot handle `CancelOrder`: no matching core for trigger instrument {trigger_instrument_id}.") # pragma: no cover (design-time error) + self._log.error( + f"Cannot handle `CancelOrder`: no matching core for trigger instrument {trigger_instrument_id}.", + ) + return if not matching_core.order_exists(order.client_order_id) and order.is_open_c() and not order.is_pending_cancel_c(): # Order not held in the emulator - self._send_exec_command(command) + self._manager.send_exec_command(command) else: - self._cancel_order(matching_core, order) + self._manager.cancel_order(order) cdef void _handle_cancel_all_orders(self, CancelAllOrders command): cdef MatchingCore matching_core = self._matching_cores.get(command.instrument_id) @@ -453,9 +556,9 @@ cdef class OrderEmulator(Actor): cdef Order order for order in orders: - self._cancel_order(matching_core, order) + self._manager.cancel_order(order) - cdef void _check_monitoring(self, StrategyId strategy_id, PositionId position_id): + cpdef void _check_monitoring(self, StrategyId strategy_id, PositionId position_id): if strategy_id not in self._subscribed_strategies: # Subscribe to all strategy events self._msgbus.subscribe(topic=f"events.order.{strategy_id.to_str()}", handler=self.on_event) @@ -466,280 +569,32 @@ cdef class OrderEmulator(Actor): if position_id is not None and position_id not in self._monitored_positions: self._monitored_positions.add(position_id) - cdef void _create_new_submit_order( - self, - Order order, - PositionId position_id, - ClientId client_id, - ): - cdef SubmitOrder submit = SubmitOrder( - trader_id=order.trader_id, - strategy_id=order.strategy_id, - order=order, - position_id=position_id, - client_id=client_id, - command_id=UUID4(), - ts_init=self.clock.timestamp_ns(), - ) - - if order.emulation_trigger == TriggerType.NO_TRIGGER: - # Cache command - self._commands_submit_order[order.client_order_id] = submit - - if order.exec_algorithm_id is not None: - self._send_algo_command(submit) - else: - self._send_risk_command(submit) - else: - # Emulate - self._handle_submit_order(submit) - - cdef void _cancel_order(self, MatchingCore matching_core, Order order): + cpdef void _cancel_order(self, Order order): if order is None: self._log.error( f"Cannot cancel order: order for {repr(order.client_order_id)} not found.", ) return - self._log.debug(f"Cancelling order {order}.") + if self.debug: + self._log.info(f"Cancelling order {order.client_order_id!r}.", LogColor.MAGENTA) # Remove emulation trigger order.emulation_trigger = TriggerType.NO_TRIGGER cdef InstrumentId trigger_instrument_id = order.instrument_id if order.trigger_instrument_id is None else order.trigger_instrument_id - if matching_core is None: - matching_core = self._matching_cores.get(trigger_instrument_id) + cdef MatchingCore matching_core = self._matching_cores.get(trigger_instrument_id) if matching_core is not None: matching_core.delete_order(order) - self._commands_submit_order.pop(order.client_order_id, None) - - # Generate event - cdef uint64_t ts_now = self._clock.timestamp_ns() - cdef OrderCanceled event = OrderCanceled( - trader_id=order.trader_id, - strategy_id=order.strategy_id, - instrument_id=order.instrument_id, - client_order_id=order.client_order_id, - venue_order_id=order.venue_order_id, # Probably None - account_id=order.account_id, # Probably None - event_id=UUID4(), - ts_event=ts_now, - ts_init=ts_now, - ) - self._send_exec_event(event) - -# -- EVENT HANDLERS ------------------------------------------------------------------------------- - - cdef void _handle_order_rejected(self, OrderRejected rejected): - cdef Order order = self.cache.order(rejected.client_order_id) - if order is None: - self._log.error( - "Cannot handle `OrderRejected`: " - f"order for {repr(rejected.client_order_id)} not found. {rejected}", - ) - return - - if order.contingency_type != ContingencyType.NO_CONTINGENCY: - self._handle_contingencies(order) - - cdef void _handle_order_canceled(self, OrderCanceled canceled): - cdef Order order = self.cache.order(canceled.client_order_id) - if order is None: - self._log.error( - "Cannot handle `OrderCanceled`: " - f"order for {repr(canceled.client_order_id)} not found. {canceled}", - ) - return - - if order.contingency_type != ContingencyType.NO_CONTINGENCY: - self._handle_contingencies(order) - - cdef void _handle_order_expired(self, OrderExpired expired): - cdef Order order = self.cache.order(expired.client_order_id) - if order is None: - self._log.error( - "Cannot handle `OrderExpired`: " - f"order for {repr(expired.client_order_id)} not found. {expired}", - ) - return - - if order.contingency_type != ContingencyType.NO_CONTINGENCY: - self._handle_contingencies(order) - - cdef void _handle_order_updated(self, OrderUpdated updated): - cdef Order order = self.cache.order(updated.client_order_id) - if order is None: - self._log.error( - "Cannot handle `OrderUpdated`: " - f"order for {repr(updated.client_order_id)} not found. {updated}", - ) - return - - if order.contingency_type != ContingencyType.NO_CONTINGENCY: - self._handle_contingencies(order) - - cdef void _handle_order_filled(self, OrderFilled filled): - cdef Order order = self.cache.order(filled.client_order_id) - if order is None: - self._log.error( - "Cannot handle `OrderFilled`: " - f"order for {repr(filled.client_order_id)} not found. {filled}", - ) - return - - cdef MatchingCore matching_core = None - if order.is_closed_c(): - matching_core = self._matching_cores.get(order.instrument_id) - if matching_core is not None: - matching_core.delete_order(order) - - cdef dict exec_algorithm_index = {} - cdef: - PositionId position_id - ClientId client_id - ClientOrderId client_order_id - SubmitOrderList submit_order_list - Order child_order - Order spawned_order - Order primary_order - list exec_spawn_orders - uint64_t raw_filled_qty - Quantity filled_qty - if order.contingency_type == ContingencyType.OTO: - assert order.linked_order_ids - position_id = self.cache.position_id(order.client_order_id) - client_id = self.cache.client_id(order.client_order_id) - for client_order_id in order.linked_order_ids: - child_order = self.cache.order(client_order_id) - assert child_order, f"Cannot find child order for {repr(client_order_id)}" - if child_order.is_closed_c(): - continue - if not child_order.client_order_id in self._commands_submit_order: - self._create_new_submit_order( - order=child_order, - position_id=position_id, - client_id=client_id, - ) - continue - - # Check if execution algorithm spawned order (only update based on primary) - if order.exec_spawn_id is None: - return - - primary_order = self.cache.order(order.exec_spawn_id) - if primary_order is None: - self._log.error(f"Cannot find primary order {repr(order.exec_spawn_id)}.") - return - - # Check if primary already pending cancel or completed (no need to update) - if primary_order.status == OrderStatus.PENDING_CANCEL or primary_order.is_closed_c(): - return - - raw_filled_qty = 0 - filled_qty = None - - # Check total size of execution spawn sequence - exec_spawn_orders = self.cache.orders_for_exec_spawn(order.exec_spawn_id) - for spawned_order in exec_spawn_orders: - raw_filled_qty += spawned_order.filled_qty._mem.raw - raw_filled_qty += order.filled_qty._mem.raw - if raw_filled_qty != child_order.quantity._mem.raw: - filled_qty = Quantity.from_raw_c(raw_filled_qty, order.filled_qty._mem.precision) - self._log.info( - f"Updating quantity for {child_order} to {filled_qty}.", - LogColor.MAGENTA, - ) - self._update_order_quantity(child_order, filled_qty) - elif order.contingency_type == ContingencyType.OCO: - # Cancel all OCO orders - for client_order_id in order.linked_order_ids: - contingent_order = self.cache.order(client_order_id) - assert contingent_order - if contingent_order.client_order_id != order.client_order_id and not contingent_order.is_closed_c(): - self._cancel_order(matching_core, contingent_order) - elif order.contingency_type == ContingencyType.OUO: - self._handle_contingencies(order) - - cdef void _handle_position_event(self, PositionEvent event): - pass # TBC - - cdef void _handle_contingencies(self, Order order): - assert order.linked_order_ids - - cdef MatchingCore matching_core = None - if order.is_closed_c(): - matching_core = self._matching_cores.get(order.instrument_id) - if matching_core is not None: - matching_core.delete_order(order) - - if order.exec_spawn_id is not None: - # Do not handle contingencies based on spawned execution algorithm orders - return - - cdef ClientOrderId client_order_id - cdef Order contingent_order - for client_order_id in order.linked_order_ids: - contingent_order = self.cache.order(client_order_id) - assert contingent_order - if client_order_id == order.client_order_id: - continue # Already being handled - if contingent_order.is_closed_c() or contingent_order.emulation_trigger == TriggerType.NO_TRIGGER: - self._commands_submit_order.pop(order.client_order_id, None) - continue # Already completed - - if order.is_closed_c(): - self._cancel_order(matching_core, contingent_order) - elif order.quantity._mem.raw != contingent_order.quantity._mem.raw: - self._update_order_quantity(contingent_order, order.quantity) - elif order.leaves_qty._mem.raw != contingent_order.leaves_qty._mem.raw: - self._update_order_quantity(contingent_order, order.leaves_qty) - - cdef void _update_order_quantity(self, Order order, Quantity new_quantity): - # Generate event - cdef uint64_t ts_now = self._clock.timestamp_ns() - cdef OrderUpdated event = OrderUpdated( - trader_id=order.trader_id, - strategy_id=order.strategy_id, - instrument_id=order.instrument_id, - client_order_id=order.client_order_id, - venue_order_id=None, # Not yet assigned by any venue - account_id=order.account_id, # Probably None - quantity=new_quantity, - price=None, - trigger_price=None, - event_id=UUID4(), - ts_event=ts_now, - ts_init=ts_now, - ) - - order.apply(event) - self.cache.update_order(order) - - self._send_risk_event(event) - # ------------------------------------------------------------------------------------------------- cpdef void _trigger_stop_order(self, Order order): - cdef OrderTriggered event if ( order.order_type == OrderType.STOP_LIMIT or order.order_type == OrderType.LIMIT_IF_TOUCHED or order.order_type == OrderType.TRAILING_STOP_LIMIT ): - # Generate event - event = OrderTriggered( - trader_id=order.trader_id, - strategy_id=order.strategy_id, - instrument_id=order.instrument_id, - client_order_id=order.client_order_id, - venue_order_id=order.venue_order_id, # Probably None - account_id=order.account_id, # Probably None - event_id=UUID4(), - ts_event=self._clock.timestamp_ns(), - ts_init=self._clock.timestamp_ns(), - ) - order.apply(event) self._fill_limit_order(order) elif ( order.order_type == OrderType.STOP_MARKET @@ -748,25 +603,18 @@ cdef class OrderEmulator(Actor): ): self._fill_market_order(order) else: - raise RuntimeError("invalid `OrderType`") # pragma: no cover (design-time error) + raise RuntimeError(f"invalid `OrderType`, was {order.type_string_c()}") # pragma: no cover (design-time error) cpdef void _fill_market_order(self, Order order): # Fetch command - cdef SubmitOrder command = self._commands_submit_order.pop(order.client_order_id, None) + cdef SubmitOrder command = self._manager.pop_submit_order_command(order.client_order_id) if command is None: - self._log.debug( - f"`SubmitOrder` command for {repr(order.client_order_id)} not found.", - ) - return - - self.log.info(f"Releasing {order}...") + raise RuntimeError("invalid operation `_fill_market_order` with no command") # pragma: no cover (design-time error) cdef InstrumentId trigger_instrument_id = order.instrument_id if order.trigger_instrument_id is None else order.trigger_instrument_id cdef MatchingCore matching_core = self._matching_cores.get(trigger_instrument_id) - if matching_core is None: - raise RuntimeError(f"No matching core for trigger instrument {trigger_instrument_id}") - - matching_core.delete_order(order) + if matching_core is not None: + matching_core.delete_order(order) order.emulation_trigger = TriggerType.NO_TRIGGER cdef MarketOrder transformed = MarketOrder.transform(order, self.clock.timestamp_ns()) @@ -789,10 +637,41 @@ cdef class OrderEmulator(Actor): msg=transformed.last_event_c(), ) + # Determine triggered price + if order.side == OrderSide.BUY: + released_price = matching_core.ask + elif order.side == OrderSide.SELL: + released_price = matching_core.bid + else: + raise RuntimeError("invalid `OrderSide`") # pragma: no cover (design-time error) + + # Generate event + cdef OrderReleased event = OrderReleased( + trader_id=order.trader_id, + strategy_id=order.strategy_id, + instrument_id=order.instrument_id, + client_order_id=order.client_order_id, + released_price=released_price, + event_id=UUID4(), + ts_init=self._clock.timestamp_ns(), + ) + transformed.apply(event) + self.cache.update_order(transformed) + + self._manager.send_risk_event(event) + + self.log.info(f"Releasing {transformed}...", LogColor.MAGENTA) + + # Publish event + self._msgbus.publish_c( + topic=f"events.order.{transformed.strategy_id.to_str()}", + msg=event, + ) + if order.exec_algorithm_id is not None: - self._send_algo_command(command) + self._manager.send_algo_command(command) else: - self._send_exec_command(command) + self._manager.send_exec_command(command) cpdef void _fill_limit_order(self, Order order): if order.order_type == OrderType.LIMIT: @@ -800,21 +679,14 @@ cdef class OrderEmulator(Actor): return # Fetch command - cdef SubmitOrder command = self._commands_submit_order.pop(order.client_order_id, None) + cdef SubmitOrder command = self._manager.pop_submit_order_command(order.client_order_id) if command is None: - self._log.debug( - f"`SubmitOrder` command for {repr(order.client_order_id)} not found.", - ) - return - - self.log.info(f"Releasing {order}...") + return # Order already released cdef InstrumentId trigger_instrument_id = order.instrument_id if order.trigger_instrument_id is None else order.trigger_instrument_id cdef MatchingCore matching_core = self._matching_cores.get(trigger_instrument_id) - if matching_core is None: - raise RuntimeError(f"No matching core for trigger instrument {trigger_instrument_id}") - - matching_core.delete_order(order) + if matching_core is not None: + matching_core.delete_order(order) order.emulation_trigger = TriggerType.NO_TRIGGER cdef LimitOrder transformed = LimitOrder.transform(order, self.clock.timestamp_ns()) @@ -837,10 +709,41 @@ cdef class OrderEmulator(Actor): msg=transformed.last_event_c(), ) + # Determine triggered price + if order.side == OrderSide.BUY: + released_price = matching_core.ask + elif order.side == OrderSide.SELL: + released_price = matching_core.bid + else: + raise RuntimeError("invalid `OrderSide`") # pragma: no cover (design-time error) + + # Generate event + cdef OrderReleased event = OrderReleased( + trader_id=order.trader_id, + strategy_id=order.strategy_id, + instrument_id=order.instrument_id, + client_order_id=order.client_order_id, + released_price=released_price, + event_id=UUID4(), + ts_init=self._clock.timestamp_ns(), + ) + transformed.apply(event) + self.cache.update_order(transformed) + + self._manager.send_risk_event(event) + + self.log.info(f"Releasing {transformed}...", LogColor.MAGENTA) + + # Publish event + self._msgbus.publish_c( + topic=f"events.order.{transformed.strategy_id.to_str()}", + msg=event, + ) + if order.exec_algorithm_id is not None: - self._send_algo_command(command) + self._manager.send_algo_command(command) else: - self._send_exec_command(command) + self._manager.send_exec_command(command) cpdef void on_quote_tick(self, QuoteTick tick): if not self._log.is_bypassed: @@ -851,8 +754,8 @@ cdef class OrderEmulator(Actor): self._log.error(f"Cannot handle `QuoteTick`: no matching core for instrument {tick.instrument_id}.") return - matching_core.set_bid_raw(tick._mem.bid.raw) - matching_core.set_ask_raw(tick._mem.ask.raw) + matching_core.set_bid_raw(tick._mem.bid_price.raw) + matching_core.set_ask_raw(tick._mem.ask_price.raw) self._iterate_orders(matching_core) @@ -900,9 +803,9 @@ cdef class OrderEmulator(Actor): cdef QuoteTick quote_tick = self.cache.quote_tick(matching_core.instrument_id) cdef TradeTick trade_tick = self.cache.trade_tick(matching_core.instrument_id) if bid is None and quote_tick is not None: - bid = quote_tick.bid + bid = quote_tick.bid_price if ask is None and quote_tick is not None: - ask = quote_tick.ask + ask = quote_tick.ask_price if last is None and trade_tick is not None: last = trade_tick.price # TODO(cs): ------------------------------------------------------------ @@ -916,7 +819,7 @@ cdef class OrderEmulator(Actor): ask=ask, last=last, ) - except RuntimeError as e: + except RuntimeError as e: # pragma: no cover (design-time error) self._log.warning(f"Cannot calculate trailing stop order: {e}") return @@ -942,32 +845,6 @@ cdef class OrderEmulator(Actor): ts_init=ts_now, ) order.apply(event) + self.cache.update_order(order) - self._send_risk_event(event) - -# -- EGRESS --------------------------------------------------------------------------------------- - - cdef void _send_algo_command(self, TradingCommand command): - if not self.log.is_bypassed: - self.log.info(f"{CMD}{SENT} {command}.") - self._msgbus.send(endpoint=f"{command.exec_algorithm_id}.execute", msg=command) - - cdef void _send_risk_command(self, TradingCommand command): - if not self.log.is_bypassed: - self.log.info(f"{CMD}{SENT} {command}.") - self._msgbus.send(endpoint="RiskEngine.execute", msg=command) - - cdef void _send_exec_command(self, TradingCommand command): - if not self.log.is_bypassed: - self.log.info(f"{CMD}{SENT} {command}.") - self._msgbus.send(endpoint="ExecEngine.execute", msg=command) - - cdef void _send_risk_event(self, OrderEvent event): - if not self.log.is_bypassed: - self.log.info(f"{EVT}{SENT} {event}.") - self._msgbus.send(endpoint="RiskEngine.process", msg=event) - - cdef void _send_exec_event(self, OrderEvent event): - if not self.log.is_bypassed: - self.log.info(f"{EVT}{SENT} {event}.") - self._msgbus.send(endpoint="ExecEngine.process", msg=event) + self._manager.send_risk_event(event) diff --git a/nautilus_trader/execution/engine.pxd b/nautilus_trader/execution/engine.pxd index 25a6759a2448..72784e738113 100644 --- a/nautilus_trader/execution/engine.pxd +++ b/nautilus_trader/execution/engine.pxd @@ -30,6 +30,7 @@ from nautilus_trader.model.enums_c cimport OrderSide from nautilus_trader.model.events.order cimport OrderEvent from nautilus_trader.model.events.order cimport OrderFilled from nautilus_trader.model.identifiers cimport InstrumentId +from nautilus_trader.model.identifiers cimport PositionId from nautilus_trader.model.identifiers cimport StrategyId from nautilus_trader.model.identifiers cimport Venue from nautilus_trader.model.instruments.base cimport Instrument @@ -111,6 +112,8 @@ cdef class ExecutionEngine(Component): cpdef void _handle_event(self, OrderEvent event) cpdef OmsType _determine_oms_type(self, OrderFilled fill) cpdef void _determine_position_id(self, OrderFilled fill, OmsType oms_type) + cpdef PositionId _determine_hedging_position_id(self, OrderFilled fill) + cpdef PositionId _determine_netting_position_id(self, OrderFilled fill) cpdef void _apply_event_to_order(self, Order order, OrderEvent event) cpdef void _handle_order_fill(self, Order order, OrderFilled fill, OmsType oms_type) cpdef Position _open_position(self, Instrument instrument, Position position, OrderFilled fill, OmsType oms_type) diff --git a/nautilus_trader/execution/engine.pyx b/nautilus_trader/execution/engine.pyx index 159643a053e1..59eb25ee3856 100644 --- a/nautilus_trader/execution/engine.pyx +++ b/nautilus_trader/execution/engine.pyx @@ -649,14 +649,14 @@ cdef class ExecutionEngine(Component): cdef StrategyId strategy_id for strategy_id, count in counts.items(): self._pos_id_generator.set_count(strategy_id, count) - self._log.info(f"Set PositionId count for {repr(strategy_id)} to {count}.") + self._log.info(f"Set PositionId count for {strategy_id!r} to {count}.") cpdef Price _last_px_for_conversion(self, InstrumentId instrument_id, OrderSide order_side): cdef Price last_px = None cdef QuoteTick last_quote = self._cache.quote_tick(instrument_id) cdef TradeTick last_trade = self._cache.trade_tick(instrument_id) if last_quote is not None: - last_px = last_quote.ask if order_side == OrderSide.BUY else last_quote.bid + last_px = last_quote.ask_price if order_side == OrderSide.BUY else last_quote.bid_price else: if last_trade is not None: last_px = last_trade.price @@ -714,7 +714,7 @@ cdef class ExecutionEngine(Component): self._cache.update_order(order) self._msgbus.publish_c( - topic=f"events.order.{order.strategy_id.to_str()}", + topic=f"events.order.{order.strategy_id}", msg=denied, ) @@ -844,14 +844,14 @@ cdef class ExecutionEngine(Component): cdef Order order = self._cache.order(event.client_order_id) if order is None: self._log.warning( - f"Order with {repr(event.client_order_id)} " + f"Order with {event.client_order_id!r} " f"not found in the cache to apply {event}." ) if event.venue_order_id is None: self._log.error( f"Cannot apply event to any order: " - f"{repr(event.client_order_id)} not found in the cache " + f"{event.client_order_id!r} not found in the cache " f"with no `VenueOrderId`." ) return # Cannot process event further @@ -861,7 +861,7 @@ cdef class ExecutionEngine(Component): if client_order_id is None: self._log.error( f"Cannot apply event to any order: " - f"{repr(event.client_order_id)} and {repr(event.venue_order_id)} " + f"{event.client_order_id!r} and {event.venue_order_id!r} " f"not found in the cache." ) return # Cannot process event further @@ -871,15 +871,15 @@ cdef class ExecutionEngine(Component): if order is None: self._log.error( f"Cannot apply event to any order: " - f"{repr(event.client_order_id)} and {repr(event.venue_order_id)} " + f"{event.client_order_id!r} and {event.venue_order_id!r} " f"not found in the cache." ) return # Cannot process event further # Set the correct ClientOrderId for the event - event.client_order_id = client_order_id + event.set_client_order_id(client_order_id) self._log.info( - f"Order with {repr(client_order_id)} was found in the cache.", + f"Order with {client_order_id!r} was found in the cache.", color=LogColor.GREEN, ) @@ -911,40 +911,92 @@ cdef class ExecutionEngine(Component): cdef PositionId position_id = self._cache.position_id(fill.client_order_id) if self.debug: self._log.debug( - f"Determining position ID for {repr(fill.client_order_id)}, " - f"position_id={repr(position_id)}.", + f"Determining position ID for {fill.client_order_id!r}, " + f"position_id={position_id!r}.", LogColor.MAGENTA, ) if position_id is not None: if fill.position_id is not None and fill.position_id != position_id: self._log.error( "Incorrect position ID assigned to fill: " - f"cached={repr(position_id)}, assigned={repr(fill.position_id)}. " + f"cached={position_id!r}, assigned={fill.position_id!r}. " "re-assigning from cache.", ) # Assign position ID to fill fill.position_id = position_id if self.debug: - self._log.debug(f"Assigned {repr(position_id)} to {fill}.", LogColor.MAGENTA) + self._log.debug(f"Assigned {position_id!r} to {fill}.", LogColor.MAGENTA) return if oms_type == OmsType.HEDGING: - if fill.position_id is not None: - # Already assigned - return - # Assign new position ID - position_id = self._pos_id_generator.generate(fill.strategy_id) - fill.position_id = position_id - if self.debug: - self._log.debug(f"Generated {repr(position_id)} for {fill}.", LogColor.MAGENTA) + position_id = self._determine_hedging_position_id(fill) elif oms_type == OmsType.NETTING: # Assign netted position ID - fill.position_id = PositionId(f"{fill.instrument_id.to_str()}-{fill.strategy_id.to_str()}") + position_id = self._determine_netting_position_id(fill) else: raise ValueError( # pragma: no cover (design-time error) f"invalid `OmsType`, was {oms_type}", # pragma: no cover (design-time error) ) + fill.position_id = position_id + + # TODO(cs): Optimize away the need to fetch order from cache + cdef Order order = self._cache.order(fill.client_order_id) + if order is None: + raise RuntimeError( + f"Order for {fill.client_order_id!r} not found to determine position ID.", + ) + + # Check execution algorithm position ID + if order.exec_algorithm_id is None or order.exec_spawn_id is None: + return + + cdef Order primary = self._cache.order(order.exec_spawn_id) + assert primary is not None + if primary.position_id is None: + primary.position_id = position_id + self._cache.add_position_id( + position_id, + primary.instrument_id.venue, + primary.client_order_id, + primary.strategy_id, + ) + self._log.debug(f"Assigned primary order {position_id!r}.", LogColor.MAGENTA) + + cpdef PositionId _determine_hedging_position_id(self, OrderFilled fill): + if fill.position_id is not None: + if self.debug: + self._log.debug(f"Already had a position ID of: {fill.position_id!r}", LogColor.MAGENTA) + # Already assigned + return fill.position_id + + cdef Order order = self._cache.order(fill.client_order_id) + if order is None: + raise RuntimeError( + f"Order for {fill.client_order_id!r} not found to determine position ID.", + ) + + cdef: + list exec_spawn_orders + Order spawned_order + if order.exec_spawn_id is not None: + exec_spawn_orders = self._cache.orders_for_exec_spawn(order.exec_spawn_id) + for spawned_order in exec_spawn_orders: + if spawned_order.position_id is not None: + if self.debug: + self._log.debug(f"Found spawned {spawned_order.position_id!r} for {fill}.", LogColor.MAGENTA) + # Use position ID for execution spawn + return spawned_order.position_id + + # Assign new position ID + position_id = self._pos_id_generator.generate(fill.strategy_id) + if self.debug: + self._log.debug(f"Generated {position_id!r} for {fill}.", LogColor.MAGENTA) + return position_id + + cpdef PositionId _determine_netting_position_id(self, OrderFilled fill): + return PositionId(f"{fill.instrument_id}-{fill.strategy_id}") + cpdef void _apply_event_to_order(self, Order order, OrderEvent event): try: order.apply(event) @@ -954,12 +1006,12 @@ cdef class ExecutionEngine(Component): except (ValueError, KeyError) as e: # ValueError: Protection against invalid IDs # KeyError: Protection against duplicate fills - self._log.exception(f"Error on applying {repr(event)} to {repr(order)}", e) + self._log.exception(f"Error on applying {event!r} to {order!r}", e) return self._cache.update_order(order) self._msgbus.publish_c( - topic=f"events.order.{event.strategy_id.to_str()}", + topic=f"events.order.{event.strategy_id}", msg=event, ) @@ -999,8 +1051,6 @@ cdef class ExecutionEngine(Component): for client_order_id in order.linked_order_ids or []: contingent_order = self._cache.order(client_order_id) if contingent_order is not None and contingent_order.position_id is None: - if contingent_order.is_reduce_only and contingent_order.quantity._mem.raw > position.quantity._mem.raw: - return # Cannot yet assign position ID as will reject `reduce_only` orders contingent_order.position_id = position.id self._cache.add_position_id( order.position_id, @@ -1020,7 +1070,7 @@ cdef class ExecutionEngine(Component): self._cache.update_position(position) except KeyError as e: # Protected against duplicate OrderFilled - self._log.exception(f"Error on applying {repr(fill)} to {repr(position)}", e) + self._log.exception(f"Error on applying {fill!r} to {position!r}", e) return # Not re-raising to avoid crashing engine cdef PositionOpened event = PositionOpened.create_c( @@ -1031,7 +1081,7 @@ cdef class ExecutionEngine(Component): ) self._msgbus.publish_c( - topic=f"events.position.{event.strategy_id.to_str()}", + topic=f"events.position.{event.strategy_id}", msg=event, ) @@ -1042,7 +1092,7 @@ cdef class ExecutionEngine(Component): position.apply(fill) except KeyError as e: # Protected against duplicate OrderFilled - self._log.exception(f"Error on applying {repr(fill)} to {repr(position)}", e) + self._log.exception(f"Error on applying {fill!r} to {position!r}", e) return # Not re-raising to avoid crashing engine self._cache.update_position(position) @@ -1064,7 +1114,7 @@ cdef class ExecutionEngine(Component): ) self._msgbus.publish_c( - topic=f"events.position.{event.strategy_id.to_str()}", + topic=f"events.position.{event.strategy_id}", msg=event, ) diff --git a/nautilus_trader/execution/manager.pxd b/nautilus_trader/execution/manager.pxd new file mode 100644 index 000000000000..459c40399818 --- /dev/null +++ b/nautilus_trader/execution/manager.pxd @@ -0,0 +1,87 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2023 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from nautilus_trader.cache.cache cimport Cache +from nautilus_trader.common.clock cimport Clock +from nautilus_trader.common.logging cimport Logger +from nautilus_trader.common.logging cimport LoggerAdapter +from nautilus_trader.core.message cimport Event +from nautilus_trader.core.uuid cimport UUID4 +from nautilus_trader.execution.messages cimport CancelAllOrders +from nautilus_trader.execution.messages cimport CancelOrder +from nautilus_trader.execution.messages cimport ModifyOrder +from nautilus_trader.execution.messages cimport SubmitOrder +from nautilus_trader.execution.messages cimport SubmitOrderList +from nautilus_trader.execution.messages cimport TradingCommand +from nautilus_trader.model.events.order cimport OrderCanceled +from nautilus_trader.model.events.order cimport OrderEvent +from nautilus_trader.model.events.order cimport OrderExpired +from nautilus_trader.model.events.order cimport OrderFilled +from nautilus_trader.model.events.order cimport OrderRejected +from nautilus_trader.model.events.order cimport OrderUpdated +from nautilus_trader.model.events.position cimport PositionEvent +from nautilus_trader.model.identifiers cimport ClientId +from nautilus_trader.model.identifiers cimport ClientOrderId +from nautilus_trader.model.identifiers cimport InstrumentId +from nautilus_trader.model.identifiers cimport PositionId +from nautilus_trader.model.identifiers cimport StrategyId +from nautilus_trader.model.objects cimport Price +from nautilus_trader.model.objects cimport Quantity +from nautilus_trader.model.orders.base cimport Order +from nautilus_trader.msgbus.bus cimport MessageBus + + +cdef class OrderManager: + cdef Clock _clock + cdef LoggerAdapter _log + cdef MessageBus _msgbus + cdef Cache _cache + + cdef readonly bint debug + + cdef dict _submit_order_commands + cdef object _submit_order_handler + cdef object _cancel_order_handler + + cpdef dict get_submit_order_commands(self) + cpdef void cache_submit_order_command(self, SubmitOrder command) + cpdef SubmitOrder pop_submit_order_command(self, ClientOrderId client_order_id) + cpdef void reset(self) + +# -- COMMAND HANDLERS ----------------------------------------------------------------------------- + + cpdef void cancel_order(self, Order order) + cpdef void create_new_submit_order(self, Order order, PositionId position_id=*, ClientId client_id=*) + +# -- EVENT HANDLERS ------------------------------------------------------------------------------- + + cpdef void handle_position_event(self, PositionEvent event) + cpdef void handle_order_rejected(self, OrderRejected rejected) + cpdef void handle_order_canceled(self, OrderCanceled canceled) + cpdef void handle_order_expired(self, OrderExpired expired) + cpdef void handle_order_updated(self, OrderUpdated updated) + cpdef void handle_order_filled(self, OrderFilled filled) + cpdef void handle_contingencies(self, Order order) + cpdef void handle_contingencies_update(self, Order order) + cpdef void update_order_quantity(self, Order order, Quantity new_quantity) + +# -- EGRESS --------------------------------------------------------------------------------------- + + cpdef void send_emulator_command(self, TradingCommand command) + cpdef void send_algo_command(self, TradingCommand command) + cpdef void send_risk_command(self, TradingCommand command) + cpdef void send_exec_command(self, TradingCommand command) + cpdef void send_risk_event(self, OrderEvent event) + cpdef void send_exec_event(self, OrderEvent event) diff --git a/nautilus_trader/execution/manager.pyx b/nautilus_trader/execution/manager.pyx new file mode 100644 index 000000000000..ca7430dc08e8 --- /dev/null +++ b/nautilus_trader/execution/manager.pyx @@ -0,0 +1,550 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2023 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from libc.stdint cimport uint8_t +from libc.stdint cimport uint64_t + +from nautilus_trader.cache.cache cimport Cache +from nautilus_trader.common.clock cimport Clock +from nautilus_trader.common.logging cimport CMD +from nautilus_trader.common.logging cimport EVT +from nautilus_trader.common.logging cimport RECV +from nautilus_trader.common.logging cimport SENT +from nautilus_trader.common.logging cimport LogColor +from nautilus_trader.common.logging cimport Logger +from nautilus_trader.common.logging cimport LoggerAdapter +from nautilus_trader.core.correctness cimport Condition +from nautilus_trader.core.message cimport Event +from nautilus_trader.core.uuid cimport UUID4 +from nautilus_trader.execution.messages cimport CancelAllOrders +from nautilus_trader.execution.messages cimport CancelOrder +from nautilus_trader.execution.messages cimport ModifyOrder +from nautilus_trader.execution.messages cimport SubmitOrder +from nautilus_trader.execution.messages cimport SubmitOrderList +from nautilus_trader.execution.messages cimport TradingCommand +from nautilus_trader.model.enums_c cimport ContingencyType +from nautilus_trader.model.enums_c cimport OrderStatus +from nautilus_trader.model.enums_c cimport TriggerType +from nautilus_trader.model.events.order cimport OrderCanceled +from nautilus_trader.model.events.order cimport OrderEvent +from nautilus_trader.model.events.order cimport OrderExpired +from nautilus_trader.model.events.order cimport OrderFilled +from nautilus_trader.model.events.order cimport OrderRejected +from nautilus_trader.model.events.order cimport OrderUpdated +from nautilus_trader.model.events.position cimport PositionEvent +from nautilus_trader.model.identifiers cimport ClientId +from nautilus_trader.model.identifiers cimport ClientOrderId +from nautilus_trader.model.identifiers cimport InstrumentId +from nautilus_trader.model.identifiers cimport PositionId +from nautilus_trader.model.identifiers cimport StrategyId +from nautilus_trader.model.objects cimport Price +from nautilus_trader.model.objects cimport Quantity +from nautilus_trader.model.orders.base cimport Order +from nautilus_trader.msgbus.bus cimport MessageBus + + +cdef class OrderManager: + """ + Provides a generic order execution manager. + + Parameters + ---------- + clock : Clock + The clock for the order manager. + logger : Logger + The logger for the order manager. + msgbus : MessageBus + The message bus for the order manager. + cache : Cache + The cache for the order manager. + component_name : str + The component name for the order manager. + submit_order_handler : Callable[[SubmitOrder], None], optional + The handler to call when submitting orders. + cancel_order_handler : Callable[[Order], None], optional + The handler to call when canceling orders. + debug : bool, default False + If debug mode is active (will provide extra debug logging). + + Raises + ------ + TypeError + If `submit_order_handler` is not of type `Callable`. + TypeError + If `cancel_order_handler` is not of type `Callable`. + """ + + def __init__( + self, + Clock clock not None, + Logger logger not None, + MessageBus msgbus, + Cache cache not None, + str component_name not None, + submit_order_handler: Optional[Callable[[SubmitOrder], None]] = None, + cancel_order_handler: Optional[Callable[[Order], None]] = None, + bint debug = False, + ): + Condition.valid_string(component_name, "component_name") + Condition.callable_or_none(submit_order_handler, "submit_order_handler") + Condition.callable_or_none(cancel_order_handler, "cancel_order_handler") + + self._clock = clock + self._log = LoggerAdapter(component_name=component_name, logger=logger) + self._msgbus = msgbus + self._cache = cache + + self.debug = debug + + self._submit_order_commands: dict[ClientOrderId, SubmitOrder] = {} + self._submit_order_handler: Callable[[SubmitOrder], None] = submit_order_handler + self._cancel_order_handler: Callable[[Order], None] = cancel_order_handler + + cpdef dict get_submit_order_commands(self): + """ + Return the managers cached submit order commands. + + Returns + ------- + dict[ClientOrderId, SubmitOrder] + + """ + return self._submit_order_commands.copy() + + cpdef void cache_submit_order_command(self, SubmitOrder command): + """ + Cache the given submit order `command` with the manager. + + Parameters + ---------- + command : SubmitOrder + The submit order command to cache. + + """ + Condition.not_none(command, "command") + + self._submit_order_commands[command.order.client_order_id] = command + + cpdef SubmitOrder pop_submit_order_command(self, ClientOrderId client_order_id): + """ + Pop the submit order command for the given `client_order_id` out of the managers + cache (if found). + + Parameters + ---------- + client_order_id : ClientOrderId + The client order ID for the command to pop. + + Returns + ------- + SubmitOrder or ``None`` + + """ + Condition.not_none(client_order_id, "client_order_id") + + return self._submit_order_commands.pop(client_order_id, None) + + cpdef void reset(self): + """ + Reset the manager, clearing all stateful values. + """ + self._submit_order_commands.clear() + + cpdef void cancel_order(self, Order order): + """ + Cancel the given `order` with the manager. + + Parameters + ---------- + order : Order + The order to cancel. + + """ + Condition.not_none(order, "order") + + if self.debug: + self._log.info(f"Cancelling order {order}.", LogColor.MAGENTA) + + self._submit_order_commands.pop(order.client_order_id, None) + + if self._cancel_order_handler is not None: + self._cancel_order_handler(order) + + # Generate event + cdef uint64_t ts_now = self._clock.timestamp_ns() + cdef OrderCanceled event = OrderCanceled( + trader_id=order.trader_id, + strategy_id=order.strategy_id, + instrument_id=order.instrument_id, + client_order_id=order.client_order_id, + venue_order_id=order.venue_order_id, # Probably None + account_id=order.account_id, # Probably None + event_id=UUID4(), + ts_event=ts_now, + ts_init=ts_now, + ) + self.send_exec_event(event) + + cpdef void create_new_submit_order( + self, + Order order, + PositionId position_id = None, + ClientId client_id = None, + ): + """ + Create a new submit order command for the given `order`. + + Parameters + ---------- + order : Order + The order for the command. + position_id : PositionId, optional + The position ID for the command. + client_id : ClientId, optional + The client ID for the command. + + """ + Condition.not_none(order, "order") + + if self.debug: + self._log.info( + f"Creating new `SubmitOrder` command for {order}, {position_id=}, {client_id=}.", + LogColor.MAGENTA, + ) + + cdef SubmitOrder submit = SubmitOrder( + trader_id=order.trader_id, + strategy_id=order.strategy_id, + order=order, + position_id=position_id, + client_id=client_id, + command_id=UUID4(), + ts_init=self._clock.timestamp_ns(), + ) + + if order.emulation_trigger == TriggerType.NO_TRIGGER: + # Cache command + self.cache_submit_order_command(submit) + + if order.exec_algorithm_id is not None: + self.send_algo_command(submit) + else: + self.send_risk_command(submit) + else: + self._submit_order_handler(submit) + +# -- EVENT HANDLERS ------------------------------------------------------------------------------- + + cpdef void handle_position_event(self, PositionEvent event): + Condition.not_none(event, "event") + # TBC + + cpdef void handle_order_rejected(self, OrderRejected rejected): + Condition.not_none(rejected, "rejected") + + cdef Order order = self._cache.order(rejected.client_order_id) + if order is None: + self._log.error( # pragma: no cover (design-time error) + "Cannot handle `OrderRejected`: " + f"order for {repr(rejected.client_order_id)} not found. {rejected}.", + ) + return + + if order.contingency_type != ContingencyType.NO_CONTINGENCY: + self.handle_contingencies(order) + + cpdef void handle_order_canceled(self, OrderCanceled canceled): + Condition.not_none(canceled, "canceled") + + cdef Order order = self._cache.order(canceled.client_order_id) + if order is None: + self._log.error( # pragma: no cover (design-time error) + "Cannot handle `OrderCanceled`: " + f"order for {repr(canceled.client_order_id)} not found. {canceled}.", + ) + return + + if order.contingency_type != ContingencyType.NO_CONTINGENCY: + self.handle_contingencies(order) + + cpdef void handle_order_expired(self, OrderExpired expired): + Condition.not_none(expired, "expired") + + cdef Order order = self._cache.order(expired.client_order_id) + if order is None: + self._log.error( # pragma: no cover (design-time error) + "Cannot handle `OrderExpired`: " + f"order for {repr(expired.client_order_id)} not found. {expired}.", + ) + return + + if order.contingency_type != ContingencyType.NO_CONTINGENCY: + self.handle_contingencies(order) + + cpdef void handle_order_updated(self, OrderUpdated updated): + Condition.not_none(updated, "updated") + + cdef Order order = self._cache.order(updated.client_order_id) + if order is None: + self._log.error( # pragma: no cover (design-time error) + "Cannot handle `OrderUpdated`: " + f"order for {repr(updated.client_order_id)} not found. {updated}.", + ) + return + + if order.contingency_type != ContingencyType.NO_CONTINGENCY: + self.handle_contingencies_update(order) + + cpdef void handle_order_filled(self, OrderFilled filled): + Condition.not_none(filled, "filled") + + if self.debug: + self._log.info(f"Handling fill for {filled.client_order_id}.", LogColor.MAGENTA) + + cdef Order order = self._cache.order(filled.client_order_id) + if order is None: # pragma: no cover (design-time error) + self._log.error( + "Cannot handle `OrderFilled`: " + f"order for {repr(filled.client_order_id)} not found. {filled}.", + ) + return + + cdef: + PositionId position_id + ClientId client_id + ClientOrderId client_order_id + Order child_order + Order primary_order + Order spawn_order + Quantity parent_quantity + Quantity parent_filled_qty + if order.contingency_type == ContingencyType.OTO: + Condition.not_empty(order.linked_order_ids, "order.linked_order_ids") + + position_id = self._cache.position_id(order.client_order_id) + client_id = self._cache.client_id(order.client_order_id) + + if order.exec_spawn_id is not None: + # Determine total quantities of execution spawn sequence + parent_quantity = self._cache.exec_spawn_total_quantity(order.exec_spawn_id) + parent_filled_qty = self._cache.exec_spawn_total_filled_qty(order.exec_spawn_id) + else: + parent_quantity = order.quantity + parent_filled_qty = order.filled_qty + + for client_order_id in order.linked_order_ids: + child_order = self._cache.order(client_order_id) + if child_order is None: + raise RuntimeError(f"Cannot find OTO child order for {repr(client_order_id)}") # pragma: no cover + + if self.debug: + self._log.info(f"Processing OTO child order {child_order}.", LogColor.MAGENTA) + + if not child_order.is_active_local_c(): + continue + + if child_order.position_id is None: + child_order.position_id = position_id + + if parent_filled_qty._mem.raw != child_order.leaves_qty._mem.raw: + self.update_order_quantity(child_order, parent_filled_qty) + elif parent_quantity._mem.raw != child_order.quantity._mem.raw: + self.update_order_quantity(child_order, parent_quantity) + + if child_order.status_c() not in (OrderStatus.INITIALIZED, OrderStatus.EMULATED) or self._submit_order_handler is None: + return # Order does not need to be released + + if not child_order.client_order_id in self._submit_order_commands: + self.create_new_submit_order( + order=child_order, + position_id=position_id, + client_id=client_id, + ) + elif order.contingency_type == ContingencyType.OCO: + # Cancel all OCO orders + for client_order_id in order.linked_order_ids: + contingent_order = self._cache.order(client_order_id) + if contingent_order is None: + raise RuntimeError(f"Cannot find OCO contingent order for {repr(client_order_id)}") # pragma: no cover + + if self.debug: + self._log.info(f"Processing OCO contingent order {contingent_order}.", LogColor.MAGENTA) + + if contingent_order.is_closed_c(): + continue + if contingent_order.client_order_id != order.client_order_id: + self.cancel_order(contingent_order) + elif order.contingency_type == ContingencyType.OUO: + self.handle_contingencies(order) + + cpdef void handle_contingencies(self, Order order): + Condition.not_none(order, "order") + Condition.not_empty(order.linked_order_ids, "order.linked_order_ids") + + if self.debug: + self._log.info( + f"Handling contingencies for {order.client_order_id}.", LogColor.MAGENTA, + ) + + cdef: + Quantity filled_qty + Quantity leaves_qty + bint is_spawn_active = False + if order.exec_spawn_id is not None: + # Determine total quantities of execution spawn sequence + filled_qty = self._cache.exec_spawn_total_filled_qty(order.exec_spawn_id) + leaves_qty = self._cache.exec_spawn_total_leaves_qty(order.exec_spawn_id, active_only=True) + is_spawn_active = leaves_qty._mem.raw > 0 + else: + filled_qty = order.filled_qty + leaves_qty = order.leaves_qty + + cdef ClientOrderId client_order_id + cdef Order contingent_order + for client_order_id in order.linked_order_ids: + contingent_order = self._cache.order(client_order_id) + if contingent_order is None: + raise RuntimeError(f"Cannot find contingent order for {repr(client_order_id)}") # pragma: no cover + if client_order_id == order.client_order_id: + continue # Already being handled + if contingent_order.is_closed_c() or not contingent_order.is_active_local_c(): + self._submit_order_commands.pop(order.client_order_id, None) + continue # Already completed + + if order.contingency_type == ContingencyType.OTO: + if self.debug: + self._log.info(f"Processing OTO child order {contingent_order}.", LogColor.MAGENTA) + self._log.info(f"{filled_qty=}, {contingent_order.quantity=}.", LogColor.YELLOW) + if order.is_closed_c() and filled_qty._mem.raw == 0 and (order.exec_spawn_id is None or not is_spawn_active): + self.cancel_order(contingent_order) + elif filled_qty._mem.raw > 0 and filled_qty._mem.raw != contingent_order.quantity._mem.raw: + self.update_order_quantity(contingent_order, filled_qty) + elif order.contingency_type == ContingencyType.OUO: + if self.debug: + self._log.info(f"Processing OUO contingent order {client_order_id}, {leaves_qty=}, {contingent_order.leaves_qty=}.", LogColor.MAGENTA) + if leaves_qty._mem.raw == 0 and order.exec_spawn_id is not None: + self.cancel_order(contingent_order) + elif order.is_closed_c() and (order.exec_spawn_id is None or not is_spawn_active): + self.cancel_order(contingent_order) + elif leaves_qty._mem.raw != contingent_order.leaves_qty._mem.raw: + self.update_order_quantity(contingent_order, leaves_qty) + + cpdef void handle_contingencies_update(self, Order order): + Condition.not_none(order, "order") + + if self.debug: + self._log.info( + f"Handling contingencies update for {order.client_order_id}", LogColor.MAGENTA, + ) + + cdef: + Quantity quantity + if order.exec_spawn_id is not None: + # Determine total quantity of execution spawn sequence + quantity = self._cache.exec_spawn_total_quantity(order.exec_spawn_id, active_only=True) + else: + quantity = order.quantity + + if quantity._mem.raw == 0: + return + + cdef ClientOrderId client_order_id + cdef Order contingent_order + for client_order_id in order.linked_order_ids: + contingent_order = self._cache.order(client_order_id) + assert contingent_order + if client_order_id == order.client_order_id: + continue # Already being handled # pragma: no cover + if contingent_order.is_closed_c() or contingent_order.emulation_trigger == TriggerType.NO_TRIGGER: + continue # Already completed # pragma: no cover + + if order.contingency_type == ContingencyType.OTO: + if quantity._mem.raw != contingent_order.quantity._mem.raw: + self.update_order_quantity(contingent_order, quantity) + elif order.contingency_type == ContingencyType.OUO: + if quantity._mem.raw != contingent_order.quantity._mem.raw: + self.update_order_quantity(contingent_order, quantity) + + cpdef void update_order_quantity(self, Order order, Quantity new_quantity): + if self.debug: + self._log.info( + f"Update contingency order {order.client_order_id} quantity to {new_quantity}.", + LogColor.MAGENTA, + ) + + # Generate event + cdef uint64_t ts_now = self._clock.timestamp_ns() + cdef OrderUpdated event = OrderUpdated( + trader_id=order.trader_id, + strategy_id=order.strategy_id, + instrument_id=order.instrument_id, + client_order_id=order.client_order_id, + venue_order_id=None, # Not yet assigned by any venue + account_id=order.account_id, # Probably None + quantity=new_quantity, + price=None, + trigger_price=None, + event_id=UUID4(), + ts_event=ts_now, + ts_init=ts_now, + ) + order.apply(event) + self._cache.update_order(order) + + self.send_risk_event(event) + +# -- EGRESS --------------------------------------------------------------------------------------- + + cpdef void send_emulator_command(self, TradingCommand command): + Condition.not_none(command, "command") + + if not self._log.is_bypassed: + self._log.info(f"{CMD}{SENT} {command}.") # pragma: no cover (no logging in tests) + self._msgbus.send(endpoint="OrderEmulator.execute", msg=command) + + cpdef void send_algo_command(self, TradingCommand command): + Condition.not_none(command, "command") + + if not self._log.is_bypassed: + self._log.info(f"{CMD}{SENT} {command}.") # pragma: no cover (no logging in tests) + self._msgbus.send(endpoint=f"{command.exec_algorithm_id}.execute", msg=command) + + cpdef void send_risk_command(self, TradingCommand command): + Condition.not_none(command, "command") + + if not self._log.is_bypassed: + self._log.info(f"{CMD}{SENT} {command}.") # pragma: no cover (no logging in tests) + self._msgbus.send(endpoint="RiskEngine.execute", msg=command) + + cpdef void send_exec_command(self, TradingCommand command): + Condition.not_none(command, "command") + + if not self._log.is_bypassed: + self._log.info(f"{CMD}{SENT} {command}.") # pragma: no cover (no logging in tests) + self._msgbus.send(endpoint="ExecEngine.execute", msg=command) + + cpdef void send_risk_event(self, OrderEvent event): + Condition.not_none(event, "event") + + if not self._log.is_bypassed: + self._log.info(f"{EVT}{SENT} {event}.") # pragma: no cover (no logging in tests) + self._msgbus.send(endpoint="RiskEngine.process", msg=event) + + cpdef void send_exec_event(self, OrderEvent event): + Condition.not_none(event, "event") + + if not self._log.is_bypassed: + self._log.info(f"{EVT}{SENT} {event}.") # pragma: no cover (no logging in tests) + self._msgbus.send(endpoint="ExecEngine.process", msg=event) diff --git a/nautilus_trader/execution/matching_core.pyx b/nautilus_trader/execution/matching_core.pyx index fe002f42ed13..bd5e3dae0fb6 100644 --- a/nautilus_trader/execution/matching_core.pyx +++ b/nautilus_trader/execution/matching_core.pyx @@ -236,7 +236,7 @@ cdef class MatchingCore: cdef Order order for order in self._orders_bid + self._orders_ask: # Lists implicitly copied if order.is_closed_c(): - continue # Orders state has changed since iteration started + continue # Orders state has changed since iteration started # pragma: no cover self.match_order(order) # -- MATCHING ------------------------------------------------------------------------------------- @@ -427,7 +427,7 @@ cdef inline int64_t order_sort_key(Order order): price = order.price return price._mem.raw if order.is_triggered else trigger_price._mem.raw else: - raise RuntimeError( + raise RuntimeError( # pragma: no cover (design-time error) f"invalid order type to sort in book, " f"was {order_type_to_str(order.order_type)}", ) diff --git a/nautilus_trader/execution/messages.pyx b/nautilus_trader/execution/messages.pyx index d9b6be3b1e45..4ae1bc80fed8 100644 --- a/nautilus_trader/execution/messages.pyx +++ b/nautilus_trader/execution/messages.pyx @@ -21,6 +21,7 @@ from libc.stdint cimport uint64_t from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.uuid cimport UUID4 +from nautilus_trader.model.enums_c cimport TriggerType from nautilus_trader.model.enums_c cimport order_side_from_str from nautilus_trader.model.enums_c cimport order_side_to_str from nautilus_trader.model.events.order cimport OrderInitialized @@ -258,7 +259,7 @@ cdef class SubmitOrderList(TradingCommand): self.order_list = order_list self.exec_algorithm_id = order_list.first.exec_algorithm_id self.position_id = position_id - self.has_emulated_order = True if any(o.is_emulated for o in order_list.orders) else False + self.has_emulated_order = True if any(o.emulation_trigger != TriggerType.NO_TRIGGER for o in order_list.orders) else False def __str__(self) -> str: return ( diff --git a/nautilus_trader/execution/trailing.pyx b/nautilus_trader/execution/trailing.pyx index 8d353202704d..16d3bd7242d0 100644 --- a/nautilus_trader/execution/trailing.pyx +++ b/nautilus_trader/execution/trailing.pyx @@ -42,7 +42,7 @@ cdef class TrailingStopCalculator: ): Condition.not_none(price_increment, "price_increment") if order.order_type not in (OrderType.TRAILING_STOP_MARKET, OrderType.TRAILING_STOP_LIMIT): - raise TypeError(f"invalid `OrderType` for calculation, was {order.type_string_c()}") + raise TypeError(f"invalid `OrderType` for calculation, was {order.type_string_c()}") # pragma: no cover (design-time error) cdef int64_t trailing_offset_raw = int(order.trailing_offset * int(FIXED_SCALAR)) cdef int64_t limit_offset_raw = 0 @@ -65,7 +65,7 @@ cdef class TrailingStopCalculator: or order.trigger_type == TriggerType.MARK_PRICE ): if last is None: - raise RuntimeError( + raise RuntimeError( # pragma: no cover (design-time error) f"cannot process trailing stop, " f"no LAST price for {order.instrument_id} " f"(add trade ticks or use bars)", @@ -112,13 +112,13 @@ cdef class TrailingStopCalculator: new_price = temp_price elif order.trigger_type == TriggerType.BID_ASK: if bid is None: - raise RuntimeError( + raise RuntimeError( # pragma: no cover (design-time error) f"cannot process trailing stop, " f"no BID price for {order.instrument_id} " f"(add quote ticks or use bars)", ) if ask is None: - raise RuntimeError( + raise RuntimeError( # pragma: no cover (design-time error) f"cannot process trailing stop, " f"no ASK price for {order.instrument_id} " f"(add quote ticks or use bars)", @@ -170,19 +170,19 @@ cdef class TrailingStopCalculator: new_price = temp_price elif order.trigger_type == TriggerType.LAST_OR_BID_ASK: if last is None: - raise RuntimeError( + raise RuntimeError( # pragma: no cover (design-time error) f"cannot process trailing stop, " f"no LAST price for {order.instrument_id} " f"(add trade ticks or use bars)", ) if bid is None: - raise RuntimeError( + raise RuntimeError( # pragma: no cover (design-time error) f"cannot process trailing stop, " f"no BID price for {order.instrument_id} " f"(add quote ticks or use bars)", ) if ask is None: - raise RuntimeError( + raise RuntimeError( # pragma: no cover (design-time error) f"cannot process trailing stop, " f"no ASK price for {order.instrument_id} " f"(add quote ticks or use bars)", @@ -277,7 +277,7 @@ cdef class TrailingStopCalculator: if price is None or price._mem.raw < temp_price._mem.raw: new_price = temp_price else: - raise RuntimeError( + raise RuntimeError( # pragma: no cover (design-time error) f"cannot process trailing stop, " f"`TriggerType.{trigger_type_to_str(order.trigger_type)}` " f"not currently supported", @@ -302,7 +302,7 @@ cdef class TrailingStopCalculator: elif trailing_offset_type == TrailingOffsetType.TICKS: offset *= price_increment.as_f64_c() else: - raise RuntimeError( + raise RuntimeError( # pragma: no cover (design-time error) f"cannot process trailing stop, " f"`TrailingOffsetType` {trailing_offset_type_to_str(trailing_offset_type)} " f"not currently supported", diff --git a/nautilus_trader/indicators/bollinger_bands.pyx b/nautilus_trader/indicators/bollinger_bands.pyx index 64bad8a07d3a..8e144acf9f92 100644 --- a/nautilus_trader/indicators/bollinger_bands.pyx +++ b/nautilus_trader/indicators/bollinger_bands.pyx @@ -84,8 +84,8 @@ cdef class BollingerBands(Indicator): """ Condition.not_none(tick, "tick") - cdef double ask = Price.raw_to_f64_c(tick._mem.ask.raw) - cdef double bid = Price.raw_to_f64_c(tick._mem.bid.raw) + cdef double bid = Price.raw_to_f64_c(tick._mem.bid_price.raw) + cdef double ask = Price.raw_to_f64_c(tick._mem.ask_price.raw) cdef double mid = (ask + bid) / 2.0 self.update_raw(ask, bid, mid) diff --git a/nautilus_trader/indicators/donchian_channel.pyx b/nautilus_trader/indicators/donchian_channel.pyx index 977ca89e569f..a4bbff4447b9 100644 --- a/nautilus_trader/indicators/donchian_channel.pyx +++ b/nautilus_trader/indicators/donchian_channel.pyx @@ -67,8 +67,8 @@ cdef class DonchianChannel(Indicator): """ Condition.not_none(tick, "tick") - cdef double ask = Price.raw_to_f64_c(tick._mem.ask.raw) - cdef double bid = Price.raw_to_f64_c(tick._mem.bid.raw) + cdef double ask = Price.raw_to_f64_c(tick._mem.ask_price.raw) + cdef double bid = Price.raw_to_f64_c(tick._mem.bid_price.raw) self.update_raw(ask, bid) cpdef void handle_trade_tick(self, TradeTick tick): diff --git a/nautilus_trader/indicators/spread_analyzer.pyx b/nautilus_trader/indicators/spread_analyzer.pyx index c711d49cc618..7c002bb3987f 100644 --- a/nautilus_trader/indicators/spread_analyzer.pyx +++ b/nautilus_trader/indicators/spread_analyzer.pyx @@ -77,8 +77,8 @@ cdef class SpreadAnalyzer(Indicator): if len(self._spreads) == self.capacity: self._set_initialized(True) - cdef double ask = Price.raw_to_f64_c(tick._mem.ask.raw) - cdef double bid = Price.raw_to_f64_c(tick._mem.bid.raw) + cdef double bid = Price.raw_to_f64_c(tick._mem.bid_price.raw) + cdef double ask = Price.raw_to_f64_c(tick._mem.ask_price.raw) cdef double spread = ask - bid self.current = spread diff --git a/nautilus_trader/infrastructure/__init__.py b/nautilus_trader/infrastructure/__init__.py index 27a98bd6c513..71291f51cf50 100644 --- a/nautilus_trader/infrastructure/__init__.py +++ b/nautilus_trader/infrastructure/__init__.py @@ -16,6 +16,6 @@ The `infrastructure` subpackage provides technology specific infrastructure implementations. -Out of the box a `Redis `_ backed cache is implemented. +An *out of the box* `Redis ` backed cache is implemented. """ diff --git a/nautilus_trader/infrastructure/cache.pxd b/nautilus_trader/infrastructure/cache.pxd index 43539d1ca3d9..6b241fe341f4 100644 --- a/nautilus_trader/infrastructure/cache.pxd +++ b/nautilus_trader/infrastructure/cache.pxd @@ -28,8 +28,19 @@ cdef class RedisCacheDatabase(CacheDatabase): cdef str _key_positions cdef str _key_actors cdef str _key_strategies + + cdef str _key_index_order_ids cdef str _key_index_order_position cdef str _key_index_order_client + cdef str _key_index_orders + cdef str _key_index_orders_open + cdef str _key_index_orders_closed + cdef str _key_index_orders_emulated + cdef str _key_index_orders_inflight + cdef str _key_index_positions + cdef str _key_index_positions_open + cdef str _key_index_positions_closed + cdef str _key_snapshots_orders cdef str _key_snapshots_positions cdef str _key_heartbeat diff --git a/nautilus_trader/infrastructure/cache.pyx b/nautilus_trader/infrastructure/cache.pyx index 2a0226df0f2d..67d318e1ac56 100644 --- a/nautilus_trader/infrastructure/cache.pyx +++ b/nautilus_trader/infrastructure/cache.pyx @@ -19,6 +19,7 @@ from typing import Optional from nautilus_trader.config import CacheDatabaseConfig from cpython.datetime cimport datetime +from libc.stdint cimport uint64_t from nautilus_trader.accounting.accounts.base cimport Account from nautilus_trader.accounting.factory cimport AccountFactory @@ -33,6 +34,7 @@ from nautilus_trader.execution.messages cimport SubmitOrderList from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.data.tick cimport QuoteTick from nautilus_trader.model.enums_c cimport OrderType +from nautilus_trader.model.enums_c cimport TriggerType from nautilus_trader.model.enums_c cimport currency_type_from_str from nautilus_trader.model.enums_c cimport currency_type_to_str from nautilus_trader.model.enums_c cimport order_type_to_str @@ -48,9 +50,11 @@ from nautilus_trader.model.identifiers cimport OrderListId from nautilus_trader.model.identifiers cimport PositionId from nautilus_trader.model.identifiers cimport StrategyId from nautilus_trader.model.identifiers cimport TraderId +from nautilus_trader.model.identifiers cimport VenueOrderId from nautilus_trader.model.instruments.base cimport Instrument from nautilus_trader.model.instruments.synthetic cimport SyntheticInstrument from nautilus_trader.model.objects cimport Money +from nautilus_trader.model.objects cimport Price from nautilus_trader.model.orders.base cimport Order from nautilus_trader.model.orders.limit cimport LimitOrder from nautilus_trader.model.orders.market cimport MarketOrder @@ -77,8 +81,19 @@ cdef str _ORDERS = "orders" cdef str _POSITIONS = "positions" cdef str _ACTORS = "actors" cdef str _STRATEGIES = "strategies" + +cdef str _INDEX_ORDER_IDS = "index:order_ids" cdef str _INDEX_ORDER_POSITION = "index:order_position" cdef str _INDEX_ORDER_CLIENT = "index:order_client" +cdef str _INDEX_ORDERS = "index:orders" +cdef str _INDEX_ORDERS_OPEN = "index:orders_open" +cdef str _INDEX_ORDERS_CLOSED = "index:orders_closed" +cdef str _INDEX_ORDERS_EMULATED = "index:orders_emulated" +cdef str _INDEX_ORDERS_INFLIGHT = "index:orders_inflight" +cdef str _INDEX_POSITIONS = "index:positions" +cdef str _INDEX_POSITIONS_OPEN = "index:positions_open" +cdef str _INDEX_POSITIONS_CLOSED = "index:positions_closed" + cdef str _SNAPSHOTS_ORDERS = "snapshots:orders" cdef str _SNAPSHOTS_POSITIONS = "snapshots:positions" cdef str _HEARTBEAT = "health:heartbeat" @@ -142,8 +157,17 @@ cdef class RedisCacheDatabase(CacheDatabase): self._key_actors = f"{self._key_trader}:{_ACTORS}:" # noqa self._key_strategies = f"{self._key_trader}:{_STRATEGIES}:" # noqa + self._key_index_order_ids = f"{self._key_trader}:{_INDEX_ORDER_IDS}:" self._key_index_order_position = f"{self._key_trader}:{_INDEX_ORDER_POSITION}:" self._key_index_order_client = f"{self._key_trader}:{_INDEX_ORDER_CLIENT}:" + self._key_index_orders = f"{self._key_trader}:{_INDEX_ORDERS}" + self._key_index_orders_open = f"{self._key_trader}:{_INDEX_ORDERS_OPEN}" + self._key_index_orders_closed = f"{self._key_trader}:{_INDEX_ORDERS_CLOSED}" + self._key_index_orders_emulated = f"{self._key_trader}:{_INDEX_ORDERS_EMULATED}" + self._key_index_orders_inflight = f"{self._key_trader}:{_INDEX_ORDERS_INFLIGHT}" + self._key_index_positions = f"{self._key_trader}:{_INDEX_POSITIONS}" + self._key_index_positions_open = f"{self._key_trader}:{_INDEX_POSITIONS_OPEN}" + self._key_index_positions_closed = f"{self._key_trader}:{_INDEX_POSITIONS_CLOSED}" self._key_snapshots_orders = f"{self._key_trader}:{_SNAPSHOTS_ORDERS}:" self._key_snapshots_positions = f"{self._key_trader}:{_SNAPSHOTS_POSITIONS}:" @@ -556,7 +580,8 @@ cdef class RedisCacheDatabase(CacheDatabase): if event.order_type == OrderType.MARKET: order = MarketOrder.transform(order, event.ts_init) elif event.order_type == OrderType.LIMIT: - order = LimitOrder.transform(order, event.ts_init) + price = Price.from_str_c(event.options["price"]) + order = LimitOrder.transform(order, event.ts_init, price) else: raise RuntimeError( # pragma: no cover (design-time error) f"Cannot transform order to {order_type_to_str(event.order_type)}", # pragma: no cover (design-time error) @@ -613,11 +638,6 @@ cdef class RedisCacheDatabase(CacheDatabase): # Check event integrity if event in position._events: raise RuntimeError(f"Corrupt cache with duplicate event for position {event}") - if event.trade_id in position._trade_ids: - raise RuntimeError( - f"Duplicate {event.trade_id!r}, " - f"existing {position.id!r} trade_ids={position._trade_ids}", - ) position.apply(event) @@ -827,6 +847,11 @@ cdef class RedisCacheDatabase(CacheDatabase): f"The {repr(order.client_order_id)} already existed and was appended to.", ) + self._redis.sadd(self._key_index_orders, order.client_order_id.to_str()) + + if order.emulation_trigger != TriggerType.NO_TRIGGER: + self._redis.sadd(self._key_index_orders_emulated, order.client_order_id.to_str()) + self._log.debug(f"Added {order}.") if position_id is not None: @@ -856,8 +881,34 @@ cdef class RedisCacheDatabase(CacheDatabase): f"The {repr(position.id)} already existed and was appended to.", ) + self._redis.sadd(self._key_index_positions, position.id.to_str()) + self._redis.sadd(self._key_index_positions_open, position.id.to_str()) + self._log.debug(f"Added {position}.") + cpdef void index_venue_order_id(self, ClientOrderId client_order_id, VenueOrderId venue_order_id): + """ + Add an index entry for the given `venue_order_id` to `client_order_id`. + + Parameters + ---------- + client_order_id : ClientOrderId + The client order ID to index. + venue_order_id : VenueOrderId + The venue order ID to index. + + """ + Condition.not_none(client_order_id, "client_order_id") + Condition.not_none(venue_order_id, "venue_order_id") + + self._redis.hset( + self._key_index_order_ids, + client_order_id.to_str(), + venue_order_id.to_str(), + ) + + self._log.debug(f"Indexed {client_order_id!r} -> {venue_order_id!r}") + cpdef void index_order_position(self, ClientOrderId client_order_id, PositionId position_id): """ Add an index entry for the given `client_order_id` to `position_id`. @@ -970,6 +1021,30 @@ cdef class RedisCacheDatabase(CacheDatabase): if reply == 1: # Reply = The length of the list after the push operation self._log.error(f"The updated Order(id={order.client_order_id.to_str()}) did not already exist.") + if order.venue_order_id is not None: + # Assumes order_id does not change + self.index_venue_order_id(order.client_order_id, order.venue_order_id) + + # Update in-flight state + if order.is_inflight_c(): + self._redis.sadd(self._key_index_orders_inflight, order.client_order_id.to_str()) + else: + self._redis.srem(self._key_index_orders_inflight, order.client_order_id.to_str()) + + # Update open/closed state + if order.is_open_c(): + self._redis.srem(self._key_index_orders_closed, order.client_order_id.to_str()) + self._redis.sadd(self._key_index_orders_open, order.client_order_id.to_str()) + elif order.is_closed_c(): + self._redis.srem(self._key_index_orders_open, order.client_order_id.to_str()) + self._redis.sadd(self._key_index_orders_closed, order.client_order_id.to_str()) + + # Update emulation state + if order.emulation_trigger == TriggerType.NO_TRIGGER: + self._redis.srem(self._key_index_orders_emulated, order.client_order_id.to_str()) + else: + self._redis.sadd(self._key_index_orders_emulated, order.client_order_id.to_str()) + self._log.debug(f"Updated {order}.") cpdef void update_position(self, Position position): @@ -987,6 +1062,13 @@ cdef class RedisCacheDatabase(CacheDatabase): cdef bytes serialized_event = self._serializer.serialize(position.last_event_c()) cdef int reply = self._redis.rpush(self._key_positions + position.id.to_str(), serialized_event) + if position.is_open_c(): + self._redis.sadd(self._key_index_positions_open, position.id.to_str()) + self._redis.srem(self._key_index_positions_closed, position.id.to_str()) + elif position.is_closed_c(): + self._redis.sadd(self._key_index_positions_closed, position.id.to_str()) + self._redis.srem(self._key_index_positions_open, position.id.to_str()) + self._log.debug(f"Updated {position}.") cpdef void snapshot_order_state(self, Order order): @@ -1008,7 +1090,7 @@ cdef class RedisCacheDatabase(CacheDatabase): self._log.debug(f"Added state snapshot {order}.") - cpdef void snapshot_position_state(self, Position position, Money unrealized_pnl = None): + cpdef void snapshot_position_state(self, Position position, uint64_t ts_snapshot, Money unrealized_pnl = None): """ Snapshot the state of the given `position`. @@ -1016,6 +1098,8 @@ cdef class RedisCacheDatabase(CacheDatabase): ---------- position : Position The position for the state snapshot. + ts_snapshot : uint64_t + The UNIX timestamp (nanoseconds) when the snapshot was taken. unrealized_pnl : Money, optional The unrealized PnL for the state snapshot. @@ -1027,6 +1111,8 @@ cdef class RedisCacheDatabase(CacheDatabase): if unrealized_pnl is not None: position_state["unrealized_pnl"] = unrealized_pnl.to_str() + position_state["ts_snapshot"] = ts_snapshot + cdef bytes snapshot_bytes = self._serializer.serialize(position_state) self._redis.rpush(self._key_snapshots_positions + position.id.to_str(), snapshot_bytes) diff --git a/nautilus_trader/live/data_engine.py b/nautilus_trader/live/data_engine.py index 8d819074fd33..475e68f42e9b 100644 --- a/nautilus_trader/live/data_engine.py +++ b/nautilus_trader/live/data_engine.py @@ -16,11 +16,11 @@ from __future__ import annotations import asyncio +from asyncio import Queue from nautilus_trader.cache.cache import Cache from nautilus_trader.common.clock import LiveClock from nautilus_trader.common.logging import Logger -from nautilus_trader.common.queue import Queue from nautilus_trader.config import LiveDataEngineConfig from nautilus_trader.core.correctness import PyCondition from nautilus_trader.core.data import Data @@ -80,10 +80,10 @@ def __init__( ) self._loop: asyncio.AbstractEventLoop = loop - self._cmd_queue: Queue = Queue(maxsize=config.qsize) - self._req_queue: Queue = Queue(maxsize=config.qsize) - self._res_queue: Queue = Queue(maxsize=config.qsize) - self._data_queue: Queue = Queue(maxsize=config.qsize) + self._cmd_queue: asyncio.Queue = Queue(maxsize=config.qsize) + self._req_queue: asyncio.Queue = Queue(maxsize=config.qsize) + self._res_queue: asyncio.Queue = Queue(maxsize=config.qsize) + self._data_queue: asyncio.Queue = Queue(maxsize=config.qsize) # Async tasks self._cmd_queue_task: asyncio.Task | None = None @@ -234,8 +234,8 @@ def execute(self, command: DataCommand) -> None: Warnings -------- - This method should only be called from the same thread the event loop is - running on. + This method is not thread-safe and should only be called from the same thread the event + loop is running on. Calling it from a different thread may lead to unexpected behavior. """ PyCondition.not_none(command, "command") @@ -248,7 +248,8 @@ def execute(self, command: DataCommand) -> None: f"Blocking on `_cmd_queue.put` as queue full at " f"{self._cmd_queue.qsize()} items.", ) - self._loop.create_task(self._cmd_queue.put(command)) # Blocking until qsize reduces + # Schedule the `put` operation to be executed once there is space in the queue + self._loop.create_task(self._cmd_queue.put(command)) def request(self, request: DataRequest) -> None: """ @@ -264,8 +265,8 @@ def request(self, request: DataRequest) -> None: Warnings -------- - This method should only be called from the same thread the event loop is - running on. + This method is not thread-safe and should only be called from the same thread the event + loop is running on. Calling it from a different thread may lead to unexpected behavior. """ PyCondition.not_none(request, "request") @@ -278,7 +279,8 @@ def request(self, request: DataRequest) -> None: f"Blocking on `_req_queue.put` as queue full at " f"{self._req_queue.qsize()} items.", ) - self._loop.create_task(self._req_queue.put(request)) # Blocking until qsize reduces + # Schedule the `put` operation to be executed once there is space in the queue + self._loop.create_task(self._req_queue.put(request)) def response(self, response: DataResponse) -> None: """ @@ -294,8 +296,8 @@ def response(self, response: DataResponse) -> None: Warnings -------- - This method should only be called from the same thread the event loop is - running on. + This method is not thread-safe and should only be called from the same thread the event + loop is running on. Calling it from a different thread may lead to unexpected behavior. """ PyCondition.not_none(response, "response") @@ -305,9 +307,10 @@ def response(self, response: DataResponse) -> None: except asyncio.QueueFull: self._log.warning( f"Blocking on `_res_queue.put` as queue full at " - f"{self._res_queue.qsize()} items.", + f"{self._res_queue.qsize():_} items.", ) - self._loop.create_task(self._res_queue.put(response)) # Blocking until qsize reduces + # Schedule the `put` operation to be executed once there is space in the queue + self._loop.create_task(self._res_queue.put(response)) def process(self, data: Data) -> None: """ @@ -323,8 +326,8 @@ def process(self, data: Data) -> None: Warnings -------- - This method should only be called from the same thread the event loop is - running on. + This method is not thread-safe and should only be called from the same thread the event + loop is running on. Calling it from a different thread may lead to unexpected behavior. """ PyCondition.not_none(data, "data") @@ -335,9 +338,10 @@ def process(self, data: Data) -> None: except asyncio.QueueFull: self._log.warning( f"Blocking on `_data_queue.put` as queue full at " - f"{self._data_queue.qsize()} items.", + f"{self._data_queue.qsize():_} items.", ) - self._loop.create_task(self._data_queue.put(data)) # Blocking until qsize reduces + # Schedule the `put` operation to be executed once there is space in the queue + self._loop.create_task(self._data_queue.put(data)) # -- INTERNAL ------------------------------------------------------------------------------------- diff --git a/nautilus_trader/live/execution_engine.py b/nautilus_trader/live/execution_engine.py index 84cb11e9591e..18c997d36f89 100644 --- a/nautilus_trader/live/execution_engine.py +++ b/nautilus_trader/live/execution_engine.py @@ -17,6 +17,7 @@ import asyncio import math +from asyncio import Queue from decimal import Decimal from typing import Any @@ -24,7 +25,6 @@ from nautilus_trader.common.clock import LiveClock from nautilus_trader.common.enums import LogColor from nautilus_trader.common.logging import Logger -from nautilus_trader.common.queue import Queue from nautilus_trader.config import LiveExecEngineConfig from nautilus_trader.core.correctness import PyCondition from nautilus_trader.core.datetime import dt_to_unix_nanos @@ -117,8 +117,14 @@ def __init__( ) self._loop: asyncio.AbstractEventLoop = loop - self._cmd_queue: Queue = Queue(maxsize=config.qsize) - self._evt_queue: Queue = Queue(maxsize=config.qsize) + self._cmd_queue: asyncio.Queue = Queue(maxsize=config.qsize) + self._evt_queue: asyncio.Queue = Queue(maxsize=config.qsize) + + # Async tasks + self._cmd_queue_task: asyncio.Task | None = None + self._evt_queue_task: asyncio.Task | None = None + self._inflight_check_task: asyncio.Task | None = None + self._kill: bool = False # Settings self._reconciliation: bool = config.reconciliation @@ -136,12 +142,6 @@ def __init__( self._log.info(f"{config.inflight_check_interval_ms=}", LogColor.BLUE) self._log.info(f"{config.inflight_check_threshold_ms=}", LogColor.BLUE) - # Async tasks - self._cmd_queue_task: asyncio.Task | None = None - self._evt_queue_task: asyncio.Task | None = None - self._inflight_check_task: asyncio.Task | None = None - self._kill: bool = False - # Register endpoints self._msgbus.register(endpoint="ExecEngine.reconcile_report", handler=self.reconcile_report) self._msgbus.register( @@ -264,8 +264,8 @@ def execute(self, command: TradingCommand) -> None: Warnings -------- - This method should only be called from the same thread the event loop is - running on. + This method is not thread-safe and should only be called from the same thread the event + loop is running on. Calling it from a different thread may lead to unexpected behavior. """ PyCondition.not_none(command, "command") @@ -276,9 +276,10 @@ def execute(self, command: TradingCommand) -> None: except asyncio.QueueFull: self._log.warning( f"Blocking on `_cmd_queue.put` as queue full " - f"at {self._cmd_queue.qsize()} items.", + f"at {self._cmd_queue.qsize():_} items.", ) - self._loop.create_task(self._cmd_queue.put(command)) # Blocking until qsize reduces + # Schedule the `put` operation to be executed once there is space in the queue + self._loop.create_task(self._cmd_queue.put(command)) def process(self, event: OrderEvent) -> None: """ @@ -294,8 +295,8 @@ def process(self, event: OrderEvent) -> None: Warnings -------- - This method should only be called from the same thread the event loop is - running on. + This method is not thread-safe and should only be called from the same thread the event + loop is running on. Calling it from a different thread may lead to unexpected behavior. """ PyCondition.not_none(event, "event") @@ -305,9 +306,10 @@ def process(self, event: OrderEvent) -> None: except asyncio.QueueFull: self._log.warning( f"Blocking on `_evt_queue.put` as queue full " - f"at {self._evt_queue.qsize()} items.", + f"at {self._evt_queue.qsize():_} items.", ) - self._loop.create_task(self._evt_queue.put(event)) # Blocking until qsize reduces + # Schedule the `put` operation to be executed once there is space in the queue + self._loop.create_task(self._evt_queue.put(event)) # -- INTERNAL ------------------------------------------------------------------------------------- @@ -537,7 +539,7 @@ def _reconcile_mass_status( # Check for duplicate trade IDs for trade_report in trades: if trade_report.trade_id in reconciled_trades: - self._log.error( + self._log.warning( f"Duplicate {trade_report.trade_id!r} detected: {trade_report}.", ) reconciled_trades.add(trade_report.trade_id) diff --git a/nautilus_trader/live/node.py b/nautilus_trader/live/node.py index 843912e7472b..a646ba816e12 100644 --- a/nautilus_trader/live/node.py +++ b/nautilus_trader/live/node.py @@ -21,7 +21,6 @@ from datetime import timedelta from nautilus_trader.cache.base import CacheFacade -from nautilus_trader.common.enums import LogColor from nautilus_trader.common.logging import Logger from nautilus_trader.config import TradingNodeConfig from nautilus_trader.core.correctness import PyCondition @@ -273,6 +272,89 @@ def run(self) -> None: except RuntimeError as e: self.kernel.log.exception("Error on run", e) + async def run_async(self) -> None: + """ + Start and run the trading node asynchronously. + """ + try: + if not self._is_built: + raise RuntimeError( + "The trading nodes clients have not been built. " + "Run `node.build()` prior to start.", + ) + + self._is_running = True + await self.kernel.start_async() + + if self.kernel.loop.is_running(): + self.kernel.log.info("RUNNING.") + else: + self.kernel.log.warning("Event loop is not running.") + + # Continue to run while engines are running... + tasks: list[asyncio.Task] = [ + self.kernel.data_engine.get_cmd_queue_task(), + self.kernel.data_engine.get_req_queue_task(), + self.kernel.data_engine.get_res_queue_task(), + self.kernel.data_engine.get_data_queue_task(), + self.kernel.risk_engine.get_cmd_queue_task(), + self.kernel.risk_engine.get_evt_queue_task(), + self.kernel.exec_engine.get_cmd_queue_task(), + self.kernel.exec_engine.get_evt_queue_task(), + ] + + if self._config.heartbeat_interval: + self._task_heartbeats = asyncio.create_task( + self.maintain_heartbeat(self._config.heartbeat_interval), + ) + if self._config.cache and self._config.cache.snapshot_positions_interval: + self._task_position_snapshots = asyncio.create_task( + self.snapshot_open_positions(self._config.cache.snapshot_positions_interval), + ) + + await asyncio.gather(*tasks) + except asyncio.CancelledError as e: + self.kernel.log.error(str(e)) + + async def maintain_heartbeat(self, interval: float) -> None: + """ + Maintain heartbeats at the given `interval` while the node is running. + + Parameters + ---------- + interval : float + The interval (seconds) between heartbeats. + + """ + try: + while True: + await asyncio.sleep(interval) + self.cache.heartbeat(self.kernel.clock.utc_now()) + except asyncio.CancelledError: + pass + + async def snapshot_open_positions(self, interval: float) -> None: + """ + Snapshot the state of all open positions at the configured interval. + + Parameters + ---------- + interval : float + The interval (seconds) between open position state snapshotting. + + """ + try: + while True: + await asyncio.sleep(interval) + open_positions = self.kernel.cache.positions_open() + for position in open_positions: + self.cache.snapshot_position_state( + position=position, + ts_snapshot=self.kernel.clock.timestamp_ns(), + ) + except asyncio.CancelledError: + pass + def stop(self) -> None: """ Stop the trading node gracefully. @@ -290,7 +372,32 @@ def stop(self) -> None: except RuntimeError as e: self.kernel.log.exception("Error on stop", e) - def dispose(self) -> None: # noqa: C901 + async def stop_async(self) -> None: + """ + Stop the trading node gracefully, asynchronously. + + After a specified delay the internal `Trader` residual state will be checked. + + If save strategy is configured, then strategy states will be saved. + + """ + self.kernel.log.info("STOPPING...") + + if self._task_heartbeats: + self.kernel.log.info("Cancelling `task_heartbeats` task...") + self._task_heartbeats.cancel() + self._task_heartbeats = None + + if self._task_position_snapshots: + self.kernel.log.info("Cancelling `task_position_snapshots` task...") + self._task_position_snapshots.cancel() + self._task_position_snapshots = None + + await self.kernel.stop_async() + + self._is_running = False + + def dispose(self) -> None: """ Dispose of the trading node. @@ -324,26 +431,7 @@ def dispose(self) -> None: # noqa: C901 self.kernel.log.debug(str(self.kernel.risk_engine.get_cmd_queue_task())) self.kernel.log.debug(str(self.kernel.risk_engine.get_evt_queue_task())) - if self.kernel.trader.is_running: - self.kernel.trader.stop() - if self.kernel.data_engine.is_running: - self.kernel.data_engine.stop() - if self.kernel.risk_engine.is_running: - self.kernel.risk_engine.stop() - if self.kernel.exec_engine.is_running: - self.kernel.exec_engine.stop() - if self.kernel.emulator.is_running: - self.kernel.emulator.stop() - - self.kernel.trader.dispose() - self.kernel.data_engine.dispose() - self.kernel.risk_engine.dispose() - self.kernel.exec_engine.dispose() - self.kernel.emulator.dispose() - - # Cleanup writer - if self.kernel.writer is not None: - self.kernel.writer.close() + self.kernel.dispose() if self.kernel.executor: self.kernel.log.info("Shutting down executor...") @@ -378,172 +466,3 @@ def dispose(self) -> None: # noqa: C901 def _loop_sig_handler(self, sig: signal.Signals) -> None: self.kernel.log.warning(f"Received {sig!s}, shutting down...") self.stop() - - async def maintain_heartbeat(self, interval: float) -> None: - """ - Maintain heartbeats at the given `interval` while the node is running. - - Parameters - ---------- - interval : float - The interval (seconds) between heartbeats. - - """ - try: - while True: - await asyncio.sleep(interval) - self.cache.heartbeat(self.kernel.clock.utc_now()) - except asyncio.CancelledError: - pass - - async def snapshot_open_positions(self, interval: float) -> None: - """ - Snapshot the state of all open positions at the configured interval. - - Parameters - ---------- - interval : float - The interval (seconds) between open position state snapshotting. - - """ - try: - while True: - await asyncio.sleep(interval) - open_positions = self.kernel.cache.positions_open() - for position in open_positions: - self.cache.snapshot_position_state(position) - except asyncio.CancelledError: - pass - - async def run_async(self) -> None: - """ - Start and run the trading node asynchronously. - """ - try: - if not self._is_built: - raise RuntimeError( - "The trading nodes clients have not been built. " - "Run `node.build()` prior to start.", - ) - - self._is_running = True - await self.kernel.start() - - if self.kernel.loop.is_running(): - self.kernel.log.info("RUNNING.") - else: - self.kernel.log.warning("Event loop is not running.") - - # Continue to run while engines are running... - tasks: list[asyncio.Task] = [ - self.kernel.data_engine.get_cmd_queue_task(), - self.kernel.data_engine.get_req_queue_task(), - self.kernel.data_engine.get_res_queue_task(), - self.kernel.data_engine.get_data_queue_task(), - self.kernel.risk_engine.get_cmd_queue_task(), - self.kernel.risk_engine.get_evt_queue_task(), - self.kernel.exec_engine.get_cmd_queue_task(), - self.kernel.exec_engine.get_evt_queue_task(), - ] - - if self._config.heartbeat_interval: - self._task_heartbeats = asyncio.create_task( - self.maintain_heartbeat(self._config.heartbeat_interval), - ) - if self._config.cache and self._config.cache.snapshot_positions_interval: - self._task_position_snapshots = asyncio.create_task( - self.snapshot_open_positions(self._config.cache.snapshot_positions_interval), - ) - - await asyncio.gather(*tasks) - except asyncio.CancelledError as e: - self.kernel.log.error(str(e)) - - async def stop_async(self) -> None: # noqa (too complex) - """ - Stop the trading node gracefully, asynchronously. - - After a specified delay the internal `Trader` residual state will be checked. - - If save strategy is configured, then strategy states will be saved. - - """ - self.kernel.log.info("STOPPING...") - - if self._task_heartbeats: - self.kernel.log.info("Cancelling `task_heartbeats` task...") - self._task_heartbeats.cancel() - self._task_heartbeats = None - - if self._task_position_snapshots: - self.kernel.log.info("Cancelling `task_position_snapshots` task...") - self._task_position_snapshots.cancel() - self._task_position_snapshots = None - - if self.kernel.trader.is_running: - self.kernel.trader.stop() - self.kernel.log.info( - f"Awaiting post stop ({self._config.timeout_post_stop}s timeout)...", - color=LogColor.BLUE, - ) - await asyncio.sleep(self._config.timeout_post_stop) - self.kernel.trader.check_residuals() - - if self.kernel.save_state: - self.kernel.trader.save() - - # Disconnect all clients - self.kernel.data_engine.disconnect() - self.kernel.exec_engine.disconnect() - - self.kernel.log.info( - f"Awaiting engine disconnections " - f"({self._config.timeout_disconnection}s timeout)...", - color=LogColor.BLUE, - ) - if not await self._await_engines_disconnected(): - self.kernel.log.error( - f"Timed out ({self._config.timeout_disconnection}s) waiting for engines to disconnect." - f"\nStatus" - f"\n------" - f"\nDataEngine.check_disconnected() == {self.kernel.data_engine.check_disconnected()}" - f"\nExecEngine.check_disconnected() == {self.kernel.exec_engine.check_disconnected()}", - ) - - if self.kernel.data_engine.is_running: - self.kernel.data_engine.stop() - if self.kernel.risk_engine.is_running: - self.kernel.risk_engine.stop() - if self.kernel.exec_engine.is_running: - self.kernel.exec_engine.stop() - if self.kernel.emulator.is_running: - self.kernel.emulator.stop() - - # Clean up remaining timers - timer_names = self.kernel.clock.timer_names - self.kernel.clock.cancel_timers() - - for name in timer_names: - self.kernel.log.info(f"Cancelled Timer(name={name}).") - - # Flush writer - if self.kernel.writer is not None: - self.kernel.writer.flush() - - self.kernel.log.info("STOPPED.") - self._is_running = False - - async def _await_engines_disconnected(self) -> bool: - seconds = self._config.timeout_disconnection - timeout: timedelta = self.kernel.clock.utc_now() + timedelta(seconds=seconds) - while True: - await asyncio.sleep(0) - if self.kernel.clock.utc_now() >= timeout: - return False - if not self.kernel.data_engine.check_disconnected(): - continue - if not self.kernel.exec_engine.check_disconnected(): - continue - break - - return True # Engines disconnected diff --git a/nautilus_trader/live/risk_engine.py b/nautilus_trader/live/risk_engine.py index b03642af9f15..6e6208845170 100644 --- a/nautilus_trader/live/risk_engine.py +++ b/nautilus_trader/live/risk_engine.py @@ -16,11 +16,11 @@ from __future__ import annotations import asyncio +from asyncio import Queue from nautilus_trader.cache.base import CacheFacade from nautilus_trader.common.clock import LiveClock from nautilus_trader.common.logging import Logger -from nautilus_trader.common.queue import Queue from nautilus_trader.config import LiveRiskEngineConfig from nautilus_trader.core.correctness import PyCondition from nautilus_trader.core.message import Command @@ -83,8 +83,8 @@ def __init__( ) self._loop: asyncio.AbstractEventLoop = loop - self._cmd_queue: Queue = Queue(maxsize=config.qsize) - self._evt_queue: Queue = Queue(maxsize=config.qsize) + self._cmd_queue: asyncio.Queue = Queue(maxsize=config.qsize) + self._evt_queue: asyncio.Queue = Queue(maxsize=config.qsize) # Async tasks self._cmd_queue_task: asyncio.Task | None = None @@ -167,8 +167,8 @@ def execute(self, command: Command) -> None: Warnings -------- - This method should only be called from the same thread the event loop is - running on. + This method is not thread-safe and should only be called from the same thread the event + loop is running on. Calling it from a different thread may lead to unexpected behavior. """ PyCondition.not_none(command, "command") @@ -178,8 +178,10 @@ def execute(self, command: Command) -> None: self._cmd_queue.put_nowait(command) except asyncio.QueueFull: self._log.warning( - f"Blocking on `_cmd_queue.put` as queue full at {self._cmd_queue.qsize()} items.", + f"Blocking on `_cmd_queue.put` as queue full " + f"at {self._cmd_queue.qsize():_} items.", ) + # Schedule the `put` operation to be executed once there is space in the queue self._loop.create_task(self._cmd_queue.put(command)) def process(self, event: Event) -> None: @@ -196,8 +198,8 @@ def process(self, event: Event) -> None: Warnings -------- - This method should only be called from the same thread the event loop is - running on. + This method is not thread-safe and should only be called from the same thread the event + loop is running on. Calling it from a different thread may lead to unexpected behavior. """ PyCondition.not_none(event, "event") @@ -207,9 +209,11 @@ def process(self, event: Event) -> None: self._evt_queue.put_nowait(event) except asyncio.QueueFull: self._log.warning( - f"Blocking on `_evt_queue.put` as queue full at {self._evt_queue.qsize()} items.", + f"Blocking on `_evt_queue.put` as queue full " + f"at {self._evt_queue.qsize():_} items.", ) - self._evt_queue.put(event) # Block until qsize reduces below maxsize + # Schedule the `put` operation to be executed once there is space in the queue + self._loop.create_task(self._evt_queue.put(event)) # -- INTERNAL ------------------------------------------------------------------------------------- diff --git a/nautilus_trader/model/data/bar.pyx b/nautilus_trader/model/data/bar.pyx index 0c465cba5109..bd713f0f24b9 100644 --- a/nautilus_trader/model/data/bar.pyx +++ b/nautilus_trader/model/data/bar.pyx @@ -14,6 +14,7 @@ # ------------------------------------------------------------------------------------------------- from cpython.datetime cimport timedelta +from libc.stdint cimport uint8_t from libc.stdint cimport uint64_t from nautilus_trader.core.correctness cimport Condition @@ -915,6 +916,49 @@ cdef class Bar(Data): """ return Bar.to_dict_c(obj) + @staticmethod + def from_pyo3(list pyo3_bars) -> list[Bar]: + """ + Return bars converted from the given pyo3 provided objects. + + Parameters + ---------- + pyo3_bars : list[RustBar] + The Rust pyo3 bars to convert from. + + Returns + ------- + list[Bar] + + """ + cdef list output = [] + + cdef BarType bar_type = None + cdef uint8_t price_prec = 0 + cdef uint8_t volume_prec = 0 + + cdef: + Bar bar + for pyo3_bar in pyo3_bars: + if bar_type is None: + bar_type = BarType.from_str_c(str(pyo3_bar.bar_type)) + price_prec = pyo3_bar.open.precision + volume_prec = pyo3_bar.volume.precision + + bar = Bar( + bar_type, + Price.from_raw_c(pyo3_bar.open.raw, price_prec), + Price.from_raw_c(pyo3_bar.high.raw, price_prec), + Price.from_raw_c(pyo3_bar.low.raw, price_prec), + Price.from_raw_c(pyo3_bar.close.raw, price_prec), + Quantity.from_raw_c(pyo3_bar.volume.raw, volume_prec), + pyo3_bar.ts_event, + pyo3_bar.ts_init, + ) + output.append(bar) + + return output + cpdef bint is_single_price(self): """ If the OHLC are all equal to a single price. diff --git a/nautilus_trader/model/data/book.pyx b/nautilus_trader/model/data/book.pyx index 0d0ba0f852f9..f47e469eb44b 100644 --- a/nautilus_trader/model/data/book.pyx +++ b/nautilus_trader/model/data/book.pyx @@ -81,6 +81,26 @@ cdef class BookOrder: order_id, ) + def __getstate__(self): + return ( + self._mem.side, + self._mem.price.raw, + self._mem.price.precision, + self._mem.size.raw , + self._mem.size.precision, + self._mem.order_id, + ) + + def __setstate__(self, state): + self._mem = book_order_from_raw( + state[0], + state[1], + state[2], + state[3], + state[4], + state[5], + ) + def __eq__(self, BookOrder other) -> bool: return book_order_eq(&self._mem, &other._mem) @@ -233,10 +253,10 @@ cdef class OrderBookDelta(Data): The UNIX timestamp (nanoseconds) when the data event occurred. ts_init : uint64_t The UNIX timestamp (nanoseconds) when the data object was initialized. - sequence : uint64_t, default 0 - The unique sequence number for the update. If default 0 then will increment the `sequence`. flags : uint8_t, default 0 (no flags) A combination of packet end with matching engine status. + sequence : uint64_t, default 0 + The unique sequence number for the update. If default 0 then will increment the `sequence`. """ def __init__( @@ -246,8 +266,8 @@ cdef class OrderBookDelta(Data): BookOrder order, uint64_t ts_event, uint64_t ts_init, - uint64_t sequence=0, uint8_t flags=0, + uint64_t sequence=0, ): # Placeholder for now cdef BookOrder_t book_order = order._mem if order is not None else book_order_from_raw( @@ -268,6 +288,42 @@ cdef class OrderBookDelta(Data): ts_init, ) + def __getstate__(self): + return ( + self.instrument_id.value, + self._mem.action, + self._mem.order.side, + self._mem.order.price.raw, + self._mem.order.price.precision, + self._mem.order.size.raw , + self._mem.order.size.precision, + self._mem.order.order_id, + self._mem.flags, + self._mem.sequence, + self._mem.ts_event, + self._mem.ts_init, + ) + + def __setstate__(self, state): + cdef InstrumentId instrument_id = InstrumentId.from_str_c(state[0]) + cdef BookOrder_t book_order = book_order_from_raw( + state[2], + state[3], + state[4], + state[5], + state[6], + state[7], + ) + self._mem = orderbook_delta_new( + instrument_id._mem, + state[1], + book_order, + state[8], + state[9], + state[10], + state[11], + ) + def __eq__(self, OrderBookDelta other) -> bool: return orderbook_delta_eq(&self._mem, &other._mem) @@ -280,10 +336,10 @@ cdef class OrderBookDelta(Data): f"instrument_id={self.instrument_id}, " f"action={book_action_to_str(self.action)}, " f"order={self.order}, " - f"ts_event={self.ts_event}, " - f"ts_init={self.ts_init}, " - f"sequence={self.sequence}, " - f"flags={self.flags})" + f"flags={self._mem.flags}, " + f"sequence={self._mem.sequence}, " + f"ts_event={self._mem.ts_event}, " + f"ts_init={self._mem.ts_init})" ) @property @@ -509,6 +565,56 @@ cdef class OrderBookDelta(Data): """ return OrderBookDelta.clear_c(instrument_id, ts_event, ts_init, sequence) + @staticmethod + def from_pyo3(list pyo3_deltas) -> list[OrderBookDelta]: + """ + Return order book deltas converted from the given pyo3 provided objects. + + Parameters + ---------- + pyo3_deltas : list[RustOrderBookDelta] + The Rust pyo3 order book deltas to convert from. + + Returns + ------- + list[OrderBookDelta] + + """ + cdef list output = [] + + cdef InstrumentId instrument_id = None + cdef uint8_t price_prec = 0 + cdef uint8_t size_prec = 0 + + cdef: + OrderBookDelta delta + BookOrder book_order + for pyo3_delta in pyo3_deltas: + if instrument_id is None: + instrument_id = InstrumentId.from_str_c(pyo3_delta.instrument_id.value) + price_prec = pyo3_delta.order.price.precision + size_prec = pyo3_delta.order.size.precision + + book_order = BookOrder( + pyo3_delta.order.side.value, + Price.from_raw_c(pyo3_delta.order.price.raw, price_prec), + Quantity.from_raw_c(pyo3_delta.order.size.raw, size_prec), + pyo3_delta.order.order_id, + ) + + delta = OrderBookDelta( + instrument_id, + pyo3_delta.action.value, + book_order, + pyo3_delta.ts_event, + pyo3_delta.ts_init, + pyo3_delta.sequence, + pyo3_delta.flags, + ) + output.append(delta) + + return output + cdef class OrderBookDeltas(Data): """ diff --git a/nautilus_trader/model/data/tick.pxd b/nautilus_trader/model/data/tick.pxd index 72ac7f0c17cc..2f81cb4e9261 100644 --- a/nautilus_trader/model/data/tick.pxd +++ b/nautilus_trader/model/data/tick.pxd @@ -48,12 +48,12 @@ cdef class QuoteTick(Data): @staticmethod cdef QuoteTick from_raw_c( InstrumentId instrument_id, - int64_t raw_bid, - int64_t raw_ask, + int64_t bid_price_raw, + int64_t ask_price_raw, uint8_t bid_price_prec, uint8_t ask_price_prec, - uint64_t raw_bid_size, - uint64_t raw_ask_size, + uint64_t bid_size_raw, + uint64_t ask_size_raw, uint8_t bid_size_prec, uint8_t ask_size_prec, uint64_t ts_event, @@ -87,9 +87,9 @@ cdef class TradeTick(Data): @staticmethod cdef TradeTick from_raw_c( InstrumentId instrument_id, - int64_t raw_price, + int64_t price_raw, uint8_t price_prec, - uint64_t raw_size, + uint64_t size_raw, uint8_t size_prec, AggressorSide aggressor_side, TradeId trade_id, diff --git a/nautilus_trader/model/data/tick.pyx b/nautilus_trader/model/data/tick.pyx index a8c1b1e5c3dc..81e639accf1a 100644 --- a/nautilus_trader/model/data/tick.pyx +++ b/nautilus_trader/model/data/tick.pyx @@ -13,6 +13,9 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from nautilus_trader.core.nautilus_pyo3.model import QuoteTick as RustQuoteTick +from nautilus_trader.core.nautilus_pyo3.model import TradeTick as RustTradeTick + from libc.stdint cimport int64_t from libc.stdint cimport uint8_t from libc.stdint cimport uint64_t @@ -56,9 +59,9 @@ cdef class QuoteTick(Data): ---------- instrument_id : InstrumentId The quotes instrument ID. - bid : Price + bid_price : Price The top of book bid price. - ask : Price + ask_price : Price The top of book ask price. bid_size : Quantity The top of book bid size. @@ -80,22 +83,22 @@ cdef class QuoteTick(Data): def __init__( self, InstrumentId instrument_id not None, - Price bid not None, - Price ask not None, + Price bid_price not None, + Price ask_price not None, Quantity bid_size not None, Quantity ask_size not None, uint64_t ts_event, uint64_t ts_init, ): - Condition.equal(bid._mem.precision, ask._mem.precision, "bid.precision", "ask.precision") + Condition.equal(bid_price._mem.precision, ask_price._mem.precision, "bid_price.precision", "ask_price.precision") Condition.equal(bid_size._mem.precision, ask_size._mem.precision, "bid_size.precision", "ask_size.precision") self._mem = quote_tick_new( instrument_id._mem, - bid._mem.raw, - ask._mem.raw, - bid._mem.precision, - ask._mem.precision, + bid_price._mem.raw, + ask_price._mem.raw, + bid_price._mem.precision, + ask_price._mem.precision, bid_size._mem.raw, ask_size._mem.raw, bid_size._mem.precision, @@ -107,10 +110,10 @@ cdef class QuoteTick(Data): def __getstate__(self): return ( self.instrument_id.value, - self._mem.bid.raw, - self._mem.ask.raw, - self._mem.bid.precision, - self._mem.ask.precision, + self._mem.bid_price.raw, + self._mem.ask_price.raw, + self._mem.bid_price.precision, + self._mem.ask_price.precision, self._mem.bid_size.raw, self._mem.ask_size.raw, self._mem.bid_size.precision, @@ -163,7 +166,7 @@ cdef class QuoteTick(Data): return InstrumentId.from_mem_c(self._mem.instrument_id) @property - def bid(self) -> Price: + def bid_price(self) -> Price: """ Return the top of book bid price. @@ -172,10 +175,10 @@ cdef class QuoteTick(Data): Price """ - return Price.from_raw_c(self._mem.bid.raw, self._mem.bid.precision) + return Price.from_raw_c(self._mem.bid_price.raw, self._mem.bid_price.precision) @property - def ask(self) -> Price: + def ask_price(self) -> Price: """ Return the top of book ask price. @@ -184,7 +187,7 @@ cdef class QuoteTick(Data): Price """ - return Price.from_raw_c(self._mem.ask.raw, self._mem.ask.precision) + return Price.from_raw_c(self._mem.ask_price.raw, self._mem.ask_price.precision) @property def bid_size(self) -> Quantity: @@ -239,8 +242,8 @@ cdef class QuoteTick(Data): Condition.not_none(values, "values") return QuoteTick( instrument_id=InstrumentId.from_str_c(values["instrument_id"]), - bid=Price.from_str_c(values["bid"]), - ask=Price.from_str_c(values["ask"]), + bid_price=Price.from_str_c(values["bid_price"]), + ask_price=Price.from_str_c(values["ask_price"]), bid_size=Quantity.from_str_c(values["bid_size"]), ask_size=Quantity.from_str_c(values["ask_size"]), ts_event=values["ts_event"], @@ -253,8 +256,8 @@ cdef class QuoteTick(Data): return { "type": type(obj).__name__, "instrument_id": str(obj.instrument_id), - "bid": str(obj.bid), - "ask": str(obj.ask), + "bid_price": str(obj.bid_price), + "ask_price": str(obj.ask_price), "bid_size": str(obj.bid_size), "ask_size": str(obj.ask_size), "ts_event": obj.ts_event, @@ -264,12 +267,12 @@ cdef class QuoteTick(Data): @staticmethod cdef QuoteTick from_raw_c( InstrumentId instrument_id, - int64_t raw_bid, - int64_t raw_ask, + int64_t bid_price_raw, + int64_t ask_price_raw, uint8_t bid_price_prec, uint8_t ask_price_prec, - uint64_t raw_bid_size, - uint64_t raw_ask_size, + uint64_t bid_size_raw, + uint64_t ask_size_raw, uint8_t bid_size_prec, uint8_t ask_size_prec, uint64_t ts_event, @@ -278,12 +281,12 @@ cdef class QuoteTick(Data): cdef QuoteTick tick = QuoteTick.__new__(QuoteTick) tick._mem = quote_tick_new( instrument_id._mem, - raw_bid, - raw_ask, + bid_price_raw, + ask_price_raw, bid_price_prec, ask_price_prec, - raw_bid_size, - raw_ask_size, + bid_size_raw, + ask_size_raw, bid_size_prec, ask_size_prec, ts_event, @@ -343,12 +346,12 @@ cdef class QuoteTick(Data): @staticmethod def from_raw( InstrumentId instrument_id, - int64_t raw_bid, - int64_t raw_ask, + int64_t bid_price_raw, + int64_t ask_price_raw, uint8_t bid_price_prec, uint8_t ask_price_prec, - uint64_t raw_bid_size, - uint64_t raw_ask_size, + uint64_t bid_size_raw , + uint64_t ask_size_raw, uint8_t bid_size_prec, uint8_t ask_size_prec, uint64_t ts_event, @@ -361,17 +364,17 @@ cdef class QuoteTick(Data): ---------- instrument_id : InstrumentId The quotes instrument ID. - raw_bid : int64_t + bid_price_raw : int64_t The raw top of book bid price (as a scaled fixed precision integer). - raw_ask : int64_t + ask_price_raw : int64_t The raw top of book ask price (as a scaled fixed precision integer). bid_price_prec : uint8_t The bid price precision. ask_price_prec : uint8_t The ask price precision. - raw_bid_size : Quantity + bid_size_raw : uint64_t The raw top of book bid size (as a scaled fixed precision integer). - raw_ask_size : Quantity + ask_size_raw : uint64_t The raw top of book ask size (as a scaled fixed precision integer). bid_size_prec : uint8_t The bid size precision. @@ -399,12 +402,12 @@ cdef class QuoteTick(Data): return QuoteTick.from_raw_c( instrument_id, - raw_bid, - raw_ask, + bid_price_raw, + ask_price_raw, bid_price_prec, ask_price_prec, - raw_bid_size, - raw_ask_size, + bid_size_raw, + ask_size_raw, bid_size_prec, ask_size_prec, ts_event, @@ -440,6 +443,56 @@ cdef class QuoteTick(Data): """ return QuoteTick.to_dict_c(obj) + @staticmethod + def from_pyo3(list pyo3_ticks) -> list[QuoteTick]: + """ + Return quote ticks converted from the given pyo3 provided objects. + + Parameters + ---------- + pyo3_ticks : list[RustQuoteTick] + The Rust pyo3 quote ticks to convert from. + + Returns + ------- + list[QuoteTick] + + """ + cdef list output = [] + + cdef InstrumentId instrument_id = None + cdef uint8_t bid_prec = 0 + cdef uint8_t ask_prec = 0 + cdef uint8_t bid_size_prec = 0 + cdef uint8_t ask_size_prec = 0 + + cdef: + QuoteTick tick + for pyo3_tick in pyo3_ticks: + if instrument_id is None: + instrument_id = InstrumentId.from_str_c(pyo3_tick.instrument_id.value) + bid_prec = pyo3_tick.bid_price.precision + ask_prec = pyo3_tick.ask_price.precision + bid_size_prec = pyo3_tick.bid_size.precision + ask_size_prec = pyo3_tick.ask_size.precision + + tick = QuoteTick.from_raw_c( + instrument_id, + pyo3_tick.bid_price.raw, + pyo3_tick.ask_price.raw, + bid_prec, + ask_prec, + pyo3_tick.bid_size.raw, + pyo3_tick.ask_size.raw, + bid_size_prec, + ask_size_prec, + pyo3_tick.ts_event, + pyo3_tick.ts_init, + ) + output.append(tick) + + return output + cpdef Price extract_price(self, PriceType price_type): """ Extract the price for the given price type. @@ -455,11 +508,11 @@ cdef class QuoteTick(Data): """ if price_type == PriceType.MID: - return Price.from_raw_c(((self._mem.bid.raw + self._mem.ask.raw) / 2), self._mem.bid.precision + 1) + return Price.from_raw_c(((self._mem.bid_price.raw + self._mem.ask_price.raw) / 2), self._mem.bid_price.precision + 1) elif price_type == PriceType.BID: - return self.bid + return self.bid_price elif price_type == PriceType.ASK: - return self.ask + return self.ask_price else: raise ValueError(f"Cannot extract with PriceType {price_type_to_str(price_type)}") @@ -668,9 +721,9 @@ cdef class TradeTick(Data): @staticmethod cdef TradeTick from_raw_c( InstrumentId instrument_id, - int64_t raw_price, + int64_t price_raw, uint8_t price_prec, - uint64_t raw_size, + uint64_t size_raw, uint8_t size_prec, AggressorSide aggressor_side, TradeId trade_id, @@ -680,9 +733,9 @@ cdef class TradeTick(Data): cdef TradeTick tick = TradeTick.__new__(TradeTick) tick._mem = trade_tick_new( instrument_id._mem, - raw_price, + price_raw, price_prec, - raw_size, + size_raw, size_prec, aggressor_side, trade_id._mem, @@ -770,9 +823,9 @@ cdef class TradeTick(Data): @staticmethod def from_raw( InstrumentId instrument_id, - int64_t raw_price, + int64_t price_raw, uint8_t price_prec, - uint64_t raw_size, + uint64_t size_raw, uint8_t size_prec, AggressorSide aggressor_side, TradeId trade_id, @@ -786,11 +839,11 @@ cdef class TradeTick(Data): ---------- instrument_id : InstrumentId The trade instrument ID. - raw_price : int64_t + price_raw : int64_t The traded raw price (as a scaled fixed precision integer). price_prec : uint8_t The traded price precision. - raw_size : uint64_t + size_raw : uint64_t The traded raw size (as a scaled fixed precision integer). size_prec : uint8_t The traded size precision. @@ -810,9 +863,9 @@ cdef class TradeTick(Data): """ return TradeTick.from_raw_c( instrument_id, - raw_price, + price_raw, price_prec, - raw_size, + size_raw, size_prec, aggressor_side, trade_id, @@ -848,3 +901,47 @@ cdef class TradeTick(Data): """ return TradeTick.to_dict_c(obj) + + @staticmethod + def from_pyo3(list pyo3_ticks) -> list[TradeTick]: + """ + Return trade ticks converted from the given pyo3 provided objects. + + Parameters + ---------- + pyo3_ticks : list[RustTradeTick] + The Rust pyo3 trade ticks to convert from. + + Returns + ------- + list[TradeTick] + + """ + cdef list output = [] + + cdef InstrumentId instrument_id = None + cdef uint8_t price_prec = 0 + cdef uint8_t size_prec = 0 + + cdef: + TradeTick tick + for pyo3_tick in pyo3_ticks: + if instrument_id is None: + instrument_id = InstrumentId.from_str_c(pyo3_tick.instrument_id.value) + price_prec = pyo3_tick.price.precision + size_prec = pyo3_tick.price.precision + + tick = TradeTick.from_raw_c( + instrument_id, + pyo3_tick.price.raw, + price_prec, + pyo3_tick.size.raw, + size_prec, + pyo3_tick.aggressor_side.value, + TradeId(pyo3_tick.trade_id.value), + pyo3_tick.ts_event, + pyo3_tick.ts_init, + ) + output.append(tick) + + return output diff --git a/nautilus_trader/model/events/__init__.py b/nautilus_trader/model/events/__init__.py index 6e813acf48f5..c12c3cf95d70 100644 --- a/nautilus_trader/model/events/__init__.py +++ b/nautilus_trader/model/events/__init__.py @@ -21,6 +21,7 @@ from nautilus_trader.model.events.order import OrderCanceled from nautilus_trader.model.events.order import OrderCancelRejected from nautilus_trader.model.events.order import OrderDenied +from nautilus_trader.model.events.order import OrderEmulated from nautilus_trader.model.events.order import OrderEvent from nautilus_trader.model.events.order import OrderExpired from nautilus_trader.model.events.order import OrderFilled @@ -29,6 +30,7 @@ from nautilus_trader.model.events.order import OrderPendingCancel from nautilus_trader.model.events.order import OrderPendingUpdate from nautilus_trader.model.events.order import OrderRejected +from nautilus_trader.model.events.order import OrderReleased from nautilus_trader.model.events.order import OrderSubmitted from nautilus_trader.model.events.order import OrderTriggered from nautilus_trader.model.events.order import OrderUpdated @@ -44,6 +46,7 @@ "OrderCanceled", "OrderCancelRejected", "OrderDenied", + "OrderEmulated", "OrderEvent", "OrderExpired", "OrderFilled", @@ -52,6 +55,7 @@ "OrderPendingCancel", "OrderPendingUpdate", "OrderRejected", + "OrderReleased", "OrderSubmitted", "OrderTriggered", "OrderUpdated", diff --git a/nautilus_trader/model/events/account.pxd b/nautilus_trader/model/events/account.pxd index 9c5380f47240..1ce1562796c1 100644 --- a/nautilus_trader/model/events/account.pxd +++ b/nautilus_trader/model/events/account.pxd @@ -13,13 +13,20 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from libc.stdint cimport uint64_t + from nautilus_trader.core.message cimport Event +from nautilus_trader.core.uuid cimport UUID4 from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.enums_c cimport AccountType from nautilus_trader.model.identifiers cimport AccountId cdef class AccountState(Event): + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init + cdef readonly AccountId account_id """The account ID associated with the event.\n\n:returns: `AccountId`""" cdef readonly AccountType account_type diff --git a/nautilus_trader/model/events/account.pyx b/nautilus_trader/model/events/account.pyx index ff036f09b533..55ecb9843728 100644 --- a/nautilus_trader/model/events/account.pyx +++ b/nautilus_trader/model/events/account.pyx @@ -76,7 +76,6 @@ cdef class AccountState(Event): uint64_t ts_init, ): Condition.not_empty(balances, "balances") - super().__init__(event_id, ts_event, ts_init) self.account_id = account_id self.account_type = account_type @@ -86,6 +85,16 @@ cdef class AccountState(Event): self.is_reported = reported self.info = info + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init + + def __eq__(self, Event other) -> bool: + return self._event_id == other.id + + def __hash__(self) -> int: + return hash(self._event_id) + def __repr__(self) -> str: return ( f"{type(self).__name__}(" @@ -95,9 +104,46 @@ cdef class AccountState(Event): f"is_reported={self.is_reported}, " f"balances=[{', '.join([str(b) for b in self.balances])}], " f"margins=[{', '.join([str(m) for m in self.margins])}], " - f"event_id={self.id.to_str()})" + f"event_id={self._event_id.to_str()})" ) + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init + + @staticmethod cdef AccountState from_dict_c(dict values): Condition.not_none(values, "values") @@ -127,9 +173,9 @@ cdef class AccountState(Event): "margins": msgspec.json.encode([m.to_dict() for m in obj.margins]), "reported": obj.is_reported, "info": msgspec.json.encode(obj.info), - "event_id": obj.id.to_str(), - "ts_event": obj.ts_event, - "ts_init": obj.ts_init, + "event_id": obj._event_id.to_str(), + "ts_event": obj._ts_event, + "ts_init": obj._ts_init, } @staticmethod diff --git a/nautilus_trader/model/events/order.pxd b/nautilus_trader/model/events/order.pxd index 5dd8f076e48e..c7f116343535 100644 --- a/nautilus_trader/model/events/order.pxd +++ b/nautilus_trader/model/events/order.pxd @@ -13,8 +13,16 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from libc.stdint cimport uint64_t + from nautilus_trader.core.message cimport Event +from nautilus_trader.core.rust.model cimport OrderAccepted_t from nautilus_trader.core.rust.model cimport OrderDenied_t +from nautilus_trader.core.rust.model cimport OrderEmulated_t +from nautilus_trader.core.rust.model cimport OrderRejected_t +from nautilus_trader.core.rust.model cimport OrderReleased_t +from nautilus_trader.core.rust.model cimport OrderSubmitted_t +from nautilus_trader.core.uuid cimport UUID4 from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.enums_c cimport ContingencyType from nautilus_trader.model.enums_c cimport LiquiditySide @@ -38,23 +46,19 @@ from nautilus_trader.model.objects cimport Quantity cdef class OrderEvent(Event): - cdef readonly TraderId trader_id - """The trader ID associated with the event.\n\n:returns: `TraderId`""" - cdef readonly StrategyId strategy_id - """The strategy ID associated with the event.\n\n:returns: `StrategyId`""" - cdef readonly InstrumentId instrument_id - """The instrument ID associated with the event.\n\n:returns: `InstrumentId`""" - cdef readonly ClientOrderId client_order_id - """The client order ID associated with the event.\n\n:returns: `ClientOrderId`""" - cdef readonly VenueOrderId venue_order_id - """The venue order ID associated with the event.\n\n:returns: `VenueOrderId` or ``None``""" - cdef readonly AccountId account_id - """The account ID associated with the event.\n\n:returns: `AccountId` or ``None``""" - cdef readonly bint reconciliation - """If the event was generated during reconciliation.\n\n:returns: `bool`""" + pass # Abstract base class cdef class OrderInitialized(OrderEvent): + cdef TraderId _trader_id + cdef StrategyId _strategy_id + cdef InstrumentId _instrument_id + cdef ClientOrderId _client_order_id + cdef bint _reconciliation + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init + cdef readonly OrderSide side """The order side.\n\n:returns: `OrderSide`""" cdef readonly OrderType order_type @@ -110,7 +114,28 @@ cdef class OrderDenied(OrderEvent): +cdef class OrderEmulated(OrderEvent): + cdef OrderEmulated_t _mem + + @staticmethod + cdef OrderEmulated from_dict_c(dict values) + + @staticmethod + cdef dict to_dict_c(OrderEmulated obj) + + +cdef class OrderReleased(OrderEvent): + cdef OrderReleased_t _mem + + @staticmethod + cdef OrderReleased from_dict_c(dict values) + + @staticmethod + cdef dict to_dict_c(OrderReleased obj) + + cdef class OrderSubmitted(OrderEvent): + cdef OrderSubmitted_t _mem @staticmethod cdef OrderSubmitted from_dict_c(dict values) @@ -120,6 +145,7 @@ cdef class OrderSubmitted(OrderEvent): cdef class OrderAccepted(OrderEvent): + cdef OrderAccepted_t _mem @staticmethod cdef OrderAccepted from_dict_c(dict values) @@ -129,8 +155,7 @@ cdef class OrderAccepted(OrderEvent): cdef class OrderRejected(OrderEvent): - cdef readonly str reason - """The reason the order was rejected.\n\n:returns: `str`""" + cdef OrderRejected_t _mem @staticmethod cdef OrderRejected from_dict_c(dict values) @@ -140,6 +165,16 @@ cdef class OrderRejected(OrderEvent): cdef class OrderCanceled(OrderEvent): + cdef TraderId _trader_id + cdef StrategyId _strategy_id + cdef InstrumentId _instrument_id + cdef ClientOrderId _client_order_id + cdef VenueOrderId _venue_order_id + cdef AccountId _account_id + cdef bint _reconciliation + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init @staticmethod cdef OrderCanceled from_dict_c(dict values) @@ -149,6 +184,16 @@ cdef class OrderCanceled(OrderEvent): cdef class OrderExpired(OrderEvent): + cdef TraderId _trader_id + cdef StrategyId _strategy_id + cdef InstrumentId _instrument_id + cdef ClientOrderId _client_order_id + cdef VenueOrderId _venue_order_id + cdef AccountId _account_id + cdef bint _reconciliation + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init @staticmethod cdef OrderExpired from_dict_c(dict values) @@ -158,6 +203,16 @@ cdef class OrderExpired(OrderEvent): cdef class OrderTriggered(OrderEvent): + cdef TraderId _trader_id + cdef StrategyId _strategy_id + cdef InstrumentId _instrument_id + cdef ClientOrderId _client_order_id + cdef VenueOrderId _venue_order_id + cdef AccountId _account_id + cdef bint _reconciliation + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init @staticmethod cdef OrderTriggered from_dict_c(dict values) @@ -167,6 +222,16 @@ cdef class OrderTriggered(OrderEvent): cdef class OrderPendingUpdate(OrderEvent): + cdef TraderId _trader_id + cdef StrategyId _strategy_id + cdef InstrumentId _instrument_id + cdef ClientOrderId _client_order_id + cdef VenueOrderId _venue_order_id + cdef AccountId _account_id + cdef bint _reconciliation + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init @staticmethod cdef OrderPendingUpdate from_dict_c(dict values) @@ -176,6 +241,16 @@ cdef class OrderPendingUpdate(OrderEvent): cdef class OrderPendingCancel(OrderEvent): + cdef TraderId _trader_id + cdef StrategyId _strategy_id + cdef InstrumentId _instrument_id + cdef ClientOrderId _client_order_id + cdef VenueOrderId _venue_order_id + cdef AccountId _account_id + cdef bint _reconciliation + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init @staticmethod cdef OrderPendingCancel from_dict_c(dict values) @@ -185,8 +260,17 @@ cdef class OrderPendingCancel(OrderEvent): cdef class OrderModifyRejected(OrderEvent): - cdef readonly str reason - """The reason for modify order rejection.\n\n:returns: `str`""" + cdef TraderId _trader_id + cdef StrategyId _strategy_id + cdef InstrumentId _instrument_id + cdef ClientOrderId _client_order_id + cdef VenueOrderId _venue_order_id + cdef AccountId _account_id + cdef str _reason + cdef bint _reconciliation + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init @staticmethod cdef OrderModifyRejected from_dict_c(dict values) @@ -196,8 +280,17 @@ cdef class OrderModifyRejected(OrderEvent): cdef class OrderCancelRejected(OrderEvent): - cdef readonly str reason - """The reason for order cancel rejection.\n\n:returns: `str`""" + cdef TraderId _trader_id + cdef StrategyId _strategy_id + cdef InstrumentId _instrument_id + cdef ClientOrderId _client_order_id + cdef VenueOrderId _venue_order_id + cdef AccountId _account_id + cdef str _reason + cdef bint _reconciliation + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init @staticmethod cdef OrderCancelRejected from_dict_c(dict values) @@ -207,6 +300,17 @@ cdef class OrderCancelRejected(OrderEvent): cdef class OrderUpdated(OrderEvent): + cdef TraderId _trader_id + cdef StrategyId _strategy_id + cdef InstrumentId _instrument_id + cdef ClientOrderId _client_order_id + cdef VenueOrderId _venue_order_id + cdef AccountId _account_id + cdef bint _reconciliation + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init + cdef readonly Quantity quantity """The orders current quantity.\n\n:returns: `Quantity`""" cdef readonly Price price @@ -222,6 +326,17 @@ cdef class OrderUpdated(OrderEvent): cdef class OrderFilled(OrderEvent): + cdef TraderId _trader_id + cdef StrategyId _strategy_id + cdef InstrumentId _instrument_id + cdef ClientOrderId _client_order_id + cdef VenueOrderId _venue_order_id + cdef AccountId _account_id + cdef bint _reconciliation + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init + cdef readonly TradeId trade_id """The trade match ID (assigned by the venue).\n\n:returns: `TradeId`""" cdef readonly PositionId position_id diff --git a/nautilus_trader/model/events/order.pyx b/nautilus_trader/model/events/order.pyx index bf2a0faa0cf9..eed0fdfb520a 100644 --- a/nautilus_trader/model/events/order.pyx +++ b/nautilus_trader/model/events/order.pyx @@ -13,6 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from __future__ import annotations + import json from typing import Any, Optional @@ -22,13 +24,18 @@ from libc.stdint cimport uint64_t from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.message cimport Event +from nautilus_trader.core.rust.model cimport order_accepted_new from nautilus_trader.core.rust.model cimport order_denied_new -from nautilus_trader.core.rust.model cimport order_denied_reason_to_cstr +from nautilus_trader.core.rust.model cimport order_emulated_new +from nautilus_trader.core.rust.model cimport order_rejected_new +from nautilus_trader.core.rust.model cimport order_released_new +from nautilus_trader.core.rust.model cimport order_submitted_new from nautilus_trader.core.rust.model cimport strategy_id_new from nautilus_trader.core.rust.model cimport trader_id_new from nautilus_trader.core.string cimport cstr_to_pybytes from nautilus_trader.core.string cimport cstr_to_pystr from nautilus_trader.core.string cimport pystr_to_cstr +from nautilus_trader.core.string cimport ustr_to_pystr from nautilus_trader.core.uuid cimport UUID4 from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.enums_c cimport ContingencyType @@ -61,58 +68,135 @@ from nautilus_trader.model.objects cimport Quantity cdef class OrderEvent(Event): """ - The base class for all order events. - - Parameters - ---------- - trader_id : TraderId - The trader ID. - strategy_id : StrategyId - The strategy ID. - instrument_id : InstrumentId - The instrument ID. - client_order_id : ClientOrderId - The client order ID. - venue_order_id : VenueOrderId, optional with no default so ``None`` must be passed explicitly - The venue order ID (assigned by the venue). - account_id : AccountId, optional with no default so ``None`` must be passed explicitly - The account ID (with the venue). - event_id : UUID4 - The event ID. - ts_event : uint64_t - The UNIX timestamp (nanoseconds) when the order event occurred. - ts_init : uint64_t - The UNIX timestamp (nanoseconds) when the object was initialized. - reconciliation : bool - If the event was generated during reconciliation. + The abstract base class for all order events. Warnings -------- This class should not be used directly, but through a concrete subclass. """ - def __init__( - self, - TraderId trader_id not None, - StrategyId strategy_id not None, - InstrumentId instrument_id not None, - ClientOrderId client_order_id not None, - VenueOrderId venue_order_id: Optional[VenueOrderId], - AccountId account_id: Optional[AccountId], - UUID4 event_id not None, - uint64_t ts_event, - uint64_t ts_init, - bint reconciliation, - ): - super().__init__(event_id, ts_event, ts_init) + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + raise NotImplementedError("abstract property must be implemented") + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + raise NotImplementedError("abstract property must be implemented") + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + raise NotImplementedError("abstract property must be implemented") + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + raise NotImplementedError("abstract property must be implemented") + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + raise NotImplementedError("abstract property must be implemented") + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + raise NotImplementedError("abstract property must be implemented") + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + raise NotImplementedError("abstract property must be implemented") + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + raise NotImplementedError("abstract property must be implemented") + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + raise NotImplementedError("abstract property must be implemented") + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + raise NotImplementedError("abstract property must be implemented") - self.trader_id = trader_id - self.strategy_id = strategy_id - self.instrument_id = instrument_id - self.client_order_id = client_order_id - self.venue_order_id = venue_order_id - self.account_id = account_id - self.reconciliation = reconciliation + def set_client_order_id(self, ClientOrderId client_order_id): + raise NotImplementedError("abstract method must be implemented") cdef class OrderInitialized(OrderEvent): @@ -183,6 +267,8 @@ cdef class OrderInitialized(OrderEvent): ------ ValueError If `order_side` is ``NO_ORDER_SIDE``. + ValueError + If `exec_algorithm_id` is not ``None``, and `exec_spawn_id` is ``None``. """ def __init__( @@ -214,19 +300,17 @@ cdef class OrderInitialized(OrderEvent): bint reconciliation=False, ): Condition.not_equal(order_side, OrderSide.NO_ORDER_SIDE, "order_side", "NONE") - - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - None, # Pending assignment by venue - None, # Pending assignment by system - event_id, - ts_init, # Timestamp identical to ts_init - ts_init, - reconciliation, - ) + if exec_algorithm_id is not None: + Condition.not_none(exec_spawn_id, "exec_spawn_id") + + self._trader_id = trader_id + self._strategy_id = strategy_id + self._instrument_id = instrument_id + self._client_order_id = client_order_id + self._event_id = event_id + self._ts_event = ts_init # Timestamp identical to ts_init + self._ts_init = ts_init + self._reconciliation = reconciliation self.side = order_side self.order_type = order_type @@ -247,14 +331,20 @@ cdef class OrderInitialized(OrderEvent): self.exec_spawn_id = exec_spawn_id self.tags = tags + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) + def __str__(self) -> str: cdef ClientOrderId o cdef str linked_order_ids = "None" if self.linked_order_ids: - linked_order_ids = str([o.to_str() for o in self.linked_order_ids]) + linked_order_ids = str([o.value for o in self.linked_order_ids]) return ( f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " + f"instrument_id={self.instrument_id}, " f"client_order_id={self.client_order_id}, " f"side={order_side_to_str(self.side)}, " f"type={order_type_to_str(self.order_type)}, " @@ -265,14 +355,14 @@ cdef class OrderInitialized(OrderEvent): f"quote_quantity={self.quote_quantity}, " f"options={self.options}, " f"emulation_trigger={trigger_type_to_str(self.emulation_trigger)}, " - f"trigger_instrument_id={self.trigger_instrument_id}, " # Can be None + f"trigger_instrument_id={self.trigger_instrument_id}, " f"contingency_type={contingency_type_to_str(self.contingency_type)}, " - f"order_list_id={self.order_list_id}, " # Can be None + f"order_list_id={self.order_list_id}, " f"linked_order_ids={linked_order_ids}, " - f"parent_order_id={self.parent_order_id}, " # Can be None - f"exec_algorithm_id={self.exec_algorithm_id}, " # Can be None - f"exec_algorithm_params={self.exec_algorithm_params}, " # Can be None - f"exec_spawn_id={self.exec_spawn_id}, " # Can be None + f"parent_order_id={self.parent_order_id}, " + f"exec_algorithm_id={self.exec_algorithm_id}, " + f"exec_algorithm_params={self.exec_algorithm_params}, " + f"exec_spawn_id={self.exec_spawn_id}, " f"tags={self.tags})" ) @@ -280,13 +370,13 @@ cdef class OrderInitialized(OrderEvent): cdef ClientOrderId o cdef str linked_order_ids = "None" if self.linked_order_ids: - linked_order_ids = str([o.to_str() for o in self.linked_order_ids]) + linked_order_ids = str([o.value for o in self.linked_order_ids]) return ( f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " f"side={order_side_to_str(self.side)}, " f"type={order_type_to_str(self.order_type)}, " f"quantity={self.quantity.to_str()}, " @@ -296,19 +386,142 @@ cdef class OrderInitialized(OrderEvent): f"quote_quantity={self.quote_quantity}, " f"options={self.options}, " f"emulation_trigger={trigger_type_to_str(self.emulation_trigger)}, " - f"trigger_instrument_id={self.trigger_instrument_id}, " # Can be None + f"trigger_instrument_id={self.trigger_instrument_id}, " f"contingency_type={contingency_type_to_str(self.contingency_type)}, " - f"order_list_id={self.order_list_id}, " # Can be None + f"order_list_id={self.order_list_id}, " f"linked_order_ids={linked_order_ids}, " - f"parent_order_id={self.parent_order_id}, " # Can be None - f"exec_algorithm_id={self.exec_algorithm_id}, " # Can be None - f"exec_algorithm_params={self.exec_algorithm_params}, " # Can be None - f"exec_spawn_id={self.exec_spawn_id}, " # Can be None + f"parent_order_id={self.parent_order_id}, " + f"exec_algorithm_id={self.exec_algorithm_id}, " + f"exec_algorithm_params={self.exec_algorithm_params}, " + f"exec_spawn_id={self.exec_spawn_id}, " f"tags={self.tags}, " - f"event_id={self.id.to_str()}, " + f"event_id={self.id}, " f"ts_init={self.ts_init})" ) + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + return self._trader_id + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + return self._strategy_id + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return self._instrument_id + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return self._client_order_id + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return None # Pending assignment by venue + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return None # Pending assignment by system + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + return False # Internal system event + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init + @staticmethod cdef OrderInitialized from_dict_c(dict values): Condition.not_none(values, "values") @@ -353,10 +566,10 @@ cdef class OrderInitialized(OrderEvent): cdef ClientOrderId o return { "type": "OrderInitialized", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, "order_side": order_side_to_str(obj.side), "order_type": order_type_to_str(obj.order_type), "quantity": str(obj.quantity), @@ -366,16 +579,16 @@ cdef class OrderInitialized(OrderEvent): "quote_quantity": obj.quote_quantity, "options": json.dumps(obj.options), # Using vanilla json due mixed schema types "emulation_trigger": trigger_type_to_str(obj.emulation_trigger), - "trigger_instrument_id": obj.trigger_instrument_id.to_str() if obj.trigger_instrument_id is not None else None, + "trigger_instrument_id": obj.trigger_instrument_id.value if obj.trigger_instrument_id is not None else None, "contingency_type": contingency_type_to_str(obj.contingency_type), - "order_list_id": obj.order_list_id.to_str() if obj.order_list_id is not None else None, - "linked_order_ids": ",".join([o.to_str() for o in obj.linked_order_ids]) if obj.linked_order_ids is not None else None, # noqa - "parent_order_id": obj.parent_order_id.to_str() if obj.parent_order_id is not None else None, - "exec_algorithm_id": obj.exec_algorithm_id.to_str() if obj.exec_algorithm_id is not None else None, + "order_list_id": obj.order_list_id.value if obj.order_list_id is not None else None, + "linked_order_ids": ",".join([o.value for o in obj.linked_order_ids]) if obj.linked_order_ids is not None else None, # noqa + "parent_order_id": obj.parent_order_id.value if obj.parent_order_id is not None else None, + "exec_algorithm_id": obj.exec_algorithm_id.value if obj.exec_algorithm_id is not None else None, "exec_algorithm_params": json.dumps(obj.exec_algorithm_params) if obj.exec_algorithm_params else None, - "exec_spawn_id": obj.exec_spawn_id.to_str() if obj.exec_spawn_id is not None else None, + "exec_spawn_id": obj.exec_spawn_id.value if obj.exec_spawn_id is not None else None, "tags": obj.tags, - "event_id": obj.id.to_str(), + "event_id": obj.id.value, "ts_init": obj.ts_init, "reconciliation": obj.reconciliation, } @@ -451,18 +664,6 @@ cdef class OrderDenied(OrderEvent): uint64_t ts_init, ): Condition.valid_string(reason, "denied_reason") - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - None, # Never assigned - None, # Never assigned - event_id, - ts_init, # Timestamp identical to ts_init - ts_init, - reconciliation=False, # Internal system event - ) self._mem = order_denied_new( trader_id._mem, @@ -475,202 +676,1528 @@ cdef class OrderDenied(OrderEvent): ts_init, ) + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) + def __str__(self) -> str: return ( f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " f"reason={self.reason})" ) def __repr__(self) -> str: return ( f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " f"reason={self.reason}, " - f"event_id={self.id.to_str()}, " + f"event_id={self.id}, " f"ts_init={self.ts_init})" ) + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + @property - def reason(self) -> str: + def trader_id(self) -> TraderId: """ - Return the reason the order was denied. + The trader ID associated with the event. Returns ------- - str + TraderId """ - return cstr_to_pystr(order_denied_reason_to_cstr(&self._mem)) - - @staticmethod - cdef OrderDenied from_dict_c(dict values): - Condition.not_none(values, "values") - return OrderDenied( - trader_id=TraderId(values["trader_id"]), - strategy_id=StrategyId(values["strategy_id"]), - instrument_id=InstrumentId.from_str_c(values["instrument_id"]), - client_order_id=ClientOrderId(values["client_order_id"]), - reason=values["reason"], - event_id=UUID4(values["event_id"]), - ts_init=values["ts_init"], - ) - - @staticmethod - cdef dict to_dict_c(OrderDenied obj): - Condition.not_none(obj, "obj") - return { - "type": "OrderDenied", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), - "reason": obj.reason, - "event_id": obj.id.to_str(), - "ts_event": obj.ts_init, - "ts_init": obj.ts_init, - } + return TraderId.from_mem_c(self._mem.trader_id) - @staticmethod - def from_dict(dict values) -> OrderDenied: + @property + def strategy_id(self) -> TraderId: """ - Return an order denied event from the given dict values. - - Parameters - ---------- - values : dict[str, object] - The values for initialization. + The strategy ID associated with the event. Returns ------- - OrderDenied + StrategyId """ - return OrderDenied.from_dict_c(values) + return StrategyId.from_mem_c(self._mem.strategy_id) - @staticmethod - def to_dict(OrderDenied obj): + @property + def instrument_id(self) -> InstrumentId: """ - Return a dictionary representation of this object. + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return InstrumentId.from_mem_c(self._mem.instrument_id) + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return ClientOrderId.from_mem_c(self._mem.client_order_id) + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return None # No assignment from venue + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return None # No assignment + + @property + def reason(self) -> str: + """ + Return the reason the order was denied. + + Returns + ------- + str + + """ + return ustr_to_pystr(self._mem.reason) + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + return False # Internal system event + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return UUID4.from_mem_c(self._mem.event_id) + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._mem.ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._mem.ts_init + + @staticmethod + cdef OrderDenied from_dict_c(dict values): + Condition.not_none(values, "values") + return OrderDenied( + trader_id=TraderId(values["trader_id"]), + strategy_id=StrategyId(values["strategy_id"]), + instrument_id=InstrumentId.from_str_c(values["instrument_id"]), + client_order_id=ClientOrderId(values["client_order_id"]), + reason=values["reason"], + event_id=UUID4(values["event_id"]), + ts_init=values["ts_init"], + ) + + @staticmethod + cdef dict to_dict_c(OrderDenied obj): + Condition.not_none(obj, "obj") + return { + "type": "OrderDenied", + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "reason": obj.reason, + "event_id": obj.id.value, + "ts_event": obj.ts_init, + "ts_init": obj.ts_init, + } + + @staticmethod + def from_dict(dict values) -> OrderDenied: + """ + Return an order denied event from the given dict values. + + Parameters + ---------- + values : dict[str, object] + The values for initialization. + + Returns + ------- + OrderDenied + + """ + return OrderDenied.from_dict_c(values) + + @staticmethod + def to_dict(OrderDenied obj): + """ + Return a dictionary representation of this object. + + Returns + ------- + dict[str, object] + + """ + return OrderDenied.to_dict_c(obj) + + +cdef class OrderEmulated(OrderEvent): + """ + Represents an event where an order has become emulated by the Nautilus system. + + Parameters + ---------- + trader_id : TraderId + The trader ID. + strategy_id : StrategyId + The strategy ID. + instrument_id : InstrumentId + The instrument ID. + client_order_id : ClientOrderId + The client order ID. + event_id : UUID4 + The event ID. + ts_init : uint64_t + The UNIX timestamp (nanoseconds) when the object was initialized. + + """ + + def __init__( + self, + TraderId trader_id not None, + StrategyId strategy_id not None, + InstrumentId instrument_id not None, + ClientOrderId client_order_id not None, + UUID4 event_id not None, + uint64_t ts_init, + ): + self._mem = order_emulated_new( + trader_id._mem, + strategy_id._mem, + instrument_id._mem, + client_order_id._mem, + event_id._mem, + ts_init, + ts_init, + ) + + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) + + def __str__(self) -> str: + return ( + f"{type(self).__name__}(" + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id})" + ) + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(" + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"event_id={self.id}, " + f"ts_init={self.ts_init})" + ) + + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + return TraderId.from_mem_c(self._mem.trader_id) + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + return StrategyId.from_mem_c(self._mem.strategy_id) + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return InstrumentId.from_mem_c(self._mem.instrument_id) + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return ClientOrderId.from_mem_c(self._mem.client_order_id) + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return None # No assignment from venue + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return None # No assignment + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + return False # Internal system event + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return UUID4.from_mem_c(self._mem.event_id) + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._mem.ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._mem.ts_init + + @staticmethod + cdef OrderEmulated from_dict_c(dict values): + Condition.not_none(values, "values") + return OrderEmulated( + trader_id=TraderId(values["trader_id"]), + strategy_id=StrategyId(values["strategy_id"]), + instrument_id=InstrumentId.from_str_c(values["instrument_id"]), + client_order_id=ClientOrderId(values["client_order_id"]), + event_id=UUID4(values["event_id"]), + ts_init=values["ts_init"], + ) + + @staticmethod + cdef dict to_dict_c(OrderEmulated obj): + Condition.not_none(obj, "obj") + return { + "type": "OrderEmulated", + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "event_id": obj.id.value, + "ts_event": obj.ts_init, + "ts_init": obj.ts_init, + } + + @staticmethod + def from_dict(dict values) -> OrderEmulated: + """ + Return an order emulated event from the given dict values. + + Parameters + ---------- + values : dict[str, object] + The values for initialization. + + Returns + ------- + OrderEmulated + + """ + return OrderEmulated.from_dict_c(values) + + @staticmethod + def to_dict(OrderEmulated obj): + """ + Return a dictionary representation of this object. + + Returns + ------- + dict[str, object] + + """ + return OrderEmulated.to_dict_c(obj) + + +cdef class OrderReleased(OrderEvent): + """ + Represents an event where an order was released from the `OrderEmulator` by the Nautilus system. + + Parameters + ---------- + trader_id : TraderId + The trader ID. + strategy_id : StrategyId + The strategy ID. + instrument_id : InstrumentId + The instrument ID. + client_order_id : ClientOrderId + The client order ID. + released_price : Price + The price which released the order from the emulator. + event_id : UUID4 + The event ID. + ts_init : uint64_t + The UNIX timestamp (nanoseconds) when the object was initialized. + + """ + + def __init__( + self, + TraderId trader_id not None, + StrategyId strategy_id not None, + InstrumentId instrument_id not None, + ClientOrderId client_order_id not None, + Price released_price not None, + UUID4 event_id not None, + uint64_t ts_init, + ): + self._mem = order_released_new( + trader_id._mem, + strategy_id._mem, + instrument_id._mem, + client_order_id._mem, + released_price._mem, + event_id._mem, + ts_init, + ts_init, + ) + + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) + + def __str__(self) -> str: + return ( + f"{type(self).__name__}(" + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"released_price={self.released_price})" + ) + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(" + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"released_price={self.released_price}, " + f"event_id={self.id}, " + f"ts_init={self.ts_init})" + ) + + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + return TraderId.from_mem_c(self._mem.trader_id) + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + return StrategyId.from_mem_c(self._mem.strategy_id) + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return InstrumentId.from_mem_c(self._mem.instrument_id) + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return ClientOrderId.from_mem_c(self._mem.client_order_id) + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return None # No assignment from venue + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return None # No assignment + + @property + def released_price(self) -> Price: + """ + The released price for the event. + + Returns + ------- + Price + + """ + return Price.from_mem_c(self._mem.released_price) + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + return False # Internal system event + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return UUID4.from_mem_c(self._mem.event_id) + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._mem.ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._mem.ts_init + + @staticmethod + cdef OrderReleased from_dict_c(dict values): + Condition.not_none(values, "values") + return OrderReleased( + trader_id=TraderId(values["trader_id"]), + strategy_id=StrategyId(values["strategy_id"]), + instrument_id=InstrumentId.from_str_c(values["instrument_id"]), + client_order_id=ClientOrderId(values["client_order_id"]), + released_price=Price.from_str_c(values["released_price"]), + event_id=UUID4(values["event_id"]), + ts_init=values["ts_init"], + ) + + @staticmethod + cdef dict to_dict_c(OrderReleased obj): + Condition.not_none(obj, "obj") + return { + "type": "OrderReleased", + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "released_price": str(obj.released_price), + "event_id": obj.id.value, + "ts_event": obj.ts_init, + "ts_init": obj.ts_init, + } + + @staticmethod + def from_dict(dict values) -> OrderReleased: + """ + Return an order released event from the given dict values. + + Parameters + ---------- + values : dict[str, object] + The values for initialization. + + Returns + ------- + OrderReleased + + """ + return OrderReleased.from_dict_c(values) + + @staticmethod + def to_dict(OrderReleased obj): + """ + Return a dictionary representation of this object. + + Returns + ------- + dict[str, object] + + """ + return OrderReleased.to_dict_c(obj) + + +cdef class OrderSubmitted(OrderEvent): + """ + Represents an event where an order has been submitted by the system to the + trading venue. + + Parameters + ---------- + trader_id : TraderId + The trader ID. + strategy_id : StrategyId + The strategy ID. + instrument_id : InstrumentId + The instrument ID. + client_order_id : ClientOrderId + The client order ID. + account_id : AccountId + The account ID (with the venue). + event_id : UUID4 + The event ID. + ts_event : uint64_t + The UNIX timestamp (nanoseconds) when the order submitted event occurred. + ts_init : uint64_t + The UNIX timestamp (nanoseconds) when the object was initialized. + """ + + def __init__( + self, + TraderId trader_id not None, + StrategyId strategy_id not None, + InstrumentId instrument_id not None, + ClientOrderId client_order_id not None, + AccountId account_id not None, + UUID4 event_id not None, + uint64_t ts_event, + uint64_t ts_init, + ): + self._mem = order_submitted_new( + trader_id._mem, + strategy_id._mem, + instrument_id._mem, + client_order_id._mem, + account_id._mem, + event_id._mem, + ts_event, + ts_init, + ) + + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) + + def __str__(self) -> str: + return ( + f"{type(self).__name__}(" + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"account_id={self.account_id}, " + f"ts_event={self.ts_event})" + ) + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(" + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"account_id={self.account_id}, " + f"event_id={self.id}, " + f"ts_event={self.ts_event}, " + f"ts_init={self.ts_init})" + ) + + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + return TraderId.from_mem_c(self._mem.trader_id) + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + return StrategyId.from_mem_c(self._mem.strategy_id) + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return InstrumentId.from_mem_c(self._mem.instrument_id) + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return ClientOrderId.from_mem_c(self._mem.client_order_id) + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return None # Pending assignment by venue + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return AccountId.from_mem_c(self._mem.account_id) + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + return False # Internal system event + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return UUID4.from_mem_c(self._mem.event_id) + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._mem.ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._mem.ts_init + + @staticmethod + cdef OrderSubmitted from_dict_c(dict values): + Condition.not_none(values, "values") + return OrderSubmitted( + trader_id=TraderId(values["trader_id"]), + strategy_id=StrategyId(values["strategy_id"]), + instrument_id=InstrumentId.from_str_c(values["instrument_id"]), + client_order_id=ClientOrderId(values["client_order_id"]), + account_id=AccountId(values["account_id"]), + event_id=UUID4(values["event_id"]), + ts_event=values["ts_event"], + ts_init=values["ts_init"], + ) + + @staticmethod + cdef dict to_dict_c(OrderSubmitted obj): + Condition.not_none(obj, "obj") + return { + "type": "OrderSubmitted", + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "account_id": obj.account_id.value, + "event_id": obj.id.value, + "ts_event": obj.ts_event, + "ts_init": obj.ts_init, + } + + @staticmethod + def from_dict(dict values) -> OrderSubmitted: + """ + Return an order submitted event from the given dict values. + + Parameters + ---------- + values : dict[str, object] + The values for initialization. + + Returns + ------- + OrderSubmitted + + """ + return OrderSubmitted.from_dict_c(values) + + @staticmethod + def to_dict(OrderSubmitted obj): + """ + Return a dictionary representation of this object. + + Returns + ------- + dict[str, object] + + """ + return OrderSubmitted.to_dict_c(obj) + + +cdef class OrderAccepted(OrderEvent): + """ + Represents an event where an order has been accepted by the trading venue. + + This event often corresponds to a `NEW` OrdStatus <39> field in FIX + trade reports. + + Parameters + ---------- + trader_id : TraderId + The trader ID. + strategy_id : StrategyId + The strategy ID. + instrument_id : InstrumentId + The instrument ID. + client_order_id : ClientOrderId + The client order ID. + venue_order_id : VenueOrderId + The venue order ID (assigned by the venue). + account_id : AccountId + The account ID (with the venue). + event_id : UUID4 + The event ID. + ts_event : uint64_t + The UNIX timestamp (nanoseconds) when the order accepted event occurred. + ts_init : uint64_t + The UNIX timestamp (nanoseconds) when the object was initialized. + reconciliation : bool, default False + If the event was generated during reconciliation. + + References + ---------- + https://www.onixs.biz/fix-dictionary/5.0.SP2/tagNum_39.html + """ + + def __init__( + self, + TraderId trader_id not None, + StrategyId strategy_id not None, + InstrumentId instrument_id not None, + ClientOrderId client_order_id not None, + VenueOrderId venue_order_id not None, + AccountId account_id not None, + UUID4 event_id not None, + uint64_t ts_event, + uint64_t ts_init, + bint reconciliation=False, + ): + self._mem = order_accepted_new( + trader_id._mem, + strategy_id._mem, + instrument_id._mem, + client_order_id._mem, + venue_order_id._mem, + account_id._mem, + event_id._mem, + ts_event, + ts_init, + reconciliation, + ) + + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) + + def __str__(self) -> str: + return ( + f"{type(self).__name__}(" + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " + f"ts_event={self.ts_event})" + ) + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(" + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " + f"event_id={self.id}, " + f"ts_event={self.ts_event}, " + f"ts_init={self.ts_init})" + ) + + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + return TraderId.from_mem_c(self._mem.trader_id) + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + return StrategyId.from_mem_c(self._mem.strategy_id) + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return InstrumentId.from_mem_c(self._mem.instrument_id) + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return ClientOrderId.from_mem_c(self._mem.client_order_id) + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return VenueOrderId.from_mem_c(self._mem.venue_order_id) + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return AccountId.from_mem_c(self._mem.account_id) + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + return self._mem.reconciliation + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return UUID4.from_mem_c(self._mem.event_id) + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._mem.ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._mem.ts_init + + @staticmethod + cdef OrderAccepted from_dict_c(dict values): + Condition.not_none(values, "values") + return OrderAccepted( + trader_id=TraderId(values["trader_id"]), + strategy_id=StrategyId(values["strategy_id"]), + instrument_id=InstrumentId.from_str_c(values["instrument_id"]), + client_order_id=ClientOrderId(values["client_order_id"]), + venue_order_id=VenueOrderId(values["venue_order_id"]), + account_id=AccountId(values["account_id"]), + event_id=UUID4(values["event_id"]), + ts_event=values["ts_event"], + ts_init=values["ts_init"], + reconciliation=values.get("reconciliation", False), + ) + + @staticmethod + cdef dict to_dict_c(OrderAccepted obj): + Condition.not_none(obj, "obj") + return { + "type": "OrderAccepted", + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "venue_order_id": obj.venue_order_id.value, + "account_id": obj.account_id.value, + "event_id": obj.id.value, + "ts_event": obj.ts_event, + "ts_init": obj.ts_init, + "reconciliation": obj.reconciliation, + } + + @staticmethod + def from_dict(dict values) -> OrderAccepted: + """ + Return an order accepted event from the given dict values. + + Parameters + ---------- + values : dict[str, object] + The values for initialization. + + Returns + ------- + OrderAccepted + + """ + return OrderAccepted.from_dict_c(values) + + @staticmethod + def to_dict(OrderAccepted obj): + """ + Return a dictionary representation of this object. Returns ------- dict[str, object] """ - return OrderDenied.to_dict_c(obj) + return OrderAccepted.to_dict_c(obj) + + +cdef class OrderRejected(OrderEvent): + """ + Represents an event where an order has been rejected by the trading venue. + + Parameters + ---------- + trader_id : TraderId + The trader ID. + strategy_id : StrategyId + The strategy ID. + instrument_id : InstrumentId + The instrument ID. + client_order_id : ClientOrderId + The client order ID. + account_id : AccountId + The account ID (with the venue). + reason : datetime + The order rejected reason. + event_id : UUID4 + The event ID. + ts_event : uint64_t + The UNIX timestamp (nanoseconds) when the order rejected event occurred. + ts_init : uint64_t + The UNIX timestamp (nanoseconds) when the object was initialized. + reconciliation : bool, default False + If the event was generated during reconciliation. + + Raises + ------ + ValueError + If `reason` is not a valid string. + """ + + def __init__( + self, + TraderId trader_id not None, + StrategyId strategy_id not None, + InstrumentId instrument_id not None, + ClientOrderId client_order_id not None, + AccountId account_id not None, + str reason not None, + UUID4 event_id not None, + uint64_t ts_event, + uint64_t ts_init, + bint reconciliation=False, + ): + Condition.valid_string(reason, "reason") + + self._mem = order_rejected_new( + trader_id._mem, + strategy_id._mem, + instrument_id._mem, + client_order_id._mem, + account_id._mem, + pystr_to_cstr(reason), + event_id._mem, + ts_event, + ts_init, + reconciliation, + ) + + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) + + def __str__(self) -> str: + return ( + f"{type(self).__name__}(" + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"account_id={self.account_id}, " + f"reason='{self.reason}', " + f"ts_event={self.ts_event})" + ) + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(" + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"account_id={self.account_id}, " + f"reason='{self.reason}', " + f"event_id={self.id}, " + f"ts_event={self.ts_event}, " + f"ts_init={self.ts_init})" + ) + + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + return TraderId.from_mem_c(self._mem.trader_id) + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + return StrategyId.from_mem_c(self._mem.strategy_id) + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return InstrumentId.from_mem_c(self._mem.instrument_id) + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return ClientOrderId.from_mem_c(self._mem.client_order_id) + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return None # Not assigned + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return AccountId.from_mem_c(self._mem.account_id) + + @property + def reason(self) -> str: + """ + Return the reason the order was rejected. + + Returns + ------- + str + + """ + return ustr_to_pystr(self._mem.reason) + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + return self._mem.reconciliation + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return UUID4.from_mem_c(self._mem.event_id) + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + Returns + ------- + int -cdef class OrderSubmitted(OrderEvent): - """ - Represents an event where an order has been submitted by the system to the - trading venue. + """ + return self._mem.ts_event - Parameters - ---------- - trader_id : TraderId - The trader ID. - strategy_id : StrategyId - The strategy ID. - instrument_id : InstrumentId - The instrument ID. - client_order_id : ClientOrderId - The client order ID. - account_id : AccountId - The account ID (with the venue). - event_id : UUID4 - The event ID. - ts_event : uint64_t - The UNIX timestamp (nanoseconds) when the order submitted event occurred. - ts_init : uint64_t + @property + def ts_init(self) -> int: + """ The UNIX timestamp (nanoseconds) when the object was initialized. - """ - - def __init__( - self, - TraderId trader_id not None, - StrategyId strategy_id not None, - InstrumentId instrument_id not None, - ClientOrderId client_order_id not None, - AccountId account_id not None, - UUID4 event_id not None, - uint64_t ts_event, - uint64_t ts_init, - ): - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - None, # Pending accepted - account_id, - event_id, - ts_event, - ts_init, - reconciliation=False, # Internal system event - ) - self.account_id = account_id - - def __str__(self) -> str: - return ( - f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"account_id={self.account_id.to_str()}, " - f"ts_event={self.ts_event})" - ) + Returns + ------- + int - def __repr__(self) -> str: - return ( - f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"account_id={self.account_id.to_str()}, " - f"event_id={self.id.to_str()}, " - f"ts_event={self.ts_event}, " - f"ts_init={self.ts_init})" - ) + """ + return self._mem.ts_init @staticmethod - cdef OrderSubmitted from_dict_c(dict values): + cdef OrderRejected from_dict_c(dict values): Condition.not_none(values, "values") - return OrderSubmitted( + return OrderRejected( trader_id=TraderId(values["trader_id"]), strategy_id=StrategyId(values["strategy_id"]), instrument_id=InstrumentId.from_str_c(values["instrument_id"]), client_order_id=ClientOrderId(values["client_order_id"]), account_id=AccountId(values["account_id"]), + reason=values["reason"], event_id=UUID4(values["event_id"]), ts_event=values["ts_event"], ts_init=values["ts_init"], + reconciliation=values.get("reconciliation", False), ) @staticmethod - cdef dict to_dict_c(OrderSubmitted obj): + cdef dict to_dict_c(OrderRejected obj): Condition.not_none(obj, "obj") return { - "type": "OrderSubmitted", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), - "account_id": obj.account_id.to_str(), - "event_id": obj.id.to_str(), + "type": "OrderRejected", + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "account_id": obj.account_id.value, + "reason": obj.reason, + "event_id": obj.id.value, "ts_event": obj.ts_event, "ts_init": obj.ts_init, + "reconciliation": obj.reconciliation, } @staticmethod - def from_dict(dict values) -> OrderSubmitted: + def from_dict(dict values) -> OrderRejected: """ - Return an order submitted event from the given dict values. + Return an order rejected event from the given dict values. Parameters ---------- @@ -679,13 +2206,13 @@ cdef class OrderSubmitted(OrderEvent): Returns ------- - OrderSubmitted + OrderRejected """ - return OrderSubmitted.from_dict_c(values) + return OrderRejected.from_dict_c(values) @staticmethod - def to_dict(OrderSubmitted obj): + def to_dict(OrderRejected obj): """ Return a dictionary representation of this object. @@ -694,15 +2221,12 @@ cdef class OrderSubmitted(OrderEvent): dict[str, object] """ - return OrderSubmitted.to_dict_c(obj) + return OrderRejected.to_dict_c(obj) -cdef class OrderAccepted(OrderEvent): +cdef class OrderCanceled(OrderEvent): """ - Represents an event where an order has been accepted by the trading venue. - - This event often corresponds to a `NEW` OrdStatus <39> field in FIX - trade reports. + Represents an event where an order has been canceled at the trading venue. Parameters ---------- @@ -714,22 +2238,18 @@ cdef class OrderAccepted(OrderEvent): The instrument ID. client_order_id : ClientOrderId The client order ID. - venue_order_id : VenueOrderId + venue_order_id : VenueOrderId, optional with no default so ``None`` must be passed explicitly The venue order ID (assigned by the venue). - account_id : AccountId + account_id : AccountId, optional with no default so ``None`` must be passed explicitly The account ID (with the venue). event_id : UUID4 The event ID. ts_event : uint64_t - The UNIX timestamp (nanoseconds) when the order accepted event occurred. + The UNIX timestamp (nanoseconds) when order canceled event occurred. ts_init : uint64_t The UNIX timestamp (nanoseconds) when the object was initialized. reconciliation : bool, default False If the event was generated during reconciliation. - - References - ---------- - https://www.onixs.biz/fix-dictionary/5.0.SP2/tagNum_39.html """ def __init__( @@ -738,209 +2258,189 @@ cdef class OrderAccepted(OrderEvent): StrategyId strategy_id not None, InstrumentId instrument_id not None, ClientOrderId client_order_id not None, - VenueOrderId venue_order_id not None, - AccountId account_id not None, + VenueOrderId venue_order_id: Optional[VenueOrderId], + AccountId account_id: Optional[AccountId], UUID4 event_id not None, uint64_t ts_event, uint64_t ts_init, bint reconciliation=False, ): - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - venue_order_id, - account_id, - event_id, - ts_event, - ts_init, - reconciliation, - ) + self._trader_id = trader_id + self._strategy_id = strategy_id + self._instrument_id = instrument_id + self._client_order_id = client_order_id + self._venue_order_id = venue_order_id + self._account_id = account_id + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init + self._reconciliation = reconciliation + + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) def __str__(self) -> str: return ( f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id.to_str()}, " - f"account_id={self.account_id.to_str()}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " f"ts_event={self.ts_event})" ) def __repr__(self) -> str: return ( f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id.to_str()}, " - f"account_id={self.account_id.to_str()}, " - f"event_id={self.id.to_str()}, " + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " + f"event_id={self.id}, " f"ts_event={self.ts_event}, " f"ts_init={self.ts_init})" ) - @staticmethod - cdef OrderAccepted from_dict_c(dict values): - Condition.not_none(values, "values") - return OrderAccepted( - trader_id=TraderId(values["trader_id"]), - strategy_id=StrategyId(values["strategy_id"]), - instrument_id=InstrumentId.from_str_c(values["instrument_id"]), - client_order_id=ClientOrderId(values["client_order_id"]), - venue_order_id=VenueOrderId(values["venue_order_id"]), - account_id=AccountId(values["account_id"]), - event_id=UUID4(values["event_id"]), - ts_event=values["ts_event"], - ts_init=values["ts_init"], - reconciliation=values.get("reconciliation", False), - ) + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id - @staticmethod - cdef dict to_dict_c(OrderAccepted obj): - Condition.not_none(obj, "obj") - return { - "type": "OrderAccepted", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), - "venue_order_id": obj.venue_order_id.to_str(), - "account_id": obj.account_id.to_str(), - "event_id": obj.id.to_str(), - "ts_event": obj.ts_event, - "ts_init": obj.ts_init, - "reconciliation": obj.reconciliation, - } + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId - @staticmethod - def from_dict(dict values) -> OrderAccepted: """ - Return an order accepted event from the given dict values. + return self._trader_id - Parameters - ---------- - values : dict[str, object] - The values for initialization. + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. Returns ------- - OrderAccepted + StrategyId """ - return OrderAccepted.from_dict_c(values) + return self._strategy_id - @staticmethod - def to_dict(OrderAccepted obj): + @property + def instrument_id(self) -> InstrumentId: """ - Return a dictionary representation of this object. + The instrument ID associated with the event. Returns ------- - dict[str, object] + InstrumentId """ - return OrderAccepted.to_dict_c(obj) + return self._instrument_id + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. -cdef class OrderRejected(OrderEvent): - """ - Represents an event where an order has been rejected by the trading venue. + Returns + ------- + ClientOrderId - Parameters - ---------- - trader_id : TraderId - The trader ID. - strategy_id : StrategyId - The strategy ID. - instrument_id : InstrumentId - The instrument ID. - client_order_id : ClientOrderId - The client order ID. - account_id : AccountId - The account ID (with the venue). - reason : datetime - The order rejected reason. - event_id : UUID4 - The event ID. - ts_event : uint64_t - The UNIX timestamp (nanoseconds) when the order rejected event occurred. - ts_init : uint64_t - The UNIX timestamp (nanoseconds) when the object was initialized. - reconciliation : bool, default False + """ + return self._client_order_id + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return self._venue_order_id + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return self._account_id + + @property + def reconciliation(self) -> bool: + """ If the event was generated during reconciliation. - Raises - ------ - ValueError - If `reason` is not a valid string. - """ + Returns + ------- + bool - def __init__( - self, - TraderId trader_id not None, - StrategyId strategy_id not None, - InstrumentId instrument_id not None, - ClientOrderId client_order_id not None, - AccountId account_id not None, - str reason not None, - UUID4 event_id not None, - uint64_t ts_event, - uint64_t ts_init, - bint reconciliation=False, - ): - Condition.valid_string(reason, "reason") - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - None, # Not always assigned on rejection - account_id, # We know the account with the venue - event_id, - ts_event, - ts_init, - reconciliation, - ) + """ + return self._reconciliation - self.reason = reason + @property + def id(self) -> UUID4: + """ + The event message identifier. - def __str__(self) -> str: - return ( - f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"account_id={self.account_id.to_str()}, " - f"reason='{self.reason}', " - f"ts_event={self.ts_event})" - ) + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int - def __repr__(self) -> str: - return ( - f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"account_id={self.account_id.to_str()}, " - f"reason='{self.reason}', " - f"event_id={self.id.to_str()}, " - f"ts_event={self.ts_event}, " - f"ts_init={self.ts_init})" - ) + """ + return self._ts_init @staticmethod - cdef OrderRejected from_dict_c(dict values): + cdef OrderCanceled from_dict_c(dict values): Condition.not_none(values, "values") - return OrderRejected( + cdef str v = values["venue_order_id"] + cdef str a = values["account_id"] + return OrderCanceled( trader_id=TraderId(values["trader_id"]), strategy_id=StrategyId(values["strategy_id"]), instrument_id=InstrumentId.from_str_c(values["instrument_id"]), client_order_id=ClientOrderId(values["client_order_id"]), - account_id=AccountId(values["account_id"]), - reason=values["reason"], + venue_order_id=VenueOrderId(v) if v is not None else None, + account_id=AccountId(a) if a is not None else None, event_id=UUID4(values["event_id"]), ts_event=values["ts_event"], ts_init=values["ts_init"], @@ -948,26 +2448,26 @@ cdef class OrderRejected(OrderEvent): ) @staticmethod - cdef dict to_dict_c(OrderRejected obj): + cdef dict to_dict_c(OrderCanceled obj): Condition.not_none(obj, "obj") return { - "type": "OrderRejected", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), - "account_id": obj.account_id.to_str(), - "reason": obj.reason, - "event_id": obj.id.to_str(), + "type": "OrderCanceled", + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "venue_order_id": obj.venue_order_id.value if obj.venue_order_id is not None else None, + "account_id": obj.account_id.value if obj.account_id is not None else None, + "event_id": obj.id.value, "ts_event": obj.ts_event, "ts_init": obj.ts_init, "reconciliation": obj.reconciliation, } @staticmethod - def from_dict(dict values) -> OrderRejected: + def from_dict(dict values) -> OrderCanceled: """ - Return an order rejected event from the given dict values. + Return an order canceled event from the given dict values. Parameters ---------- @@ -976,13 +2476,13 @@ cdef class OrderRejected(OrderEvent): Returns ------- - OrderRejected + OrderCanceled """ - return OrderRejected.from_dict_c(values) + return OrderCanceled.from_dict_c(values) @staticmethod - def to_dict(OrderRejected obj): + def to_dict(OrderCanceled obj): """ Return a dictionary representation of this object. @@ -991,12 +2491,12 @@ cdef class OrderRejected(OrderEvent): dict[str, object] """ - return OrderRejected.to_dict_c(obj) + return OrderCanceled.to_dict_c(obj) -cdef class OrderCanceled(OrderEvent): +cdef class OrderExpired(OrderEvent): """ - Represents an event where an order has been canceled at the trading venue. + Represents an event where an order has expired at the trading venue. Parameters ---------- @@ -1015,7 +2515,7 @@ cdef class OrderCanceled(OrderEvent): event_id : UUID4 The event ID. ts_event : uint64_t - The UNIX timestamp (nanoseconds) when order canceled event occurred. + The UNIX timestamp (nanoseconds) when the order expired event occurred. ts_init : uint64_t The UNIX timestamp (nanoseconds) when the object was initialized. reconciliation : bool, default False @@ -1035,49 +2535,176 @@ cdef class OrderCanceled(OrderEvent): uint64_t ts_init, bint reconciliation=False, ): - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - venue_order_id, - account_id, - event_id, - ts_event, - ts_init, - reconciliation, - ) + self._trader_id = trader_id + self._strategy_id = strategy_id + self._instrument_id = instrument_id + self._client_order_id = client_order_id + self._venue_order_id = venue_order_id + self._account_id = account_id + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init + self._reconciliation = reconciliation + + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) def __str__(self) -> str: return ( f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " f"ts_event={self.ts_event})" ) def __repr__(self) -> str: return ( f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None - f"event_id={self.id.to_str()}, " + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " + f"event_id={self.id}, " f"ts_event={self.ts_event}, " f"ts_init={self.ts_init})" ) + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + return self._trader_id + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + return self._strategy_id + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return self._instrument_id + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return self._client_order_id + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return self._venue_order_id + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return self._account_id + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + return self._reconciliation + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init + @staticmethod - cdef OrderCanceled from_dict_c(dict values): + cdef OrderExpired from_dict_c(dict values): Condition.not_none(values, "values") cdef str v = values["venue_order_id"] cdef str a = values["account_id"] - return OrderCanceled( + return OrderExpired( trader_id=TraderId(values["trader_id"]), strategy_id=StrategyId(values["strategy_id"]), instrument_id=InstrumentId.from_str_c(values["instrument_id"]), @@ -1091,26 +2718,26 @@ cdef class OrderCanceled(OrderEvent): ) @staticmethod - cdef dict to_dict_c(OrderCanceled obj): + cdef dict to_dict_c(OrderExpired obj): Condition.not_none(obj, "obj") return { - "type": "OrderCanceled", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), - "venue_order_id": obj.venue_order_id.to_str() if obj.venue_order_id is not None else None, - "account_id": obj.account_id.to_str() if obj.account_id is not None else None, - "event_id": obj.id.to_str(), + "type": "OrderExpired", + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "venue_order_id": obj.venue_order_id.value if obj.venue_order_id is not None else None, + "account_id": obj.account_id.value if obj.account_id is not None else None, + "event_id": obj.id.value, "ts_event": obj.ts_event, "ts_init": obj.ts_init, "reconciliation": obj.reconciliation, } @staticmethod - def from_dict(dict values) -> OrderCanceled: + def from_dict(dict values) -> OrderExpired: """ - Return an order canceled event from the given dict values. + Return an order expired event from the given dict values. Parameters ---------- @@ -1119,13 +2746,13 @@ cdef class OrderCanceled(OrderEvent): Returns ------- - OrderCanceled + OrderExpired """ - return OrderCanceled.from_dict_c(values) + return OrderExpired.from_dict_c(values) @staticmethod - def to_dict(OrderCanceled obj): + def to_dict(OrderExpired obj): """ Return a dictionary representation of this object. @@ -1134,12 +2761,14 @@ cdef class OrderCanceled(OrderEvent): dict[str, object] """ - return OrderCanceled.to_dict_c(obj) + return OrderExpired.to_dict_c(obj) -cdef class OrderExpired(OrderEvent): +cdef class OrderTriggered(OrderEvent): """ - Represents an event where an order has expired at the trading venue. + Represents an event where an order has triggered. + + Applicable to :class:`StopLimit` orders only. Parameters ---------- @@ -1158,7 +2787,7 @@ cdef class OrderExpired(OrderEvent): event_id : UUID4 The event ID. ts_event : uint64_t - The UNIX timestamp (nanoseconds) when the order expired event occurred. + The UNIX timestamp (nanoseconds) when the order triggered event occurred. ts_init : uint64_t The UNIX timestamp (nanoseconds) when the object was initialized. reconciliation : bool, default False @@ -1178,49 +2807,176 @@ cdef class OrderExpired(OrderEvent): uint64_t ts_init, bint reconciliation=False, ): - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - venue_order_id, - account_id, - event_id, - ts_event, - ts_init, - reconciliation, - ) + self._trader_id = trader_id + self._strategy_id = strategy_id + self._instrument_id = instrument_id + self._client_order_id = client_order_id + self._venue_order_id = venue_order_id + self._account_id = account_id + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init + self._reconciliation = reconciliation + + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) def __str__(self) -> str: return ( f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " f"ts_event={self.ts_event})" ) - def __repr__(self) -> str: - return ( - f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None - f"event_id={self.id.to_str()}, " - f"ts_event={self.ts_event}, " - f"ts_init={self.ts_init})" - ) + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(" + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " + f"event_id={self.id}, " + f"ts_event={self.ts_event}, " + f"ts_init={self.ts_init})" + ) + + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + return self._trader_id + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + return self._strategy_id + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return self._instrument_id + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return self._client_order_id + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return self._venue_order_id + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return self._account_id + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + return self._reconciliation + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init @staticmethod - cdef OrderExpired from_dict_c(dict values): + cdef OrderTriggered from_dict_c(dict values): Condition.not_none(values, "values") cdef str v = values["venue_order_id"] cdef str a = values["account_id"] - return OrderExpired( + return OrderTriggered( trader_id=TraderId(values["trader_id"]), strategy_id=StrategyId(values["strategy_id"]), instrument_id=InstrumentId.from_str_c(values["instrument_id"]), @@ -1234,26 +2990,26 @@ cdef class OrderExpired(OrderEvent): ) @staticmethod - cdef dict to_dict_c(OrderExpired obj): + cdef dict to_dict_c(OrderTriggered obj): Condition.not_none(obj, "obj") return { - "type": "OrderExpired", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), - "venue_order_id": obj.venue_order_id.to_str() if obj.venue_order_id is not None else None, - "account_id": obj.account_id.to_str() if obj.account_id is not None else None, - "event_id": obj.id.to_str(), + "type": "OrderTriggered", + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "venue_order_id": obj.venue_order_id.value if obj.venue_order_id is not None else None, + "account_id": obj.account_id.value if obj.account_id is not None else None, + "event_id": obj.id.value, "ts_event": obj.ts_event, "ts_init": obj.ts_init, "reconciliation": obj.reconciliation, } @staticmethod - def from_dict(dict values) -> OrderExpired: + def from_dict(dict values) -> OrderTriggered: """ - Return an order expired event from the given dict values. + Return an order triggered event from the given dict values. Parameters ---------- @@ -1262,13 +3018,13 @@ cdef class OrderExpired(OrderEvent): Returns ------- - OrderExpired + OrderTriggered """ - return OrderExpired.from_dict_c(values) + return OrderTriggered.from_dict_c(values) @staticmethod - def to_dict(OrderExpired obj): + def to_dict(OrderTriggered obj): """ Return a dictionary representation of this object. @@ -1277,14 +3033,13 @@ cdef class OrderExpired(OrderEvent): dict[str, object] """ - return OrderExpired.to_dict_c(obj) + return OrderTriggered.to_dict_c(obj) -cdef class OrderTriggered(OrderEvent): +cdef class OrderPendingUpdate(OrderEvent): """ - Represents an event where an order has triggered. - - Applicable to :class:`StopLimit` orders only. + Represents an event where an `ModifyOrder` command has been sent to the + trading venue. Parameters ---------- @@ -1303,7 +3058,7 @@ cdef class OrderTriggered(OrderEvent): event_id : UUID4 The event ID. ts_event : uint64_t - The UNIX timestamp (nanoseconds) when the order triggered event occurred. + The UNIX timestamp (nanoseconds) when the order pending update event occurred. ts_init : uint64_t The UNIX timestamp (nanoseconds) when the object was initialized. reconciliation : bool, default False @@ -1323,51 +3078,176 @@ cdef class OrderTriggered(OrderEvent): uint64_t ts_init, bint reconciliation=False, ): - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - venue_order_id, - account_id, - event_id, - ts_event, - ts_init, - reconciliation, - ) - - self.account_id = account_id + self._trader_id = trader_id + self._strategy_id = strategy_id + self._instrument_id = instrument_id + self._client_order_id = client_order_id + self._venue_order_id = venue_order_id + self._account_id = account_id + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init + self._reconciliation = reconciliation + + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) def __str__(self) -> str: return ( f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " f"ts_event={self.ts_event})" ) def __repr__(self) -> str: return ( f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None - f"event_id={self.id.to_str()}, " + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " + f"event_id={self.id}, " f"ts_event={self.ts_event}, " f"ts_init={self.ts_init})" ) + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + return self._trader_id + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + return self._strategy_id + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return self._instrument_id + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return self._client_order_id + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return self._venue_order_id + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return self._account_id + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + return self._reconciliation + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init + @staticmethod - cdef OrderTriggered from_dict_c(dict values): + cdef OrderPendingUpdate from_dict_c(dict values): Condition.not_none(values, "values") cdef str v = values["venue_order_id"] cdef str a = values["account_id"] - return OrderTriggered( + return OrderPendingUpdate( trader_id=TraderId(values["trader_id"]), strategy_id=StrategyId(values["strategy_id"]), instrument_id=InstrumentId.from_str_c(values["instrument_id"]), @@ -1381,26 +3261,26 @@ cdef class OrderTriggered(OrderEvent): ) @staticmethod - cdef dict to_dict_c(OrderTriggered obj): + cdef dict to_dict_c(OrderPendingUpdate obj): Condition.not_none(obj, "obj") return { - "type": "OrderTriggered", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), - "venue_order_id": obj.venue_order_id.to_str() if obj.venue_order_id is not None else None, - "account_id": obj.account_id.to_str() if obj.account_id is not None else None, - "event_id": obj.id.to_str(), + "type": "OrderPendingUpdate", + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "venue_order_id": obj.venue_order_id.value if obj.venue_order_id is not None else None, + "account_id": obj.account_id.value if obj.account_id is not None else None, + "event_id": obj.id.value, "ts_event": obj.ts_event, "ts_init": obj.ts_init, "reconciliation": obj.reconciliation, } @staticmethod - def from_dict(dict values) -> OrderTriggered: + def from_dict(dict values) -> OrderPendingUpdate: """ - Return an order triggered event from the given dict values. + Return an order pending update event from the given dict values. Parameters ---------- @@ -1409,13 +3289,13 @@ cdef class OrderTriggered(OrderEvent): Returns ------- - OrderTriggered + OrderPendingUpdate """ - return OrderTriggered.from_dict_c(values) + return OrderPendingUpdate.from_dict_c(values) @staticmethod - def to_dict(OrderTriggered obj): + def to_dict(OrderPendingUpdate obj): """ Return a dictionary representation of this object. @@ -1424,12 +3304,12 @@ cdef class OrderTriggered(OrderEvent): dict[str, object] """ - return OrderTriggered.to_dict_c(obj) + return OrderPendingUpdate.to_dict_c(obj) -cdef class OrderPendingUpdate(OrderEvent): +cdef class OrderPendingCancel(OrderEvent): """ - Represents an event where an `ModifyOrder` command has been sent to the + Represents an event where a `CancelOrder` command has been sent to the trading venue. Parameters @@ -1449,7 +3329,7 @@ cdef class OrderPendingUpdate(OrderEvent): event_id : UUID4 The event ID. ts_event : uint64_t - The UNIX timestamp (nanoseconds) when the order pending update event occurred. + The UNIX timestamp (nanoseconds) when the order pending cancel event occurred. ts_init : uint64_t The UNIX timestamp (nanoseconds) when the object was initialized. reconciliation : bool, default False @@ -1469,186 +3349,169 @@ cdef class OrderPendingUpdate(OrderEvent): uint64_t ts_init, bint reconciliation=False, ): - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - venue_order_id, - account_id, - event_id, - ts_event, - ts_init, - reconciliation, - ) + self._trader_id = trader_id + self._strategy_id = strategy_id + self._instrument_id = instrument_id + self._client_order_id = client_order_id + self._venue_order_id = venue_order_id + self._account_id = account_id + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init + self._reconciliation = reconciliation + + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) def __str__(self) -> str: return ( f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " f"ts_event={self.ts_event})" ) def __repr__(self) -> str: return ( f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None - f"event_id={self.id.to_str()}, " + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " + f"event_id={self.id}, " f"ts_event={self.ts_event}, " f"ts_init={self.ts_init})" ) - @staticmethod - cdef OrderPendingUpdate from_dict_c(dict values): - Condition.not_none(values, "values") - cdef str v = values["venue_order_id"] - cdef str a = values["account_id"] - return OrderPendingUpdate( - trader_id=TraderId(values["trader_id"]), - strategy_id=StrategyId(values["strategy_id"]), - instrument_id=InstrumentId.from_str_c(values["instrument_id"]), - client_order_id=ClientOrderId(values["client_order_id"]), - venue_order_id=VenueOrderId(v) if v is not None else None, - account_id=AccountId(a) if a is not None else None, - event_id=UUID4(values["event_id"]), - ts_event=values["ts_event"], - ts_init=values["ts_init"], - reconciliation=values.get("reconciliation", False), - ) + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id - @staticmethod - cdef dict to_dict_c(OrderPendingUpdate obj): - Condition.not_none(obj, "obj") - return { - "type": "OrderPendingUpdate", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), - "venue_order_id": obj.venue_order_id.to_str() if obj.venue_order_id is not None else None, - "account_id": obj.account_id.to_str() if obj.account_id is not None else None, - "event_id": obj.id.to_str(), - "ts_event": obj.ts_event, - "ts_init": obj.ts_init, - "reconciliation": obj.reconciliation, - } + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId - @staticmethod - def from_dict(dict values) -> OrderPendingUpdate: """ - Return an order pending update event from the given dict values. + return self._trader_id - Parameters - ---------- - values : dict[str, object] - The values for initialization. + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. Returns ------- - OrderPendingUpdate + StrategyId """ - return OrderPendingUpdate.from_dict_c(values) + return self._strategy_id - @staticmethod - def to_dict(OrderPendingUpdate obj): + @property + def instrument_id(self) -> InstrumentId: """ - Return a dictionary representation of this object. + The instrument ID associated with the event. Returns ------- - dict[str, object] + InstrumentId """ - return OrderPendingUpdate.to_dict_c(obj) + return self._instrument_id + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. -cdef class OrderPendingCancel(OrderEvent): - """ - Represents an event where a `CancelOrder` command has been sent to the - trading venue. + Returns + ------- + ClientOrderId - Parameters - ---------- - trader_id : TraderId - The trader ID. - strategy_id : StrategyId - The strategy ID. - instrument_id : InstrumentId - The instrument ID. - client_order_id : ClientOrderId - The client order ID. - venue_order_id : VenueOrderId, optional with no default so ``None`` must be passed explicitly - The venue order ID (assigned by the venue). - account_id : AccountId, optional with no default so ``None`` must be passed explicitly - The account ID (with the venue). - event_id : UUID4 - The event ID. - ts_event : uint64_t - The UNIX timestamp (nanoseconds) when the order pending cancel event occurred. - ts_init : uint64_t - The UNIX timestamp (nanoseconds) when the object was initialized. - reconciliation : bool, default False + """ + return self._client_order_id + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return self._venue_order_id + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return self._account_id + + @property + def reconciliation(self) -> bool: + """ If the event was generated during reconciliation. - """ - def __init__( - self, - TraderId trader_id not None, - StrategyId strategy_id not None, - InstrumentId instrument_id not None, - ClientOrderId client_order_id not None, - VenueOrderId venue_order_id: Optional[VenueOrderId], - AccountId account_id: Optional[AccountId], - UUID4 event_id not None, - uint64_t ts_event, - uint64_t ts_init, - bint reconciliation=False, - ): - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - venue_order_id, - account_id, - event_id, - ts_event, - ts_init, - reconciliation, - ) + Returns + ------- + bool - def __str__(self) -> str: - return ( - f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None - f"ts_event={self.ts_event})" - ) + """ + return self._reconciliation - def __repr__(self) -> str: - return ( - f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None - f"event_id={self.id.to_str()}, " - f"ts_event={self.ts_event}, " - f"ts_init={self.ts_init})" - ) + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init @staticmethod cdef OrderPendingCancel from_dict_c(dict values): @@ -1673,13 +3536,13 @@ cdef class OrderPendingCancel(OrderEvent): Condition.not_none(obj, "obj") return { "type": "OrderPendingCancel", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), - "venue_order_id": obj.venue_order_id.to_str() if obj.venue_order_id is not None else None, - "account_id": obj.account_id.to_str() if obj.account_id is not None else None, - "event_id": obj.id.to_str(), + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "venue_order_id": obj.venue_order_id.value if obj.venue_order_id is not None else None, + "account_id": obj.account_id.value if obj.account_id is not None else None, + "event_id": obj.id.value, "ts_event": obj.ts_event, "ts_init": obj.ts_init, "reconciliation": obj.reconciliation, @@ -1766,28 +3629,32 @@ cdef class OrderModifyRejected(OrderEvent): bint reconciliation=False, ): Condition.valid_string(reason, "reason") - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - venue_order_id, - account_id, - event_id, - ts_event, - ts_init, - reconciliation, - ) - self.reason = reason + self._strategy_id = strategy_id + self._trader_id = trader_id + self._instrument_id = instrument_id + self._client_order_id = client_order_id + self._venue_order_id = venue_order_id + self._account_id = account_id + self._reason = reason + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init + self._reconciliation = reconciliation + + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) def __str__(self) -> str: return ( f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " f"reason={self.reason}, " f"ts_event={self.ts_event})" ) @@ -1795,18 +3662,153 @@ cdef class OrderModifyRejected(OrderEvent): def __repr__(self) -> str: return ( f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " f"reason={self.reason}, " - f"event_id={self.id.to_str()}, " + f"event_id={self.id}, " f"ts_event={self.ts_event}, " f"ts_init={self.ts_init})" ) + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + return self._trader_id + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + return self._strategy_id + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return self._instrument_id + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return self._client_order_id + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return self._venue_order_id + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return self._account_id + + @property + def reason(self) -> str: + """ + Return the reason the order was rejected. + + Returns + ------- + str + + """ + return self._reason + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + return self._reconciliation + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init + @staticmethod cdef OrderModifyRejected from_dict_c(dict values): Condition.not_none(values, "values") @@ -1831,14 +3833,14 @@ cdef class OrderModifyRejected(OrderEvent): Condition.not_none(obj, "obj") return { "type": "OrderModifyRejected", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), - "venue_order_id": obj.venue_order_id.to_str() if obj.venue_order_id is not None else None, - "account_id": obj.account_id.to_str() if obj.account_id is not None else None, + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "venue_order_id": obj.venue_order_id.value if obj.venue_order_id is not None else None, + "account_id": obj.account_id.value if obj.account_id is not None else None, "reason": obj.reason, - "event_id": obj.id.to_str(), + "event_id": obj.id.value, "ts_event": obj.ts_event, "ts_init": obj.ts_init, "reconciliation": obj.reconciliation, @@ -1904,67 +3906,206 @@ cdef class OrderCancelRejected(OrderEvent): reconciliation : bool, default False If the event was generated during reconciliation. - Raises - ------ - ValueError - If `reason` is not a valid string. - """ + Raises + ------ + ValueError + If `reason` is not a valid string. + """ + + def __init__( + self, + TraderId trader_id not None, + StrategyId strategy_id not None, + InstrumentId instrument_id not None, + ClientOrderId client_order_id not None, + VenueOrderId venue_order_id: Optional[VenueOrderId], + AccountId account_id: Optional[AccountId], + str reason not None, + UUID4 event_id not None, + uint64_t ts_event, + uint64_t ts_init, + bint reconciliation=False, + ): + Condition.valid_string(reason, "reason") + + self._strategy_id = strategy_id + self._trader_id = trader_id + self._instrument_id = instrument_id + self._client_order_id = client_order_id + self._venue_order_id = venue_order_id + self._account_id = account_id + self._reason = reason + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init + self._reconciliation = reconciliation + + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) + + def __str__(self) -> str: + return ( + f"{type(self).__name__}(" + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " + f"reason={self.reason}, " + f"ts_event={self.ts_event})" + ) + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(" + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " + f"reason={self.reason}, " + f"event_id={self.id}, " + f"ts_event={self.ts_event}, " + f"ts_init={self.ts_init})" + ) + + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + return self._trader_id + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + return self._strategy_id + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return self._instrument_id + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return self._client_order_id + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return self._venue_order_id + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return self._account_id + + @property + def reason(self) -> str: + """ + Return the reason the order was rejected. + + Returns + ------- + str + + """ + return self._reason + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool - def __init__( - self, - TraderId trader_id not None, - StrategyId strategy_id not None, - InstrumentId instrument_id not None, - ClientOrderId client_order_id not None, - VenueOrderId venue_order_id: Optional[VenueOrderId], - AccountId account_id: Optional[AccountId], - str reason not None, - UUID4 event_id not None, - uint64_t ts_event, - uint64_t ts_init, - bint reconciliation=False, - ): - Condition.valid_string(reason, "reason") - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - venue_order_id, - account_id, - event_id, - ts_event, - ts_init, - reconciliation, - ) + """ + return self._reconciliation - self.reason = reason + @property + def id(self) -> UUID4: + """ + The event message identifier. - def __str__(self) -> str: - return ( - f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None - f"reason={self.reason}, " - f"ts_event={self.ts_event})" - ) + Returns + ------- + UUID4 - def __repr__(self) -> str: - return ( - f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None - f"reason={self.reason}, " - f"event_id={self.id.to_str()}, " - f"ts_event={self.ts_event}, " - f"ts_init={self.ts_init})" - ) + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init @staticmethod cdef OrderCancelRejected from_dict_c(dict values): @@ -1990,14 +4131,14 @@ cdef class OrderCancelRejected(OrderEvent): Condition.not_none(obj, "obj") return { "type": "OrderCancelRejected", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), - "venue_order_id": obj.venue_order_id.to_str() if obj.venue_order_id is not None else None, - "account_id": obj.account_id.to_str() if obj.account_id is not None else None, + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "venue_order_id": obj.venue_order_id.value if obj.venue_order_id is not None else None, + "account_id": obj.account_id.value if obj.account_id is not None else None, "reason": obj.reason, - "event_id": obj.id.to_str(), + "event_id": obj.id.value, "ts_event": obj.ts_event, "ts_init": obj.ts_init, "reconciliation": obj.reconciliation, @@ -2090,30 +4231,34 @@ cdef class OrderUpdated(OrderEvent): ): Condition.positive(quantity, "quantity") - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - venue_order_id, - account_id, - event_id, - ts_event, - ts_init, - reconciliation, - ) + self._strategy_id = strategy_id + self._trader_id = trader_id + self._instrument_id = instrument_id + self._client_order_id = client_order_id + self._venue_order_id = venue_order_id + self._account_id = account_id + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init + self._reconciliation = reconciliation self.quantity = quantity self.price = price self.trigger_price = trigger_price + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) + def __str__(self) -> str: return ( f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " f"quantity={self.quantity.to_str()}, " f"price={self.price}, " f"trigger_price={self.trigger_price}, " @@ -2123,20 +4268,143 @@ cdef class OrderUpdated(OrderEvent): def __repr__(self) -> str: return ( f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id}, " # Can be None - f"account_id={self.account_id}, " # Can be None + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " f"quantity={self.quantity.to_str()}, " f"price={self.price}, " f"trigger_price={self.trigger_price}, " - f"event_id={self.id.to_str()}, " + f"event_id={self.id}, " f"ts_event={self.ts_event}, " f"ts_init={self.ts_init})" ) + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + return self._trader_id + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + return self._strategy_id + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return self._instrument_id + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return self._client_order_id + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return self._venue_order_id + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return self._account_id + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + return self._reconciliation + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init + @staticmethod cdef OrderUpdated from_dict_c(dict values): Condition.not_none(values, "values") @@ -2165,16 +4433,16 @@ cdef class OrderUpdated(OrderEvent): Condition.not_none(obj, "obj") return { "type": "OrderUpdated", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), - "venue_order_id": obj.venue_order_id.to_str() if obj.venue_order_id is not None else None, - "account_id": obj.account_id.to_str() if obj.account_id is not None else None, + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "venue_order_id": obj.venue_order_id.value if obj.venue_order_id is not None else None, + "account_id": obj.account_id.value if obj.account_id is not None else None, "quantity": str(obj.quantity), "price": str(obj.price) if obj.price is not None else None, "trigger_price": str(obj.trigger_price) if obj.trigger_price is not None else None, - "event_id": obj.id.to_str(), + "event_id": obj.id.value, "ts_event": obj.ts_event, "ts_init": obj.ts_init, "reconciliation": obj.reconciliation, @@ -2293,18 +4561,17 @@ cdef class OrderFilled(OrderEvent): if info is None: info = {} - super().__init__( - trader_id, - strategy_id, - instrument_id, - client_order_id, - venue_order_id, - account_id, - event_id, - ts_event, - ts_init, - reconciliation, - ) + + self._strategy_id = strategy_id + self._trader_id = trader_id + self._instrument_id = instrument_id + self._client_order_id = client_order_id + self._venue_order_id = venue_order_id + self._account_id = account_id + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init + self._reconciliation = reconciliation self.trade_id = trade_id self.position_id = position_id @@ -2317,18 +4584,24 @@ cdef class OrderFilled(OrderEvent): self.liquidity_side = liquidity_side self.info = info + def __eq__(self, Event other) -> bool: + return self.id == other.id + + def __hash__(self) -> int: + return hash(self.id) + def __str__(self) -> str: return ( f"{type(self).__name__}(" - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id.to_str()}, " - f"account_id={self.account_id.to_str()}, " - f"trade_id={self.trade_id.to_str()}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " + f"trade_id={self.trade_id}, " f"position_id={self.position_id}, " f"order_side={order_side_to_str(self.order_side)}, " f"order_type={order_type_to_str(self.order_type)}, " - f"last_qty={self.last_qty.to_str()}, " + f"last_qty={self.last_qty}, " f"last_px={self.last_px} {self.currency.code}, " f"commission={self.commission.to_str()}, " f"liquidity_side={liquidity_side_to_str(self.liquidity_side)}, " @@ -2338,25 +4611,148 @@ cdef class OrderFilled(OrderEvent): def __repr__(self) -> str: return ( f"{type(self).__name__}(" - f"trader_id={self.trader_id.to_str()}, " - f"strategy_id={self.strategy_id.to_str()}, " - f"instrument_id={self.instrument_id.to_str()}, " - f"client_order_id={self.client_order_id.to_str()}, " - f"venue_order_id={self.venue_order_id.to_str()}, " - f"account_id={self.account_id.to_str()}, " - f"trade_id={self.trade_id.to_str()}, " + f"trader_id={self.trader_id}, " + f"strategy_id={self.strategy_id}, " + f"instrument_id={self.instrument_id}, " + f"client_order_id={self.client_order_id}, " + f"venue_order_id={self.venue_order_id}, " + f"account_id={self.account_id}, " + f"trade_id={self.trade_id}, " f"position_id={self.position_id}, " f"order_side={order_side_to_str(self.order_side)}, " f"order_type={order_type_to_str(self.order_type)}, " - f"last_qty={self.last_qty.to_str()}, " + f"last_qty={self.last_qty}, " f"last_px={self.last_px} {self.currency.code}, " f"commission={self.commission.to_str()}, " f"liquidity_side={liquidity_side_to_str(self.liquidity_side)}, " - f"event_id={self.id.to_str()}, " + f"event_id={self.id}, " f"ts_event={self.ts_event}, " f"ts_init={self.ts_init})" ) + def set_client_order_id(self, ClientOrderId client_order_id): + self._client_order_id = client_order_id + + @property + def trader_id(self) -> TraderId: + """ + The trader ID associated with the event. + + Returns + ------- + TraderId + + """ + return self._trader_id + + @property + def strategy_id(self) -> TraderId: + """ + The strategy ID associated with the event. + + Returns + ------- + StrategyId + + """ + return self._strategy_id + + @property + def instrument_id(self) -> InstrumentId: + """ + The instrument ID associated with the event. + + Returns + ------- + InstrumentId + + """ + return self._instrument_id + + @property + def client_order_id(self) -> ClientOrderId: + """ + The client order ID associated with the event. + + Returns + ------- + ClientOrderId + + """ + return self._client_order_id + + @property + def venue_order_id(self) -> VenueOrderId | None: + """ + The venue order ID associated with the event. + + Returns + ------- + VenueOrderId or `None` + + """ + return self._venue_order_id + + @property + def account_id(self) -> AccountId | None: + """ + The account ID associated with the event. + + Returns + ------- + AccountId or `None` + + """ + return self._account_id + + @property + def reconciliation(self) -> bool: + """ + If the event was generated during reconciliation. + + Returns + ------- + bool + + """ + return self._reconciliation + + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init + @staticmethod cdef OrderFilled from_dict_c(dict values): Condition.not_none(values, "values") @@ -2390,14 +4786,14 @@ cdef class OrderFilled(OrderEvent): Condition.not_none(obj, "obj") return { "type": "OrderFilled", - "trader_id": obj.trader_id.to_str(), - "strategy_id": obj.strategy_id.to_str(), - "instrument_id": obj.instrument_id.to_str(), - "client_order_id": obj.client_order_id.to_str(), - "venue_order_id": obj.venue_order_id.to_str(), - "account_id": obj.account_id.to_str(), - "trade_id": obj.trade_id.to_str(), - "position_id": obj.position_id.to_str() if obj.position_id else None, + "trader_id": obj.trader_id.value, + "strategy_id": obj.strategy_id.value, + "instrument_id": obj.instrument_id.value, + "client_order_id": obj.client_order_id.value, + "venue_order_id": obj.venue_order_id.value, + "account_id": obj.account_id.value, + "trade_id": obj.trade_id.value, + "position_id": obj.position_id.value if obj.position_id else None, "order_side": order_side_to_str(obj.order_side), "order_type": order_type_to_str(obj.order_type), "last_qty": str(obj.last_qty), @@ -2405,7 +4801,7 @@ cdef class OrderFilled(OrderEvent): "currency": obj.currency.code, "commission": obj.commission.to_str(), "liquidity_side": liquidity_side_to_str(obj.liquidity_side), - "event_id": obj.id.to_str(), + "event_id": obj.id.value, "ts_event": obj.ts_event, "ts_init": obj.ts_init, "info": msgspec.json.encode(obj.info) if obj.info is not None else None, diff --git a/nautilus_trader/model/events/position.pxd b/nautilus_trader/model/events/position.pxd index ae44ebb4792e..50d0481c7c65 100644 --- a/nautilus_trader/model/events/position.pxd +++ b/nautilus_trader/model/events/position.pxd @@ -34,6 +34,10 @@ from nautilus_trader.model.position cimport Position cdef class PositionEvent(Event): + cdef UUID4 _event_id + cdef uint64_t _ts_event + cdef uint64_t _ts_init + cdef readonly TraderId trader_id """The trader ID associated with the event.\n\n:returns: `TraderId`""" cdef readonly StrategyId strategy_id diff --git a/nautilus_trader/model/events/position.pyx b/nautilus_trader/model/events/position.pyx index d978b949e51c..e0d4976e6009 100644 --- a/nautilus_trader/model/events/position.pyx +++ b/nautilus_trader/model/events/position.pyx @@ -130,8 +130,6 @@ cdef class PositionEvent(Event): uint64_t ts_event, uint64_t ts_init, ): - super().__init__(event_id, ts_event, ts_init) - self.trader_id = trader_id self.strategy_id = strategy_id self.instrument_id = instrument_id @@ -156,6 +154,16 @@ cdef class PositionEvent(Event): self.ts_closed = ts_closed self.duration_ns = duration_ns + self._event_id = event_id + self._ts_event = ts_event + self._ts_init = ts_init + + def __eq__(self, Event other) -> bool: + return self._event_id == other.id + + def __hash__(self) -> int: + return hash(self._event_id) + def __str__(self) -> str: return ( f"{type(self).__name__}(" @@ -203,12 +211,48 @@ cdef class PositionEvent(Event): f"realized_pnl={self.realized_pnl.to_str()}, " f"unrealized_pnl={self.unrealized_pnl.to_str()}, " f"ts_opened={self.ts_opened}, " - f"ts_last={self.ts_event}, " + f"ts_last={self._ts_event}, " f"ts_closed={self.ts_closed}, " f"duration_ns={self.duration_ns}, " - f"event_id={self.id.to_str()})" + f"event_id={self._event_id.to_str()})" ) + @property + def id(self) -> UUID4: + """ + The event message identifier. + + Returns + ------- + UUID4 + + """ + return self._event_id + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the event occurred. + + Returns + ------- + int + + """ + return self._ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._ts_init + cdef class PositionOpened(PositionEvent): """ @@ -390,9 +434,9 @@ cdef class PositionOpened(PositionEvent): "avg_px_open": obj.avg_px_open, "realized_pnl": obj.realized_pnl.to_str(), "duration_ns": obj.duration_ns, - "event_id": obj.id.to_str(), - "ts_event": obj.ts_event, - "ts_init": obj.ts_init, + "event_id": obj._event_id.to_str(), + "ts_event": obj._ts_event, + "ts_init": obj._ts_init, } @staticmethod @@ -655,10 +699,10 @@ cdef class PositionChanged(PositionEvent): "realized_return": obj.realized_return, "realized_pnl": obj.realized_pnl.to_str(), "unrealized_pnl": obj.unrealized_pnl.to_str(), - "event_id": obj.id.to_str(), + "event_id": obj._event_id.to_str(), "ts_opened": obj.ts_opened, - "ts_event": obj.ts_event, - "ts_init": obj.ts_init, + "ts_event": obj._ts_event, + "ts_init": obj._ts_init, } @staticmethod @@ -926,11 +970,11 @@ cdef class PositionClosed(PositionEvent): "avg_px_close": obj.avg_px_close, "realized_return": obj.realized_return, "realized_pnl": obj.realized_pnl.to_str(), - "event_id": obj.id.to_str(), + "event_id": obj._event_id.to_str(), "ts_opened": obj.ts_opened, "ts_closed": obj.ts_closed, "duration_ns": obj.duration_ns, - "ts_init": obj.ts_init, + "ts_init": obj._ts_init, } @staticmethod diff --git a/nautilus_trader/model/identifiers.pxd b/nautilus_trader/model/identifiers.pxd index 2c59c479520a..1232fe2c8ea3 100644 --- a/nautilus_trader/model/identifiers.pxd +++ b/nautilus_trader/model/identifiers.pxd @@ -45,7 +45,6 @@ cdef class Venue(Identifier): @staticmethod cdef Venue from_mem_c(Venue_t mem) - cpdef bint is_synthetic(self) @@ -54,44 +53,57 @@ cdef class InstrumentId(Identifier): @staticmethod cdef InstrumentId from_mem_c(InstrumentId_t mem) - @staticmethod cdef InstrumentId from_str_c(str value) - cpdef bint is_synthetic(self) cdef class ComponentId(Identifier): cdef ComponentId_t _mem + @staticmethod + cdef ComponentId from_mem_c(ComponentId_t mem) + cdef class ClientId(Identifier): cdef ClientId_t _mem + @staticmethod + cdef ClientId from_mem_c(ClientId_t mem) + cdef class TraderId(Identifier): cdef TraderId_t _mem + @staticmethod + cdef TraderId from_mem_c(TraderId_t mem) + cpdef str get_tag(self) cdef class StrategyId(Identifier): cdef StrategyId_t _mem - cpdef str get_tag(self) - cpdef bint is_external(self) - + @staticmethod + cdef StrategyId from_mem_c(StrategyId_t mem) @staticmethod cdef StrategyId external_c() + cpdef str get_tag(self) + cpdef bint is_external(self) cdef class ExecAlgorithmId(Identifier): cdef ExecAlgorithmId_t _mem + @staticmethod + cdef ExecAlgorithmId from_mem_c(ExecAlgorithmId_t mem) + cdef class AccountId(Identifier): cdef AccountId_t _mem + @staticmethod + cdef AccountId from_mem_c(AccountId_t mem) cpdef str get_issuer(self) cpdef str get_id(self) @@ -99,16 +111,24 @@ cdef class AccountId(Identifier): cdef class ClientOrderId(Identifier): cdef ClientOrderId_t _mem + @staticmethod + cdef ClientOrderId from_mem_c(ClientOrderId_t mem) cpdef bint is_this_trader(self, TraderId trader_id) cdef class VenueOrderId(Identifier): cdef VenueOrderId_t _mem + @staticmethod + cdef VenueOrderId from_mem_c(VenueOrderId_t mem) + cdef class OrderListId(Identifier): cdef OrderListId_t _mem + @staticmethod + cdef OrderListId from_mem_c(OrderListId_t mem) + cdef class PositionId(Identifier): cdef PositionId_t _mem diff --git a/nautilus_trader/model/identifiers.pyx b/nautilus_trader/model/identifiers.pyx index 21cf12cac1bd..d82b8d165bad 100644 --- a/nautilus_trader/model/identifiers.pyx +++ b/nautilus_trader/model/identifiers.pyx @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from libc.stdio cimport printf +from libc.string cimport strcmp from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.rust.model cimport account_id_hash @@ -135,10 +135,10 @@ cdef class Symbol(Identifier): def __eq__(self, Symbol other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.value == other._mem.value + return strcmp(self._mem.value, other._mem.value) == 0 - def __hash__ (self) -> int: - return symbol_hash(&self._mem) + def __hash__(self) -> int: + return hash(self.to_str()) @staticmethod cdef Symbol from_mem_c(Symbol_t mem): @@ -178,10 +178,10 @@ cdef class Venue(Identifier): def __eq__(self, Venue other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.value == other._mem.value + return strcmp(self._mem.value, other._mem.value) == 0 - def __hash__ (self) -> int: - return venue_hash(&self._mem) + def __hash__(self) -> int: + return hash(self.to_str()) @staticmethod cdef Venue from_mem_c(Venue_t mem): @@ -259,13 +259,10 @@ cdef class InstrumentId(Identifier): def __eq__(self, InstrumentId other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.symbol.value == other._mem.symbol.value and self._mem.venue.value == other._mem.venue.value + return strcmp(self._mem.symbol.value, other._mem.symbol.value) == 0 and strcmp(self._mem.venue.value, other._mem.venue.value) == 0 - def __hash__ (self) -> int: - return instrument_id_hash(&self._mem) - - cdef str to_str(self): - return cstr_to_pystr(instrument_id_to_cstr(&self._mem)) + def __hash__(self) -> int: + return hash(self.to_str()) @staticmethod cdef InstrumentId from_mem_c(InstrumentId_t mem): @@ -279,6 +276,9 @@ cdef class InstrumentId(Identifier): instrument_id._mem = instrument_id_new_from_cstr(pystr_to_cstr(value)) return instrument_id + cdef str to_str(self): + return cstr_to_pystr(instrument_id_to_cstr(&self._mem)) + @staticmethod def from_str(value: str) -> InstrumentId: """ @@ -344,10 +344,16 @@ cdef class ComponentId(Identifier): def __eq__(self, ComponentId other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.value == other._mem.value + return strcmp(self._mem.value, other._mem.value) == 0 def __hash__(self) -> int: - return component_id_hash(&self._mem) + return hash(self.to_str()) + + @staticmethod + cdef ComponentId from_mem_c(ComponentId_t mem): + cdef ComponentId component_id = ComponentId.__new__(ComponentId) + component_id._mem = mem + return component_id cdef str to_str(self): return ustr_to_pystr(self._mem.value) @@ -385,10 +391,16 @@ cdef class ClientId(Identifier): def __eq__(self, ClientId other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.value == other._mem.value + return strcmp(self._mem.value, other._mem.value) == 0 def __hash__(self) -> int: - return client_id_hash(&self._mem) + return hash(self.to_str()) + + @staticmethod + cdef ClientId from_mem_c(ClientId_t mem): + cdef ClientId client_id = ClientId.__new__(ClientId) + client_id._mem = mem + return client_id cdef str to_str(self): return ustr_to_pystr(self._mem.value) @@ -435,10 +447,16 @@ cdef class TraderId(Identifier): def __eq__(self, TraderId other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.value == other._mem.value + return strcmp(self._mem.value, other._mem.value) == 0 def __hash__(self) -> int: - return trader_id_hash(&self._mem) + return hash(self.to_str()) + + @staticmethod + cdef TraderId from_mem_c(TraderId_t mem): + cdef TraderId trader_id = TraderId.__new__(TraderId) + trader_id._mem = mem + return trader_id cdef str to_str(self): return ustr_to_pystr(self._mem.value) @@ -502,10 +520,20 @@ cdef class StrategyId(Identifier): def __eq__(self, StrategyId other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.value == other._mem.value + return strcmp(self._mem.value, other._mem.value) == 0 def __hash__(self) -> int: - return strategy_id_hash(&self._mem) + return hash(self.to_str()) + + @staticmethod + cdef StrategyId from_mem_c(StrategyId_t mem): + cdef StrategyId strategy_id = StrategyId.__new__(StrategyId) + strategy_id._mem = mem + return strategy_id + + @staticmethod + cdef StrategyId external_c(): + return EXTERNAL_STRATEGY_ID cdef str to_str(self): return ustr_to_pystr(self._mem.value) @@ -534,10 +562,6 @@ cdef class StrategyId(Identifier): """ return self == EXTERNAL_STRATEGY_ID - @staticmethod - cdef StrategyId external_c(): - return EXTERNAL_STRATEGY_ID - cdef class ExecAlgorithmId(Identifier): """ @@ -567,10 +591,16 @@ cdef class ExecAlgorithmId(Identifier): def __eq__(self, ExecAlgorithmId other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.value == other._mem.value + return strcmp(self._mem.value, other._mem.value) == 0 def __hash__(self) -> int: - return exec_algorithm_id_hash(&self._mem) + return hash(self.to_str()) + + @staticmethod + cdef ExecAlgorithmId from_mem_c(ExecAlgorithmId_t mem): + cdef ExecAlgorithmId exec_algorithm_id = ExecAlgorithmId.__new__(ExecAlgorithmId) + exec_algorithm_id._mem = mem + return exec_algorithm_id cdef str to_str(self): return ustr_to_pystr(self._mem.value) @@ -616,10 +646,16 @@ cdef class AccountId(Identifier): def __eq__(self, AccountId other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.value == other._mem.value + return strcmp(self._mem.value, other._mem.value) == 0 - def __hash__ (self) -> int: - return account_id_hash(&self._mem) + def __hash__(self) -> int: + return hash(self.to_str()) + + @staticmethod + cdef AccountId from_mem_c(AccountId_t mem): + cdef AccountId account_id = AccountId.__new__(AccountId) + account_id._mem = mem + return account_id cdef str to_str(self): return ustr_to_pystr(self._mem.value) @@ -679,10 +715,16 @@ cdef class ClientOrderId(Identifier): def __eq__(self, ClientOrderId other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.value == other._mem.value + return strcmp(self._mem.value, other._mem.value) == 0 - def __hash__ (self) -> int: - return client_order_id_hash(&self._mem) + def __hash__(self) -> int: + return hash(self.to_str()) + + @staticmethod + cdef ClientOrderId from_mem_c(ClientOrderId_t mem): + cdef ClientOrderId client_order_id = ClientOrderId.__new__(ClientOrderId) + client_order_id._mem = mem + return client_order_id cdef str to_str(self): return ustr_to_pystr(self._mem.value) @@ -738,10 +780,16 @@ cdef class VenueOrderId(Identifier): def __eq__(self, VenueOrderId other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.value == other._mem.value + return strcmp(self._mem.value, other._mem.value) == 0 + + def __hash__(self) -> int: + return hash(self.to_str()) - def __hash__ (self) -> int: - return venue_order_id_hash(&self._mem) + @staticmethod + cdef VenueOrderId from_mem_c(VenueOrderId_t mem): + cdef VenueOrderId venue_order_id = VenueOrderId.__new__(VenueOrderId) + venue_order_id._mem = mem + return venue_order_id cdef str to_str(self): return ustr_to_pystr(self._mem.value) @@ -775,10 +823,16 @@ cdef class OrderListId(Identifier): def __eq__(self, OrderListId other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.value == other._mem.value + return strcmp(self._mem.value, other._mem.value) == 0 - def __hash__ (self) -> int: - return order_list_id_hash(&self._mem) + def __hash__(self) -> int: + return hash(self.to_str()) + + @staticmethod + cdef OrderListId from_mem_c(OrderListId_t mem): + cdef OrderListId order_list_id = OrderListId.__new__(OrderListId) + order_list_id._mem = mem + return order_list_id cdef str to_str(self): return ustr_to_pystr(self._mem.value) @@ -812,16 +866,10 @@ cdef class PositionId(Identifier): def __eq__(self, PositionId other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.value == other._mem.value - - def __hash__ (self) -> int: - return position_id_hash(&self._mem) + return strcmp(self._mem.value, other._mem.value) == 0 - cdef str to_str(self): - return ustr_to_pystr(self._mem.value) - - cdef bint is_virtual_c(self): - return self.to_str().startswith("P-") + def __hash__(self) -> int: + return hash(self.to_str()) @staticmethod cdef PositionId from_mem_c(PositionId_t mem): @@ -829,6 +877,12 @@ cdef class PositionId(Identifier): position_id._mem = mem return position_id + cdef str to_str(self): + return ustr_to_pystr(self._mem.value) + + cdef bint is_virtual_c(self): + return self.to_str().startswith("P-") + cdef class TradeId(Identifier): """ @@ -867,16 +921,16 @@ cdef class TradeId(Identifier): def __eq__(self, TradeId other) -> bool: if other is None: raise RuntimeError("other was None in __eq__") - return self._mem.value == other._mem.value + return strcmp(self._mem.value, other._mem.value) == 0 - def __hash__ (self) -> int: - return trade_id_hash(&self._mem) - - cdef str to_str(self): - return ustr_to_pystr(self._mem.value) + def __hash__(self) -> int: + return hash(self.to_str()) @staticmethod cdef TradeId from_mem_c(TradeId_t mem): cdef TradeId trade_id = TradeId.__new__(TradeId) trade_id._mem = mem return trade_id + + cdef str to_str(self): + return ustr_to_pystr(self._mem.value) diff --git a/nautilus_trader/model/objects.pyx b/nautilus_trader/model/objects.pyx index 001b08930915..a4886ec3762b 100644 --- a/nautilus_trader/model/objects.pyx +++ b/nautilus_trader/model/objects.pyx @@ -26,6 +26,7 @@ from cpython.object cimport Py_GT from cpython.object cimport Py_LE from cpython.object cimport Py_LT from cpython.object cimport PyObject_RichCompareBool +from libc.math cimport isnan from libc.stdint cimport int64_t from libc.stdint cimport uint8_t from libc.stdint cimport uint64_t @@ -104,6 +105,10 @@ cdef class Quantity: def __init__(self, double value, uint8_t precision): Condition.true(precision <= 9, f"invalid `precision` greater than max 9, was {precision}") + if isnan(value): + raise ValueError( + f"invalid `value`, was {value:_}", + ) if value > QUANTITY_MAX: raise ValueError( f"invalid `value` greater than `QUANTITY_MAX` {QUANTITY_MAX:_}, was {value:_}", @@ -223,6 +228,18 @@ cdef class Quantity: def __repr__(self) -> str: return f"{type(self).__name__}('{self}')" + @property + def raw(self) -> uint64_t: + """ + Return the raw memory representation of the quantity value. + + Returns + ------- + uint64_t + + """ + return self._mem.raw + @property def precision(self) -> int: """ @@ -496,6 +513,10 @@ cdef class Price: def __init__(self, double value, uint8_t precision): Condition.true(precision <= 9, f"invalid `precision` greater than max 9, was {precision}") + if isnan(value): + raise ValueError( + f"invalid `value`, was {value:_}", + ) if value > PRICE_MAX: raise ValueError( f"invalid `value` greater than `PRICE_MAX` {PRICE_MAX:_}, was {value:_}", @@ -615,6 +636,18 @@ cdef class Price: def __repr__(self) -> str: return f"{type(self).__name__}('{self}')" + @property + def raw(self) -> int64_t: + """ + Return the raw memory representation of the price value. + + Returns + ------- + int64_t + + """ + return self._mem.raw + @property def precision(self) -> int: """ @@ -828,6 +861,10 @@ cdef class Money: def __init__(self, value, Currency currency not None): cdef double value_f64 = 0.0 if value is None else float(value) + if isnan(value_f64): + raise ValueError( + f"invalid `value`, was {value:_}", + ) if value_f64 > MONEY_MAX: raise ValueError( f"invalid `value` greater than `MONEY_MAX` {MONEY_MAX:_}, was {value:_}", @@ -953,6 +990,18 @@ cdef class Money: def __repr__(self) -> str: return f"{type(self).__name__}('{str(self)}', {self.currency_code_c()})" + @property + def raw(self) -> int64_t: + """ + Return the raw memory representation of the money amount. + + Returns + ------- + int64_t + + """ + return self._mem.raw + @property def currency(self) -> Currency: return Currency.from_str_c(self.currency_code_c()) diff --git a/nautilus_trader/model/orderbook/book.pyx b/nautilus_trader/model/orderbook/book.pyx index 6a185bc71487..3170375900bb 100644 --- a/nautilus_trader/model/orderbook/book.pyx +++ b/nautilus_trader/model/orderbook/book.pyx @@ -13,6 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +import pickle from operator import itemgetter import pandas as pd @@ -111,6 +112,27 @@ cdef class OrderBook(Data): f"{self.pprint()}" ) + def __getstate__(self): + cdef list orders = [o for level in self.bids() + self.asks() for o in level.orders()] + return ( + self.instrument_id.value, + self.book_type.value, + pickle.dumps(orders), + ) + + def __setstate__(self, state): + cdef InstrumentId instrument_id = InstrumentId.from_str_c(state[0]) + self._mem = orderbook_new( + instrument_id._mem, + state[1], + ) + + cdef list orders = pickle.loads(state[2]) + + cdef int64_t i + for i in range(len(orders)): + self.add(orders[i], 0, 0) + @property def instrument_id(self) -> InstrumentId: """ @@ -524,12 +546,12 @@ cdef class OrderBook(Data): ) cdef CVec raw_fills_vec = orderbook_simulate_fills(&self._mem, submit_order) - cdef tuple[Price_t, Quantity_t]* raw_fills = raw_fills_vec.ptr + cdef (Price_t, Quantity_t)* raw_fills = <(Price_t, Quantity_t)*>raw_fills_vec.ptr cdef list fills = [] cdef: uint64_t i - tuple[Price_t, Quantity_t] raw_fill + (Price_t, Quantity_t) raw_fill Price fill_price Quantity fill_size for i in range(raw_fills_vec.len): diff --git a/nautilus_trader/model/orders/base.pxd b/nautilus_trader/model/orders/base.pxd index af89948646d0..c2a0cec5bf6d 100644 --- a/nautilus_trader/model/orders/base.pxd +++ b/nautilus_trader/model/orders/base.pxd @@ -50,8 +50,9 @@ from nautilus_trader.model.objects cimport Price from nautilus_trader.model.objects cimport Quantity -cdef tuple VALID_STOP_ORDER_TYPES -cdef tuple VALID_LIMIT_ORDER_TYPES +cdef set VALID_STOP_ORDER_TYPES +cdef set VALID_LIMIT_ORDER_TYPES +cdef set LOCAL_ACTIVE_ORDER_STATUS cdef class Order: @@ -156,6 +157,7 @@ cdef class Order: cdef bint is_passive_c(self) cdef bint is_aggressive_c(self) cdef bint is_emulated_c(self) + cdef bint is_active_local_c(self) cdef bint is_primary_c(self) cdef bint is_spawned_c(self) cdef bint is_contingency_c(self) diff --git a/nautilus_trader/model/orders/base.pyx b/nautilus_trader/model/orders/base.pyx index 76d878c78e3f..279af9fcad57 100644 --- a/nautilus_trader/model/orders/base.pyx +++ b/nautilus_trader/model/orders/base.pyx @@ -37,6 +37,7 @@ from nautilus_trader.model.events.order cimport OrderAccepted from nautilus_trader.model.events.order cimport OrderCanceled from nautilus_trader.model.events.order cimport OrderCancelRejected from nautilus_trader.model.events.order cimport OrderDenied +from nautilus_trader.model.events.order cimport OrderEmulated from nautilus_trader.model.events.order cimport OrderEvent from nautilus_trader.model.events.order cimport OrderExpired from nautilus_trader.model.events.order cimport OrderFilled @@ -45,6 +46,7 @@ from nautilus_trader.model.events.order cimport OrderModifyRejected from nautilus_trader.model.events.order cimport OrderPendingCancel from nautilus_trader.model.events.order cimport OrderPendingUpdate from nautilus_trader.model.events.order cimport OrderRejected +from nautilus_trader.model.events.order cimport OrderReleased from nautilus_trader.model.events.order cimport OrderSubmitted from nautilus_trader.model.events.order cimport OrderTriggered from nautilus_trader.model.events.order cimport OrderUpdated @@ -53,38 +55,51 @@ from nautilus_trader.model.objects cimport Money from nautilus_trader.model.objects cimport Quantity -VALID_STOP_ORDER_TYPES = ( +VALID_STOP_ORDER_TYPES = { OrderType.STOP_MARKET, OrderType.STOP_LIMIT, OrderType.MARKET_IF_TOUCHED, OrderType.LIMIT_IF_TOUCHED, -) +} -VALID_LIMIT_ORDER_TYPES = ( +VALID_LIMIT_ORDER_TYPES = { OrderType.LIMIT, OrderType.STOP_LIMIT, OrderType.LIMIT_IF_TOUCHED, OrderType.MARKET_TO_LIMIT, -) +} + +LOCAL_ACTIVE_ORDER_STATUS = { + OrderStatus.INITIALIZED, + OrderStatus.EMULATED, + OrderStatus.RELEASED, +} # OrderStatus being used as trigger cdef dict _ORDER_STATE_TABLE = { (OrderStatus.INITIALIZED, OrderStatus.DENIED): OrderStatus.DENIED, + (OrderStatus.INITIALIZED, OrderStatus.EMULATED): OrderStatus.EMULATED, # Emulated orders + (OrderStatus.INITIALIZED, OrderStatus.RELEASED): OrderStatus.RELEASED, # Emulated orders (OrderStatus.INITIALIZED, OrderStatus.SUBMITTED): OrderStatus.SUBMITTED, - (OrderStatus.INITIALIZED, OrderStatus.ACCEPTED): OrderStatus.ACCEPTED, # Covers external orders - (OrderStatus.INITIALIZED, OrderStatus.REJECTED): OrderStatus.REJECTED, # Covers external orders - (OrderStatus.INITIALIZED, OrderStatus.CANCELED): OrderStatus.CANCELED, # Covers emulated and external orders - (OrderStatus.INITIALIZED, OrderStatus.EXPIRED): OrderStatus.EXPIRED, # Covers emulated and external orders - (OrderStatus.INITIALIZED, OrderStatus.TRIGGERED): OrderStatus.TRIGGERED, # Covers emulated and external orders + (OrderStatus.INITIALIZED, OrderStatus.REJECTED): OrderStatus.REJECTED, # External orders + (OrderStatus.INITIALIZED, OrderStatus.ACCEPTED): OrderStatus.ACCEPTED, # External orders + (OrderStatus.INITIALIZED, OrderStatus.CANCELED): OrderStatus.CANCELED, # External orders + (OrderStatus.INITIALIZED, OrderStatus.EXPIRED): OrderStatus.EXPIRED, # External orders + (OrderStatus.INITIALIZED, OrderStatus.TRIGGERED): OrderStatus.TRIGGERED, # External orders + (OrderStatus.EMULATED, OrderStatus.CANCELED): OrderStatus.CANCELED, # Emulated orders + (OrderStatus.EMULATED, OrderStatus.EXPIRED): OrderStatus.EXPIRED, # Emulated orders + (OrderStatus.EMULATED, OrderStatus.RELEASED): OrderStatus.RELEASED, # Emulated orders + (OrderStatus.RELEASED, OrderStatus.DENIED): OrderStatus.DENIED, # Emulated orders + (OrderStatus.RELEASED, OrderStatus.SUBMITTED): OrderStatus.SUBMITTED, # Emulated orders + (OrderStatus.RELEASED, OrderStatus.CANCELED): OrderStatus.CANCELED, # Execution Algo (OrderStatus.SUBMITTED, OrderStatus.PENDING_UPDATE): OrderStatus.PENDING_UPDATE, (OrderStatus.SUBMITTED, OrderStatus.PENDING_CANCEL): OrderStatus.PENDING_CANCEL, (OrderStatus.SUBMITTED, OrderStatus.REJECTED): OrderStatus.REJECTED, - (OrderStatus.SUBMITTED, OrderStatus.CANCELED): OrderStatus.CANCELED, # Covers FOK and IOC cases + (OrderStatus.SUBMITTED, OrderStatus.CANCELED): OrderStatus.CANCELED, # FOK and IOC cases (OrderStatus.SUBMITTED, OrderStatus.ACCEPTED): OrderStatus.ACCEPTED, - (OrderStatus.SUBMITTED, OrderStatus.TRIGGERED): OrderStatus.TRIGGERED, # Covers emulated StopLimit order (OrderStatus.SUBMITTED, OrderStatus.PARTIALLY_FILLED): OrderStatus.PARTIALLY_FILLED, (OrderStatus.SUBMITTED, OrderStatus.FILLED): OrderStatus.FILLED, - (OrderStatus.ACCEPTED, OrderStatus.REJECTED): OrderStatus.REJECTED, # Covers StopLimit order + (OrderStatus.ACCEPTED, OrderStatus.REJECTED): OrderStatus.REJECTED, # StopLimit order (OrderStatus.ACCEPTED, OrderStatus.PENDING_UPDATE): OrderStatus.PENDING_UPDATE, (OrderStatus.ACCEPTED, OrderStatus.PENDING_CANCEL): OrderStatus.PENDING_CANCEL, (OrderStatus.ACCEPTED, OrderStatus.CANCELED): OrderStatus.CANCELED, @@ -342,13 +357,16 @@ cdef class Order: return self.order_type == OrderType.MARKET cdef bint is_emulated_c(self): - return self.emulation_trigger != TriggerType.NO_TRIGGER + return self._fsm.state == OrderStatus.EMULATED + + cdef bint is_active_local_c(self): + return self._fsm.state in LOCAL_ACTIVE_ORDER_STATUS cdef bint is_primary_c(self): - return self.exec_algorithm_id is not None and self.exec_spawn_id is None + return self.exec_algorithm_id is not None and self.exec_spawn_id == self.client_order_id cdef bint is_spawned_c(self): - return self.exec_spawn_id is not None + return self.exec_algorithm_id is not None and self.exec_spawn_id != self.client_order_id cdef bint is_contingency_c(self): return self.contingency_type != ContingencyType.NO_CONTINGENCY @@ -383,8 +401,6 @@ cdef class Order: ) cdef bint is_inflight_c(self): - if self.emulation_trigger != TriggerType.NO_TRIGGER: - return False return ( self._fsm.state == OrderStatus.SUBMITTED or self._fsm.state == OrderStatus.PENDING_CANCEL @@ -589,6 +605,23 @@ cdef class Order: """ return self.is_emulated_c() + @property + def is_active_local(self): + """ + Return whether the order is active and held in the local system. + + An order is considered active local when its status is any of; + - ``INITIALIZED`` + - ``EMULATED`` + - ``RELEASED`` + + Returns + ------- + bool + + """ + return self.is_active_local_c() + @property def is_primary(self): """ @@ -919,6 +952,13 @@ cdef class Order: elif isinstance(event, OrderDenied): self._fsm.trigger(OrderStatus.DENIED) self._denied(event) + elif isinstance(event, OrderEmulated): + self._fsm.trigger(OrderStatus.EMULATED) + # self._emulated(event) + elif isinstance(event, OrderReleased): + self._fsm.trigger(OrderStatus.RELEASED) + self.emulation_trigger = TriggerType.NO_TRIGGER + # self._released(event) elif isinstance(event, OrderSubmitted): self._fsm.trigger(OrderStatus.SUBMITTED) self._submitted(event) diff --git a/nautilus_trader/model/position.pxd b/nautilus_trader/model/position.pxd index faf11e0d407a..1f37b17053e2 100644 --- a/nautilus_trader/model/position.pxd +++ b/nautilus_trader/model/position.pxd @@ -124,6 +124,7 @@ cdef class Position: cpdef Money total_pnl(self, Price last) cpdef list commissions(self) + cdef void _check_duplicate_trade_id(self, OrderFilled fill) cdef void _handle_buy_order_fill(self, OrderFilled fill) cdef void _handle_sell_order_fill(self, OrderFilled fill) cdef double _calculate_avg_px(self, double avg_px, double qty, double last_px, double last_qty) diff --git a/nautilus_trader/model/position.pyx b/nautilus_trader/model/position.pyx index e3ac730f72f4..696d31bfffff 100644 --- a/nautilus_trader/model/position.pyx +++ b/nautilus_trader/model/position.pyx @@ -433,7 +433,7 @@ cdef class Position: """ Condition.not_none(fill, "fill") - Condition.not_in(fill.trade_id, self._trade_ids, "fill.trade_id", "_trade_ids") + self._check_duplicate_trade_id(fill) if self.side == PositionSide.FLAT: # Reset position @@ -619,6 +619,20 @@ cdef class Position: """ return list(self._commissions.values()) + cdef void _check_duplicate_trade_id(self, OrderFilled fill): + # Check all previous fills for matching trade ID and composite key + cdef: + OrderFilled p_fill + for p_fill in self._events: + if fill.trade_id != p_fill.trade_id: + continue + if ( + fill.order_side == p_fill.order_side + and fill.last_px == p_fill.last_px + and fill.last_qty == p_fill.last_qty + ): + raise ValueError(f"Duplicate {fill.trade_id!r} in events {fill} {p_fill}") + cdef void _handle_buy_order_fill(self, OrderFilled fill): # Initialize realized PnL for fill cdef double realized_pnl diff --git a/nautilus_trader/msgbus/bus.pyx b/nautilus_trader/msgbus/bus.pyx index 1dd1ca338cf6..4e1bbfe327db 100644 --- a/nautilus_trader/msgbus/bus.pyx +++ b/nautilus_trader/msgbus/bus.pyx @@ -388,7 +388,7 @@ cdef class MessageBus: subscription if you are certain of what you're doing**. If an inappropriate priority is assigned then the handler may receive messages before core system components have been able to process necessary calculations and - produce potential side effects for logically sound behaviour. + produce potential side effects for logically sound behavior. """ Condition.valid_string(topic, "topic") diff --git a/nautilus_trader/persistence/loaders.py b/nautilus_trader/persistence/loaders.py index 4fccd852e492..cd056a615b67 100644 --- a/nautilus_trader/persistence/loaders.py +++ b/nautilus_trader/persistence/loaders.py @@ -142,13 +142,11 @@ def load(file_path) -> pd.DataFrame: df = df.rename( columns={ "ask_amount": "ask_size", - "ask_price": "ask", - "bid_price": "bid", "bid_amount": "bid_size", }, ) - return df[["bid", "ask", "bid_size", "ask_size"]] + return df[["bid_price", "ask_price", "bid_size", "ask_size"]] class ParquetTickDataLoader: diff --git a/nautilus_trader/persistence/wranglers.pxd b/nautilus_trader/persistence/wranglers.pxd index 1e34d6837f56..d20319fc70dd 100644 --- a/nautilus_trader/persistence/wranglers.pxd +++ b/nautilus_trader/persistence/wranglers.pxd @@ -31,18 +31,18 @@ cdef class QuoteTickDataWrangler: cpdef QuoteTick _build_tick_from_raw( self, - int64_t raw_bid, - int64_t raw_ask, - uint64_t raw_bid_size, - uint64_t raw_ask_size, + int64_t bid_price_raw, + int64_t ask_price_raw, + uint64_t bid_size_raw, + uint64_t ask_size_raw, uint64_t ts_event, uint64_t ts_init, ) cpdef QuoteTick _build_tick( self, - double bid, - double ask, + double bid_price, + double ask_price, double bid_size, double ask_size, uint64_t ts_event, @@ -56,8 +56,8 @@ cdef class TradeTickDataWrangler: cpdef TradeTick _build_tick_from_raw( self, - int64_t raw_price, - uint64_t raw_size, + int64_t price_raw, + uint64_t size_raw, AggressorSide aggressor_side, str trade_id, uint64_t ts_event, diff --git a/nautilus_trader/persistence/wranglers.pyx b/nautilus_trader/persistence/wranglers.pyx index c3f6b364453f..a5818df809fb 100644 --- a/nautilus_trader/persistence/wranglers.pyx +++ b/nautilus_trader/persistence/wranglers.pyx @@ -87,7 +87,7 @@ cdef class QuoteTickDataWrangler: """ Process the given tick dataset into Nautilus `QuoteTick` objects. - Expects columns ['bid', 'ask'] with 'timestamp' index. + Expects columns ['bid_price', 'ask_price'] with 'timestamp' index. Note: The 'bid_size' and 'ask_size' columns are optional, will then use the `default_volume`. @@ -112,6 +112,12 @@ cdef class QuoteTickDataWrangler: as_utc_index(data) + columns = { + "bid": "bid_price", + "ask": "ask_price", + } + data.rename(columns=columns, inplace=True) + if "bid_size" not in data.columns: data["bid_size"] = float(default_volume) if "ask_size" not in data.columns: @@ -122,8 +128,8 @@ cdef class QuoteTickDataWrangler: return list(map( self._build_tick, - data["bid"], - data["ask"], + data["bid_price"], + data["ask_price"], data["bid_size"], data["ask_size"], ts_events, @@ -183,29 +189,29 @@ cdef class QuoteTickDataWrangler: ask_data["volume"] = float(default_volume * 4) cdef dict data_open = { - "bid": bid_data["open"], - "ask": ask_data["open"], + "bid_price": bid_data["open"], + "ask_price": ask_data["open"], "bid_size": bid_data["volume"] / 4, "ask_size": ask_data["volume"] / 4, } cdef dict data_high = { - "bid": bid_data["high"], - "ask": ask_data["high"], + "bid_price": bid_data["high"], + "ask_price": ask_data["high"], "bid_size": bid_data["volume"] / 4, "ask_size": ask_data["volume"] / 4, } cdef dict data_low = { - "bid": bid_data["low"], - "ask": ask_data["low"], + "bid_price": bid_data["low"], + "ask_price": ask_data["low"], "bid_size": bid_data["volume"] / 4, "ask_size": ask_data["volume"] / 4, } cdef dict data_close = { - "bid": bid_data["close"], - "ask": ask_data["close"], + "bid_price": bid_data["close"], + "ask_price": ask_data["close"], "bid_size": bid_data["volume"] / 4, "ask_size": ask_data["volume"] / 4, } @@ -242,8 +248,8 @@ cdef class QuoteTickDataWrangler: if is_raw: return list(map( self._build_tick_from_raw, - df_ticks_final["bid"], - df_ticks_final["ask"], + df_ticks_final["bid_price"], + df_ticks_final["ask_price"], df_ticks_final["bid_size"], df_ticks_final["ask_size"], ts_events, @@ -252,8 +258,8 @@ cdef class QuoteTickDataWrangler: else: return list(map( self._build_tick, - df_ticks_final["bid"], - df_ticks_final["ask"], + df_ticks_final["bid_price"], + df_ticks_final["ask_price"], df_ticks_final["bid_size"], df_ticks_final["ask_size"], ts_events, @@ -263,21 +269,21 @@ cdef class QuoteTickDataWrangler: # cpdef method for Python wrap() (called with map) cpdef QuoteTick _build_tick_from_raw( self, - int64_t raw_bid, - int64_t raw_ask, - uint64_t raw_bid_size, - uint64_t raw_ask_size, + int64_t bid_price_raw, + int64_t ask_price_raw, + uint64_t bid_size_raw, + uint64_t ask_size_raw, uint64_t ts_event, uint64_t ts_init, ): return QuoteTick.from_raw_c( self.instrument.id, - raw_bid, - raw_ask, + bid_price_raw, + ask_price_raw, self.instrument.price_precision, self.instrument.price_precision, - raw_bid_size, - raw_ask_size, + bid_size_raw, + ask_size_raw, self.instrument.size_precision, self.instrument.size_precision, ts_event, @@ -383,8 +389,8 @@ cdef class TradeTickDataWrangler: # cpdef method for Python wrap() (called with map) cpdef TradeTick _build_tick_from_raw( self, - int64_t raw_price, - uint64_t raw_size, + int64_t price_raw, + uint64_t size_raw, AggressorSide aggressor_side, str trade_id, uint64_t ts_event, @@ -392,9 +398,9 @@ cdef class TradeTickDataWrangler: ): return TradeTick.from_raw_c( self.instrument.id, - raw_price, + price_raw, self.instrument.price_precision, - raw_size, + size_raw, self.instrument.size_precision, aggressor_side, TradeId(trade_id), diff --git a/nautilus_trader/persistence/wranglers_v2.py b/nautilus_trader/persistence/wranglers_v2.py index 0a38cac53f94..beabf5956b61 100644 --- a/nautilus_trader/persistence/wranglers_v2.py +++ b/nautilus_trader/persistence/wranglers_v2.py @@ -182,7 +182,7 @@ def from_pandas( """ Process the given `data` into Nautilus `QuoteTick` objects. - Expects columns ['bid', 'ask'] with 'timestamp' index. + Expects columns ['bid_price', 'ask_price'] with 'timestamp' index. Note: The 'bid_size' and 'ask_size' columns are optional, will then use the `default_size`. @@ -206,14 +206,16 @@ def from_pandas( # Rename columns df = df.rename( columns={ + "bid": "bid_price", + "ask": "ask_price", "timestamp": "ts_event", "ts_recv": "ts_init", }, ) # Scale prices and quantities - df["bid"] = (df["bid"] * 1e9).astype(pd.Int64Dtype()) - df["ask"] = (df["ask"] * 1e9).astype(pd.Int64Dtype()) + df["bid_price"] = (df["bid_price"] * 1e9).astype(pd.Int64Dtype()) + df["ask_price"] = (df["ask_price"] * 1e9).astype(pd.Int64Dtype()) # Create bid_size and ask_size columns if "bid_size" in df.columns: @@ -245,7 +247,7 @@ def from_pandas( df["ts_init"] = df["ts_event"] + ts_init_delta # Reorder the columns and drop index column - df = df[["bid", "ask", "bid_size", "ask_size", "ts_event", "ts_init"]] + df = df[["bid_price", "ask_price", "bid_size", "ask_size", "ts_event", "ts_init"]] df = df.reset_index(drop=True) table = pa.Table.from_pandas(df) diff --git a/nautilus_trader/portfolio/portfolio.pyx b/nautilus_trader/portfolio/portfolio.pyx index 9dfc8477f60e..37c98aff577f 100644 --- a/nautilus_trader/portfolio/portfolio.pyx +++ b/nautilus_trader/portfolio/portfolio.pyx @@ -109,7 +109,7 @@ cdef class Portfolio(PortfolioFacade): log=self._log, ) - self._venue = None # Venue for specific portfolio behaviour (Interactive Brokers) + self._venue = None # Venue for specific portfolio behavior (Interactive Brokers) self._unrealized_pnls: dict[InstrumentId, Money] = {} self._net_positions: dict[InstrumentId, Decimal] = {} self._pending_calcs: set[InstrumentId] = set() @@ -1092,9 +1092,9 @@ cdef class Portfolio(PortfolioFacade): cdef QuoteTick quote_tick = self._cache.quote_tick(position.instrument_id) if quote_tick is not None: if position.side == PositionSide.LONG: - return quote_tick.bid + return quote_tick.bid_price elif position.side == PositionSide.SHORT: - return quote_tick.ask + return quote_tick.ask_price else: # pragma: no cover (design-time error) raise RuntimeError( f"invalid `PositionSide`, was {position_side_to_str(position.side)}", diff --git a/nautilus_trader/risk/engine.pyx b/nautilus_trader/risk/engine.pyx index 4ca67cef3f76..0171c35f071e 100644 --- a/nautilus_trader/risk/engine.pyx +++ b/nautilus_trader/risk/engine.pyx @@ -659,9 +659,9 @@ cdef class RiskEngine(Component): last_quote = self._cache.quote_tick(instrument.id) if last_quote is not None: if order.side == OrderSide.BUY: - last_px = last_quote.ask + last_px = last_quote.ask_price elif order.side == OrderSide.SELL: - last_px = last_quote.bid + last_px = last_quote.bid_price else: # pragma: no cover (design-time error) raise RuntimeError(f"invalid `OrderSide`") else: diff --git a/nautilus_trader/serialization/arrow/schema.py b/nautilus_trader/serialization/arrow/schema.py index a9a913e4c22d..79713de84bc7 100644 --- a/nautilus_trader/serialization/arrow/schema.py +++ b/nautilus_trader/serialization/arrow/schema.py @@ -80,9 +80,9 @@ QuoteTick: pa.schema( { "instrument_id": pa.dictionary(pa.int64(), pa.string()), - "bid": pa.string(), + "bid_price": pa.string(), "bid_size": pa.string(), - "ask": pa.string(), + "ask_price": pa.string(), "ask_size": pa.string(), "ts_event": pa.uint64(), "ts_init": pa.uint64(), diff --git a/nautilus_trader/serialization/arrow/schema_v2.py b/nautilus_trader/serialization/arrow/schema_v2.py index ed1b28ebc5d2..73e38954fa0c 100644 --- a/nautilus_trader/serialization/arrow/schema_v2.py +++ b/nautilus_trader/serialization/arrow/schema_v2.py @@ -43,9 +43,9 @@ ), QuoteTick: pa.schema( { - "bid": pa.int64(), + "bid_price": pa.int64(), "bid_size": pa.uint64(), - "ask": pa.int64(), + "ask_price": pa.int64(), "ask_size": pa.uint64(), "ts_event": pa.uint64(), "ts_init": pa.uint64(), diff --git a/nautilus_trader/serialization/base.pyx b/nautilus_trader/serialization/base.pyx index 4e5fe74f2d77..d5d77592c9b7 100644 --- a/nautilus_trader/serialization/base.pyx +++ b/nautilus_trader/serialization/base.pyx @@ -37,6 +37,7 @@ from nautilus_trader.model.events.order cimport OrderAccepted from nautilus_trader.model.events.order cimport OrderCanceled from nautilus_trader.model.events.order cimport OrderCancelRejected from nautilus_trader.model.events.order cimport OrderDenied +from nautilus_trader.model.events.order cimport OrderEmulated from nautilus_trader.model.events.order cimport OrderExpired from nautilus_trader.model.events.order cimport OrderFilled from nautilus_trader.model.events.order cimport OrderInitialized @@ -44,6 +45,7 @@ from nautilus_trader.model.events.order cimport OrderModifyRejected from nautilus_trader.model.events.order cimport OrderPendingCancel from nautilus_trader.model.events.order cimport OrderPendingUpdate from nautilus_trader.model.events.order cimport OrderRejected +from nautilus_trader.model.events.order cimport OrderReleased from nautilus_trader.model.events.order cimport OrderSubmitted from nautilus_trader.model.events.order cimport OrderTriggered from nautilus_trader.model.events.order cimport OrderUpdated @@ -74,12 +76,14 @@ _OBJECT_TO_DICT_MAP: dict[str, Callable[[None], dict]] = { OrderCancelRejected.__name__: OrderCancelRejected.to_dict_c, OrderCanceled.__name__: OrderCanceled.to_dict_c, OrderDenied.__name__: OrderDenied.to_dict_c, + OrderEmulated.__name__: OrderEmulated.to_dict_c, OrderExpired.__name__: OrderExpired.to_dict_c, OrderFilled.__name__: OrderFilled.to_dict_c, OrderInitialized.__name__: OrderInitialized.to_dict_c, OrderPendingCancel.__name__: OrderPendingCancel.to_dict_c, OrderPendingUpdate.__name__: OrderPendingUpdate.to_dict_c, OrderRejected.__name__: OrderRejected.to_dict_c, + OrderReleased.__name__: OrderReleased.to_dict_c, OrderSubmitted.__name__: OrderSubmitted.to_dict_c, OrderTriggered.__name__: OrderTriggered.to_dict_c, OrderModifyRejected.__name__: OrderModifyRejected.to_dict_c, @@ -121,11 +125,13 @@ _OBJECT_FROM_DICT_MAP: dict[str, Callable[[dict], Any]] = { OrderCancelRejected.__name__: OrderCancelRejected.from_dict_c, OrderCanceled.__name__: OrderCanceled.from_dict_c, OrderDenied.__name__: OrderDenied.from_dict_c, + OrderEmulated.__name__: OrderEmulated.from_dict_c, OrderExpired.__name__: OrderExpired.from_dict_c, OrderFilled.__name__: OrderFilled.from_dict_c, OrderInitialized.__name__: OrderInitialized.from_dict_c, OrderPendingCancel.__name__: OrderPendingCancel.from_dict_c, OrderPendingUpdate.__name__: OrderPendingUpdate.from_dict_c, + OrderReleased.__name__: OrderReleased.from_dict_c, OrderRejected.__name__: OrderRejected.from_dict_c, OrderSubmitted.__name__: OrderSubmitted.from_dict_c, OrderTriggered.__name__: OrderTriggered.from_dict_c, diff --git a/nautilus_trader/system/kernel.py b/nautilus_trader/system/kernel.py index be8c72439f7f..664af7bfd24c 100644 --- a/nautilus_trader/system/kernel.py +++ b/nautilus_trader/system/kernel.py @@ -52,8 +52,11 @@ from nautilus_trader.config.common import ExecAlgorithmFactory from nautilus_trader.config.common import LoggingConfig from nautilus_trader.config.common import NautilusKernelConfig +from nautilus_trader.config.common import TracingConfig from nautilus_trader.core.correctness import PyCondition from nautilus_trader.core.datetime import nanos_to_millis +from nautilus_trader.core.nautilus_pyo3 import LogGuard +from nautilus_trader.core.nautilus_pyo3 import set_global_log_collector from nautilus_trader.core.uuid import UUID4 from nautilus_trader.data.engine import DataEngine from nautilus_trader.execution.algorithm import ExecAlgorithm @@ -143,6 +146,15 @@ def __init__( # noqa (too complex) f"environment {self._environment} not recognized", # pragma: no cover (design-time error) ) + # Set the global tracing collector + # This should only be set once for the whole duration of the application + tracing: TracingConfig = config.tracing or TracingConfig() + self._log_guard: LogGuard = set_global_log_collector( + tracing.stdout_level, + tracing.stderr_level, + tracing.file_level, + ) + logging: LoggingConfig = config.logging or LoggingConfig() # Setup the logger with a `LiveClock` initially, @@ -301,7 +313,7 @@ def __init__( # noqa (too complex) cache=self._cache, clock=self._clock, logger=self._logger, - config=None, # No configuration for now + config=config.emulator, ) ######################################################################## @@ -692,132 +704,117 @@ def catalog(self) -> ParquetDataCatalog | None: """ return self._catalog - async def start(self) -> None: + def start(self) -> None: + """ + Start the Nautilus system kernel. + """ self._log.info("STARTING...") - if self._config.cache_database is not None and self._config.cache_database.flush_on_start: - self._cache.flush_db() + self._start_engines() + self._connect_clients() + self._emulator.start() + self._initialize_portfolio() + self._trader.start() - # Start system - self._data_engine.start() - self._risk_engine.start() - self._exec_engine.start() + async def start_async(self) -> None: + """ + Start the Nautilus system kernel in an asynchronous context with an event loop. - # Connect all clients - self._data_engine.connect() - self._exec_engine.connect() + Raises + ------ + RuntimeError + If no event loop has been assigned to the kernel. - if self._loop is not None: - # Await engine connection and initialization - self._log.info( - f"Awaiting engine connections and initializations " - f"({self._config.timeout_connection}s timeout)...", - color=LogColor.BLUE, - ) - if not await self._await_engines_connected(): - self._log.warning( - f"Timed out ({self._config.timeout_connection}s) waiting for engines to connect and initialize." - f"\nStatus" - f"\n------" - f"\nDataEngine.check_connected() == {self._data_engine.check_connected()}" - f"\nExecEngine.check_connected() == {self._exec_engine.check_connected()}", - ) - return + """ + if self.loop is None: + raise RuntimeError("no event loop has been assigned to the kernel") - # Await execution state reconciliation - self._log.info( - f"Awaiting execution state reconciliation " - f"({self._config.timeout_reconciliation}s timeout)...", - color=LogColor.BLUE, - ) - if not await self._exec_engine.reconcile_state( - timeout_secs=self._config.timeout_reconciliation, - ): - self._log.error("Execution state could not be reconciled.") - return + self._log.info("STARTING...") - if self._exec_engine.reconciliation: - self._log.info("State reconciled.", color=LogColor.GREEN) + self._register_executor() + self._start_engines() + self._connect_clients() - self._emulator.start() + if not await self._await_engines_connected(): + return - # Initialize portfolio - self._portfolio.initialize_orders() - self._portfolio.initialize_positions() + if not await self._await_execution_reconciliation(): + return - if self._loop is not None: - # Await portfolio initialization - self._log.info( - "Awaiting portfolio initialization " - f"({self._config.timeout_portfolio}s timeout)...", - color=LogColor.BLUE, - ) - if not await self._await_portfolio_initialized(): - self._log.warning( - f"Timed out ({self._config.timeout_portfolio}s) waiting for portfolio to initialize." - f"\nStatus" - f"\n------" - f"\nPortfolio.initialized == {self._portfolio.initialized}", - ) - return - self._log.info("Portfolio initialized.", color=LogColor.GREEN) + self._emulator.start() + self._initialize_portfolio() + + if not await self._await_portfolio_initialization(): + return - # Start trader and strategies self._trader.start() - async def _await_engines_connected(self) -> bool: - # - The data engine clients will be set connected when all - # instruments are received and updated with the data engine. - # - The execution engine clients will be set connected when all - # accounts are updated and the current order and position status is - # reconciled. - # Thus any delay here will be due to blocking network I/O. - seconds = self._config.timeout_connection - timeout: timedelta = self.clock.utc_now() + timedelta(seconds=seconds) - while True: - await asyncio.sleep(0) - if self.clock.utc_now() >= timeout: - return False - if not self._data_engine.check_connected(): - continue - if not self._exec_engine.check_connected(): - continue - break + async def stop(self) -> None: + """ + Stop the Nautilus system kernel. + """ + self.log.info("STOPPING...") - return True # Engines connected + if self._trader.is_running: + self._trader.stop() - async def _await_portfolio_initialized(self) -> bool: - # - The portfolio will be set initialized when all margin and unrealized - # PnL calculations are completed (maybe waiting on first quotes). - # Thus any delay here will be due to blocking network I/O. - seconds = self._config.timeout_portfolio - timeout: timedelta = self._clock.utc_now() + timedelta(seconds=seconds) - while True: - await asyncio.sleep(0) - if self._clock.utc_now() >= timeout: - return False - if not self._portfolio.initialized: - continue - break + if self.save_state: + self._trader.save() + + self._disconnect_clients() + + self._stop_engines() + self._cancel_timers() + self._flush_writer() + + self._log.info("STOPPED.") + + async def stop_async(self) -> None: + """ + Stop the Nautilus system kernel asynchronously. + + After a specified delay the internal `Trader` residual state will be checked. + + If save strategy is configured, then strategy states will be saved. + + Raises + ------ + RuntimeError + If no event loop has been assigned to the kernel. - return True # Portfolio initialized + """ + if self.loop is None: + raise RuntimeError("no event loop has been assigned to the kernel") + + self.log.info("STOPPING...") + + if self._trader.is_running: + self._trader.stop() + await self._await_trader_residuals() + + if self.save_state: + self._trader.save() + + self._disconnect_clients() + + await self._await_engines_disconnected() + + self._stop_engines() + self._cancel_timers() + self._flush_writer() + + self._log.info("STOPPED.") def dispose(self) -> None: """ - Dispose of the kernel releasing system resources. + Dispose of the Nautilus kernel, releasing system resources. Calling this method multiple times has the same effect as calling it once (it is idempotent). Once called, it cannot be reversed, and no other methods should be called on this instance. """ - # Stop all engines - if self.data_engine.is_running: - self.data_engine.stop() - if self.risk_engine.is_running: - self.risk_engine.stop() - if self.exec_engine.is_running: - self.exec_engine.stop() + self._stop_engines() # Dispose all engines if not self.data_engine.is_disposed: @@ -834,7 +831,17 @@ def dispose(self) -> None: self._writer.close() def cancel_all_tasks(self) -> None: - PyCondition.not_none(self.loop, "self.loop") + """ + Cancel all tasks currently running for the Nautilus kernel. + + Raises + ------ + RuntimeError + If no event loop has been assigned to the kernel. + + """ + if self.loop is None: + raise RuntimeError("no event loop has been assigned to the kernel") to_cancel = asyncio.tasks.all_tasks(self.loop) if not to_cancel: @@ -860,8 +867,182 @@ def cancel_all_tasks(self) -> None: if task.exception() is not None: self.loop.call_exception_handler( { - "message": "unhandled exception during asyncio.run() shutdown", + "message": "unhandled exception during `asyncio.run()` shutdown", "exception": task.exception(), "task": task, }, ) + + def _register_executor(self) -> None: + for actor in self.trader.actors(): + actor.register_executor(self._loop, self._executor) + for strategy in self.trader.strategies(): + strategy.register_executor(self._loop, self._executor) + for exec_algorithm in self.trader.exec_algorithms(): + exec_algorithm.register_executor(self._loop, self._executor) + + def _start_engines(self) -> None: + if self._config.cache_database is not None and self._config.cache_database.flush_on_start: + self._cache.flush_db() + + self._data_engine.start() + self._risk_engine.start() + self._exec_engine.start() + + def _stop_engines(self) -> None: + if self._data_engine.is_running: + self._data_engine.stop() + if self._risk_engine.is_running: + self._risk_engine.stop() + if self._exec_engine.is_running: + self._exec_engine.stop() + if self._emulator.is_running: + self._emulator.stop() + + def _connect_clients(self) -> None: + self._data_engine.connect() + self._exec_engine.connect() + + def _disconnect_clients(self) -> None: + self._data_engine.disconnect() + self._exec_engine.disconnect() + + def _initialize_portfolio(self) -> None: + self._portfolio.initialize_orders() + self._portfolio.initialize_positions() + + async def _await_engines_connected(self) -> bool: + self._log.info( + f"Awaiting engine connections and initializations " + f"({self._config.timeout_connection}s timeout)...", + color=LogColor.BLUE, + ) + if not await self._check_engines_connected(): + self._log.warning( + f"Timed out ({self._config.timeout_connection}s) waiting for engines to connect and initialize." + f"\nStatus" + f"\n------" + f"\nDataEngine.check_connected() == {self._data_engine.check_connected()}" + f"\nExecEngine.check_connected() == {self._exec_engine.check_connected()}", + ) + return False + + return True + + async def _await_engines_disconnected(self) -> None: + self._log.info( + f"Awaiting engine disconnections " + f"({self._config.timeout_disconnection}s timeout)...", + color=LogColor.BLUE, + ) + if not await self._check_engines_disconnected(): + self._log.error( + f"Timed out ({self._config.timeout_disconnection}s) waiting for engines to disconnect." + f"\nStatus" + f"\n------" + f"\nDataEngine.check_disconnected() == {self._data_engine.check_disconnected()}" + f"\nExecEngine.check_disconnected() == {self._exec_engine.check_disconnected()}", + ) + + async def _await_execution_reconciliation(self) -> bool: + self._log.info( + f"Awaiting execution state reconciliation " + f"({self._config.timeout_reconciliation}s timeout)...", + color=LogColor.BLUE, + ) + if not await self._exec_engine.reconcile_state( + timeout_secs=self._config.timeout_reconciliation, + ): + self._log.error("Execution state could not be reconciled.") + return False + + self._log.info("Execution state reconciled.", color=LogColor.GREEN) + return True + + async def _await_portfolio_initialization(self) -> bool: + self._log.info( + "Awaiting portfolio initialization " f"({self._config.timeout_portfolio}s timeout)...", + color=LogColor.BLUE, + ) + if not await self._check_portfolio_initialized(): + self._log.warning( + f"Timed out ({self._config.timeout_portfolio}s) waiting for portfolio to initialize." + f"\nStatus" + f"\n------" + f"\nPortfolio.initialized == {self._portfolio.initialized}", + ) + return False + + self._log.info("Portfolio initialized.", color=LogColor.GREEN) + return True + + async def _await_trader_residuals(self) -> None: + self._log.info( + f"Awaiting post stop ({self._config.timeout_post_stop}s timeout)...", + color=LogColor.BLUE, + ) + await asyncio.sleep(self._config.timeout_post_stop) + self._trader.check_residuals() + + async def _check_engines_connected(self) -> bool: + # - The data engine clients will be set connected when all + # instruments are received and updated with the data engine. + # - The execution engine clients will be set connected when all + # accounts are updated and the current order and position status is + # reconciled. + # Thus any delay here will be due to blocking network I/O. + seconds = self._config.timeout_connection + timeout: timedelta = self.clock.utc_now() + timedelta(seconds=seconds) + while True: + await asyncio.sleep(0) + if self.clock.utc_now() >= timeout: + return False + if not self._data_engine.check_connected(): + continue + if not self._exec_engine.check_connected(): + continue + break + + return True + + async def _check_engines_disconnected(self) -> bool: + seconds = self._config.timeout_disconnection + timeout: timedelta = self._clock.utc_now() + timedelta(seconds=seconds) + while True: + await asyncio.sleep(0) + if self._clock.utc_now() >= timeout: + return False + if not self._data_engine.check_disconnected(): + continue + if not self._exec_engine.check_disconnected(): + continue + break + + return True + + async def _check_portfolio_initialized(self) -> bool: + # - The portfolio will be set initialized when all margin and unrealized + # PnL calculations are completed (maybe waiting on first quotes). + # Thus any delay here will be due to blocking network I/O. + seconds = self._config.timeout_portfolio + timeout: timedelta = self._clock.utc_now() + timedelta(seconds=seconds) + while True: + await asyncio.sleep(0) + if self._clock.utc_now() >= timeout: + return False + if not self._portfolio.initialized: + continue + break + + return True + + def _cancel_timers(self) -> None: + timer_names = self._clock.timer_names + self._clock.cancel_timers() + + for name in timer_names: + self._log.info(f"Canceled Timer(name={name}).") + + def _flush_writer(self) -> None: + if self._writer is not None: + self._writer.flush() diff --git a/tests/mem_leak_tests/tracemalloc_snapshot_fixture.py b/nautilus_trader/test_kit/fixtures/memory.py similarity index 71% rename from tests/mem_leak_tests/tracemalloc_snapshot_fixture.py rename to nautilus_trader/test_kit/fixtures/memory.py index c931dd5be6bd..0f2c70a8c53b 100644 --- a/tests/mem_leak_tests/tracemalloc_snapshot_fixture.py +++ b/nautilus_trader/test_kit/fixtures/memory.py @@ -1,3 +1,18 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2023 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 gc import tracemalloc diff --git a/nautilus_trader/test_kit/mocks/data.py b/nautilus_trader/test_kit/mocks/data.py index ba408780acf8..cbedbd3bd242 100644 --- a/nautilus_trader/test_kit/mocks/data.py +++ b/nautilus_trader/test_kit/mocks/data.py @@ -80,8 +80,8 @@ def parse_csv_tick(df, instrument_id): ts = pd.Timestamp(r[0], tz="UTC").value tick = QuoteTick( instrument_id=instrument_id, - bid=Price(r[1], 5), - ask=Price(r[2], 5), + bid_price=Price(r[1], 5), + ask_price=Price(r[2], 5), bid_size=Quantity.from_int(1_000_000), ask_size=Quantity.from_int(1_000_000), ts_event=ts, diff --git a/nautilus_trader/test_kit/providers.py b/nautilus_trader/test_kit/providers.py index 99f47ceea8e7..c2af31a579f4 100644 --- a/nautilus_trader/test_kit/providers.py +++ b/nautilus_trader/test_kit/providers.py @@ -25,8 +25,6 @@ from fsspec.implementations.local import LocalFileSystem from pandas.io.parsers.readers import TextFileReader -from nautilus_trader.adapters.betfair.common import BETFAIR_TICK_SCHEME -from nautilus_trader.adapters.betfair.constants import BETFAIR_VENUE from nautilus_trader.core.correctness import PyCondition from nautilus_trader.core.datetime import dt_to_unix_nanos from nautilus_trader.core.datetime import secs_to_nanos @@ -46,7 +44,6 @@ from nautilus_trader.model.identifiers import Symbol from nautilus_trader.model.identifiers import TradeId from nautilus_trader.model.identifiers import Venue -from nautilus_trader.model.instruments import BettingInstrument from nautilus_trader.model.instruments import CryptoFuture from nautilus_trader.model.instruments import CryptoPerpetual from nautilus_trader.model.instruments import CurrencyPair @@ -461,63 +458,6 @@ def aapl_option() -> OptionsContract: ts_init=0, ) - @staticmethod - def betting_instrument( - market_id: str = "1.179082386", - selection_id: str = "50214", - selection_handicap: Optional[str] = None, - ) -> BettingInstrument: - return BettingInstrument( - venue_name=BETFAIR_VENUE.value, - betting_type="ODDS", - competition_id="12282733", - competition_name="NFL", - event_country_code="GB", - event_id="29678534", - event_name="NFL", - event_open_date=pd.Timestamp("2022-02-07 23:30:00+00:00"), - event_type_id="6423", - event_type_name="American Football", - market_id=market_id, - market_name="AFC Conference Winner", - market_start_time=pd.Timestamp("2022-02-07 23:30:00+00:00"), - market_type="SPECIAL", - selection_handicap=selection_handicap, - selection_id=selection_id, - selection_name="Kansas City Chiefs", - currency="GBP", - tick_scheme_name=BETFAIR_TICK_SCHEME.name, - ts_event=0, - ts_init=0, - ) - - @staticmethod - def betting_instrument_handicap() -> BettingInstrument: - return BettingInstrument.from_dict( - { - "venue_name": "BETFAIR", - "event_type_id": "61420", - "event_type_name": "Australian Rules", - "competition_id": "11897406", - "competition_name": "AFL", - "event_id": "30777079", - "event_name": "GWS v Richmond", - "event_country_code": "AU", - "event_open_date": "2021-08-13T09:50:00+00:00", - "betting_type": "ASIAN_HANDICAP_DOUBLE_LINE", - "market_id": "1.186249896", - "market_name": "Handicap", - "market_start_time": "2021-08-13T09:50:00+00:00", - "market_type": "HANDICAP", - "selection_id": "5304641", - "selection_name": "GWS", - "selection_handicap": "-5.5", - "currency": "AUD", - "ts_event": 0, - "ts_init": 0, - }, - ) - @staticmethod def synthetic_instrument() -> SyntheticInstrument: return SyntheticInstrument( diff --git a/nautilus_trader/test_kit/stubs/data.py b/nautilus_trader/test_kit/stubs/data.py index 8d263a651209..b40d9d211c88 100644 --- a/nautilus_trader/test_kit/stubs/data.py +++ b/nautilus_trader/test_kit/stubs/data.py @@ -72,8 +72,8 @@ def ticker(instrument_id: Optional[InstrumentId] = None) -> Ticker: @staticmethod def quote_tick( instrument: Optional[Instrument] = None, - bid: float = 1.0, - ask: float = 1.0, + bid_price: float = 1.0, + ask_price: float = 1.0, bid_size: float = 100_000.0, ask_size: float = 100_000.0, ts_event: int = 0, @@ -82,8 +82,8 @@ def quote_tick( inst: Instrument = instrument or TestInstrumentProvider.default_fx_ccy("AUD/USD") return QuoteTick( instrument_id=inst.id, - bid=inst.make_price(bid), - ask=inst.make_price(ask), + bid_price=inst.make_price(bid_price), + ask_price=inst.make_price(ask_price), bid_size=inst.make_qty(bid_size), ask_size=inst.make_qty(ask_size), ts_event=ts_event, diff --git a/nautilus_trader/test_kit/stubs/events.py b/nautilus_trader/test_kit/stubs/events.py index ca597a33bdcc..da913f1cb97b 100644 --- a/nautilus_trader/test_kit/stubs/events.py +++ b/nautilus_trader/test_kit/stubs/events.py @@ -34,6 +34,7 @@ from nautilus_trader.model.events import OrderPendingCancel from nautilus_trader.model.events import OrderPendingUpdate from nautilus_trader.model.events import OrderRejected +from nautilus_trader.model.events import OrderReleased from nautilus_trader.model.events import OrderSubmitted from nautilus_trader.model.events import OrderTriggered from nautilus_trader.model.events import OrderUpdated @@ -151,6 +152,21 @@ def betting_account_state(account_id: Optional[AccountId] = None) -> AccountStat ts_init=0, ) + @staticmethod + def order_released( + order: Order, + released_price: Optional[Price] = None, + ) -> OrderReleased: + return OrderReleased( + trader_id=order.trader_id, + strategy_id=order.strategy_id, + instrument_id=order.instrument_id, + client_order_id=order.client_order_id, + released_price=released_price or Price.from_str("1.00000"), + event_id=UUID4(), + ts_init=0, + ) + @staticmethod def order_submitted( order: Order, diff --git a/nautilus_trader/trading/strategy.pxd b/nautilus_trader/trading/strategy.pxd index 73339431ec85..b1c7777df9a1 100644 --- a/nautilus_trader/trading/strategy.pxd +++ b/nautilus_trader/trading/strategy.pxd @@ -129,10 +129,8 @@ cdef class Strategy(Actor): cdef OrderDenied _generate_order_denied(self, Order order, str reason) cdef OrderPendingUpdate _generate_order_pending_update(self, Order order) cdef OrderPendingCancel _generate_order_pending_cancel(self, Order order) - cdef OrderCanceled _generate_order_canceled(self, Order order) cdef void _deny_order(self, Order order, str reason) cdef void _deny_order_list(self, OrderList order_list, str reason) - cdef void _cancel_algo_order(self, Order order) # -- EGRESS --------------------------------------------------------------------------------------- diff --git a/nautilus_trader/trading/strategy.pyx b/nautilus_trader/trading/strategy.pyx index c1677cd0f7cd..c7127070b4dc 100644 --- a/nautilus_trader/trading/strategy.pyx +++ b/nautilus_trader/trading/strategy.pyx @@ -63,6 +63,7 @@ from nautilus_trader.model.data.tick cimport QuoteTick from nautilus_trader.model.data.tick cimport TradeTick from nautilus_trader.model.enums_c cimport OrderStatus from nautilus_trader.model.enums_c cimport TimeInForce +from nautilus_trader.model.enums_c cimport TriggerType from nautilus_trader.model.enums_c cimport oms_type_from_str from nautilus_trader.model.enums_c cimport order_side_to_str from nautilus_trader.model.enums_c cimport position_side_to_str @@ -82,6 +83,7 @@ from nautilus_trader.model.identifiers cimport StrategyId from nautilus_trader.model.identifiers cimport TraderId from nautilus_trader.model.objects cimport Price from nautilus_trader.model.objects cimport Quantity +from nautilus_trader.model.orders.base cimport LOCAL_ACTIVE_ORDER_STATUS from nautilus_trader.model.orders.base cimport VALID_LIMIT_ORDER_TYPES from nautilus_trader.model.orders.base cimport VALID_STOP_ORDER_TYPES from nautilus_trader.model.orders.base cimport Order @@ -472,7 +474,7 @@ cdef class Strategy(Actor): """ Condition.true(self.trader_id is not None, "The strategy has not been registered") Condition.not_none(order, "order") - Condition.equal(order.status, OrderStatus.INITIALIZED, "order", "order_status") + Condition.equal(order.status_c(), OrderStatus.INITIALIZED, "order", "order_status") # Publish initialized event self._msgbus.publish_c( @@ -501,7 +503,7 @@ cdef class Strategy(Actor): self._set_gtd_expiry(order) # Route order - if order.is_emulated_c(): + if order.emulation_trigger != TriggerType.NO_TRIGGER: self._send_emulator_command(command) elif order.exec_algorithm_id is not None: self._send_algo_command(command, order.exec_algorithm_id) @@ -555,7 +557,7 @@ cdef class Strategy(Actor): cdef Order order for order in order_list.orders: - Condition.equal(order.status, OrderStatus.INITIALIZED, "order", "order_status") + Condition.equal(order.status_c(), OrderStatus.INITIALIZED, "order", "order_status") # Publish initialized event self._msgbus.publish_c( topic=f"events.order.{order.strategy_id.to_str()}", @@ -700,7 +702,7 @@ cdef class Strategy(Actor): return # Cannot send command cdef OrderPendingUpdate event - if order.status != OrderStatus.INITIALIZED and not order.is_emulated_c(): + if not order.is_active_local_c(): # Generate and apply event event = self._generate_order_pending_update(order) try: @@ -762,8 +764,10 @@ cdef class Strategy(Actor): ) return # Cannot send command + cdef OrderStatus order_status = order.status_c() + cdef OrderPendingCancel event - if order.status != OrderStatus.INITIALIZED and not order.is_emulated_c(): + if order_status not in LOCAL_ACTIVE_ORDER_STATUS: # Generate and apply event event = self._generate_order_pending_cancel(order) try: @@ -790,11 +794,10 @@ cdef class Strategy(Actor): client_id=client_id, ) - if order.exec_algorithm_id is not None: - self._send_algo_command(command, order.exec_algorithm_id) - if order.is_emulated_c(): self._send_emulator_command(command) + elif order.exec_algorithm_id is not None and order.is_active_local_c(): + self._send_algo_command(command, order.exec_algorithm_id) else: self._send_risk_command(command) @@ -863,7 +866,7 @@ cdef class Strategy(Actor): OrderPendingCancel event Order order for order in open_orders + emulated_orders: - if order.is_emulated_c(): + if order.status_c() == OrderStatus.INITIALIZED or order.is_emulated_c(): continue event = self._generate_order_pending_cancel(order) try: @@ -892,7 +895,7 @@ cdef class Strategy(Actor): exec_algorithm_orders = self.cache.orders_for_exec_algorithm(exec_algorithm_id) for order in exec_algorithm_orders: if order.strategy_id == self.id and not order.is_closed_c(): - self._cancel_algo_order(order) + self.cancel_order(order) self._send_risk_command(command) self._send_emulator_command(command) @@ -1401,20 +1404,6 @@ cdef class Strategy(Actor): ts_init=ts_now, ) - cdef OrderCanceled _generate_order_canceled(self, Order order): - cdef uint64_t ts_now = self._clock.timestamp_ns() - return OrderCanceled( - trader_id=order.trader_id, - strategy_id=order.strategy_id, - instrument_id=order.instrument_id, - client_order_id=order.client_order_id, - venue_order_id=order.venue_order_id, - account_id=order.account_id, - event_id=UUID4(), - ts_event=ts_now, - ts_init=ts_now, - ) - cdef void _deny_order(self, Order order, str reason): self._log.error(f"Order denied: {reason}.") @@ -1444,27 +1433,11 @@ cdef class Strategy(Actor): if not order.is_closed_c(): self._deny_order(order=order, reason=reason) - cdef void _cancel_algo_order(self, Order order): - # Generate event - cdef OrderCanceled event = self._generate_order_canceled(order) - - try: - order.apply(event) - except InvalidStateTrigger as e: - self._log.warning(f"InvalidStateTrigger: {e}, did not apply {event}") - return - - self.cache.update_order(order) - - # Publish denied event - self._msgbus.publish_c( - topic=f"events.order.{order.strategy_id.to_str()}", - msg=event, - ) - # -- EGRESS --------------------------------------------------------------------------------------- cdef void _send_emulator_command(self, TradingCommand command): + if not self.log.is_bypassed: + self.log.info(f"{CMD}{SENT} {command}.") self._msgbus.send(endpoint="OrderEmulator.execute", msg=command) cdef void _send_algo_command(self, TradingCommand command, ExecAlgorithmId exec_algorithm_id): diff --git a/poetry.lock b/poetry.lock index f3e2a869cb63..587be9a00c15 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. [[package]] name = "aiohttp" version = "3.8.5" description = "Async http client/server framework (asyncio)" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -112,6 +113,7 @@ speedups = ["Brotli", "aiodns", "cchardet"] name = "aiosignal" version = "1.3.1" description = "aiosignal: a list of registered asynchronous callbacks" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -126,6 +128,7 @@ frozenlist = ">=1.1.0" name = "alabaster" version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -135,19 +138,21 @@ files = [ [[package]] name = "async-timeout" -version = "4.0.2" +version = "4.0.3" description = "Timeout context manager for asyncio programs" +category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, - {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] [[package]] name = "attrs" version = "23.1.0" description = "Classes Without Boilerplate" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -166,6 +171,7 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte name = "babel" version = "2.12.1" description = "Internationalization utilities" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -177,6 +183,7 @@ files = [ name = "beautifulsoup4" version = "4.12.2" description = "Screen-scraping library" +category = "dev" optional = false python-versions = ">=3.6.0" files = [ @@ -195,6 +202,7 @@ lxml = ["lxml"] name = "betfair-parser" version = "0.4.6" description = "A betfair parser" +category = "main" optional = true python-versions = ">=3.9,<4.0" files = [ @@ -209,6 +217,7 @@ msgspec = ">=0.16" name = "black" version = "23.7.0" description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -255,6 +264,7 @@ uvloop = ["uvloop (>=0.15.2)"] name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -266,6 +276,7 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." +category = "dev" optional = false python-versions = "*" files = [ @@ -340,19 +351,21 @@ pycparser = "*" [[package]] name = "cfgv" -version = "3.3.1" +version = "3.4.0" description = "Validate configuration and produce human readable error messages." +category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8" files = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[package]] name = "charset-normalizer" version = "3.2.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -435,13 +448,14 @@ files = [ [[package]] name = "click" -version = "8.1.6" +version = "8.1.7" description = "Composable command line interface toolkit" +category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -451,6 +465,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -460,71 +475,64 @@ files = [ [[package]] name = "coverage" -version = "7.2.7" +version = "7.3.0" description = "Code coverage measurement for Python" +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"}, - {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"}, - {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"}, - {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"}, - {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"}, - {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"}, - {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"}, - {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"}, - {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"}, - {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"}, - {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"}, - {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"}, - {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"}, - {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"}, - {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"}, - {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"}, - {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"}, - {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"}, - {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"}, - {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"}, - {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"}, - {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"}, - {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"}, - {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"}, - {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"}, - {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"}, - {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"}, - {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"}, - {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"}, - {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"}, - {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"}, - {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"}, - {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"}, + {file = "coverage-7.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db76a1bcb51f02b2007adacbed4c88b6dee75342c37b05d1822815eed19edee5"}, + {file = "coverage-7.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c02cfa6c36144ab334d556989406837336c1d05215a9bdf44c0bc1d1ac1cb637"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:477c9430ad5d1b80b07f3c12f7120eef40bfbf849e9e7859e53b9c93b922d2af"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce2ee86ca75f9f96072295c5ebb4ef2a43cecf2870b0ca5e7a1cbdd929cf67e1"}, + {file = "coverage-7.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68d8a0426b49c053013e631c0cdc09b952d857efa8f68121746b339912d27a12"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3eb0c93e2ea6445b2173da48cb548364f8f65bf68f3d090404080d338e3a689"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:90b6e2f0f66750c5a1178ffa9370dec6c508a8ca5265c42fbad3ccac210a7977"}, + {file = "coverage-7.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96d7d761aea65b291a98c84e1250cd57b5b51726821a6f2f8df65db89363be51"}, + {file = "coverage-7.3.0-cp310-cp310-win32.whl", hash = "sha256:63c5b8ecbc3b3d5eb3a9d873dec60afc0cd5ff9d9f1c75981d8c31cfe4df8527"}, + {file = "coverage-7.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:97c44f4ee13bce914272589b6b41165bbb650e48fdb7bd5493a38bde8de730a1"}, + {file = "coverage-7.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:74c160285f2dfe0acf0f72d425f3e970b21b6de04157fc65adc9fd07ee44177f"}, + {file = "coverage-7.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b543302a3707245d454fc49b8ecd2c2d5982b50eb63f3535244fd79a4be0c99d"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad0f87826c4ebd3ef484502e79b39614e9c03a5d1510cfb623f4a4a051edc6fd"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13c6cbbd5f31211d8fdb477f0f7b03438591bdd077054076eec362cf2207b4a7"}, + {file = "coverage-7.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac440c43e9b479d1241fe9d768645e7ccec3fb65dc3a5f6e90675e75c3f3e3a"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3c9834d5e3df9d2aba0275c9f67989c590e05732439b3318fa37a725dff51e74"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4c8e31cf29b60859876474034a83f59a14381af50cbe8a9dbaadbf70adc4b214"}, + {file = "coverage-7.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7a9baf8e230f9621f8e1d00c580394a0aa328fdac0df2b3f8384387c44083c0f"}, + {file = "coverage-7.3.0-cp311-cp311-win32.whl", hash = "sha256:ccc51713b5581e12f93ccb9c5e39e8b5d4b16776d584c0f5e9e4e63381356482"}, + {file = "coverage-7.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:887665f00ea4e488501ba755a0e3c2cfd6278e846ada3185f42d391ef95e7e70"}, + {file = "coverage-7.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d000a739f9feed900381605a12a61f7aaced6beae832719ae0d15058a1e81c1b"}, + {file = "coverage-7.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:59777652e245bb1e300e620ce2bef0d341945842e4eb888c23a7f1d9e143c446"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9737bc49a9255d78da085fa04f628a310c2332b187cd49b958b0e494c125071"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5247bab12f84a1d608213b96b8af0cbb30d090d705b6663ad794c2f2a5e5b9fe"}, + {file = "coverage-7.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ac9a1de294773b9fa77447ab7e529cf4fe3910f6a0832816e5f3d538cfea9a"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:85b7335c22455ec12444cec0d600533a238d6439d8d709d545158c1208483873"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:36ce5d43a072a036f287029a55b5c6a0e9bd73db58961a273b6dc11a2c6eb9c2"}, + {file = "coverage-7.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a4576e984f96d9fce61766ffaed0115d5dab1419e4f63d6992b480c2bd60b"}, + {file = "coverage-7.3.0-cp312-cp312-win32.whl", hash = "sha256:56afbf41fa4a7b27f6635bc4289050ac3ab7951b8a821bca46f5b024500e6321"}, + {file = "coverage-7.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f297e0c1ae55300ff688568b04ff26b01c13dfbf4c9d2b7d0cb688ac60df479"}, + {file = "coverage-7.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac0dec90e7de0087d3d95fa0533e1d2d722dcc008bc7b60e1143402a04c117c1"}, + {file = "coverage-7.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:438856d3f8f1e27f8e79b5410ae56650732a0dcfa94e756df88c7e2d24851fcd"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1084393c6bda8875c05e04fce5cfe1301a425f758eb012f010eab586f1f3905e"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49ab200acf891e3dde19e5aa4b0f35d12d8b4bd805dc0be8792270c71bd56c54"}, + {file = "coverage-7.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67e6bbe756ed458646e1ef2b0778591ed4d1fcd4b146fc3ba2feb1a7afd4254"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f39c49faf5344af36042b293ce05c0d9004270d811c7080610b3e713251c9b0"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7df91fb24c2edaabec4e0eee512ff3bc6ec20eb8dccac2e77001c1fe516c0c84"}, + {file = "coverage-7.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:34f9f0763d5fa3035a315b69b428fe9c34d4fc2f615262d6be3d3bf3882fb985"}, + {file = "coverage-7.3.0-cp38-cp38-win32.whl", hash = "sha256:bac329371d4c0d456e8d5f38a9b0816b446581b5f278474e416ea0c68c47dcd9"}, + {file = "coverage-7.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b859128a093f135b556b4765658d5d2e758e1fae3e7cc2f8c10f26fe7005e543"}, + {file = "coverage-7.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed8d310afe013db1eedd37176d0839dc66c96bcfcce8f6607a73ffea2d6ba"}, + {file = "coverage-7.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61260ec93f99f2c2d93d264b564ba912bec502f679793c56f678ba5251f0393"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97af9554a799bd7c58c0179cc8dbf14aa7ab50e1fd5fa73f90b9b7215874ba28"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3558e5b574d62f9c46b76120a5c7c16c4612dc2644c3d48a9f4064a705eaee95"}, + {file = "coverage-7.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37d5576d35fcb765fca05654f66aa71e2808d4237d026e64ac8b397ffa66a56a"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:07ea61bcb179f8f05ffd804d2732b09d23a1238642bf7e51dad62082b5019b34"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:80501d1b2270d7e8daf1b64b895745c3e234289e00d5f0e30923e706f110334e"}, + {file = "coverage-7.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4eddd3153d02204f22aef0825409091a91bf2a20bce06fe0f638f5c19a85de54"}, + {file = "coverage-7.3.0-cp39-cp39-win32.whl", hash = "sha256:2d22172f938455c156e9af2612650f26cceea47dc86ca048fa4e0b2d21646ad3"}, + {file = "coverage-7.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:60f64e2007c9144375dd0f480a54d6070f00bb1a28f65c408370544091c9bc9e"}, + {file = "coverage-7.3.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:5492a6ce3bdb15c6ad66cb68a0244854d9917478877a25671d70378bdc8562d0"}, + {file = "coverage-7.3.0.tar.gz", hash = "sha256:49dbb19cdcafc130f597d9e04a29d0a032ceedf729e41b181f51cd170e6ee865"}, ] [package.dependencies] @@ -535,34 +543,35 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "41.0.2" +version = "41.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711"}, - {file = "cryptography-41.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182"}, - {file = "cryptography-41.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5"}, - {file = "cryptography-41.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58"}, - {file = "cryptography-41.0.2-cp37-abi3-win32.whl", hash = "sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76"}, - {file = "cryptography-41.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766"}, - {file = "cryptography-41.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa"}, - {file = "cryptography-41.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f"}, - {file = "cryptography-41.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0"}, - {file = "cryptography-41.0.2.tar.gz", hash = "sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507"}, + {file = "cryptography-41.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47"}, + {file = "cryptography-41.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c"}, + {file = "cryptography-41.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae"}, + {file = "cryptography-41.0.3-cp37-abi3-win32.whl", hash = "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306"}, + {file = "cryptography-41.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906"}, + {file = "cryptography-41.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84"}, + {file = "cryptography-41.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1"}, + {file = "cryptography-41.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4"}, + {file = "cryptography-41.0.3.tar.gz", hash = "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34"}, ] [package.dependencies] @@ -582,6 +591,7 @@ test-randomorder = ["pytest-randomly"] name = "css-html-js-minify" version = "2.5.5" description = "CSS HTML JS Minifier" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -593,6 +603,7 @@ files = [ name = "cython" version = "3.0.0" description = "The Cython compiler for writing C extensions in the Python language." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -660,6 +671,7 @@ files = [ name = "distlib" version = "0.3.7" description = "Distribution utilities" +category = "dev" optional = false python-versions = "*" files = [ @@ -671,6 +683,7 @@ files = [ name = "docformatter" version = "1.7.5" description = "Formats docstrings to follow PEP 257" +category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -689,6 +702,7 @@ tomli = ["tomli (>=2.0.0,<3.0.0)"] name = "docker" version = "6.1.3" description = "A Python library for the Docker Engine API." +category = "main" optional = true python-versions = ">=3.7" files = [ @@ -710,6 +724,7 @@ ssh = ["paramiko (>=2.4.3)"] name = "docutils" version = "0.19" description = "Docutils -- Python Documentation Utilities" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -719,13 +734,14 @@ files = [ [[package]] name = "eventkit" -version = "1.0.0" +version = "1.0.1" description = "Event-driven data pipelines" +category = "main" optional = true python-versions = "*" files = [ - {file = "eventkit-1.0.0-py3-none-any.whl", hash = "sha256:c3c1ae6e15cda9970c3996b0aaeda48431fc6b8674c01e7a7ff77a13629cc021"}, - {file = "eventkit-1.0.0.tar.gz", hash = "sha256:c9c4bb8a9685e4131e845882512a630d6a57acee148f38af286562a76873e4a9"}, + {file = "eventkit-1.0.1-py3-none-any.whl", hash = "sha256:6060a6aa04d5c5d20f2e55b7c17e2a22e8d31f88f2c2791d60eab3301aa040da"}, + {file = "eventkit-1.0.1.tar.gz", hash = "sha256:56b99a6205f61cd995aa5e0036e37bd61f052f7d32560e60b6fe45e319a7ef3a"}, ] [package.dependencies] @@ -733,13 +749,14 @@ numpy = "*" [[package]] name = "exceptiongroup" -version = "1.1.2" +version = "1.1.3" description = "Backport of PEP 654 (exception groups)" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, - {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, + {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, + {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, ] [package.extras] @@ -749,6 +766,7 @@ test = ["pytest (>=6)"] name = "execnet" version = "2.0.2" description = "execnet: rapid multi-Python deployment" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -763,6 +781,7 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] name = "filelock" version = "3.12.2" description = "A platform independent file lock." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -778,6 +797,7 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p name = "frozendict" version = "2.3.8" description = "A simple immutable dictionary" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -824,6 +844,7 @@ files = [ name = "frozenlist" version = "1.4.0" description = "A list-like structure which implements collections.abc.MutableSequence" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -894,6 +915,7 @@ files = [ name = "fsspec" version = "2023.6.0" description = "File-system specification" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -929,6 +951,7 @@ tqdm = ["tqdm"] name = "hiredis" version = "2.2.3" description = "Python wrapper for hiredis" +category = "main" optional = true python-versions = ">=3.7" files = [ @@ -1027,6 +1050,7 @@ files = [ name = "ib-insync" version = "0.9.86" description = "Python sync/async framework for Interactive Brokers API" +category = "main" optional = true python-versions = ">=3.6" files = [ @@ -1040,13 +1064,14 @@ nest-asyncio = "*" [[package]] name = "identify" -version = "2.5.26" +version = "2.5.27" description = "File identification library for Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.26-py2.py3-none-any.whl", hash = "sha256:c22a8ead0d4ca11f1edd6c9418c3220669b3b7533ada0a0ffa6cc0ef85cf9b54"}, - {file = "identify-2.5.26.tar.gz", hash = "sha256:7243800bce2f58404ed41b7c002e53d4d22bcf3ae1b7900c2d7aefd95394bf7f"}, + {file = "identify-2.5.27-py2.py3-none-any.whl", hash = "sha256:fdb527b2dfe24602809b2201e033c2a113d7bdf716db3ca8e3243f735dcecaba"}, + {file = "identify-2.5.27.tar.gz", hash = "sha256:287b75b04a0e22d727bc9a41f0d4f3c1bcada97490fa6eabb5b28f0e9097e733"}, ] [package.extras] @@ -1056,6 +1081,7 @@ license = ["ukkonen"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1067,6 +1093,7 @@ files = [ name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1078,6 +1105,7 @@ files = [ name = "importlib-metadata" version = "6.8.0" description = "Read metadata from Python packages" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1097,6 +1125,7 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1108,6 +1137,7 @@ files = [ name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1125,6 +1155,7 @@ i18n = ["Babel (>=2.7)"] name = "linkify-it-py" version = "2.0.2" description = "Links recognition library with FULL unicode support." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1145,6 +1176,7 @@ test = ["coverage", "pytest", "pytest-cov"] name = "lxml" version = "4.9.3" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" files = [ @@ -1252,6 +1284,7 @@ source = ["Cython (>=0.29.35)"] name = "markdown-it-py" version = "2.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1276,6 +1309,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1335,6 +1369,7 @@ files = [ name = "mdit-py-plugins" version = "0.3.5" description = "Collection of plugins for markdown-it-py" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1354,6 +1389,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1363,40 +1399,41 @@ files = [ [[package]] name = "msgspec" -version = "0.17.0" +version = "0.18.1" description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML." +category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "msgspec-0.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7bb4fa0021198cc87ece9fd4cbaa7241782f6c1d7b4fab71bb59c30e83c95c8"}, - {file = "msgspec-0.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab367dd0b8a8c989d295e8ef086305e41a3c73fa3208322da4826f04e24b3c5"}, - {file = "msgspec-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccef9b4216c28f470855540d2649cbcf9eefd74c17984259ed3cbbaaa818c5d"}, - {file = "msgspec-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fb9f68ae2068a96b29d92cc4d142440e616b0a077b53a68b3445c62a2f31edc"}, - {file = "msgspec-0.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3adc3f4016aa5ea1bd51c8d5c2a38172f1965dd231e03defed04ff2a0effb361"}, - {file = "msgspec-0.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:98995159d95b4ed65c8cc185bd877fec67f41cb23925237a82efb066ef778fa8"}, - {file = "msgspec-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e7e016cd79dc12935b6f5e8ff358ea5908cb84756c84a9fa2f3d8e5405740f3"}, - {file = "msgspec-0.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1a0c8390014ee2b65d0d36d9cc28e74092166e8f44adf3dcb85e1ef25933787"}, - {file = "msgspec-0.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8096ca82e9a3e6fa09106bfac27eb8296c4cead02fdd297bb8af87254e58bad"}, - {file = "msgspec-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:025986a5b1fd81c3dd1bb591bbc01ac03844b57685fcf4a14ed07ae1967b6d03"}, - {file = "msgspec-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b0c52118aec347d15f75354b2fa27f1c9cd29b6ec12b74ef2ef988a0d702f5d"}, - {file = "msgspec-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:86ca5710174970d9aeefc2ced4fcc4a27f5ff065840356cf1c747399aa4ad5a8"}, - {file = "msgspec-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3d39dc3a44a479f35afcc21d97c4ea90a385afee0b766c7780ca2bda99480e3c"}, - {file = "msgspec-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:79c3ab0bac7dd6492f6c03e347f0e9e5187d9658b8e7c85fdc96dd4820de970b"}, - {file = "msgspec-0.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:832e841d17bc2b4d74eee6aa16c818074cc9104c9a79466dbd49162600af8e9b"}, - {file = "msgspec-0.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:80021321fea9be9e03c7675f6bdce06d9c91dbfb4aec78044cd243491ed49fa9"}, - {file = "msgspec-0.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51aec8ae03348525749f4eff4ed59fc7369383877ad65e0f00de3e357c4ecfa6"}, - {file = "msgspec-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93fcff4e7923050cbfb3be8b4763a95862c3bf35d198684f3ebc3c486a1f66a5"}, - {file = "msgspec-0.17.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:396908a124f633c3e267ec01b5bffdbefd592b15ddfda63c1af798e0102c35c8"}, - {file = "msgspec-0.17.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:23581f8c1ffa488c2c3c78359868ee408528dad5e02af94e87e8d4773d118399"}, - {file = "msgspec-0.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:bc3f9fbb3d127a0c9734d43870b565a809c1fe2a0913d04170fa1222dfb131dc"}, - {file = "msgspec-0.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:556413e59a8017c12a177218b3d3c16bb7217dc0371853b61e4c98df19515088"}, - {file = "msgspec-0.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de90a979e48e95ab6175a59f3f256a88984ff599edd5d3948993dfd6ecff83d0"}, - {file = "msgspec-0.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce2a7da8669887b17bed92d3143aa0cc936fabdadcdd43c9ed09cfaca71a2fc5"}, - {file = "msgspec-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0aded9cd083d3a966610b6b01787106fa6aab495486420163a7b872704767da0"}, - {file = "msgspec-0.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b5e49b9a302acee0808466b1af31c675d7cf8fd5ee78874109b5a44cb40c67e8"}, - {file = "msgspec-0.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59ab1e250a84516413dee3542ff2b43f46fe521adfdc01e628198ce94553ff1e"}, - {file = "msgspec-0.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:5f1b7c9d9f41b8c7724dc206aadbbe84d1641d20a1ff6dab04f0caf2a5bfd438"}, - {file = "msgspec-0.17.0.tar.gz", hash = "sha256:4e2f2faeeb7aef5c2e4c78e375443134238ee966e54bba75cdb1936939673da7"}, + {file = "msgspec-0.18.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:262e5f1a981644f5e9b28a984d6df238eac0ed2c37d788f40abaf10d380b0424"}, + {file = "msgspec-0.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3f4a7d5897984f59baf51976682f52f4d2eff88aa64eec8b7f5b80b7a2b6dc5"}, + {file = "msgspec-0.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6d21f0da90d1f3e7f65123d375c2b590bfbe3a014920ea812e1a022027b60d3"}, + {file = "msgspec-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebb71f51ca2f62d0d1cf9585f763e3d9fd8a85a0f00d682112916532964692ae"}, + {file = "msgspec-0.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:52bb422d2b2e80e86d72439edb1a372ff016c0c5d9f44d277b220511486b3f9a"}, + {file = "msgspec-0.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:119ff9d5412eccf53f0d7ab43b587a58f6f806a40ae6b14fd8140173b1cc0285"}, + {file = "msgspec-0.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:36da63a8f64292fff7c814bd3cbbba669f0fa5068c149b4a386dba662ace5621"}, + {file = "msgspec-0.18.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b132ae2691623b3fbee730604671da3bf57ca97d648f1a46a35ea09c6f490fcf"}, + {file = "msgspec-0.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:944af5f9ec66de8e21b30ccafdab3e87be11d757ca9304c01e3b2efc373f3293"}, + {file = "msgspec-0.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e96b71e98728830ea3d20acf65969ba7059dfc9e32581039f05e88b8461d98b"}, + {file = "msgspec-0.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:914ab6407fdce1bc795583ba0428f49baf6eedd40df117200ec8fe7666ca2ec4"}, + {file = "msgspec-0.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13980627b9ba28fac2e051757ef5edbe0bebfef411d648e58d2100b86c5eca03"}, + {file = "msgspec-0.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a6bd460e2057a6d9ca6d0e2b848a441c6e30977012d1fca43e4bc1702c9abca5"}, + {file = "msgspec-0.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:c63f9dc8c33ca56903d7957ee7a5d0b3ece6b345b8b10f44b226787a4410c8e1"}, + {file = "msgspec-0.18.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a88d2fb82eb59de0f0f365a27dcea2020440883770b99353e0a3e0aa8ef552a"}, + {file = "msgspec-0.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f936f6ea351c2e8c0d53875a6a71d9887603e3faadbe380172d22283e011f1b3"}, + {file = "msgspec-0.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597324c72b504f04fb283078b748e664f7f7fc5337bb493e0996511bab895e7e"}, + {file = "msgspec-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94eca90ac47a24cb07b3702c863a6b0881830ea02de7ac58a00f778b4b7762b"}, + {file = "msgspec-0.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c256b21baa1336d651aa9955d69d3f8d0e971d78e0e75fd28a89742af7dcff65"}, + {file = "msgspec-0.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9721948182d471c4f1655634f1970628a7321ec58a2cfb89d35af78c2fc2ebfa"}, + {file = "msgspec-0.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:f01fe87b592185a11091206a745fdcba5b40d4c4342d1089959df49f18d6a700"}, + {file = "msgspec-0.18.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:966ee615b8227c208276863c4269a01dfe8c3b2538b8e1ee33bb01010fe7a3af"}, + {file = "msgspec-0.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d94555a79013c100b1345e0a5a6d36bfa915e74831e42de3a97cb81313426ea7"}, + {file = "msgspec-0.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c63a0f49540e2a888acec8c154066b6ef985813ce27132eb38e1f0c7f49df27"}, + {file = "msgspec-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0784a23d1b5b8a6fe4c9f5b9c1779adc5bff4de0fe388545dc2a4f1b5b90e546"}, + {file = "msgspec-0.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4bd273e373e220437a22a5ad83a90e3dd648c4e625f48e42e172026de77a3038"}, + {file = "msgspec-0.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a0f5070d77658ac953f63114fbaafc6ec967e9e61993ff70cc2432e938a3cf3e"}, + {file = "msgspec-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:fe22658000c165be1055c8df3f7ecc3db85be838d47d1ce1e0526b10747bdf0f"}, + {file = "msgspec-0.18.1.tar.gz", hash = "sha256:a7d837e370cfe5afb941e9c922dbdbee9c854b21bafdebaf068bdf15c43ec21d"}, ] [package.extras] @@ -1410,6 +1447,7 @@ yaml = ["pyyaml"] name = "multidict" version = "6.0.4" description = "multidict implementation" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1491,37 +1529,39 @@ files = [ [[package]] name = "mypy" -version = "1.4.1" +version = "1.5.1" description = "Optional static typing for Python" +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, - {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, - {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, - {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, - {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, - {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, - {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, - {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, - {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, - {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, - {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, - {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, - {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, - {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, - {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, - {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, - {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, - {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, - {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, - {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, - {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, - {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, - {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, - {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, - {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, - {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, + {file = "mypy-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70"}, + {file = "mypy-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0"}, + {file = "mypy-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12"}, + {file = "mypy-1.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d"}, + {file = "mypy-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4"}, + {file = "mypy-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243"}, + {file = "mypy-1.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275"}, + {file = "mypy-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373"}, + {file = "mypy-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161"}, + {file = "mypy-1.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a"}, + {file = "mypy-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160"}, + {file = "mypy-1.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2"}, + {file = "mypy-1.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb"}, + {file = "mypy-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14"}, + {file = "mypy-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb"}, + {file = "mypy-1.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693"}, + {file = "mypy-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770"}, + {file = "mypy-1.5.1-py3-none-any.whl", hash = "sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5"}, + {file = "mypy-1.5.1.tar.gz", hash = "sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92"}, ] [package.dependencies] @@ -1532,13 +1572,13 @@ typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] reports = ["lxml"] [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1550,6 +1590,7 @@ files = [ name = "myst-parser" version = "0.18.1" description = "An extended commonmark compliant parser, with bridges to docutils & sphinx." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1576,6 +1617,7 @@ testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=6,<7)", "pytest-cov", name = "nautilus-ibapi" version = "1019.1" description = "Python IB API" +category = "main" optional = true python-versions = ">=3.9,<3.12" files = [ @@ -1587,6 +1629,7 @@ files = [ name = "nest-asyncio" version = "1.5.7" description = "Patch asyncio to allow nested event loops" +category = "main" optional = true python-versions = ">=3.5" files = [ @@ -1598,6 +1641,7 @@ files = [ name = "nodeenv" version = "1.8.0" description = "Node.js virtual environment builder" +category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ @@ -1610,42 +1654,44 @@ setuptools = "*" [[package]] name = "numpy" -version = "1.25.1" +version = "1.25.2" description = "Fundamental package for array computing in Python" +category = "main" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:77d339465dff3eb33c701430bcb9c325b60354698340229e1dff97745e6b3efa"}, - {file = "numpy-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d736b75c3f2cb96843a5c7f8d8ccc414768d34b0a75f466c05f3a739b406f10b"}, - {file = "numpy-1.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a90725800caeaa160732d6b31f3f843ebd45d6b5f3eec9e8cc287e30f2805bf"}, - {file = "numpy-1.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c6c9261d21e617c6dc5eacba35cb68ec36bb72adcff0dee63f8fbc899362588"}, - {file = "numpy-1.25.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0def91f8af6ec4bb94c370e38c575855bf1d0be8a8fbfba42ef9c073faf2cf19"}, - {file = "numpy-1.25.1-cp310-cp310-win32.whl", hash = "sha256:fd67b306320dcadea700a8f79b9e671e607f8696e98ec255915c0c6d6b818503"}, - {file = "numpy-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:c1516db588987450b85595586605742879e50dcce923e8973f79529651545b57"}, - {file = "numpy-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6b82655dd8efeea69dbf85d00fca40013d7f503212bc5259056244961268b66e"}, - {file = "numpy-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e8f6049c4878cb16960fbbfb22105e49d13d752d4d8371b55110941fb3b17800"}, - {file = "numpy-1.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41a56b70e8139884eccb2f733c2f7378af06c82304959e174f8e7370af112e09"}, - {file = "numpy-1.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5154b1a25ec796b1aee12ac1b22f414f94752c5f94832f14d8d6c9ac40bcca6"}, - {file = "numpy-1.25.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:38eb6548bb91c421261b4805dc44def9ca1a6eef6444ce35ad1669c0f1a3fc5d"}, - {file = "numpy-1.25.1-cp311-cp311-win32.whl", hash = "sha256:791f409064d0a69dd20579345d852c59822c6aa087f23b07b1b4e28ff5880fcb"}, - {file = "numpy-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:c40571fe966393b212689aa17e32ed905924120737194b5d5c1b20b9ed0fb171"}, - {file = "numpy-1.25.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3d7abcdd85aea3e6cdddb59af2350c7ab1ed764397f8eec97a038ad244d2d105"}, - {file = "numpy-1.25.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a180429394f81c7933634ae49b37b472d343cccb5bb0c4a575ac8bbc433722f"}, - {file = "numpy-1.25.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d412c1697c3853c6fc3cb9751b4915859c7afe6a277c2bf00acf287d56c4e625"}, - {file = "numpy-1.25.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20e1266411120a4f16fad8efa8e0454d21d00b8c7cee5b5ccad7565d95eb42dd"}, - {file = "numpy-1.25.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f76aebc3358ade9eacf9bc2bb8ae589863a4f911611694103af05346637df1b7"}, - {file = "numpy-1.25.1-cp39-cp39-win32.whl", hash = "sha256:247d3ffdd7775bdf191f848be8d49100495114c82c2bd134e8d5d075fb386a1c"}, - {file = "numpy-1.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:1d5d3c68e443c90b38fdf8ef40e60e2538a27548b39b12b73132456847f4b631"}, - {file = "numpy-1.25.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:35a9527c977b924042170a0887de727cd84ff179e478481404c5dc66b4170009"}, - {file = "numpy-1.25.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d3fe3dd0506a28493d82dc3cf254be8cd0d26f4008a417385cbf1ae95b54004"}, - {file = "numpy-1.25.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:012097b5b0d00a11070e8f2e261128c44157a8689f7dedcf35576e525893f4fe"}, - {file = "numpy-1.25.1.tar.gz", hash = "sha256:9a3a9f3a61480cc086117b426a8bd86869c213fc4072e606f01c4e4b66eb92bf"}, + {file = "numpy-1.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3"}, + {file = "numpy-1.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f"}, + {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187"}, + {file = "numpy-1.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357"}, + {file = "numpy-1.25.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9"}, + {file = "numpy-1.25.2-cp310-cp310-win32.whl", hash = "sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044"}, + {file = "numpy-1.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545"}, + {file = "numpy-1.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418"}, + {file = "numpy-1.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f"}, + {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2"}, + {file = "numpy-1.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf"}, + {file = "numpy-1.25.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364"}, + {file = "numpy-1.25.2-cp311-cp311-win32.whl", hash = "sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d"}, + {file = "numpy-1.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4"}, + {file = "numpy-1.25.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3"}, + {file = "numpy-1.25.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926"}, + {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca"}, + {file = "numpy-1.25.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295"}, + {file = "numpy-1.25.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f"}, + {file = "numpy-1.25.2-cp39-cp39-win32.whl", hash = "sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01"}, + {file = "numpy-1.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901"}, + {file = "numpy-1.25.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf"}, + {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, ] [[package]] name = "numpydoc" version = "1.5.0" description = "Sphinx extension to support docstrings in Numpy format" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1664,6 +1710,7 @@ testing = ["matplotlib", "pytest", "pytest-cov"] name = "packaging" version = "23.1" description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1675,6 +1722,7 @@ files = [ name = "pandas" version = "2.0.3" description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1742,6 +1790,7 @@ xml = ["lxml (>=4.6.3)"] name = "pathspec" version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1753,6 +1802,7 @@ files = [ name = "platformdirs" version = "3.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1768,6 +1818,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co name = "pluggy" version = "1.2.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1783,6 +1834,7 @@ testing = ["pytest", "pytest-benchmark"] name = "pre-commit" version = "3.3.3" description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1801,6 +1853,7 @@ virtualenv = ">=20.10.0" name = "psutil" version = "5.9.5" description = "Cross-platform lib for process and system monitoring in Python." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1827,6 +1880,7 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] name = "py-cpuinfo" version = "9.0.0" description = "Get CPU info with pure Python" +category = "dev" optional = false python-versions = "*" files = [ @@ -1838,6 +1892,7 @@ files = [ name = "pyarrow" version = "12.0.1" description = "Python library for Apache Arrow" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1875,6 +1930,7 @@ numpy = ">=1.16.6" name = "pycparser" version = "2.21" description = "C parser in Python" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1884,13 +1940,14 @@ files = [ [[package]] name = "pygments" -version = "2.15.1" +version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, - {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, ] [package.extras] @@ -1900,6 +1957,7 @@ plugins = ["importlib-metadata"] name = "pytest" version = "7.4.0" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1922,6 +1980,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-aiohttp" version = "1.0.4" description = "Pytest plugin for aiohttp support" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1941,6 +2000,7 @@ testing = ["coverage (==6.2)", "mypy (==0.931)"] name = "pytest-asyncio" version = "0.21.1" description = "Pytest support for asyncio" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1959,6 +2019,7 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy name = "pytest-benchmark" version = "4.0.0" description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1979,6 +2040,7 @@ histogram = ["pygal", "pygaljs"] name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1997,6 +2059,7 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pytest-mock" version = "3.11.1" description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2014,6 +2077,7 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] name = "pytest-xdist" version = "3.3.1" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2035,6 +2099,7 @@ testing = ["filelock"] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -2049,6 +2114,7 @@ six = ">=1.5" name = "python-slugify" version = "8.0.1" description = "A Python slugify application that also handles Unicode" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2067,6 +2133,7 @@ unidecode = ["Unidecode (>=1.1.1)"] name = "pytz" version = "2023.3" description = "World timezone definitions, modern and historical" +category = "main" optional = false python-versions = "*" files = [ @@ -2078,6 +2145,7 @@ files = [ name = "pywin32" version = "306" description = "Python for Window Extensions" +category = "main" optional = true python-versions = "*" files = [ @@ -2101,6 +2169,7 @@ files = [ name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2150,6 +2219,7 @@ files = [ name = "redis" version = "4.6.0" description = "Python client for Redis database and key-value store" +category = "main" optional = true python-versions = ">=3.7" files = [ @@ -2168,6 +2238,7 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" name = "requests" version = "2.31.0" description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2187,50 +2258,53 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.0.280" +version = "0.0.285" description = "An extremely fast Python linter, written in Rust." +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.0.280-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:48ed5aca381050a4e2f6d232db912d2e4e98e61648b513c350990c351125aaec"}, - {file = "ruff-0.0.280-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:ef6ee3e429fd29d6a5ceed295809e376e6ece5b0f13c7e703efaf3d3bcb30b96"}, - {file = "ruff-0.0.280-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d878370f7e9463ac40c253724229314ff6ebe4508cdb96cb536e1af4d5a9cd4f"}, - {file = "ruff-0.0.280-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83e8f372fa5627eeda5b83b5a9632d2f9c88fc6d78cead7e2a1f6fb05728d137"}, - {file = "ruff-0.0.280-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7008fc6ca1df18b21fa98bdcfc711dad5f94d0fc3c11791f65e460c48ef27c82"}, - {file = "ruff-0.0.280-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fe7118c1eae3fda17ceb409629c7f3b5a22dffa7caf1f6796776936dca1fe653"}, - {file = "ruff-0.0.280-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37359cd67d2af8e09110a546507c302cbea11c66a52d2a9b6d841d465f9962d4"}, - {file = "ruff-0.0.280-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd58af46b0221efb95966f1f0f7576df711cb53e50d2fdb0e83c2f33360116a4"}, - {file = "ruff-0.0.280-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e7c15828d09f90e97bea8feefcd2907e8c8ce3a1f959c99f9b4b3469679f33c"}, - {file = "ruff-0.0.280-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2dae8f2d9c44c5c49af01733c2f7956f808db682a4193180dedb29dd718d7bbe"}, - {file = "ruff-0.0.280-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5f972567163a20fb8c2d6afc60c2ea5ef8b68d69505760a8bd0377de8984b4f6"}, - {file = "ruff-0.0.280-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ffa7347ad11643f29de100977c055e47c988cd6d9f5f5ff83027600b11b9189"}, - {file = "ruff-0.0.280-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37dab70114671d273f203268f6c3366c035fe0c8056614069e90a65e614bfc"}, - {file = "ruff-0.0.280-py3-none-win32.whl", hash = "sha256:7784e3606352fcfb193f3cd22b2e2117c444cb879ef6609ec69deabd662b0763"}, - {file = "ruff-0.0.280-py3-none-win_amd64.whl", hash = "sha256:4a7d52457b5dfcd3ab24b0b38eefaead8e2dca62b4fbf10de4cd0938cf20ce30"}, - {file = "ruff-0.0.280-py3-none-win_arm64.whl", hash = "sha256:b7de5b8689575918e130e4384ed9f539ce91d067c0a332aedef6ca7188adac2d"}, - {file = "ruff-0.0.280.tar.gz", hash = "sha256:581c43e4ac5e5a7117ad7da2120d960a4a99e68ec4021ec3cd47fe1cf78f8380"}, + {file = "ruff-0.0.285-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:72a3a0936369b986b0e959f9090206ed3c18f9e5e439ea5b8e6867c6707aded5"}, + {file = "ruff-0.0.285-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:0d9ab6ad16742eb78919e0fba09f914f042409df40ad63423c34bb20d350162a"}, + {file = "ruff-0.0.285-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c48926156288b8ac005eb1db5e77c15e8a37309ae49d9fb6771d5cf5f777590"}, + {file = "ruff-0.0.285-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d2a60c102e7a5e147b58fc2cbea12a563c565383effc527c987ea2086a05742"}, + {file = "ruff-0.0.285-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b02aae62f922d088bb01943e1dbd861688ada13d735b78b8348a7d90121fd292"}, + {file = "ruff-0.0.285-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f572c4296d8c7ddd22c3204de4031965be524fdd1fdaaef273945932912b28c5"}, + {file = "ruff-0.0.285-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80effdf4fe69763d69eb4ab9443e186fd09e668b59fe70ba4b49f4c077d15a1b"}, + {file = "ruff-0.0.285-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5977ce304da35c263f5e082901bd7ac0bd2be845a8fcfd1a29e4d6680cddb307"}, + {file = "ruff-0.0.285-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72a087712d474fa17b915d7cb9ef807e1256182b12ddfafb105eb00aeee48d1a"}, + {file = "ruff-0.0.285-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7ce67736cd8dfe97162d1e7adfc2d9a1bac0efb9aaaff32e4042c7cde079f54b"}, + {file = "ruff-0.0.285-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5473a4c6cac34f583bff08c5f63b8def5599a0ea4dc96c0302fbd2cc0b3ecbad"}, + {file = "ruff-0.0.285-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e6b1c961d608d373a032f047a20bf3c55ad05f56c32e7b96dcca0830a2a72348"}, + {file = "ruff-0.0.285-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2933cc9631f453305399c7b8fb72b113ad76b49ae1d7103cc4afd3a423bed164"}, + {file = "ruff-0.0.285-py3-none-win32.whl", hash = "sha256:770c5eb6376de024111443022cda534fb28980a9dd3b4abc83992a8770167ba6"}, + {file = "ruff-0.0.285-py3-none-win_amd64.whl", hash = "sha256:a8c6ad6b9cd77489bf6d1510950cbbe47a843aa234adff0960bae64bd06c3b6d"}, + {file = "ruff-0.0.285-py3-none-win_arm64.whl", hash = "sha256:de44fbc6c3b25fccee473ddf851416fd4e246fc6027b2197c395b1b3b3897921"}, + {file = "ruff-0.0.285.tar.gz", hash = "sha256:45866048d1dcdcc80855998cb26c4b2b05881f9e043d2e3bfe1aa36d9a2e8f28"}, ] [[package]] name = "setuptools" -version = "68.0.0" +version = "68.1.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, - {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, + {file = "setuptools-68.1.2-py3-none-any.whl", hash = "sha256:3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b"}, + {file = "setuptools-68.1.2.tar.gz", hash = "sha256:3d4dfa6d95f1b101d695a6160a7626e15583af71a5f52176efa5d39a054d475d"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5,<=7.1.2)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2242,6 +2316,7 @@ files = [ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" optional = false python-versions = "*" files = [ @@ -2253,6 +2328,7 @@ files = [ name = "soupsieve" version = "2.4.1" description = "A modern CSS selector implementation for Beautiful Soup." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2264,6 +2340,7 @@ files = [ name = "sphinx" version = "5.3.0" description = "Python documentation generator" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2299,6 +2376,7 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] name = "sphinx-comments" version = "0.0.3" description = "Add comments and annotation to your documentation." +category = "dev" optional = false python-versions = "*" files = [ @@ -2318,6 +2396,7 @@ testing = ["beautifulsoup4", "myst-parser", "pytest", "pytest-regressions", "sph name = "sphinx-copybutton" version = "0.5.2" description = "Add a copy button to each of your code cells." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2336,6 +2415,7 @@ rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] name = "sphinx-external-toc" version = "0.3.1" description = "A sphinx extension that allows the site-map to be defined in a single YAML file." +category = "dev" optional = false python-versions = "~=3.7" files = [ @@ -2357,6 +2437,7 @@ testing = ["coverage", "pytest (>=7.1,<8.0)", "pytest-cov", "pytest-regressions" name = "sphinx-material" version = "0.0.35" description = "Material sphinx theme" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2378,6 +2459,7 @@ dev = ["black (==19.10b0)"] name = "sphinx-togglebutton" version = "0.3.2" description = "Toggle page content and collapse admonitions in Sphinx." +category = "dev" optional = false python-versions = "*" files = [ @@ -2396,45 +2478,57 @@ sphinx = ["matplotlib", "myst-nb", "numpy", "sphinx-book-theme", "sphinx-design" [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.4" +version = "1.0.7" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +category = "dev" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, - {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, + {file = "sphinxcontrib_applehelp-1.0.7-py3-none-any.whl", hash = "sha256:094c4d56209d1734e7d252f6e0b3ccc090bd52ee56807a5d9315b19c122ab15d"}, + {file = "sphinxcontrib_applehelp-1.0.7.tar.gz", hash = "sha256:39fdc8d762d33b01a7d8f026a3b7d71563ea3b72787d5f00ad8465bd9d6dfbfa"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +version = "1.0.5" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, + {file = "sphinxcontrib_devhelp-1.0.5-py3-none-any.whl", hash = "sha256:fe8009aed765188f08fcaadbb3ea0d90ce8ae2d76710b7e29ea7d047177dae2f"}, + {file = "sphinxcontrib_devhelp-1.0.5.tar.gz", hash = "sha256:63b41e0d38207ca40ebbeabcf4d8e51f76c03e78cd61abe118cf4435c73d4212"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.1" +version = "2.0.4" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +category = "dev" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, - {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, + {file = "sphinxcontrib_htmlhelp-2.0.4-py3-none-any.whl", hash = "sha256:8001661c077a73c29beaf4a79968d0726103c5605e27db92b9ebed8bab1359e9"}, + {file = "sphinxcontrib_htmlhelp-2.0.4.tar.gz", hash = "sha256:6c26a118a05b76000738429b724a0568dbde5b72391a688577da08f11891092a"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] @@ -2443,6 +2537,7 @@ test = ["html5lib", "pytest"] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -2455,30 +2550,38 @@ test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +version = "1.0.6" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, + {file = "sphinxcontrib_qthelp-1.0.6-py3-none-any.whl", hash = "sha256:bf76886ee7470b934e363da7a954ea2825650013d367728588732c7350f49ea4"}, + {file = "sphinxcontrib_qthelp-1.0.6.tar.gz", hash = "sha256:62b9d1a186ab7f5ee3356d906f648cacb7a6bdb94d201ee7adf26db55092982d"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +version = "1.1.9" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, + {file = "sphinxcontrib_serializinghtml-1.1.9-py3-none-any.whl", hash = "sha256:9b36e503703ff04f20e9675771df105e58aa029cfcbc23b8ed716019b7416ae1"}, + {file = "sphinxcontrib_serializinghtml-1.1.9.tar.gz", hash = "sha256:0c64ff898339e1fac29abd2bf5f11078f3ec413cfe9c046d3120d7ca65530b54"}, ] +[package.dependencies] +Sphinx = ">=5" + [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] @@ -2487,6 +2590,7 @@ test = ["pytest"] name = "text-unidecode" version = "1.3" description = "The most basic Text::Unidecode port" +category = "dev" optional = false python-versions = "*" files = [ @@ -2498,6 +2602,7 @@ files = [ name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2509,6 +2614,7 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2518,20 +2624,21 @@ files = [ [[package]] name = "tqdm" -version = "4.65.0" +version = "4.66.1" description = "Fast, Extensible Progress Meter" +category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"}, - {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"}, + {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, + {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["py-make (>=0.1.0)", "twine", "wheel"] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] @@ -2540,6 +2647,7 @@ telegram = ["requests"] name = "types-pyopenssl" version = "23.2.0.2" description = "Typing stubs for pyOpenSSL" +category = "dev" optional = false python-versions = "*" files = [ @@ -2552,24 +2660,26 @@ cryptography = ">=35.0.0" [[package]] name = "types-pytz" -version = "2023.3.0.0" +version = "2023.3.0.1" description = "Typing stubs for pytz" +category = "dev" optional = false python-versions = "*" files = [ - {file = "types-pytz-2023.3.0.0.tar.gz", hash = "sha256:ecdc70d543aaf3616a7e48631543a884f74205f284cefd6649ddf44c6a820aac"}, - {file = "types_pytz-2023.3.0.0-py3-none-any.whl", hash = "sha256:4fc2a7fbbc315f0b6630e0b899fd6c743705abe1094d007b0e612d10da15e0f3"}, + {file = "types-pytz-2023.3.0.1.tar.gz", hash = "sha256:1a7b8d4aac70981cfa24478a41eadfcd96a087c986d6f150d77e3ceb3c2bdfab"}, + {file = "types_pytz-2023.3.0.1-py3-none-any.whl", hash = "sha256:65152e872137926bb67a8fe6cc9cfd794365df86650c5d5fdc7b167b0f38892e"}, ] [[package]] name = "types-redis" -version = "4.6.0.3" +version = "4.6.0.5" description = "Typing stubs for redis" +category = "dev" optional = false python-versions = "*" files = [ - {file = "types-redis-4.6.0.3.tar.gz", hash = "sha256:efdef37dc0c04bf5786195651fd694f8bfdd693eac09ec4af46d90f72652558f"}, - {file = "types_redis-4.6.0.3-py3-none-any.whl", hash = "sha256:67c44c14369c33c2a300da2a50b5607c0fc888f7b85eeb7c73e15c78a0f05edd"}, + {file = "types-redis-4.6.0.5.tar.gz", hash = "sha256:5f179d10bd3ca995a8134aafcddfc3e12d52b208437c4529ef27e68acb301f38"}, + {file = "types_redis-4.6.0.5-py3-none-any.whl", hash = "sha256:4f662060247a2363c7a8f0b7e52915d68960870ff16a749a891eabcf87ed0be4"}, ] [package.dependencies] @@ -2580,6 +2690,7 @@ types-pyOpenSSL = "*" name = "types-requests" version = "2.31.0.2" description = "Typing stubs for requests" +category = "dev" optional = false python-versions = "*" files = [ @@ -2594,6 +2705,7 @@ types-urllib3 = "*" name = "types-toml" version = "0.10.8.7" description = "Typing stubs for toml" +category = "dev" optional = false python-versions = "*" files = [ @@ -2605,6 +2717,7 @@ files = [ name = "types-urllib3" version = "1.26.25.14" description = "Typing stubs for urllib3" +category = "dev" optional = false python-versions = "*" files = [ @@ -2616,6 +2729,7 @@ files = [ name = "typing-extensions" version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2627,6 +2741,7 @@ files = [ name = "tzdata" version = "2023.3" description = "Provider of IANA time zone data" +category = "main" optional = false python-versions = ">=2" files = [ @@ -2638,6 +2753,7 @@ files = [ name = "uc-micro-py" version = "1.0.2" description = "Micro subset of unicode data files for linkify-it-py projects." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2652,6 +2768,7 @@ test = ["coverage", "pytest", "pytest-cov"] name = "unidecode" version = "1.3.6" description = "ASCII transliterations of Unicode text" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -2663,6 +2780,7 @@ files = [ name = "untokenize" version = "0.1.1" description = "Transforms tokens into original source code (while preserving whitespace)." +category = "dev" optional = false python-versions = "*" files = [ @@ -2671,25 +2789,26 @@ files = [ [[package]] name = "urllib3" -version = "2.0.4" +version = "1.26.16" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"}, - {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"}, + {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, + {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uvloop" version = "0.17.0" description = "Fast implementation of asyncio event loop on top of libuv" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2732,13 +2851,14 @@ test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "my [[package]] name = "virtualenv" -version = "20.24.2" +version = "20.24.3" description = "Virtual Python Environment builder" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.2-py3-none-any.whl", hash = "sha256:43a3052be36080548bdee0b42919c88072037d50d56c28bd3f853cbe92b953ff"}, - {file = "virtualenv-20.24.2.tar.gz", hash = "sha256:fd8a78f46f6b99a67b7ec5cf73f92357891a7b3a40fd97637c27f854aae3b9e0"}, + {file = "virtualenv-20.24.3-py3-none-any.whl", hash = "sha256:95a6e9398b4967fbcb5fef2acec5efaf9aa4972049d9ae41f95e0972a683fd02"}, + {file = "virtualenv-20.24.3.tar.gz", hash = "sha256:e5c3b4ce817b0b328af041506a2a299418c98747c4b1e68cb7527e74ced23efc"}, ] [package.dependencies] @@ -2752,29 +2872,31 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "websocket-client" -version = "1.6.1" +version = "1.6.2" description = "WebSocket client for Python with low level API options" +category = "main" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "websocket-client-1.6.1.tar.gz", hash = "sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd"}, - {file = "websocket_client-1.6.1-py3-none-any.whl", hash = "sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d"}, + {file = "websocket-client-1.6.2.tar.gz", hash = "sha256:53e95c826bf800c4c465f50093a8c4ff091c7327023b10bfaff40cf1ef170eaa"}, + {file = "websocket_client-1.6.2-py3-none-any.whl", hash = "sha256:ce54f419dfae71f4bdba69ebe65bf7f0a93fe71bc009ad3a010aacc3eebad537"}, ] [package.extras] -docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] +docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] [[package]] name = "wheel" -version = "0.41.0" +version = "0.41.2" description = "A built-package format for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "wheel-0.41.0-py3-none-any.whl", hash = "sha256:7e9be3bbd0078f6147d82ed9ed957e323e7708f57e134743d2edef3a7b7972a9"}, - {file = "wheel-0.41.0.tar.gz", hash = "sha256:55a0f0a5a84869bce5ba775abfd9c462e3a6b1b7b7ec69d72c0b83d673a5114d"}, + {file = "wheel-0.41.2-py3-none-any.whl", hash = "sha256:75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8"}, + {file = "wheel-0.41.2.tar.gz", hash = "sha256:0c5ac5ff2afb79ac23ab82bab027a0be7b5dbcf2e54dc50efe4bf507de1f7985"}, ] [package.extras] @@ -2784,6 +2906,7 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"] name = "yarl" version = "1.9.2" description = "Yet another URL library" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2871,6 +2994,7 @@ multidict = ">=4.0" name = "zipp" version = "3.16.2" description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2891,4 +3015,4 @@ redis = ["hiredis", "redis"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "bad79248270b2ff740e10db6e9a3ed431cc897006281f17ac3a86cdffb18198c" +content-hash = "ef98e5c5636a949aee34ee2dd396c1b70448abca84971e16f510a845a5936652" diff --git a/pyproject.toml b/pyproject.toml index 56b3eb1999bb..819a77f931a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautilus_trader" -version = "1.176.0" +version = "1.177.0" description = "A high-performance algorithmic trading platform and event-driven backtester" authors = ["Nautech Systems "] license = "LGPL-3.0-or-later" @@ -33,7 +33,7 @@ include = [ requires = [ "setuptools", "poetry-core>=1.6.1", - "numpy>=1.25.1", + "numpy>=1.25.2", "Cython==3.0.0", ] build-backend = "poetry.core.masonry.api" @@ -48,14 +48,14 @@ cython = "==3.0.0" click = "^8.1.5" frozendict = "^2.3.8" fsspec = ">=2022.5.0" -msgspec = "^0.17.0" -numpy = "^1.25.1" +msgspec = "^0.18.1" +numpy = "^1.25.2" pandas = "^2.0.3" psutil = "^5.9.5" pyarrow = "^12.0.1" pytz = "^2023.3.0" toml = "^0.10.2" -tqdm = "^4.65.0" +tqdm = "^4.66.1" uvloop = {version = "^0.17.0", markers = "sys_platform != 'win32'"} hiredis = {version = "^2.2.3", optional = true} redis = {version = "^4.6.0", optional = true} @@ -76,19 +76,20 @@ optional = true [tool.poetry.group.dev.dependencies] black = "^23.7.0" docformatter = "^1.7.5" -mypy = "^1.4.1" +mypy = "^1.5.1" pre-commit = "^3.3.3" -ruff = "^0.0.280" +ruff = "^0.0.285" types-pytz = "^2023.3" types-redis = "^4.6" types-requests = "^2.31" types-toml = "^0.10.2" +urllib3 = "==1.26.16" # Temp pin for poetry compatibility [tool.poetry.group.test] optional = true [tool.poetry.group.test.dependencies] -coverage = "^7.2.7" +coverage = "^7.3.0" pytest = "^7.4.0" pytest-aiohttp = "^1.0.4" pytest-asyncio = "^0.21.1" diff --git a/tests/integration_tests/adapters/betfair/conftest.py b/tests/integration_tests/adapters/betfair/conftest.py index 5b73e9a6ae73..f67a92eb8323 100644 --- a/tests/integration_tests/adapters/betfair/conftest.py +++ b/tests/integration_tests/adapters/betfair/conftest.py @@ -27,15 +27,15 @@ from nautilus_trader.model.events.account import AccountState from nautilus_trader.model.identifiers import AccountId from nautilus_trader.model.identifiers import Venue -from nautilus_trader.test_kit.providers import TestInstrumentProvider from nautilus_trader.test_kit.stubs.events import TestEventStubs from tests.integration_tests.adapters.betfair.test_kit import BetfairResponses from tests.integration_tests.adapters.betfair.test_kit import BetfairTestStubs +from tests.integration_tests.adapters.betfair.test_kit import betting_instrument @pytest.fixture() def instrument(): - return TestInstrumentProvider.betting_instrument(selection_handicap="0.0") + return betting_instrument(selection_handicap="0.0") @pytest.fixture() diff --git a/tests/integration_tests/adapters/betfair/test_betfair_client.py b/tests/integration_tests/adapters/betfair/test_betfair_client.py index 698d1175bbfd..1f22b2d01c57 100644 --- a/tests/integration_tests/adapters/betfair/test_betfair_client.py +++ b/tests/integration_tests/adapters/betfair/test_betfair_client.py @@ -60,11 +60,12 @@ from nautilus_trader.model.identifiers import ClientOrderId from nautilus_trader.model.identifiers import PositionId from nautilus_trader.model.identifiers import VenueOrderId -from nautilus_trader.test_kit.providers import TestInstrumentProvider from nautilus_trader.test_kit.stubs.commands import TestCommandStubs from nautilus_trader.test_kit.stubs.execution import TestExecStubs from nautilus_trader.test_kit.stubs.identifiers import TestIdStubs from tests.integration_tests.adapters.betfair.test_kit import BetfairResponses +from tests.integration_tests.adapters.betfair.test_kit import betting_instrument +from tests.integration_tests.adapters.betfair.test_kit import betting_instrument_handicap from tests.integration_tests.adapters.betfair.test_kit import mock_betfair_request @@ -179,7 +180,7 @@ async def test_get_account_funds(betfair_client): @pytest.mark.asyncio() async def test_place_orders(betfair_client): - instrument = TestInstrumentProvider.betting_instrument() + instrument = betting_instrument() limit_order = TestExecStubs.limit_order( instrument_id=instrument.id, order_side=OrderSide.BUY, @@ -226,7 +227,7 @@ async def test_place_orders(betfair_client): @pytest.mark.asyncio() async def test_place_orders_handicap(betfair_client): - instrument = TestInstrumentProvider.betting_instrument_handicap() + instrument = betting_instrument_handicap() limit_order = TestExecStubs.limit_order( instrument_id=instrument.id, order_side=OrderSide.BUY, @@ -273,7 +274,7 @@ async def test_place_orders_handicap(betfair_client): @pytest.mark.asyncio() async def test_place_orders_market_on_close(betfair_client): - instrument = TestInstrumentProvider.betting_instrument() + instrument = betting_instrument() market_on_close_order = TestExecStubs.market_order( order_side=OrderSide.BUY, time_in_force=TimeInForce.AT_THE_OPEN, @@ -326,7 +327,7 @@ async def test_place_orders_market_on_close(betfair_client): @pytest.mark.asyncio() async def test_replace_orders_single(betfair_client): - instrument = TestInstrumentProvider.betting_instrument() + instrument = betting_instrument() update_order_command = TestCommandStubs.modify_order_command( instrument_id=instrument.id, client_order_id=ClientOrderId("1628717246480-1.186260932-rpl-0"), @@ -360,7 +361,7 @@ async def test_replace_orders_single(betfair_client): # @pytest.mark.asyncio() # async def test_replace_orders_multi(): -# instrument = TestInstrumentProvider.betting_instrument() +# instrument = betting_instrument() # update_order_command = TestCommandStubs.modify_order_command( # instrument_id=instrument.id, # price=betfair_float_to_price(2.0), @@ -384,7 +385,7 @@ async def test_replace_orders_single(betfair_client): @pytest.mark.asyncio() async def test_cancel_orders(betfair_client): - instrument = TestInstrumentProvider.betting_instrument() + instrument = betting_instrument() cancel_command = TestCommandStubs.cancel_order_command( venue_order_id=VenueOrderId("228302937743"), ) diff --git a/tests/integration_tests/adapters/betfair/test_betfair_common.py b/tests/integration_tests/adapters/betfair/test_betfair_common.py index b2be723c0c6f..637c8cb621ba 100644 --- a/tests/integration_tests/adapters/betfair/test_betfair_common.py +++ b/tests/integration_tests/adapters/betfair/test_betfair_common.py @@ -12,10 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------------------------- + +from decimal import Decimal + +import pytest + from nautilus_trader.adapters.betfair.common import BETFAIR_TICK_SCHEME from nautilus_trader.adapters.betfair.common import MAX_BET_PRICE from nautilus_trader.adapters.betfair.common import MIN_BET_PRICE from nautilus_trader.adapters.betfair.orderbook import betfair_float_to_price +from nautilus_trader.model.objects import Price +from nautilus_trader.model.objects import Quantity +from tests.integration_tests.adapters.betfair.test_kit import betting_instrument class TestBetfairCommon: @@ -29,3 +37,48 @@ def test_min_max_bet(self): def test_betfair_ticks(self): assert self.tick_scheme.min_price == betfair_float_to_price(1.01) assert self.tick_scheme.max_price == betfair_float_to_price(1000) + + +class TestBettingInstrument: + def setup(self): + self.instrument = betting_instrument() + + def test_notional_value(self): + notional = self.instrument.notional_value( + quantity=Quantity.from_int(100), + price=Price.from_str("0.5"), + use_quote_for_inverse=False, + ).as_decimal() + # We are long 100 at 0.5 probability, aka 2.0 in odds terms + assert notional == Decimal("200.0") + + @pytest.mark.parametrize( + ("value", "n", "expected"), + [ + (101, 0, "110"), + ], + ) + def test_next_ask_price(self, value, n, expected): + result = self.instrument.next_ask_price(value, num_ticks=n) + expected = Price.from_str(expected) + assert result == expected + + @pytest.mark.parametrize( + ("value", "n", "expected"), + [ + (1.999, 0, "1.99"), + ], + ) + def test_next_bid_price(self, value, n, expected): + result = self.instrument.next_bid_price(value, num_ticks=n) + expected = Price.from_str(expected) + assert result == expected + + def test_min_max_price(self): + assert self.instrument.min_price == Price.from_str("1.01") + assert self.instrument.max_price == Price.from_str("1000") + + def test_to_dict(self): + instrument = betting_instrument() + data = instrument.to_dict(instrument) + assert data["venue_name"] == "BETFAIR" diff --git a/tests/integration_tests/adapters/betfair/test_betfair_execution.py b/tests/integration_tests/adapters/betfair/test_betfair_execution.py index 7bdfa1c7dd07..77ca2341a55a 100644 --- a/tests/integration_tests/adapters/betfair/test_betfair_execution.py +++ b/tests/integration_tests/adapters/betfair/test_betfair_execution.py @@ -55,11 +55,11 @@ from nautilus_trader.model.objects import Money from nautilus_trader.model.objects import Price from nautilus_trader.model.objects import Quantity -from nautilus_trader.test_kit.providers import TestInstrumentProvider from nautilus_trader.test_kit.stubs.commands import TestCommandStubs from nautilus_trader.test_kit.stubs.execution import TestExecStubs from tests.integration_tests.adapters.betfair.test_kit import BetfairResponses from tests.integration_tests.adapters.betfair.test_kit import BetfairStreaming +from tests.integration_tests.adapters.betfair.test_kit import betting_instrument from tests.integration_tests.adapters.betfair.test_kit import mock_betfair_request @@ -89,7 +89,7 @@ async def _setup_order_state( venue_order_id = VenueOrderId(order_id) client_order_id = ClientOrderId(order_id) if not cache.instrument(instrument_id): - instrument = TestInstrumentProvider.betting_instrument( + instrument = betting_instrument( market_id=oc.id, selection_id=str(orc.id), selection_handicap=str(orc.hc), diff --git a/tests/integration_tests/adapters/betfair/test_betfair_parsing.py b/tests/integration_tests/adapters/betfair/test_betfair_parsing.py index 5a8a31c1f26b..3f955fbae05d 100644 --- a/tests/integration_tests/adapters/betfair/test_betfair_parsing.py +++ b/tests/integration_tests/adapters/betfair/test_betfair_parsing.py @@ -88,13 +88,13 @@ from nautilus_trader.model.objects import Money from nautilus_trader.model.objects import Price from nautilus_trader.model.orderbook import OrderBook -from nautilus_trader.test_kit.providers import TestInstrumentProvider from nautilus_trader.test_kit.stubs.commands import TestCommandStubs from nautilus_trader.test_kit.stubs.execution import TestExecStubs from nautilus_trader.test_kit.stubs.identifiers import TestIdStubs from tests.integration_tests.adapters.betfair.test_kit import BetfairDataProvider from tests.integration_tests.adapters.betfair.test_kit import BetfairResponses from tests.integration_tests.adapters.betfair.test_kit import BetfairTestStubs +from tests.integration_tests.adapters.betfair.test_kit import betting_instrument from tests.integration_tests.adapters.betfair.test_kit import mock_betfair_request @@ -103,7 +103,7 @@ class TestBetfairParsingStreaming: def setup(self): - self.instrument = TestInstrumentProvider.betting_instrument() + self.instrument = betting_instrument() self.tick_scheme = BETFAIR_TICK_SCHEME def test_market_definition_to_instrument_status_updates(self): @@ -262,7 +262,7 @@ def test_order_book_integrity(self, filename, book_count) -> None: ): instrument_id = update.instrument_id if instrument_id not in books: - instrument = TestInstrumentProvider.betting_instrument( + instrument = betting_instrument( *instrument_id.value.split("|"), ) books[instrument_id] = create_betfair_order_book(instrument.id) @@ -278,7 +278,7 @@ def setup(self): self.loop = asyncio.new_event_loop() self.clock = LiveClock() self.logger = Logger(clock=self.clock, bypass=True) - self.instrument = TestInstrumentProvider.betting_instrument() + self.instrument = betting_instrument() self.client = BetfairTestStubs.betfair_client(loop=self.loop, logger=self.logger) self.provider = BetfairTestStubs.instrument_provider(self.client) self.uuid = UUID4() diff --git a/tests/integration_tests/adapters/betfair/test_betfair_persistence.py b/tests/integration_tests/adapters/betfair/test_betfair_persistence.py index 03b59034cf81..fa3491552b41 100644 --- a/tests/integration_tests/adapters/betfair/test_betfair_persistence.py +++ b/tests/integration_tests/adapters/betfair/test_betfair_persistence.py @@ -23,8 +23,8 @@ from nautilus_trader.persistence.external.core import process_raw_file from nautilus_trader.serialization.arrow.serializer import ParquetSerializer from nautilus_trader.test_kit.mocks.data import data_catalog_setup -from nautilus_trader.test_kit.providers import TestInstrumentProvider from tests import TEST_DATA_DIR +from tests.integration_tests.adapters.betfair.test_kit import betting_instrument @pytest.mark.skip(reason="Reimplementing") @@ -32,7 +32,7 @@ class TestBetfairPersistence: def setup(self): self.catalog = data_catalog_setup(protocol="memory") self.fs = self.catalog.fs - self.instrument = TestInstrumentProvider.betting_instrument() + self.instrument = betting_instrument() def test_bsp_delta_serialize(self): # Arrange diff --git a/tests/unit_tests/accounting/test_betting.py b/tests/integration_tests/adapters/betfair/test_betting_account.py similarity index 98% rename from tests/unit_tests/accounting/test_betting.py rename to tests/integration_tests/adapters/betfair/test_betting_account.py index b483551e3e2f..89cf20236087 100644 --- a/tests/unit_tests/accounting/test_betting.py +++ b/tests/integration_tests/adapters/betfair/test_betting_account.py @@ -33,17 +33,17 @@ from nautilus_trader.model.objects import Price from nautilus_trader.model.objects import Quantity from nautilus_trader.model.position import Position -from nautilus_trader.test_kit.providers import TestInstrumentProvider from nautilus_trader.test_kit.stubs.events import TestEventStubs from nautilus_trader.test_kit.stubs.execution import TestExecStubs from nautilus_trader.test_kit.stubs.identifiers import TestIdStubs +from tests.integration_tests.adapters.betfair.test_kit import betting_instrument class TestBettingAccount: def setup(self): # Fixture Setup self.trader_id = TestIdStubs.trader_id() - self.instrument = TestInstrumentProvider.betting_instrument() + self.instrument = betting_instrument() self.order_factory = OrderFactory( trader_id=self.trader_id, strategy_id=StrategyId("S-001"), diff --git a/tests/integration_tests/adapters/betfair/test_betting_tick_scheme.py b/tests/integration_tests/adapters/betfair/test_betting_tick_scheme.py new file mode 100644 index 000000000000..090af4364b7c --- /dev/null +++ b/tests/integration_tests/adapters/betfair/test_betting_tick_scheme.py @@ -0,0 +1,156 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2023 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.objects import Price +from nautilus_trader.model.tick_scheme.base import get_tick_scheme +from nautilus_trader.model.tick_scheme.implementations.fixed import FixedTickScheme +from nautilus_trader.model.tick_scheme.implementations.tiered import TieredTickScheme + + +class TestBettingTickScheme: + def setup(self) -> None: + self.tick_scheme: TieredTickScheme = get_tick_scheme("BETFAIR") + + def test_attrs(self): + assert self.tick_scheme.min_price == Price.from_str("1.01") + assert self.tick_scheme.max_price == Price.from_str("1000") + + def test_build_ticks(self): + result = self.tick_scheme.ticks[:5].tolist() + expected = [ + Price.from_str("1.01"), + Price.from_str("1.02"), + Price.from_str("1.03"), + Price.from_str("1.04"), + Price.from_str("1.05"), + ] + assert result == expected + + @pytest.mark.parametrize( + ("value", "expected"), + [ + (1.01, 0), + (1.10, 9), + (2.0, 99), + (3.5, 159), + ], + ) + def test_find_tick_idx(self, value, expected): + result = self.tick_scheme.find_tick_index(value) + assert result == expected + + @pytest.mark.parametrize( + ("value", "n", "expected"), + [ + (1.499, 0, "1.50"), + (2.000, 0, "2.0"), + (2.011, 0, "2.02"), + (2.021, 0, "2.04"), + (2.027, 2, "2.08"), + ], + ) + def test_next_ask_price(self, value, n, expected): + result = self.tick_scheme.next_ask_price(value, n=n) + expected = Price.from_str(expected) + assert result == expected + + @pytest.mark.parametrize( + ("value", "n", "expected"), + [ + (1.499, 0, "1.49"), + (2.000, 0, "2.0"), + (2.011, 0, "2.00"), + (2.021, 0, "2.02"), + (2.027, 2, "1.99"), + ], + ) + def test_next_bid_price(self, value, n, expected): + result = self.tick_scheme.next_bid_price(value=value, n=n) + expected = Price.from_str(expected) + assert result == expected + + +class TestTopix100TickScheme: + def setup(self) -> None: + self.tick_scheme = get_tick_scheme("TOPIX100") + + def test_attrs(self): + assert self.tick_scheme.min_price == Price.from_str("0.1") + assert self.tick_scheme.max_price == Price.from_int(130_000_000) + + @pytest.mark.parametrize( + ("value", "n", "expected"), + [ + (1000, 0, "1000"), + (1000.25, 0, "1000.50"), + (10_001, 0, "10_005"), + (10_000_001, 0, "10_005_000"), + (9999, 2, "10_005"), + ], + ) + def test_next_ask_price(self, value, n, expected): + result = self.tick_scheme.next_ask_price(value, n=n) + expected = Price.from_str(expected) + assert result == expected + + @pytest.mark.parametrize( + ("value", "n", "expected"), + [ + # (1000, 0, "1000"), # TODO: Fails with 999.9 + (1000.75, 0, "1000.50"), + (10_007, 0, "10_005"), + (10_000_001, 0, "10_000_000"), + (10_006, 2, "9999"), + ], + ) + def test_next_bid_price(self, value, n, expected): + result = self.tick_scheme.next_bid_price(value=value, n=n) + expected = Price.from_str(expected) + assert result == expected + + +class TestBitmexSpotTickScheme: + def setup(self) -> None: + self.tick_scheme = FixedTickScheme( + name="BitmexSpot", + price_precision=1, + increment=0.50, + min_tick=Price.from_str("0.001"), + max_tick=Price.from_str("999.999"), + ) + + @pytest.mark.parametrize( + ("value", "n", "expected"), + [ + (10.1, 0, "10.5"), + ], + ) + def test_next_ask_price(self, value, n, expected): + result = self.tick_scheme.next_ask_price(value, n=n) + expected = Price.from_str(expected) + assert result == expected + + @pytest.mark.parametrize( + ("value", "n", "expected"), + [ + (10.1, 0, "10.0"), + ], + ) + def test_next_bid_price(self, value, n, expected): + result = self.tick_scheme.next_bid_price(value=value, n=n) + expected = Price.from_str(expected) + assert result == expected diff --git a/tests/integration_tests/adapters/betfair/test_kit.py b/tests/integration_tests/adapters/betfair/test_kit.py index 65796c3192e5..6078c97eae3b 100644 --- a/tests/integration_tests/adapters/betfair/test_kit.py +++ b/tests/integration_tests/adapters/betfair/test_kit.py @@ -36,6 +36,7 @@ from betfair_parser.spec.streaming.ocm import UnmatchedOrder from nautilus_trader.adapters.betfair.client import BetfairHttpClient +from nautilus_trader.adapters.betfair.common import BETFAIR_TICK_SCHEME from nautilus_trader.adapters.betfair.constants import BETFAIR_VENUE from nautilus_trader.adapters.betfair.data import BetfairParser from nautilus_trader.adapters.betfair.historic import make_betfair_reader @@ -768,3 +769,60 @@ def betfair_feed_parsed(market_id: str = "1.166564490"): @staticmethod def badly_formatted_log(): return open(RESOURCES_PATH / "badly_formatted.txt", "rb").read() + + +def betting_instrument( + market_id: str = "1.179082386", + selection_id: str = "50214", + selection_handicap: Optional[str] = None, +) -> BettingInstrument: + return BettingInstrument( + venue_name=BETFAIR_VENUE.value, + betting_type="ODDS", + competition_id="12282733", + competition_name="NFL", + event_country_code="GB", + event_id="29678534", + event_name="NFL", + event_open_date=pd.Timestamp("2022-02-07 23:30:00+00:00"), + event_type_id="6423", + event_type_name="American Football", + market_id=market_id, + market_name="AFC Conference Winner", + market_start_time=pd.Timestamp("2022-02-07 23:30:00+00:00"), + market_type="SPECIAL", + selection_handicap=selection_handicap, + selection_id=selection_id, + selection_name="Kansas City Chiefs", + currency="GBP", + tick_scheme_name=BETFAIR_TICK_SCHEME.name, + ts_event=0, + ts_init=0, + ) + + +def betting_instrument_handicap() -> BettingInstrument: + return BettingInstrument.from_dict( + { + "venue_name": "BETFAIR", + "event_type_id": "61420", + "event_type_name": "Australian Rules", + "competition_id": "11897406", + "competition_name": "AFL", + "event_id": "30777079", + "event_name": "GWS v Richmond", + "event_country_code": "AU", + "event_open_date": "2021-08-13T09:50:00+00:00", + "betting_type": "ASIAN_HANDICAP_DOUBLE_LINE", + "market_id": "1.186249896", + "market_name": "Handicap", + "market_start_time": "2021-08-13T09:50:00+00:00", + "market_type": "HANDICAP", + "selection_id": "5304641", + "selection_name": "GWS", + "selection_handicap": "-5.5", + "currency": "AUD", + "ts_event": 0, + "ts_init": 0, + }, + ) diff --git a/tests/integration_tests/adapters/binance/test_data_spot.py b/tests/integration_tests/adapters/binance/test_data_spot.py index 4f79b49d0455..c1750ef2abc6 100644 --- a/tests/integration_tests/adapters/binance/test_data_spot.py +++ b/tests/integration_tests/adapters/binance/test_data_spot.py @@ -285,8 +285,8 @@ async def test_subscribe_quote_ticks(self, monkeypatch): assert len(handler) == 1 # <-- handler received tick assert handler[0] == QuoteTick( instrument_id=ETHUSDT_BINANCE.id, - bid=Price.from_str("4507.24000000"), - ask=Price.from_str("4507.25000000"), + bid_price=Price.from_str("4507.24000000"), + ask_price=Price.from_str("4507.25000000"), bid_size=Quantity.from_str("2.35950000"), ask_size=Quantity.from_str("2.84570000"), ts_event=handler[0].ts_init, # TODO: WIP diff --git a/tests/integration_tests/adapters/interactive_brokers/test_data.py b/tests/integration_tests/adapters/interactive_brokers/test_data.py index 724e8d6b2dbe..61877212f3eb 100644 --- a/tests/integration_tests/adapters/interactive_brokers/test_data.py +++ b/tests/integration_tests/adapters/interactive_brokers/test_data.py @@ -177,8 +177,8 @@ async def test_on_quote_tick_update_nans(data_client, instrument): { "type": "QuoteTick", "instrument_id": "AAPL.NASDAQ", - "bid": "0.00", - "ask": "0.00", + "bid_price": "0.00", + "ask_price": "0.00", "bid_size": "44600", "ask_size": "29500", "ts_event": 0, diff --git a/tests/integration_tests/adapters/interactive_brokers/test_historic.py b/tests/integration_tests/adapters/interactive_brokers/test_historic.py index c7fb3e8447dc..7af9d3dc6fe1 100644 --- a/tests/integration_tests/adapters/interactive_brokers/test_historic.py +++ b/tests/integration_tests/adapters/interactive_brokers/test_historic.py @@ -175,8 +175,8 @@ def test_parse_historic_quote_ticks(): { "type": "QuoteTick", "instrument_id": "AAPL.NASDAQ", - "bid": "0.99", - "ask": "15.30", + "bid_price": "0.99", + "ask_price": "15.30", "bid_size": "1", "ask_size": "1", "ts_event": 1646176203000000000, diff --git a/tests/integration_tests/adapters/sandbox/test_execution.py b/tests/integration_tests/adapters/sandbox/test_execution.py index 1c37af8f3eb6..6baf6f9d237b 100644 --- a/tests/integration_tests/adapters/sandbox/test_execution.py +++ b/tests/integration_tests/adapters/sandbox/test_execution.py @@ -36,8 +36,8 @@ def _make_quote_tick(instrument): return QuoteTick( instrument_id=instrument.id, - bid=Price.from_int(10), - ask=Price.from_int(10), + bid_price=Price.from_int(10), + ask_price=Price.from_int(10), bid_size=Quantity.from_int(100), ask_size=Quantity.from_int(100), ts_init=0, diff --git a/nautilus_trader/common/queue.pxd b/tests/mem_leak_tests/__init__.py similarity index 55% rename from nautilus_trader/common/queue.pxd rename to tests/mem_leak_tests/__init__.py index 2de609324af6..ca16b56e4794 100644 --- a/nautilus_trader/common/queue.pxd +++ b/tests/mem_leak_tests/__init__.py @@ -12,28 +12,3 @@ # See the License for the specific language governing permissions and # limitations under the License. # ------------------------------------------------------------------------------------------------- - - -cdef class Queue: - cdef object _queue - - cdef readonly int maxsize - """The maximum capacity of the queue before blocking.\n\n:returns: `int`""" - cdef readonly int count - """The current count of items on the queue.\n\n:returns: `int`""" - - cpdef int qsize(self) - cpdef bint empty(self) - cpdef bint full(self) - cpdef void put_nowait(self, item) - cpdef object get_nowait(self) - cpdef object peek_back(self) - cpdef object peek_front(self) - cpdef object peek_index(self, int index) - cpdef list to_list(self) - - cdef int _qsize(self) - cdef bint _empty(self) - cdef bint _full(self) - cdef void _put_nowait(self, item) - cdef object _get_nowait(self) diff --git a/tests/mem_leak_tests/memray_backtest.py b/tests/mem_leak_tests/memray_backtest.py index 97f320abccd4..fd31cfe66a15 100644 --- a/tests/mem_leak_tests/memray_backtest.py +++ b/tests/mem_leak_tests/memray_backtest.py @@ -33,67 +33,69 @@ from nautilus_trader.test_kit.providers import TestInstrumentProvider +count = 0 +total_runs = 128 + if __name__ == "__main__": - # Configure backtest engine - config = BacktestEngineConfig( - trader_id="BACKTESTER-001", - logging=LoggingConfig(bypass_logging=True), - ) - - # Build the backtest engine - engine = BacktestEngine(config=config) - - ETHUSDT_BINANCE = TestInstrumentProvider.ethusdt_binance() - - # Add a trading venue (multiple venues possible) - BINANCE = Venue("BINANCE") - engine.add_venue( - venue=BINANCE, - oms_type=OmsType.NETTING, - account_type=AccountType.CASH, # Spot CASH account (not for perpetuals or futures) - base_currency=None, # Multi-currency account - starting_balances=[Money(1_000_000.0, USDT), Money(10.0, ETH)], - ) - - # Add instruments - engine.add_instrument(ETHUSDT_BINANCE) - - # Add data - provider = TestDataProvider() - wrangler = TradeTickDataWrangler(instrument=ETHUSDT_BINANCE) - ticks = wrangler.process(provider.read_csv_ticks("binance-ethusdt-trades.csv")) - engine.add_data(ticks) - - # Configure your strategy - config = EMACrossTWAPConfig( - instrument_id=str(ETHUSDT_BINANCE.id), - bar_type="ETHUSDT.BINANCE-250-TICK-LAST-INTERNAL", - trade_size=Decimal("0.05"), - fast_ema_period=10, - slow_ema_period=20, - twap_horizon_secs=10.0, - twap_interval_secs=2.5, - ) - - # Instantiate and add your strategy - strategy = EMACrossTWAP(config=config) - engine.add_strategy(strategy=strategy) - - # Instantiate and add your execution algorithm - exec_algorithm = TWAPExecAlgorithm() - engine.add_exec_algorithm(exec_algorithm) - - count = 0 - total_runs = 128 while count < total_runs: - count += 1 print(f"Run: {count}/{total_runs}") + # Configure backtest engine + config = BacktestEngineConfig( + trader_id="BACKTESTER-001", + logging=LoggingConfig(bypass_logging=True), + ) + + # Build the backtest engine + engine = BacktestEngine(config=config) + + ETHUSDT_BINANCE = TestInstrumentProvider.ethusdt_binance() + + # Add a trading venue (multiple venues possible) + BINANCE = Venue("BINANCE") + engine.add_venue( + venue=BINANCE, + oms_type=OmsType.NETTING, + account_type=AccountType.CASH, # Spot CASH account (not for perpetuals or futures) + base_currency=None, # Multi-currency account + starting_balances=[Money(1_000_000.0, USDT), Money(10.0, ETH)], + ) + + # Add instruments + engine.add_instrument(ETHUSDT_BINANCE) + + # Add data + provider = TestDataProvider() + wrangler = TradeTickDataWrangler(instrument=ETHUSDT_BINANCE) + ticks = wrangler.process(provider.read_csv_ticks("binance-ethusdt-trades.csv")) + engine.add_data(ticks) + + # Configure your strategy + config = EMACrossTWAPConfig( + instrument_id=str(ETHUSDT_BINANCE.id), + bar_type="ETHUSDT.BINANCE-250-TICK-LAST-INTERNAL", + trade_size=Decimal("0.05"), + fast_ema_period=10, + slow_ema_period=20, + twap_horizon_secs=10.0, + twap_interval_secs=2.5, + ) + + # Instantiate and add your strategy + strategy = EMACrossTWAP(config=config) + engine.add_strategy(strategy=strategy) + + # Instantiate and add your execution algorithm + exec_algorithm = TWAPExecAlgorithm() + engine.add_exec_algorithm(exec_algorithm) + # Run the engine (from start to end of data) engine.run() # For repeated backtest runs make sure to reset the engine engine.reset() - # Good practice to dispose of the object - engine.dispose() + # Good practice to dispose of the object + engine.dispose() + + count += 1 diff --git a/tests/mem_leak_tests/tracemalloc_cross_bracket_gbpusd_bars_internal.py b/tests/mem_leak_tests/tracemalloc_cross_bracket_gbpusd_bars_internal.py index e24028e09a19..c345937e643a 100644 --- a/tests/mem_leak_tests/tracemalloc_cross_bracket_gbpusd_bars_internal.py +++ b/tests/mem_leak_tests/tracemalloc_cross_bracket_gbpusd_bars_internal.py @@ -16,8 +16,6 @@ from decimal import Decimal -from tracemalloc_snapshot_fixture import snapshot_memory - from nautilus_trader.backtest.engine import BacktestEngine from nautilus_trader.backtest.engine import BacktestEngineConfig from nautilus_trader.backtest.models import FillModel @@ -33,6 +31,7 @@ from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.objects import Money from nautilus_trader.persistence.wranglers import QuoteTickDataWrangler +from nautilus_trader.test_kit.fixtures.memory import snapshot_memory from nautilus_trader.test_kit.providers import TestDataProvider from nautilus_trader.test_kit.providers import TestInstrumentProvider diff --git a/tests/mem_leak_tests/tracemalloc_crypto_ema_cross_ethusdt_trailing_stop.py b/tests/mem_leak_tests/tracemalloc_crypto_ema_cross_ethusdt_trailing_stop.py index 4f2df10cffc3..fcd5ab872daa 100644 --- a/tests/mem_leak_tests/tracemalloc_crypto_ema_cross_ethusdt_trailing_stop.py +++ b/tests/mem_leak_tests/tracemalloc_crypto_ema_cross_ethusdt_trailing_stop.py @@ -16,8 +16,6 @@ from decimal import Decimal -from tracemalloc_snapshot_fixture import snapshot_memory - from nautilus_trader.backtest.engine import BacktestEngine from nautilus_trader.backtest.engine import BacktestEngineConfig from nautilus_trader.config.common import LoggingConfig @@ -30,6 +28,7 @@ from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.objects import Money from nautilus_trader.persistence.wranglers import TradeTickDataWrangler +from nautilus_trader.test_kit.fixtures.memory import snapshot_memory from nautilus_trader.test_kit.providers import TestDataProvider from nautilus_trader.test_kit.providers import TestInstrumentProvider diff --git a/tests/mem_leak_tests/tracemalloc_market_maker_gbpusd_bars.py b/tests/mem_leak_tests/tracemalloc_market_maker_gbpusd_bars.py index d69edf59cace..8c188a371e72 100644 --- a/tests/mem_leak_tests/tracemalloc_market_maker_gbpusd_bars.py +++ b/tests/mem_leak_tests/tracemalloc_market_maker_gbpusd_bars.py @@ -17,8 +17,6 @@ from datetime import datetime from decimal import Decimal -from tracemalloc_snapshot_fixture import snapshot_memory - from nautilus_trader.backtest.engine import BacktestEngine from nautilus_trader.backtest.engine import BacktestEngineConfig from nautilus_trader.backtest.models import FillModel @@ -33,6 +31,7 @@ from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.objects import Money from nautilus_trader.persistence.wranglers import QuoteTickDataWrangler +from nautilus_trader.test_kit.fixtures.memory import snapshot_memory from nautilus_trader.test_kit.providers import TestDataProvider from nautilus_trader.test_kit.providers import TestInstrumentProvider diff --git a/tests/mem_leak_tests/tracemalloc_trade_ticks.py b/tests/mem_leak_tests/tracemalloc_trade_ticks.py new file mode 100644 index 000000000000..44b2e4bf92e9 --- /dev/null +++ b/tests/mem_leak_tests/tracemalloc_trade_ticks.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2023 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from nautilus_trader.model.data import TradeTick +from nautilus_trader.model.enums import AggressorSide +from nautilus_trader.model.identifiers import TradeId +from nautilus_trader.model.objects import Price +from nautilus_trader.model.objects import Quantity + +# from nautilus_trader.persistence.wranglers import TradeTickDataWrangler +from nautilus_trader.test_kit.fixtures.memory import snapshot_memory + +# from nautilus_trader.test_kit.providers import TestDataProvider +from nautilus_trader.test_kit.providers import TestInstrumentProvider + + +ETHUSDT_BINANCE = TestInstrumentProvider.ethusdt_binance() + + +@snapshot_memory(1000) +def run(*args, **kwargs): + # provider = TestDataProvider() + # wrangler = TradeTickDataWrangler(instrument=ETHUSDT_BINANCE) + # _ = wrangler.process(provider.read_csv_ticks("binance-ethusdt-trades.csv")) + _ = TradeTick( + ETHUSDT_BINANCE.id, + Price.from_str("1.00000"), + Quantity.from_str("1"), + AggressorSide.BUYER, + TradeId("123456789"), + 1, + 2, + ) + + +if __name__ == "__main__": + run() diff --git a/tests/performance_tests/test_perf_identifiers.py b/tests/performance_tests/test_perf_identifiers.py new file mode 100644 index 000000000000..7db9a06a3366 --- /dev/null +++ b/tests/performance_tests/test_perf_identifiers.py @@ -0,0 +1,29 @@ +from nautilus_trader.model.identifiers import Symbol +from nautilus_trader.model.identifiers import Venue +from nautilus_trader.test_kit.performance import PerformanceBench + + +def test_symbol_equality(): + symbol = Symbol("AUD/USD") + + def symbol_equality() -> bool: + return symbol == symbol + + PerformanceBench.profile_function( + target=symbol_equality, + runs=1_000_000, + iterations=1, + ) + + +def test_venue_equality(): + venue = Venue("SIM") + + def venue_equality() -> bool: + return venue == venue + + PerformanceBench.profile_function( + target=venue_equality, + runs=1_000_000, + iterations=1, + ) diff --git a/tests/performance_tests/test_perf_objects.py b/tests/performance_tests/test_perf_objects.py index e91a46d2025b..8b34ccf65435 100644 --- a/tests/performance_tests/test_perf_objects.py +++ b/tests/performance_tests/test_perf_objects.py @@ -87,8 +87,8 @@ def test_create_quote_tick(self): def create_quote_tick(): QuoteTick( instrument_id=audusd_sim.id, - bid=Price.from_str("1.00000"), - ask=Price.from_str("1.00001"), + bid_price=Price.from_str("1.00000"), + ask_price=Price.from_str("1.00001"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, diff --git a/tests/unit_tests/analysis/test_reports.py b/tests/unit_tests/analysis/test_reports.py index bb440f13cad6..f63714928474 100644 --- a/tests/unit_tests/analysis/test_reports.py +++ b/tests/unit_tests/analysis/test_reports.py @@ -13,6 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +import pandas as pd + from nautilus_trader.accounting.accounts.margin import MarginAccount from nautilus_trader.analysis.reporter import ReportProvider from nautilus_trader.common.clock import TestClock @@ -251,5 +253,5 @@ def test_generate_positions_report(self): assert report.iloc[0]["avg_px_open"] == "1.0001" assert report.iloc[0]["avg_px_close"] == "1.0001" assert report.iloc[0]["ts_opened"] == UNIX_EPOCH - assert report.iloc[0]["ts_closed"] == UNIX_EPOCH + assert pd.isna(report.iloc[0]["ts_closed"]) assert report.iloc[0]["realized_return"] == "0.0" diff --git a/tests/unit_tests/backtest/test_config.py b/tests/unit_tests/backtest/test_config.py index 819bfc47327c..f29c83d077d7 100644 --- a/tests/unit_tests/backtest/test_config.py +++ b/tests/unit_tests/backtest/test_config.py @@ -216,7 +216,7 @@ def test_run_config_to_json(self) -> None: ) json = msgspec.json.encode(run_config) result = len(msgspec.json.encode(json)) - assert result == 978 # UNIX + assert result == 1010 # UNIX @pytest.mark.skipif(sys.platform == "win32", reason="redundant to also test Windows") def test_run_config_parse_obj(self) -> None: @@ -237,7 +237,7 @@ def test_run_config_parse_obj(self) -> None: assert isinstance(config, BacktestRunConfig) node = BacktestNode(configs=[config]) assert isinstance(node, BacktestNode) - assert len(raw) == 733 # UNIX + assert len(raw) == 757 # UNIX @pytest.mark.skipif(sys.platform == "win32", reason="redundant to also test Windows") def test_backtest_data_config_to_dict(self) -> None: @@ -258,7 +258,7 @@ def test_backtest_data_config_to_dict(self) -> None: ) json = msgspec.json.encode(run_config) result = len(msgspec.json.encode(json)) - assert result == 1834 + assert result == 1866 @pytest.mark.skipif(sys.platform == "win32", reason="redundant to also test Windows") def test_backtest_run_config_id(self) -> None: @@ -266,7 +266,7 @@ def test_backtest_run_config_id(self) -> None: print("token:", token) value: bytes = msgspec.json.encode(self.backtest_config.dict(), enc_hook=json_encoder) print("token_value:", value.decode()) - assert token == "bfa25a6e95b0dd84abc36ec4b1f77926ece791670728fa8eb7b7ffeea2b59ef4" # UNIX + assert token == "acf938231c1e196f54f34716dd4feb5e16f820087ca3b5b15bccf7b8e2e36bd0" # UNIX @pytest.mark.skip(reason="fix after merge") @pytest.mark.parametrize( diff --git a/tests/unit_tests/backtest/test_data_wranglers.py b/tests/unit_tests/backtest/test_data_wranglers.py index b9d7b5dad87e..b022f2d464ec 100644 --- a/tests/unit_tests/backtest/test_data_wranglers.py +++ b/tests/unit_tests/backtest/test_data_wranglers.py @@ -64,8 +64,8 @@ def test_process_tick_data(self): # Assert assert len(ticks) == 1000 assert ticks[0].instrument_id == usdjpy.id - assert ticks[0].bid == Price.from_str("86.655") - assert ticks[0].ask == Price.from_str("86.728") + assert ticks[0].bid_price == Price.from_str("86.655") + assert ticks[0].ask_price == Price.from_str("86.728") assert ticks[0].bid_size == Quantity.from_int(1_000_000) assert ticks[0].ask_size == Quantity.from_int(1_000_000) assert ticks[0].ts_event == 1357077600295000064 @@ -88,8 +88,8 @@ def test_process_tick_data_with_delta(self): # Assert assert len(ticks) == 1000 assert ticks[0].instrument_id == usdjpy.id - assert ticks[0].bid == Price.from_str("86.655") - assert ticks[0].ask == Price.from_str("86.728") + assert ticks[0].bid_price == Price.from_str("86.655") + assert ticks[0].ask_price == Price.from_str("86.728") assert ticks[0].bid_size == Quantity.from_int(1_000_000) assert ticks[0].ask_size == Quantity.from_int(1_000_000) assert ticks[0].ts_event == 1357077600295000064 @@ -115,8 +115,8 @@ def test_pre_process_bar_data_with_delta(self): # Assert assert len(ticks) == 400 assert ticks[0].instrument_id == usdjpy.id - assert ticks[0].bid == Price.from_str("91.715") - assert ticks[0].ask == Price.from_str("91.717") + assert ticks[0].bid_price == Price.from_str("91.715") + assert ticks[0].ask_price == Price.from_str("91.717") assert ticks[0].bid_size == Quantity.from_int(1_000_000) assert ticks[0].ask_size == Quantity.from_int(1_000_000) assert ticks[0].ts_event == 1359676799700000000 @@ -140,14 +140,14 @@ def test_pre_process_bar_data_with_random_seed(self): ) # Assert - assert ticks[0].bid == Price.from_str("91.715") - assert ticks[0].ask == Price.from_str("91.717") - assert ticks[1].bid == Price.from_str("91.653") - assert ticks[1].ask == Price.from_str("91.655") - assert ticks[2].bid == Price.from_str("91.715") - assert ticks[2].ask == Price.from_str("91.717") - assert ticks[3].bid == Price.from_str("91.653") - assert ticks[3].ask == Price.from_str("91.655") + assert ticks[0].bid_price == Price.from_str("91.715") + assert ticks[0].ask_price == Price.from_str("91.717") + assert ticks[1].bid_price == Price.from_str("91.653") + assert ticks[1].ask_price == Price.from_str("91.655") + assert ticks[2].bid_price == Price.from_str("91.715") + assert ticks[2].ask_price == Price.from_str("91.717") + assert ticks[3].bid_price == Price.from_str("91.653") + assert ticks[3].ask_price == Price.from_str("91.655") class TestTradeTickDataWrangler: @@ -320,8 +320,8 @@ def test_pre_process_with_tick_data(self): # Assert assert len(ticks) == 9999 - assert ticks[0].bid == Price.from_str("9681.92") - assert ticks[0].ask == Price.from_str("9682.00") + assert ticks[0].bid_price == Price.from_str("9681.92") + assert ticks[0].ask_price == Price.from_str("9682.00") assert ticks[0].bid_size == Quantity.from_str("0.670000") assert ticks[0].ask_size == Quantity.from_str("0.840000") assert ticks[0].ts_event == 1582329603502091776 diff --git a/tests/unit_tests/backtest/test_exchange.py b/tests/unit_tests/backtest/test_exchange.py index 71f83620397f..259e87bdc2bd 100644 --- a/tests/unit_tests/backtest/test_exchange.py +++ b/tests/unit_tests/backtest/test_exchange.py @@ -255,8 +255,8 @@ def test_process_quote_tick_updates_market(self) -> None: # Arrange tick = TestDataStubs.quote_tick( USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) # Act @@ -456,8 +456,8 @@ def test_submit_order_with_invalid_price_gets_rejected(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.exchange.process_quote_tick(tick) self.portfolio.update_quote_tick(tick) @@ -508,8 +508,8 @@ def test_submit_market_order(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -533,8 +533,8 @@ def test_submit_market_order_then_immediately_cancel_submits_and_fills(self) -> # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -558,8 +558,8 @@ def test_submit_market_order_with_fok_time_in_force_cancels_immediately(self) -> # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, bid_size=500_000, ask_size=500_000, ) @@ -587,8 +587,8 @@ def test_submit_market_order_with_ioc_time_in_force_cancels_remaining_qty(self) # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, bid_size=500_000, ask_size=500_000, ) @@ -616,8 +616,8 @@ def test_submit_limit_order_then_immediately_cancel_submits_then_cancels(self) - # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -642,8 +642,8 @@ def test_submit_post_only_limit_order_when_marketable_then_rejects(self) -> None # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -668,8 +668,8 @@ def test_submit_limit_order(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -694,8 +694,8 @@ def test_submit_limit_order_with_ioc_time_in_force_immediately_cancels(self) -> # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, bid_size=500_000, ask_size=500_000, ) @@ -726,8 +726,8 @@ def test_submit_limit_order_with_fok_time_in_force_immediately_cancels(self) -> # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, bid_size=500_000, ask_size=500_000, ) @@ -758,8 +758,8 @@ def test_submit_market_to_limit_order_less_than_available_top_of_book(self) -> N # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -783,8 +783,8 @@ def test_submit_market_to_limit_order_greater_than_available_top_of_book(self) - # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, bid_size=1_000_000, ask_size=1_000_000, ) @@ -812,8 +812,8 @@ def test_modify_market_to_limit_order_after_filling_initial_quantity(self) -> No # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, bid_size=1_000_000, ask_size=1_000_000, ) @@ -848,8 +848,8 @@ def test_submit_market_to_limit_order_becomes_limit_then_fills_remaining(self) - # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, bid_size=1_000_000, ask_size=1_000_000, ) @@ -868,8 +868,8 @@ def test_submit_market_to_limit_order_becomes_limit_then_fills_remaining(self) - tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, # <-- hit bid again + bid_price=90.002, + ask_price=90.005, # <-- hit bid again bid_size=1_000_000, ask_size=1_000_000, ) @@ -887,8 +887,8 @@ def test_submit_market_if_touched_order(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -913,8 +913,8 @@ def test_submit_limit_if_touched_order(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -940,8 +940,8 @@ def test_submit_limit_order_when_marketable_then_fills(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -968,8 +968,8 @@ def test_submit_limit_order_fills_at_correct_price(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -988,8 +988,8 @@ def test_submit_limit_order_fills_at_correct_price(self) -> None: tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=89.900, - ask=89.950, + bid_price=89.900, + ask_price=89.950, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1003,8 +1003,8 @@ def test_submit_limit_order_fills_at_most_book_volume(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, bid_size=1_000_000, ask_size=1_000_000, ) @@ -1031,8 +1031,8 @@ def test_submit_market_if_touched_order_then_fills(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1051,7 +1051,7 @@ def test_submit_market_if_touched_order_then_fills(self) -> None: # Quantity is refreshed -> Ensure we don't trade the entire amount tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - ask=90.0, + ask_price=90.0, ask_size=10_000, ) self.data_engine.process(tick) @@ -1085,8 +1085,8 @@ def test_submit_limit_if_touched_order_then_fills( # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.000, - ask=90.010, + bid_price=90.000, + ask_price=90.010, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1106,8 +1106,8 @@ def test_submit_limit_if_touched_order_then_fills( # Quantity is refreshed -> Ensure we don't trade the entire amount tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.010, # <-- in cross for purpose of test - ask=90.000, + bid_price=90.010, # <-- in cross for purpose of test + ask_price=90.000, bid_size=10_000, ask_size=10_000, ) @@ -1133,8 +1133,8 @@ def test_submit_limit_order_fills_at_most_order_volume( # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.005, - ask=90.005, + bid_price=90.005, + ask_price=90.005, bid_size=Quantity.from_int(10_000), ask_size=Quantity.from_int(10_000), ) @@ -1158,8 +1158,8 @@ def test_submit_limit_order_fills_at_most_order_volume( # Quantity is refreshed -> Ensure we don't trade the entire amount tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.005, - ask=90.005, + bid_price=90.005, + ask_price=90.005, bid_size=10_000, ask_size=10_000, ) @@ -1185,8 +1185,8 @@ def test_submit_stop_market_order_inside_market_rejects( # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1230,8 +1230,8 @@ def test_submit_stop_limit_order_inside_market_rejects( # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1273,8 +1273,8 @@ def test_submit_stop_market_order( # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1319,8 +1319,8 @@ def test_submit_stop_limit_order_when_inside_market_rejects( # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1360,8 +1360,8 @@ def test_submit_stop_limit_order(self, side, price, trigger_price) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1394,8 +1394,8 @@ def test_submit_reduce_only_order_when_no_position_rejects(self, side: OrderSide # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1429,8 +1429,8 @@ def test_submit_reduce_only_order_when_would_increase_position_rejects( # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1465,8 +1465,8 @@ def test_cancel_stop_order(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1512,8 +1512,8 @@ def test_cancel_all_orders_with_no_side_filter_cancels_all(self): # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1549,8 +1549,8 @@ def test_cancel_all_orders_with_buy_side_filter_cancels_all_buy_orders(self) -> # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1598,8 +1598,8 @@ def test_cancel_all_orders_with_sell_side_filter_cancels_all_sell_orders(self) - # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1669,8 +1669,8 @@ def test_modify_order_with_zero_quantity_rejects_modify(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1699,8 +1699,8 @@ def test_modify_post_only_limit_order_when_marketable_then_rejects_modify(self) # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1729,8 +1729,8 @@ def test_modify_limit_order_when_marketable_then_fills_order(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1761,8 +1761,8 @@ def test_modify_stop_market_order_when_price_inside_market_then_rejects_modify( # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=Price.from_str("90.002"), - ask=Price.from_str("90.005"), + bid_price=Price.from_str("90.002"), + ask_price=Price.from_str("90.005"), ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1794,8 +1794,8 @@ def test_modify_stop_market_order_when_price_valid_then_updates(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1850,8 +1850,8 @@ def test_modify_limit_if_touched( # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.005, - ask=90.005, + bid_price=90.005, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1886,8 +1886,8 @@ def test_modify_untriggered_stop_limit_order_when_price_inside_market_then_rejec # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1920,8 +1920,8 @@ def test_modify_untriggered_stop_limit_order_when_price_valid_then_amends(self) # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -1957,8 +1957,8 @@ def test_modify_triggered_post_only_stop_limit_order_when_price_inside_market_th # Arrange: Prepare market tick1 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick1) self.exchange.process_quote_tick(tick1) @@ -1978,8 +1978,8 @@ def test_modify_triggered_post_only_stop_limit_order_when_price_inside_market_th # Trigger order tick2 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.009, - ask=90.010, + bid_price=90.009, + ask_price=90.010, ) self.data_engine.process(tick2) self.exchange.process_quote_tick(tick2) @@ -2004,8 +2004,8 @@ def test_modify_triggered_stop_limit_order_when_price_inside_market_then_fills( # Arrange: Prepare market tick1 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick1) self.exchange.process_quote_tick(tick1) @@ -2025,8 +2025,8 @@ def test_modify_triggered_stop_limit_order_when_price_inside_market_then_fills( # Trigger order tick2 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.009, - ask=90.010, + bid_price=90.009, + ask_price=90.010, ) self.data_engine.process(tick2) self.exchange.process_quote_tick(tick2) @@ -2049,8 +2049,8 @@ def test_modify_triggered_stop_limit_order_when_price_valid_then_amends(self) -> # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -2069,8 +2069,8 @@ def test_modify_triggered_stop_limit_order_when_price_valid_then_amends(self) -> # Trigger order tick2 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.009, - ask=90.010, + bid_price=90.009, + ask_price=90.010, ) self.data_engine.process(tick2) self.exchange.process_quote_tick(tick2) @@ -2093,8 +2093,8 @@ def test_order_fills_gets_commissioned(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -2142,8 +2142,8 @@ def test_expire_order(self) -> None: # Arrange: Prepare market tick1 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick1) self.exchange.process_quote_tick(tick1) @@ -2162,8 +2162,8 @@ def test_expire_order(self) -> None: tick2 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=96.709, - ask=96.710, + bid_price=96.709, + ask_price=96.710, ts_event=1 * 60 * 1_000_000_000, # 1 minute in nanoseconds ts_init=1 * 60 * 1_000_000_000, # 1 minute in nanoseconds ) @@ -2179,8 +2179,8 @@ def test_process_quote_tick_fills_buy_stop_order(self) -> None: # Arrange: Prepare market tick1 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, bid_size=1_000_000, ask_size=1_000_000, ) @@ -2200,8 +2200,8 @@ def test_process_quote_tick_fills_buy_stop_order(self) -> None: # Act tick2 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=96.710, - ask=96.711, + bid_price=96.710, + ask_price=96.711, bid_size=1_000_000, ask_size=1_000_000, ) @@ -2218,8 +2218,8 @@ def test_process_quote_tick_triggers_buy_stop_limit_order(self) -> None: # Arrange: Prepare market tick1 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick1) self.exchange.process_quote_tick(tick1) @@ -2238,8 +2238,8 @@ def test_process_quote_tick_triggers_buy_stop_limit_order(self) -> None: # Act tick2 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=96.710, - ask=96.712, + bid_price=96.710, + ask_price=96.712, ) self.exchange.process_quote_tick(tick2) @@ -2252,8 +2252,8 @@ def test_process_quote_tick_rejects_triggered_post_only_buy_stop_limit_order(sel # Arrange: Prepare market tick1 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick1) self.exchange.process_quote_tick(tick1) @@ -2273,8 +2273,8 @@ def test_process_quote_tick_rejects_triggered_post_only_buy_stop_limit_order(sel # Act tick2 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.005, - ask=90.006, + bid_price=90.005, + ask_price=90.006, ts_event=1_000_000_000, ts_init=1_000_000_000, ) @@ -2289,8 +2289,8 @@ def test_process_quote_tick_fills_triggered_buy_stop_limit_order(self) -> None: # Arrange: Prepare market tick1 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick1) self.exchange.process_quote_tick(tick1) @@ -2308,8 +2308,8 @@ def test_process_quote_tick_fills_triggered_buy_stop_limit_order(self) -> None: tick2 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.006, - ask=90.007, + bid_price=90.006, + ask_price=90.007, ts_event=1_000_000_000, ts_init=1_000_000_000, ) @@ -2317,8 +2317,8 @@ def test_process_quote_tick_fills_triggered_buy_stop_limit_order(self) -> None: # Act tick3 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.000, - ask=90.001, + bid_price=90.000, + ask_price=90.001, ts_event=100_000, ts_init=100_000, ) @@ -2334,8 +2334,8 @@ def test_process_quote_tick_fills_buy_limit_order(self) -> None: # Arrange: Prepare market tick1 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick1) self.exchange.process_quote_tick(tick1) @@ -2353,8 +2353,8 @@ def test_process_quote_tick_fills_buy_limit_order(self) -> None: # Act tick2 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.000, - ask=90.001, + bid_price=90.000, + ask_price=90.001, ts_event=100_000, ts_init=100_000, ) @@ -2371,8 +2371,8 @@ def test_process_quote_tick_fills_sell_stop_order(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -2390,8 +2390,8 @@ def test_process_quote_tick_fills_sell_stop_order(self) -> None: # Act tick2 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=89.997, - ask=89.999, + bid_price=89.997, + ask_price=89.999, ) self.exchange.process_quote_tick(tick2) @@ -2406,8 +2406,8 @@ def test_process_quote_tick_fills_sell_limit_order(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -2425,8 +2425,8 @@ def test_process_quote_tick_fills_sell_limit_order(self) -> None: # Act tick2 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.101, - ask=90.102, + bid_price=90.101, + ask_price=90.102, ) self.exchange.process_quote_tick(tick2) @@ -2441,8 +2441,8 @@ def test_realized_pnl_contains_commission(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -2466,8 +2466,8 @@ def test_unrealized_pnl(self) -> None: # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -2484,8 +2484,8 @@ def test_unrealized_pnl(self) -> None: quote = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=100.003, - ask=100.004, + bid_price=100.003, + ask_price=100.004, ) self.exchange.process_quote_tick(quote) @@ -2555,8 +2555,8 @@ def test_position_flipped_when_reduce_order_exceeds_original_quantity(self) -> N # Arrange: Prepare market open_quote = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.003, + bid_price=90.002, + ask_price=90.003, bid_size=1_000_000, ask_size=1_000_000, ) @@ -2575,8 +2575,8 @@ def test_position_flipped_when_reduce_order_exceeds_original_quantity(self) -> N reduce_quote = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=100.003, - ask=100.004, + bid_price=100.003, + ask_price=100.004, bid_size=1_000_000, ask_size=1_000_000, ) @@ -2597,7 +2597,7 @@ def test_position_flipped_when_reduce_order_exceeds_original_quantity(self) -> N self.exchange.process(0) # Assert - # TODO(cs): Current behaviour erases previous position from cache + # TODO(cs): Current behavior erases previous position from cache position_open = self.cache.positions_open()[0] position_closed = self.cache.positions_closed()[0] assert position_open.side == PositionSide.SHORT @@ -2610,8 +2610,8 @@ def test_reduce_only_market_order_does_not_open_position_on_flip_scenario(self) # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=14.0, - ask=13.0, + bid_price=14.0, + ask_price=13.0, bid_size=1_000_000, ask_size=1_000_000, ) @@ -2641,8 +2641,8 @@ def test_reduce_only_limit_order_does_not_open_position_on_flip_scenario(self) - # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=14.0, - ask=13.0, + bid_price=14.0, + ask_price=13.0, bid_size=1_000_000, ask_size=1_000_000, ) @@ -2669,8 +2669,8 @@ def test_reduce_only_limit_order_does_not_open_position_on_flip_scenario(self) - tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=10.0, - ask=11.0, + bid_price=10.0, + ask_price=11.0, bid_size=1_000_000, ask_size=1_000_000, ) diff --git a/tests/unit_tests/backtest/test_exchange_bitmex.py b/tests/unit_tests/backtest/test_exchange_bitmex.py index a971002e71b4..af8436315c1c 100644 --- a/tests/unit_tests/backtest/test_exchange_bitmex.py +++ b/tests/unit_tests/backtest/test_exchange_bitmex.py @@ -149,8 +149,8 @@ def test_commission_maker_taker_order(self): # Prepare market quote1 = QuoteTick( instrument_id=XBTUSD_BITMEX.id, - bid=Price.from_str("11493.0"), - ask=Price.from_str("11493.5"), + bid_price=Price.from_str("11493.0"), + ask_price=Price.from_str("11493.5"), bid_size=Quantity.from_int(1_500_000), ask_size=Quantity.from_int(1_500_000), ts_event=0, @@ -181,8 +181,8 @@ def test_commission_maker_taker_order(self): quote2 = QuoteTick( instrument_id=XBTUSD_BITMEX.id, - bid=Price.from_str("11491.0"), - ask=Price.from_str("11491.5"), + bid_price=Price.from_str("11491.0"), + ask_price=Price.from_str("11491.5"), bid_size=Quantity.from_int(1_500_000), ask_size=Quantity.from_int(1_500_000), ts_event=0, 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 aaee62505e9b..cfcce9d4a633 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 @@ -199,8 +199,8 @@ def test_bracket_market_entry_accepts_sl_and_tp( # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=3100.00, - ask=3100.00, + bid_price=3100.00, + ask_price=3100.00, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -223,8 +223,16 @@ def test_bracket_market_entry_accepts_sl_and_tp( sl_order = bracket.orders[1] tp_order = bracket.orders[2] assert entry_order.status == OrderStatus.FILLED - assert sl_order.status == OrderStatus.ACCEPTED - assert tp_order.status == OrderStatus.ACCEPTED + assert ( + sl_order.status == OrderStatus.EMULATED + if sl_order.is_emulated + else OrderStatus.ACCEPTED + ) + assert ( + tp_order.status == OrderStatus.EMULATED + if tp_order.is_emulated + else OrderStatus.ACCEPTED + ) @pytest.mark.parametrize( ( @@ -234,6 +242,7 @@ def test_bracket_market_entry_accepts_sl_and_tp( "sl_trigger_price", "tp_trigger_price", "next_tick_price", + "expected_bracket_status", ), [ [ @@ -243,6 +252,7 @@ def test_bracket_market_entry_accepts_sl_and_tp( 3050.00, # sl_trigger_price 3150.00, # tp_price 3090.00, # next_tick_price (hits trigger) + OrderStatus.ACCEPTED, ], [ TriggerType.BID_ASK, @@ -251,6 +261,7 @@ def test_bracket_market_entry_accepts_sl_and_tp( 3050.00, # sl_trigger_price 3150.00, # tp_price 3090.00, # next_tick_price (hits trigger) + OrderStatus.EMULATED, ], [ TriggerType.NO_TRIGGER, @@ -259,6 +270,7 @@ def test_bracket_market_entry_accepts_sl_and_tp( 3150.00, # sl_trigger_price 3050.00, # tp_price 3110.00, # next_tick_price (hits trigger) + OrderStatus.ACCEPTED, ], [ TriggerType.BID_ASK, @@ -267,6 +279,7 @@ def test_bracket_market_entry_accepts_sl_and_tp( 3150.00, # sl_trigger_price 3050.00, # tp_price 3110.00, # next_tick_price (hits trigger) + OrderStatus.EMULATED, ], ], ) @@ -278,12 +291,13 @@ def test_bracket_market_if_touched_entry_triggers_passively_then_sl_tp_working( sl_trigger_price: Price, tp_trigger_price: Price, next_tick_price: Price, + expected_bracket_status: OrderStatus, ) -> None: # Arrange: Prepare market tick1 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3100.0), - ask=ETHUSDT_PERP_BINANCE.make_price(3100.0), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3100.0), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3100.0), bid_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ask_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ts_event=0, @@ -310,8 +324,8 @@ def test_bracket_market_if_touched_entry_triggers_passively_then_sl_tp_working( # Act tick2 = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=next_tick_price, - ask=next_tick_price, + bid_price=next_tick_price, + ask_price=next_tick_price, ) self.data_engine.process(tick2) self.exchange.process_quote_tick(tick2) @@ -323,8 +337,8 @@ def test_bracket_market_if_touched_entry_triggers_passively_then_sl_tp_working( sl_order = self.cache.order(bracket.orders[1].client_order_id) tp_order = self.cache.order(bracket.orders[2].client_order_id) assert entry_order.status == OrderStatus.FILLED - assert sl_order.status == OrderStatus.ACCEPTED - assert tp_order.status == OrderStatus.ACCEPTED + assert sl_order.status == expected_bracket_status + assert tp_order.status == expected_bracket_status @pytest.mark.parametrize( ( @@ -388,8 +402,8 @@ def test_bracket_limit_if_touched_entry_triggers_passively_then_limit_working( # Arrange: Prepare market tick1 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3100.0), - ask=ETHUSDT_PERP_BINANCE.make_price(3100.0), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3100.0), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3100.0), bid_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ask_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ts_event=0, @@ -416,8 +430,8 @@ def test_bracket_limit_if_touched_entry_triggers_passively_then_limit_working( # Act tick2 = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=next_tick_price, - ask=next_tick_price, + bid_price=next_tick_price, + ask_price=next_tick_price, ) self.data_engine.process(tick2) self.exchange.process_quote_tick(tick2) @@ -434,12 +448,12 @@ def test_bracket_limit_if_touched_entry_triggers_passively_then_limit_working( else OrderStatus.TRIGGERED ) assert ( - sl_order.status == OrderStatus.INITIALIZED + sl_order.status == OrderStatus.EMULATED if sl_order.is_emulated else OrderStatus.ACCEPTED ) assert ( - tp_order.status == OrderStatus.INITIALIZED + tp_order.status == OrderStatus.EMULATED if tp_order.is_emulated else OrderStatus.ACCEPTED ) @@ -506,8 +520,8 @@ def test_bracket_limit_if_touched_entry_triggers_passively_and_fills_passively( # Arrange: Prepare market tick1 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3100.0), - ask=ETHUSDT_PERP_BINANCE.make_price(3100.0), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3100.0), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3100.0), bid_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ask_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ts_event=0, @@ -534,8 +548,8 @@ def test_bracket_limit_if_touched_entry_triggers_passively_and_fills_passively( # Act tick2 = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=next_tick_price, - ask=next_tick_price, + bid_price=next_tick_price, + ask_price=next_tick_price, ) self.data_engine.process(tick2) self.exchange.process_quote_tick(tick2) @@ -548,8 +562,16 @@ def test_bracket_limit_if_touched_entry_triggers_passively_and_fills_passively( tp_order = self.cache.order(bracket.orders[2].client_order_id) assert entry_order.status == OrderStatus.FILLED assert entry_order.avg_px == entry_order.price # <-- fills at limit price - assert sl_order.status == OrderStatus.ACCEPTED - assert tp_order.status == OrderStatus.ACCEPTED + assert ( + sl_order.status == OrderStatus.EMULATED + if sl_order.is_emulated + else OrderStatus.ACCEPTED + ) + assert ( + tp_order.status == OrderStatus.EMULATED + if tp_order.is_emulated + else OrderStatus.ACCEPTED + ) @pytest.mark.parametrize( ( @@ -613,8 +635,8 @@ def test_bracket_limit_if_touched_entry_triggers_passively_and_fills_immediately # Arrange: Prepare market tick1 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3100.0), - ask=ETHUSDT_PERP_BINANCE.make_price(3100.0), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3100.0), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3100.0), bid_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ask_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ts_event=0, @@ -641,8 +663,8 @@ def test_bracket_limit_if_touched_entry_triggers_passively_and_fills_immediately # Act tick2 = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=next_tick_price, - ask=next_tick_price, + bid_price=next_tick_price, + ask_price=next_tick_price, ) self.data_engine.process(tick2) self.exchange.process_quote_tick(tick2) @@ -655,8 +677,8 @@ def test_bracket_limit_if_touched_entry_triggers_passively_and_fills_immediately tp_order = self.cache.order(bracket.orders[2].client_order_id) assert entry_order.status == OrderStatus.FILLED assert entry_order.avg_px == entry_trigger_price # <-- fills where market is at trigger - assert sl_order.status == OrderStatus.ACCEPTED - assert tp_order.status == OrderStatus.ACCEPTED + assert sl_order.status in (OrderStatus.ACCEPTED, OrderStatus.EMULATED) + assert tp_order.status in (OrderStatus.ACCEPTED, OrderStatus.EMULATED) @pytest.mark.parametrize( ( @@ -667,6 +689,7 @@ def test_bracket_limit_if_touched_entry_triggers_passively_and_fills_immediately "sl_trigger_price", "tp_price", "next_tick_price", + "expected_bracket_status", ), [ [ @@ -677,6 +700,7 @@ def test_bracket_limit_if_touched_entry_triggers_passively_and_fills_immediately 3050.00, # sl_trigger_price 3150.00, # tp_price 3094.00, # next_tick_price (moves through limit price) + OrderStatus.ACCEPTED, ], [ TriggerType.BID_ASK, @@ -686,6 +710,7 @@ def test_bracket_limit_if_touched_entry_triggers_passively_and_fills_immediately 3050.00, # sl_trigger_price 3150.00, # tp_price 3094.00, # next_tick_price (moves through limit price) + OrderStatus.EMULATED, ], [ TriggerType.NO_TRIGGER, @@ -695,6 +720,7 @@ def test_bracket_limit_if_touched_entry_triggers_passively_and_fills_immediately 3150.00, # sl_trigger_price 3050.00, # tp_price 3106.00, # next_tick_price (moves through limit price) + OrderStatus.ACCEPTED, ], [ TriggerType.BID_ASK, @@ -704,6 +730,7 @@ def test_bracket_limit_if_touched_entry_triggers_passively_and_fills_immediately 3150.00, # sl_trigger_price 3050.00, # tp_price 3106.00, # next_tick_price (moves through limit price) + OrderStatus.EMULATED, ], ], ) @@ -716,12 +743,13 @@ def test_bracket_limit_if_touched_entry_triggers_immediately_and_fills_passively sl_trigger_price: Price, tp_price: Price, next_tick_price: Price, + expected_bracket_status: OrderStatus, ) -> None: # Arrange: Prepare market tick1 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3100.0), - ask=ETHUSDT_PERP_BINANCE.make_price(3100.0), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3100.0), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3100.0), bid_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ask_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ts_event=0, @@ -748,8 +776,8 @@ def test_bracket_limit_if_touched_entry_triggers_immediately_and_fills_passively # Act tick2 = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=next_tick_price, - ask=next_tick_price, + bid_price=next_tick_price, + ask_price=next_tick_price, ) self.data_engine.process(tick2) self.exchange.process_quote_tick(tick2) @@ -762,8 +790,8 @@ def test_bracket_limit_if_touched_entry_triggers_immediately_and_fills_passively tp_order = self.cache.order(bracket.orders[2].client_order_id) assert entry_order.status == OrderStatus.FILLED assert entry_order.avg_px == entry_order.price # <-- fills at limit price - assert sl_order.status == OrderStatus.ACCEPTED - assert tp_order.status == OrderStatus.ACCEPTED + assert sl_order.status == expected_bracket_status + assert tp_order.status == expected_bracket_status @pytest.mark.parametrize( ( @@ -773,6 +801,7 @@ def test_bracket_limit_if_touched_entry_triggers_immediately_and_fills_passively "entry_price", "sl_trigger_price", "tp_price", + "expected_bracket_status", ), [ [ @@ -782,6 +811,7 @@ def test_bracket_limit_if_touched_entry_triggers_immediately_and_fills_passively 3103.00, # entry_price 3050.00, # sl_trigger_price 3150.00, # tp_price + OrderStatus.ACCEPTED, ], [ TriggerType.BID_ASK, @@ -790,6 +820,7 @@ def test_bracket_limit_if_touched_entry_triggers_immediately_and_fills_passively 3103.00, # entry_price 3050.00, # sl_trigger_price 3150.00, # tp_price + OrderStatus.EMULATED, ], [ TriggerType.NO_TRIGGER, @@ -798,6 +829,7 @@ def test_bracket_limit_if_touched_entry_triggers_immediately_and_fills_passively 3097.00, # entry_price 3150.00, # sl_trigger_price 3050.00, # tp_price + OrderStatus.ACCEPTED, ], [ TriggerType.BID_ASK, @@ -806,6 +838,7 @@ def test_bracket_limit_if_touched_entry_triggers_immediately_and_fills_passively 3097.00, # entry_price 3150.00, # sl_trigger_price 3050.00, # tp_price + OrderStatus.EMULATED, ], ], ) @@ -817,6 +850,7 @@ def test_bracket_limit_if_touched_entry_triggers_immediately_and_fills_immediate entry_price: Price, sl_trigger_price: Price, tp_price: Price, + expected_bracket_status: OrderStatus, ) -> None: # Arrange: Prepare market self.emulator.create_matching_core( @@ -826,8 +860,8 @@ def test_bracket_limit_if_touched_entry_triggers_immediately_and_fills_immediate tick1 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3100.0), - ask=ETHUSDT_PERP_BINANCE.make_price(3100.0), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3100.0), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3100.0), bid_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ask_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ts_event=0, @@ -859,5 +893,5 @@ def test_bracket_limit_if_touched_entry_triggers_immediately_and_fills_immediate tp_order = self.cache.order(bracket.orders[2].client_order_id) assert entry_order.status == OrderStatus.FILLED assert entry_order.avg_px == 3100.00 # <-- fills where market is - assert sl_order.status == OrderStatus.ACCEPTED - assert tp_order.status == OrderStatus.ACCEPTED + assert sl_order.status == expected_bracket_status + assert tp_order.status == expected_bracket_status diff --git a/tests/unit_tests/backtest/test_exchange_contingencies.py b/tests/unit_tests/backtest/test_exchange_contingencies.py index 5b70f10e564c..00f27c35cd2e 100644 --- a/tests/unit_tests/backtest/test_exchange_contingencies.py +++ b/tests/unit_tests/backtest/test_exchange_contingencies.py @@ -151,8 +151,8 @@ def test_submit_bracket_market_entry_buy_accepts_sl_and_tp(self): # Arrange: Prepare market tick = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -183,8 +183,8 @@ def test_submit_bracket_market_entry_sell_accepts_sl_and_tp(self): # Arrange: Prepare market tick = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -215,8 +215,8 @@ def test_submit_bracket_market_entry_with_immediate_modify_accepts_sl_and_tp(sel # Arrange: Prepare market tick = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -257,8 +257,8 @@ def test_submit_bracket_market_entry_with_immediate_cancel(self): # Arrange: Prepare market tick = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -294,8 +294,8 @@ def test_submit_bracket_limit_entry_buy_has_sl_tp_pending(self): # Arrange: Prepare market tick = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -328,8 +328,8 @@ def test_submit_bracket_limit_entry_sell_has_sl_tp_pending(self): # Arrange: Prepare market tick = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -362,8 +362,8 @@ def test_submit_bracket_limit_entry_buy_fills_then_triggers_sl_and_tp(self): # Arrange: Prepare market tick = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -399,8 +399,8 @@ def test_submit_bracket_limit_entry_sell_fills_then_triggers_sl_and_tp(self): # Arrange: Prepare market tick = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -436,8 +436,8 @@ def test_reject_bracket_entry_then_rejects_sl_and_tp(self): # Arrange: Prepare market tick = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -474,8 +474,8 @@ def test_filling_bracket_sl_cancels_tp_order(self): # Arrange: Prepare market tick1 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -500,8 +500,8 @@ def test_filling_bracket_sl_cancels_tp_order(self): tick2 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3150.0), - ask=ETHUSDT_PERP_BINANCE.make_price(3151.0), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3150.0), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3151.0), bid_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ask_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ts_event=0, @@ -522,8 +522,8 @@ def test_filling_bracket_tp_cancels_sl_order(self): # Arrange: Prepare market tick1 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -549,8 +549,8 @@ def test_filling_bracket_tp_cancels_sl_order(self): # Act tick2 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3150.0), - ask=ETHUSDT_PERP_BINANCE.make_price(3151.0), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3150.0), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3151.0), bid_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ask_size=ETHUSDT_PERP_BINANCE.make_qty(10.000), ts_event=0, @@ -570,8 +570,8 @@ def test_partial_fill_bracket_tp_updates_sl_order(self): # Arrange: Prepare market tick1 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -601,8 +601,8 @@ def test_partial_fill_bracket_tp_updates_sl_order(self): # Act tick2 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3150.0), - ask=ETHUSDT_PERP_BINANCE.make_price(3151.0), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3150.0), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3151.0), bid_size=ETHUSDT_PERP_BINANCE.make_qty(5.000), ask_size=ETHUSDT_PERP_BINANCE.make_qty(5.1000), ts_event=0, @@ -625,8 +625,8 @@ def test_modifying_bracket_tp_updates_sl_order(self): # Arrange: Prepare market tick1 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -674,8 +674,8 @@ def test_closing_position_cancels_bracket_ocos(self): # Arrange: Prepare market tick1 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, @@ -715,8 +715,8 @@ def test_partially_filling_position_updates_bracket_ocos(self): # Arrange: Prepare market tick1 = QuoteTick( instrument_id=ETHUSDT_PERP_BINANCE.id, - bid=ETHUSDT_PERP_BINANCE.make_price(3090.2), - ask=ETHUSDT_PERP_BINANCE.make_price(3090.5), + bid_price=ETHUSDT_PERP_BINANCE.make_price(3090.2), + ask_price=ETHUSDT_PERP_BINANCE.make_price(3090.5), bid_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ask_size=ETHUSDT_PERP_BINANCE.make_qty(15.100), ts_event=0, diff --git a/tests/unit_tests/backtest/test_exchange_l2_mbp.py b/tests/unit_tests/backtest/test_exchange_l2_mbp.py index 92fc1603bdfd..196ec73e1306 100644 --- a/tests/unit_tests/backtest/test_exchange_l2_mbp.py +++ b/tests/unit_tests/backtest/test_exchange_l2_mbp.py @@ -164,8 +164,8 @@ def test_submit_limit_order_aggressive_multiple_levels(self): quote = QuoteTick( instrument_id=USDJPY_SIM.id, - bid=Price.from_str("110.000"), - ask=Price.from_str("110.010"), + bid_price=Price.from_str("110.000"), + ask_price=Price.from_str("110.010"), bid_size=Quantity.from_int(1_500_000), ask_size=Quantity.from_int(1_500_000), ts_event=0, @@ -205,8 +205,8 @@ def test_aggressive_partial_fill(self): quote = QuoteTick( instrument_id=USDJPY_SIM.id, - bid=Price.from_str("110.000"), - ask=Price.from_str("110.010"), + bid_price=Price.from_str("110.000"), + ask_price=Price.from_str("110.010"), bid_size=Quantity.from_int(1_500_000), ask_size=Quantity.from_int(1_500_000), ts_event=0, @@ -291,8 +291,8 @@ def test_passive_partial_fill(self): # Act tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=15.0, - ask=16.0, + bid_price=15.0, + ask_price=16.0, bid_size=1_000, ask_size=1_000, ) diff --git a/tests/unit_tests/backtest/test_exchange_stop_limits.py b/tests/unit_tests/backtest/test_exchange_stop_limits.py index 8f47c2bbe77c..277e216fa236 100644 --- a/tests/unit_tests/backtest/test_exchange_stop_limits.py +++ b/tests/unit_tests/backtest/test_exchange_stop_limits.py @@ -156,8 +156,8 @@ def test_submit_stop_limit_buy_order_when_marketable_then_fills(self): # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -185,8 +185,8 @@ def test_submit_stop_limit_sell_order_when_marketable_then_fills(self): # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick) self.exchange.process_quote_tick(tick) @@ -214,8 +214,8 @@ def test_process_quote_tick_fills_buy_stop_limit_order_passively(self): # Arrange: Prepare market tick1 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick1) self.exchange.process_quote_tick(tick1) @@ -234,8 +234,8 @@ def test_process_quote_tick_fills_buy_stop_limit_order_passively(self): # Act tick2 = QuoteTick( instrument_id=USDJPY_SIM.id, - bid=Price.from_str("90.010"), - ask=Price.from_str("90.011"), + bid_price=Price.from_str("90.010"), + ask_price=Price.from_str("90.011"), bid_size=Quantity.from_int(100_000), ask_size=Quantity.from_int(100_000), ts_event=0, @@ -254,8 +254,8 @@ def test_process_quote_tick_fills_sell_stop_limit_order_passively(self): # Arrange: Prepare market tick1 = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.002, - ask=90.005, + bid_price=90.002, + ask_price=90.005, ) self.data_engine.process(tick1) self.exchange.process_quote_tick(tick1) @@ -274,8 +274,8 @@ def test_process_quote_tick_fills_sell_stop_limit_order_passively(self): # Act tick2 = QuoteTick( instrument_id=USDJPY_SIM.id, - bid=Price.from_str("89.998"), - ask=Price.from_str("89.999"), + bid_price=Price.from_str("89.998"), + ask_price=Price.from_str("89.999"), bid_size=Quantity.from_int(100_000), ask_size=Quantity.from_int(100_000), ts_event=0, diff --git a/tests/unit_tests/backtest/test_exchange_trailing_stops.py b/tests/unit_tests/backtest/test_exchange_trailing_stops.py index 5197c63ead6e..edac53ec768d 100644 --- a/tests/unit_tests/backtest/test_exchange_trailing_stops.py +++ b/tests/unit_tests/backtest/test_exchange_trailing_stops.py @@ -266,8 +266,8 @@ def test_trailing_stop_market_order_bid_ask_with_no_trigger_updates_order( # Arrange: Prepare market quote = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(quote) self.data_engine.process(quote) @@ -338,8 +338,8 @@ def test_trailing_stop_market_order_last_with_no_trigger_updates_order( # Arrange: Prepare market quote = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(quote) self.data_engine.process(quote) @@ -378,8 +378,8 @@ def test_trailing_stop_market_order_buy_bid_ask_price_when_offset_activated_upda # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(tick) self.data_engine.process(tick) @@ -399,16 +399,16 @@ def test_trailing_stop_market_order_buy_bid_ask_price_when_offset_activated_upda tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=12.0, - ask=13.0, + bid_price=12.0, + ask_price=13.0, ) self.exchange.process_quote_tick(tick) # Act: market moves against trailing stop (should not update) tick = QuoteTick( instrument_id=USDJPY_SIM.id, - bid=Price.from_str("12.500"), - ask=Price.from_str("13.500"), + bid_price=Price.from_str("12.500"), + ask_price=Price.from_str("13.500"), bid_size=Quantity.from_int(1_000_000), ask_size=Quantity.from_int(1_000_000), ts_event=0, @@ -425,8 +425,8 @@ def test_trailing_stop_market_order_sell_bid_ask_price_when_offset_activated_upd # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(tick) self.data_engine.process(tick) @@ -446,16 +446,16 @@ def test_trailing_stop_market_order_sell_bid_ask_price_when_offset_activated_upd tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=14.0, - ask=15.0, + bid_price=14.0, + ask_price=15.0, ) self.exchange.process_quote_tick(tick) # Act: market moves against trailing stop (should not update) tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.5, - ask=14.5, + bid_price=13.5, + ask_price=14.5, ) self.exchange.process_quote_tick(tick) @@ -518,8 +518,8 @@ def test_trailing_stop_limit_order_bid_ask_with_no_trigger_updates_order( # Arrange: Prepare market quote = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(quote) self.data_engine.process(quote) @@ -630,8 +630,8 @@ def test_trailing_stop_limit_order_last_with_no_trigger_updates_order( # Arrange: Prepare market quote = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(quote) self.data_engine.process(quote) @@ -672,8 +672,8 @@ def test_trailing_stop_limit_order_buy_bid_ask_price_when_offset_activated_updat # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(tick) self.data_engine.process(tick) @@ -695,16 +695,16 @@ def test_trailing_stop_limit_order_buy_bid_ask_price_when_offset_activated_updat tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=12.0, - ask=13.0, + bid_price=12.0, + ask_price=13.0, ) self.exchange.process_quote_tick(tick) # Act: market moves against trailing stop (should not update) tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=12.5, - ask=13.5, + bid_price=12.5, + ask_price=13.5, ) self.exchange.process_quote_tick(tick) @@ -718,8 +718,8 @@ def test_trailing_stop_limit_order_sell_bid_ask_price_when_offset_activated_upda # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(tick) self.data_engine.process(tick) @@ -741,16 +741,16 @@ def test_trailing_stop_limit_order_sell_bid_ask_price_when_offset_activated_upda tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=14.0, - ask=15.0, + bid_price=14.0, + ask_price=15.0, ) self.exchange.process_quote_tick(tick) # Act: market moves against trailing stop (should not update) tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.5, - ask=14.5, + bid_price=13.5, + ask_price=14.5, ) self.exchange.process_quote_tick(tick) @@ -764,8 +764,8 @@ def test_trailing_stop_limit_order_buy_bid_ask_basis_points_when_offset_activate # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(tick) self.data_engine.process(tick) @@ -787,16 +787,16 @@ def test_trailing_stop_limit_order_buy_bid_ask_basis_points_when_offset_activate tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=12.0, - ask=13.0, + bid_price=12.0, + ask_price=13.0, ) self.exchange.process_quote_tick(tick) # Act: market moves against trailing stop (should not update) tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=12.5, - ask=13.5, + bid_price=12.5, + ask_price=13.5, ) self.exchange.process_quote_tick(tick) @@ -810,8 +810,8 @@ def test_trailing_stop_limit_order_sell_bid_ask_basis_points_when_offset_activat # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(tick) self.data_engine.process(tick) @@ -833,16 +833,16 @@ def test_trailing_stop_limit_order_sell_bid_ask_basis_points_when_offset_activat tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=14.0, - ask=15.0, + bid_price=14.0, + ask_price=15.0, ) self.exchange.process_quote_tick(tick) # Act: market moves against trailing stop (should not update) tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.5, - ask=14.5, + bid_price=13.5, + ask_price=14.5, ) self.exchange.process_quote_tick(tick) @@ -856,8 +856,8 @@ def test_trailing_stop_limit_order_buy_last_ticks_when_offset_activated_updates_ # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(tick) self.data_engine.process(tick) @@ -891,16 +891,16 @@ def test_trailing_stop_limit_order_buy_last_ticks_when_offset_activated_updates_ tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=12.0, - ask=13.0, + bid_price=12.0, + ask_price=13.0, ) self.exchange.process_quote_tick(tick) # Act: market moves against trailing stop (should not update) tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=12.5, - ask=13.5, + bid_price=12.5, + ask_price=13.5, ) self.exchange.process_quote_tick(tick) @@ -914,8 +914,8 @@ def test_trailing_stop_limit_order_sell_last_ticks_when_offset_activated_updates # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(tick) self.data_engine.process(tick) @@ -949,16 +949,16 @@ def test_trailing_stop_limit_order_sell_last_ticks_when_offset_activated_updates tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=14.0, - ask=15.0, + bid_price=14.0, + ask_price=15.0, ) self.exchange.process_quote_tick(tick) # Act: market moves against trailing stop (should not update) tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.5, - ask=14.5, + bid_price=13.5, + ask_price=14.5, ) self.exchange.process_quote_tick(tick) @@ -972,8 +972,8 @@ def test_trailing_stop_limit_order_buy_bid_ask_ticks_when_offset_activated_updat # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(tick) self.data_engine.process(tick) @@ -995,16 +995,16 @@ def test_trailing_stop_limit_order_buy_bid_ask_ticks_when_offset_activated_updat tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=12.0, - ask=13.0, + bid_price=12.0, + ask_price=13.0, ) self.exchange.process_quote_tick(tick) # Act: market moves against trailing stop (should not update) tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=12.5, - ask=13.5, + bid_price=12.5, + ask_price=13.5, ) self.exchange.process_quote_tick(tick) @@ -1018,8 +1018,8 @@ def test_trailing_stop_limit_order_sell_bid_ask_ticks_when_offset_activated_upda # Arrange: Prepare market tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.0, - ask=14.0, + bid_price=13.0, + ask_price=14.0, ) self.exchange.process_quote_tick(tick) self.data_engine.process(tick) @@ -1041,16 +1041,16 @@ def test_trailing_stop_limit_order_sell_bid_ask_ticks_when_offset_activated_upda tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=14.0, - ask=15.0, + bid_price=14.0, + ask_price=15.0, ) self.exchange.process_quote_tick(tick) # Act: market moves against trailing stop (should not update) tick = TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=13.5, - ask=14.5, + bid_price=13.5, + ask_price=14.5, ) self.exchange.process_quote_tick(tick) diff --git a/tests/unit_tests/cache/test_data.py b/tests/unit_tests/cache/test_data.py index 79120cbf9b4c..5765b0c3c66f 100644 --- a/tests/unit_tests/cache/test_data.py +++ b/tests/unit_tests/cache/test_data.py @@ -390,8 +390,8 @@ def test_price_given_various_quote_price_types_when_quote_tick_returns_expected_ # Arrange tick = TestDataStubs.quote_tick( instrument=AUDUSD_SIM, - bid=1.00001, - ask=1.00003, + bid_price=1.00001, + ask_price=1.00003, ) self.cache.add_quote_tick(tick) @@ -511,8 +511,8 @@ def test_get_xrate_returns_correct_rate(self): tick = QuoteTick( instrument_id=USDJPY_SIM.id, - bid=Price.from_str("110.80000"), - ask=Price.from_str("110.80010"), + bid_price=Price.from_str("110.80000"), + ask_price=Price.from_str("110.80010"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -540,8 +540,8 @@ def test_get_xrate_with_conversion(self): tick = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("0.80000"), - ask=Price.from_str("0.80010"), + bid_price=Price.from_str("0.80000"), + ask_price=Price.from_str("0.80010"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, diff --git a/tests/unit_tests/cache/test_execution.py b/tests/unit_tests/cache/test_execution.py index 29e2a305999b..cb39cbef3d84 100644 --- a/tests/unit_tests/cache/test_execution.py +++ b/tests/unit_tests/cache/test_execution.py @@ -297,6 +297,26 @@ def test_get_strategy_ids_with_id_returns_correct_set(self): # Assert assert result == {self.strategy.id} + def test_orders_for_exec_spawn_when_not_found(self): + # Arrange, Act, Assert + assert self.cache.orders_for_exec_spawn(ClientOrderId("O-UNKNOWN")) == [] + + def test_orders_for_exec_algorithm_when_not_found(self): + # Arrange, Act, Assert + assert self.cache.orders_for_exec_algorithm(ExecAlgorithmId("UNKNOWN")) == [] + + def test_exec_spawn_total_quantity_when_not_found(self): + # Arrange, Act, Assert + assert self.cache.exec_spawn_total_quantity(ClientOrderId("O-UNKNOWN")) is None + + def test_exec_spawn_total_filled_qty_when_not_found(self): + # Arrange, Act, Assert + assert self.cache.exec_spawn_total_filled_qty(ClientOrderId("O-UNKNOWN")) is None + + def test_exec_spawn_total_leaves_qty_when_not_found(self): + # Arrange, Act, Assert + assert self.cache.exec_spawn_total_leaves_qty(ClientOrderId("O-UNKNOWN")) is None + def test_position_for_order_when_no_position_returns_none(self): # Arrange, Act, Assert assert self.cache.position_for_order(ClientOrderId("O-123456")) is None diff --git a/tests/unit_tests/common/test_actor.py b/tests/unit_tests/common/test_actor.py index 640939f82ba5..ea195ecd7bba 100644 --- a/tests/unit_tests/common/test_actor.py +++ b/tests/unit_tests/common/test_actor.py @@ -22,6 +22,7 @@ from nautilus_trader.common.clock import TestClock from nautilus_trader.common.enums import ComponentState from nautilus_trader.common.enums import LogLevel +from nautilus_trader.common.executor import TaskId from nautilus_trader.common.logging import Logger from nautilus_trader.common.logging import LoggerAdapter from nautilus_trader.config import ActorConfig @@ -66,7 +67,7 @@ class TestActor: - def setup(self): + def setup(self) -> None: # Fixture Setup self.clock = TestClock() self.logger = Logger( @@ -122,7 +123,7 @@ def setup(self): self.data_engine.start() self.exec_engine.start() - def test_actor_fully_qualified_name(self): + def test_actor_fully_qualified_name(self) -> None: # Arrange config = ActorConfig(component_id="ALPHA-01") actor = Actor(config=config) @@ -136,7 +137,7 @@ def test_actor_fully_qualified_name(self): assert result.config_path == "nautilus_trader.config.common:ActorConfig" assert result.config == {"component_id": "ALPHA-01"} - def test_id(self): + def test_id(self) -> None: # Arrange, Act actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -149,7 +150,7 @@ def test_id(self): # Assert assert actor.id == ComponentId(self.component_id) - def test_pre_initialization(self): + def test_pre_initialization(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) @@ -157,7 +158,7 @@ def test_pre_initialization(self): assert actor.state == ComponentState.PRE_INITIALIZED assert not actor.is_initialized - def test_initialization(self): + def test_initialization(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -174,7 +175,7 @@ def test_initialization(self): assert not actor.is_pending_request(UUID4()) assert actor.pending_requests() == set() - def test_register_warning_event(self): + def test_register_warning_event(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -190,7 +191,7 @@ def test_register_warning_event(self): # Assert assert True # Exception not raised - def test_deregister_warning_event(self): + def test_deregister_warning_event(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -208,7 +209,7 @@ def test_deregister_warning_event(self): # Assert assert True # Exception not raised - def test_handle_event(self): + def test_handle_event(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -226,7 +227,7 @@ def test_handle_event(self): # Assert assert True # Exception not raised - def test_on_start_when_not_overridden_does_nothing(self): + def test_on_start_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -242,7 +243,7 @@ def test_on_start_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_stop_when_not_overridden_does_nothing(self): + def test_on_stop_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -258,7 +259,7 @@ def test_on_stop_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_resume_when_not_overridden_does_nothing(self): + def test_on_resume_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -274,7 +275,7 @@ def test_on_resume_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_reset_when_not_overridden_does_nothing(self): + def test_on_reset_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -290,7 +291,7 @@ def test_on_reset_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_dispose_when_not_overridden_does_nothing(self): + def test_on_dispose_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -306,7 +307,7 @@ def test_on_dispose_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_degrade_when_not_overridden_does_nothing(self): + def test_on_degrade_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -322,7 +323,7 @@ def test_on_degrade_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_fault_when_not_overridden_does_nothing(self): + def test_on_fault_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -338,7 +339,7 @@ def test_on_fault_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_instrument_when_not_overridden_does_nothing(self): + def test_on_instrument_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -354,7 +355,7 @@ def test_on_instrument_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_order_book_when_not_overridden_does_nothing(self): + def test_on_order_book_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -370,7 +371,7 @@ def test_on_order_book_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_order_book_delta_when_not_overridden_does_nothing(self): + def test_on_order_book_delta_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -386,7 +387,7 @@ def test_on_order_book_delta_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_ticker_when_not_overridden_does_nothing(self): + def test_on_ticker_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -402,7 +403,7 @@ def test_on_ticker_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_venue_status_update_when_not_overridden_does_nothing(self): + def test_on_venue_status_update_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -418,7 +419,7 @@ def test_on_venue_status_update_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_instrument_status_update_when_not_overridden_does_nothing(self): + def test_on_instrument_status_update_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -434,7 +435,7 @@ def test_on_instrument_status_update_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_event_when_not_overridden_does_nothing(self): + def test_on_event_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -450,7 +451,7 @@ def test_on_event_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_quote_tick_when_not_overridden_does_nothing(self): + def test_on_quote_tick_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -468,7 +469,7 @@ def test_on_quote_tick_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_trade_tick_when_not_overridden_does_nothing(self): + def test_on_trade_tick_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -486,7 +487,7 @@ def test_on_trade_tick_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_bar_when_not_overridden_does_nothing(self): + def test_on_bar_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -504,7 +505,7 @@ def test_on_bar_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_historical_data_when_not_overridden_does_nothing(self): + def test_on_historical_data_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -522,7 +523,7 @@ def test_on_historical_data_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_data_when_not_overridden_does_nothing(self): + def test_on_data_when_not_overridden_does_nothing(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -546,7 +547,7 @@ def test_on_data_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_start_when_invalid_state_does_not_start(self): + def test_start_when_invalid_state_does_not_start(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -562,7 +563,7 @@ def test_start_when_invalid_state_does_not_start(self): # Assert assert actor.state == ComponentState.RUNNING - def test_stop_when_invalid_state_does_not_stop(self): + def test_stop_when_invalid_state_does_not_stop(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -578,7 +579,7 @@ def test_stop_when_invalid_state_does_not_stop(self): # Assert assert actor.state == ComponentState.READY - def test_resume_when_invalid_state_does_not_resume(self): + def test_resume_when_invalid_state_does_not_resume(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -594,7 +595,7 @@ def test_resume_when_invalid_state_does_not_resume(self): # Assert assert actor.state == ComponentState.READY - def test_reset_when_invalid_state_does_not_reset(self): + def test_reset_when_invalid_state_does_not_reset(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -610,7 +611,7 @@ def test_reset_when_invalid_state_does_not_reset(self): # Assert assert actor.state == ComponentState.READY - def test_dispose_when_invalid_state_does_not_dispose(self): + def test_dispose_when_invalid_state_does_not_dispose(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -626,7 +627,7 @@ def test_dispose_when_invalid_state_does_not_dispose(self): # Assert assert actor.state == ComponentState.DISPOSED - def test_degrade_when_invalid_state_does_not_degrade(self): + def test_degrade_when_invalid_state_does_not_degrade(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -642,7 +643,7 @@ def test_degrade_when_invalid_state_does_not_degrade(self): # Assert assert actor.state == ComponentState.READY - def test_fault_when_invalid_state_does_not_fault(self): + def test_fault_when_invalid_state_does_not_fault(self) -> None: # Arrange actor = Actor(config=ActorConfig(component_id=self.component_id)) actor.register_base( @@ -658,7 +659,7 @@ def test_fault_when_invalid_state_does_not_fault(self): # Assert assert actor.state == ComponentState.READY - def test_start_when_user_code_raises_error_logs_and_reraises(self): + def test_start_when_user_code_raises_error_logs_and_reraises(self) -> None: # Arrange actor = KaboomActor() actor.register_base( @@ -673,7 +674,7 @@ def test_start_when_user_code_raises_error_logs_and_reraises(self): actor.start() assert actor.state == ComponentState.STARTING - def test_stop_when_user_code_raises_error_logs_and_reraises(self): + def test_stop_when_user_code_raises_error_logs_and_reraises(self) -> None: # Arrange actor = KaboomActor() actor.register_base( @@ -691,7 +692,7 @@ def test_stop_when_user_code_raises_error_logs_and_reraises(self): actor.stop() assert actor.state == ComponentState.STOPPING - def test_resume_when_user_code_raises_error_logs_and_reraises(self): + def test_resume_when_user_code_raises_error_logs_and_reraises(self) -> None: # Arrange actor = KaboomActor() actor.register_base( @@ -711,7 +712,7 @@ def test_resume_when_user_code_raises_error_logs_and_reraises(self): actor.resume() assert actor.state == ComponentState.RESUMING - def test_reset_when_user_code_raises_error_logs_and_reraises(self): + def test_reset_when_user_code_raises_error_logs_and_reraises(self) -> None: # Arrange actor = KaboomActor() actor.register_base( @@ -726,7 +727,7 @@ def test_reset_when_user_code_raises_error_logs_and_reraises(self): actor.reset() assert actor.state == ComponentState.RESETTING - def test_dispose_when_user_code_raises_error_logs_and_reraises(self): + def test_dispose_when_user_code_raises_error_logs_and_reraises(self) -> None: # Arrange actor = KaboomActor() actor.register_base( @@ -741,7 +742,7 @@ def test_dispose_when_user_code_raises_error_logs_and_reraises(self): actor.dispose() assert actor.state == ComponentState.DISPOSING - def test_degrade_when_user_code_raises_error_logs_and_reraises(self): + def test_degrade_when_user_code_raises_error_logs_and_reraises(self) -> None: # Arrange actor = KaboomActor() actor.register_base( @@ -759,7 +760,7 @@ def test_degrade_when_user_code_raises_error_logs_and_reraises(self): actor.degrade() assert actor.state == ComponentState.DEGRADING - def test_fault_when_user_code_raises_error_logs_and_reraises(self): + def test_fault_when_user_code_raises_error_logs_and_reraises(self) -> None: # Arrange actor = KaboomActor() actor.register_base( @@ -777,7 +778,7 @@ def test_fault_when_user_code_raises_error_logs_and_reraises(self): actor.fault() assert actor.state == ComponentState.FAULTING - def test_handle_quote_tick_when_user_code_raises_exception_logs_and_reraises(self): + def test_handle_quote_tick_when_user_code_raises_exception_logs_and_reraises(self) -> None: # Arrange actor = KaboomActor() actor.register_base( @@ -796,7 +797,7 @@ def test_handle_quote_tick_when_user_code_raises_exception_logs_and_reraises(sel with pytest.raises(RuntimeError): actor.handle_quote_tick(tick) - def test_handle_trade_tick_when_user_code_raises_exception_logs_and_reraises(self): + def test_handle_trade_tick_when_user_code_raises_exception_logs_and_reraises(self) -> None: # Arrange actor = KaboomActor() actor.register_base( @@ -815,7 +816,7 @@ def test_handle_trade_tick_when_user_code_raises_exception_logs_and_reraises(sel with pytest.raises(RuntimeError): actor.handle_trade_tick(tick) - def test_handle_bar_when_user_code_raises_exception_logs_and_reraises(self): + def test_handle_bar_when_user_code_raises_exception_logs_and_reraises(self) -> None: # Arrange actor = KaboomActor() actor.register_base( @@ -834,7 +835,7 @@ def test_handle_bar_when_user_code_raises_exception_logs_and_reraises(self): with pytest.raises(RuntimeError): actor.handle_bar(bar) - def test_handle_data_when_user_code_raises_exception_logs_and_reraises(self): + def test_handle_data_when_user_code_raises_exception_logs_and_reraises(self) -> None: # Arrange actor = KaboomActor() actor.register_base( @@ -859,7 +860,7 @@ def test_handle_data_when_user_code_raises_exception_logs_and_reraises(self): ), ) - def test_handle_event_when_user_code_raises_exception_logs_and_reraises(self): + def test_handle_event_when_user_code_raises_exception_logs_and_reraises(self) -> None: # Arrange actor = KaboomActor() actor.register_base( @@ -878,7 +879,7 @@ def test_handle_event_when_user_code_raises_exception_logs_and_reraises(self): with pytest.raises(RuntimeError): actor.on_event(event) - def test_start(self): + def test_start(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -895,7 +896,7 @@ def test_start(self): assert "on_start" in actor.calls assert actor.state == ComponentState.RUNNING - def test_stop(self): + def test_stop(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -913,7 +914,7 @@ def test_stop(self): assert "on_stop" in actor.calls assert actor.state == ComponentState.STOPPED - def test_resume(self): + def test_resume(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -933,7 +934,7 @@ def test_resume(self): assert "on_resume" in actor.calls assert actor.state == ComponentState.RUNNING - def test_reset(self): + def test_reset(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -950,7 +951,7 @@ def test_reset(self): assert "on_reset" in actor.calls assert actor.state == ComponentState.READY - def test_dispose(self): + def test_dispose(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -969,7 +970,7 @@ def test_dispose(self): assert "on_dispose" in actor.calls assert actor.state == ComponentState.DISPOSED - def test_degrade(self): + def test_degrade(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -988,7 +989,7 @@ def test_degrade(self): assert "on_degrade" in actor.calls assert actor.state == ComponentState.DEGRADED - def test_fault(self): + def test_fault(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1007,7 +1008,7 @@ def test_fault(self): assert "on_fault" in actor.calls assert actor.state == ComponentState.FAULTED - def test_handle_instrument_with_blow_up_logs_exception(self): + def test_handle_instrument_with_blow_up_logs_exception(self) -> None: # Arrange actor = KaboomActor() actor.register_base( @@ -1024,7 +1025,7 @@ def test_handle_instrument_with_blow_up_logs_exception(self): with pytest.raises(RuntimeError): actor.handle_instrument(AUDUSD_SIM) - def test_handle_instrument_when_not_running_does_not_send_to_on_instrument(self): + def test_handle_instrument_when_not_running_does_not_send_to_on_instrument(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1041,7 +1042,7 @@ def test_handle_instrument_when_not_running_does_not_send_to_on_instrument(self) assert actor.calls == [] assert actor.store == [] - def test_handle_instrument_when_running_sends_to_on_instrument(self): + def test_handle_instrument_when_running_sends_to_on_instrument(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1060,7 +1061,7 @@ def test_handle_instrument_when_running_sends_to_on_instrument(self): assert actor.calls == ["on_start", "on_instrument"] assert actor.store[0] == AUDUSD_SIM - def test_handle_instruments_when_running_sends_to_on_instruments(self): + def test_handle_instruments_when_running_sends_to_on_instruments(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1079,7 +1080,7 @@ def test_handle_instruments_when_running_sends_to_on_instruments(self): assert actor.calls == ["on_start", "on_instrument"] assert actor.store[0] == AUDUSD_SIM - def test_handle_instruments_when_not_running_does_not_send_to_on_instrument(self): + def test_handle_instruments_when_not_running_does_not_send_to_on_instrument(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1096,7 +1097,7 @@ def test_handle_instruments_when_not_running_does_not_send_to_on_instrument(self assert actor.calls == [] assert actor.store == [] - def test_handle_ticker_when_not_running_does_not_send_to_on_quote_tick(self): + def test_handle_ticker_when_not_running_does_not_send_to_on_quote_tick(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1115,7 +1116,7 @@ def test_handle_ticker_when_not_running_does_not_send_to_on_quote_tick(self): assert actor.calls == [] assert actor.store == [] - def test_handle_ticker_when_running_sends_to_on_quote_tick(self): + def test_handle_ticker_when_running_sends_to_on_quote_tick(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1136,7 +1137,7 @@ def test_handle_ticker_when_running_sends_to_on_quote_tick(self): assert actor.calls == ["on_start", "on_ticker"] assert actor.store[0] == ticker - def test_handle_quote_tick_when_not_running_does_not_send_to_on_quote_tick(self): + def test_handle_quote_tick_when_not_running_does_not_send_to_on_quote_tick(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1155,7 +1156,7 @@ def test_handle_quote_tick_when_not_running_does_not_send_to_on_quote_tick(self) assert actor.calls == [] assert actor.store == [] - def test_handle_quote_tick_when_running_sends_to_on_quote_tick(self): + def test_handle_quote_tick_when_running_sends_to_on_quote_tick(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1176,7 +1177,7 @@ def test_handle_quote_tick_when_running_sends_to_on_quote_tick(self): assert actor.calls == ["on_start", "on_quote_tick"] assert actor.store[0] == tick - def test_handle_trade_tick_when_not_running_does_not_send_to_on_trade_tick(self): + def test_handle_trade_tick_when_not_running_does_not_send_to_on_trade_tick(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1195,7 +1196,7 @@ def test_handle_trade_tick_when_not_running_does_not_send_to_on_trade_tick(self) assert actor.calls == [] assert actor.store == [] - def test_handle_trade_tick_when_running_sends_to_on_trade_tick(self): + def test_handle_trade_tick_when_running_sends_to_on_trade_tick(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1216,7 +1217,7 @@ def test_handle_trade_tick_when_running_sends_to_on_trade_tick(self): assert actor.calls == ["on_start", "on_trade_tick"] assert actor.store == [tick] - def test_handle_bar_when_not_running_does_not_send_to_on_bar(self): + def test_handle_bar_when_not_running_does_not_send_to_on_bar(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1235,7 +1236,7 @@ def test_handle_bar_when_not_running_does_not_send_to_on_bar(self): assert actor.calls == [] assert actor.store == [] - def test_handle_bar_when_running_sends_to_on_bar(self): + def test_handle_bar_when_running_sends_to_on_bar(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1256,7 +1257,7 @@ def test_handle_bar_when_running_sends_to_on_bar(self): assert actor.calls == ["on_start", "on_bar"] assert actor.store[0] == bar - def test_handle_bars(self): + def test_handle_bars(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1265,7 +1266,7 @@ def test_handle_bars(self): clock=self.clock, logger=self.logger, ) - result = [] + result: list[Bar] = [] actor.on_historical_data = result.append actor.start() @@ -1278,7 +1279,7 @@ def test_handle_bars(self): # Assert assert result == bars - def test_handle_data_when_not_running_does_not_send_to_on_data(self): + def test_handle_data_when_not_running_does_not_send_to_on_data(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1303,7 +1304,7 @@ def test_handle_data_when_not_running_does_not_send_to_on_data(self): assert actor.calls == [] assert actor.store == [] - def test_handle_data_when_running_sends_to_on_data(self): + def test_handle_data_when_running_sends_to_on_data(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1330,7 +1331,7 @@ def test_handle_data_when_running_sends_to_on_data(self): assert actor.calls == ["on_start", "on_data"] assert actor.store[0] == data - def test_add_synthetic_instrument_when_already_exists(self): + def test_add_synthetic_instrument_when_already_exists(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1347,7 +1348,7 @@ def test_add_synthetic_instrument_when_already_exists(self): with pytest.raises(ValueError): actor.add_synthetic(synthetic) - def test_add_synthetic_instrument_when_no_synthetic(self): + def test_add_synthetic_instrument_when_no_synthetic(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1365,7 +1366,7 @@ def test_add_synthetic_instrument_when_no_synthetic(self): # Assert assert actor.cache.synthetic(synthetic.id) == synthetic - def test_update_synthetic_instrument_when_no_synthetic(self): + def test_update_synthetic_instrument_when_no_synthetic(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1381,7 +1382,7 @@ def test_update_synthetic_instrument_when_no_synthetic(self): with pytest.raises(ValueError): actor.update_synthetic(synthetic) - def test_update_synthetic_instrument(self): + def test_update_synthetic_instrument(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1403,7 +1404,136 @@ def test_update_synthetic_instrument(self): assert new_formula != original_formula assert actor.cache.synthetic(synthetic.id).formula == new_formula - def test_subscribe_custom_data(self): + def test_queued_task_ids_when_no_executor(self) -> None: + """ + Test should return empty list. + """ + # Arrange + actor = MockActor() + + # Act, Assert + assert actor.queued_task_ids() == [] + + def test_active_task_ids_when_no_executor(self) -> None: + """ + Test should return empty list. + """ + # Arrange + actor = MockActor() + + # Act, Assert + assert actor.active_task_ids() == [] + + def test_has_queued_tasks_when_no_executor(self) -> None: + """ + Test should return false. + """ + # Arrange + actor = MockActor() + + # Act, Assert + assert not actor.has_queued_tasks() + + def test_has_active_tasks_when_no_executor(self) -> None: + """ + Test should return false. + """ + # Arrange + actor = MockActor() + + # Act, Assert + assert not actor.has_active_tasks() + + def test_has_any_tasks_when_no_executor(self) -> None: + """ + Test should return false. + """ + # Arrange + actor = MockActor() + + # Act, Assert + assert not actor.has_any_tasks() + + def test_cancel_task_when_no_executor(self) -> None: + """ + Test should do nothing and log a warning. + """ + # Arrange + actor = MockActor() + actor.register_base( + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + + unknown = TaskId.create() + + # Act, Assert + actor.cancel_task(unknown) + + def test_cancel_all_tasks_when_no_executor(self) -> None: + # Arrange + actor = MockActor() + actor.register_base( + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + + # Act, Assert + actor.cancel_all_tasks() + + def test_run_in_executor_when_no_executor(self) -> None: + """ + Test should immediately execute the function and return a task ID. + """ + # Arrange + actor = MockActor() + actor.register_base( + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + + handler: list[str] = [] + func = handler.append + msg = "a" + + # Act + task_id: TaskId = actor.run_in_executor(func, (msg,)) + + # Assert + assert msg in handler + assert len(task_id.value) == 36 + + def test_queue_for_executor_when_no_executor(self) -> None: + """ + Test should immediately execute the function and return a task ID. + """ + # Arrange + actor = MockActor() + actor.register_base( + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + + handler: list[str] = [] + func = handler.append + msg = "a" + + # Act + task_id: TaskId = actor.queue_for_executor(func, (msg,)) + + # Assert + assert msg in handler + assert len(task_id.value) == 36 + + def test_subscribe_custom_data(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1425,7 +1555,7 @@ def test_subscribe_custom_data(self): == "data.NewsEvent.type=NEWS_WIRE.topic=Earthquake" ) - def test_subscribe_custom_data_with_client_id(self): + def test_subscribe_custom_data_with_client_id(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1447,7 +1577,7 @@ def test_subscribe_custom_data_with_client_id(self): == "data.NewsEvent.type=NEWS_WIRE.topic=Earthquake" ) - def test_unsubscribe_custom_data(self): + def test_unsubscribe_custom_data(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1467,7 +1597,7 @@ def test_unsubscribe_custom_data(self): assert self.data_engine.command_count == 0 assert actor.msgbus.subscriptions() == [] - def test_unsubscribe_custom_data_with_client_id(self): + def test_unsubscribe_custom_data_with_client_id(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1487,7 +1617,7 @@ def test_unsubscribe_custom_data_with_client_id(self): assert self.data_engine.command_count == 2 assert actor.msgbus.subscriptions() == [] - def test_subscribe_order_book(self): + def test_subscribe_order_book(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1503,7 +1633,7 @@ def test_subscribe_order_book(self): # Assert assert self.data_engine.command_count == 1 - def test_unsubscribe_order_book(self): + def test_unsubscribe_order_book(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1521,7 +1651,7 @@ def test_unsubscribe_order_book(self): # Assert assert self.data_engine.command_count == 2 - def test_subscribe_order_book_data(self): + def test_subscribe_order_book_data(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1537,7 +1667,7 @@ def test_subscribe_order_book_data(self): # Assert assert self.data_engine.command_count == 1 - def test_unsubscribe_order_book_deltas(self): + def test_unsubscribe_order_book_deltas(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1555,7 +1685,7 @@ def test_unsubscribe_order_book_deltas(self): # Assert assert self.data_engine.command_count == 2 - def test_subscribe_instruments(self): + def test_subscribe_instruments(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1576,7 +1706,7 @@ def test_subscribe_instruments(self): InstrumentId.from_str("USD/JPY.SIM"), ] - def test_unsubscribe_instruments(self): + def test_unsubscribe_instruments(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1593,7 +1723,7 @@ def test_unsubscribe_instruments(self): assert self.data_engine.command_count == 1 assert self.data_engine.subscribed_instruments() == [] - def test_subscribe_instrument(self): + def test_subscribe_instrument(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1611,7 +1741,7 @@ def test_subscribe_instrument(self): assert self.data_engine.command_count == 1 assert self.data_engine.subscribed_instruments() == [expected_instrument] - def test_unsubscribe_instrument(self): + def test_unsubscribe_instrument(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1630,7 +1760,7 @@ def test_unsubscribe_instrument(self): assert self.data_engine.subscribed_instruments() == [] assert self.data_engine.command_count == 2 - def test_subscribe_ticker(self): + def test_subscribe_ticker(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1648,7 +1778,7 @@ def test_subscribe_ticker(self): assert self.data_engine.subscribed_tickers() == [expected_instrument] assert self.data_engine.command_count == 1 - def test_unsubscribe_ticker(self): + def test_unsubscribe_ticker(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1667,7 +1797,7 @@ def test_unsubscribe_ticker(self): assert self.data_engine.subscribed_tickers() == [] assert self.data_engine.command_count == 2 - def test_subscribe_quote_ticks(self): + def test_subscribe_quote_ticks(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1685,7 +1815,7 @@ def test_subscribe_quote_ticks(self): assert self.data_engine.subscribed_quote_ticks() == [expected_instrument] assert self.data_engine.command_count == 1 - def test_unsubscribe_quote_ticks(self): + def test_unsubscribe_quote_ticks(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1704,7 +1834,7 @@ def test_unsubscribe_quote_ticks(self): assert self.data_engine.subscribed_quote_ticks() == [] assert self.data_engine.command_count == 2 - def test_subscribe_trade_ticks(self): + def test_subscribe_trade_ticks(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1722,7 +1852,7 @@ def test_subscribe_trade_ticks(self): assert self.data_engine.subscribed_trade_ticks() == [expected_instrument] assert self.data_engine.command_count == 1 - def test_unsubscribe_trade_ticks(self): + def test_unsubscribe_trade_ticks(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1741,7 +1871,7 @@ def test_unsubscribe_trade_ticks(self): assert self.data_engine.subscribed_trade_ticks() == [] assert self.data_engine.command_count == 2 - def test_publish_data_sends_to_subscriber(self): + def test_publish_data_sends_to_subscriber(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1751,7 +1881,7 @@ def test_publish_data_sends_to_subscriber(self): logger=self.logger, ) - handler = [] + handler: list[Data] = [] self.msgbus.subscribe( topic="data*", handler=handler.append, @@ -1767,7 +1897,7 @@ def test_publish_data_sends_to_subscriber(self): # Assert assert data in handler - def test_publish_signal_warns_invalid_type(self): + def test_publish_signal_warns_invalid_type(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1781,7 +1911,7 @@ def test_publish_signal_warns_invalid_type(self): with pytest.raises(KeyError): actor.publish_signal(name="test", value={"a": 1}, ts_event=0) - def test_publish_signal_sends_to_subscriber(self): + def test_publish_signal_sends_to_subscriber(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1791,7 +1921,7 @@ def test_publish_signal_sends_to_subscriber(self): logger=self.logger, ) - handler = [] + handler: list[Data] = [] self.msgbus.subscribe( topic="data*", handler=handler.append, @@ -1808,7 +1938,7 @@ def test_publish_signal_sends_to_subscriber(self): assert msg.ts_init == 0 assert msg.value == value - def test_publish_data_persist(self): + def test_publish_data_persist(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1836,7 +1966,7 @@ def test_publish_data_persist(self): # Assert assert catalog.fs.exists(f"{catalog.path}/genericdata_SignalTest.feather") - def test_subscribe_bars(self): + def test_subscribe_bars(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1855,7 +1985,7 @@ def test_subscribe_bars(self): assert self.data_engine.subscribed_bars() == [bar_type] assert self.data_engine.command_count == 1 - def test_unsubscribe_bars(self): + def test_unsubscribe_bars(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1876,7 +2006,7 @@ def test_unsubscribe_bars(self): assert self.data_engine.subscribed_bars() == [] assert self.data_engine.command_count == 2 - def test_subscribe_venue_status_updates(self): + def test_subscribe_venue_status_updates(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1891,9 +2021,9 @@ def test_subscribe_venue_status_updates(self): # Assert # TODO(cs): DataEngine.subscribed_venue_status_updates() - def test_request_data_sends_request_to_data_engine(self): + def test_request_data_sends_request_to_data_engine(self) -> None: # Arrange - handler = [] + handler: list[NewsEvent] = [] actor = MockActor() actor.register_base( msgbus=self.msgbus, @@ -1917,7 +2047,7 @@ def test_request_data_sends_request_to_data_engine(self): assert actor.is_pending_request(request_id) assert request_id in actor.pending_requests() - def test_request_quote_ticks_sends_request_to_data_engine(self): + def test_request_quote_ticks_sends_request_to_data_engine(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1936,9 +2066,9 @@ def test_request_quote_ticks_sends_request_to_data_engine(self): assert actor.is_pending_request(request_id) assert request_id in actor.pending_requests() - def test_request_quote_ticks_with_registered_callback(self): + def test_request_quote_ticks_with_registered_callback(self) -> None: # Arrange - handler = [] + handler: list[QuoteTick] = [] actor = MockActor() actor.register_base( msgbus=self.msgbus, @@ -1971,7 +2101,7 @@ def test_request_quote_ticks_with_registered_callback(self): assert request_id not in actor.pending_requests() assert request_id in handler - def test_request_trade_ticks_sends_request_to_data_engine(self): + def test_request_trade_ticks_sends_request_to_data_engine(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -1990,9 +2120,9 @@ def test_request_trade_ticks_sends_request_to_data_engine(self): assert actor.is_pending_request(request_id) assert request_id in actor.pending_requests() - def test_request_trade_ticks_with_registered_callback(self): + def test_request_trade_ticks_with_registered_callback(self) -> None: # Arrange - handler = [] + handler: list[TradeTick] = [] actor = MockActor() actor.register_base( msgbus=self.msgbus, @@ -2024,7 +2154,7 @@ def test_request_trade_ticks_with_registered_callback(self): assert request_id not in actor.pending_requests() assert request_id in handler - def test_request_bars_sends_request_to_data_engine(self): + def test_request_bars_sends_request_to_data_engine(self) -> None: # Arrange actor = MockActor() actor.register_base( @@ -2045,9 +2175,9 @@ def test_request_bars_sends_request_to_data_engine(self): assert actor.is_pending_request(request_id) assert request_id in actor.pending_requests() - def test_request_bars_with_registered_callback(self): + def test_request_bars_with_registered_callback(self) -> None: # Arrange - handler = [] + handler: list[Bar] = [] actor = MockActor() actor.register_base( msgbus=self.msgbus, diff --git a/tests/unit_tests/common/test_executor.py b/tests/unit_tests/common/test_executor.py new file mode 100644 index 000000000000..1eafa819bf92 --- /dev/null +++ b/tests/unit_tests/common/test_executor.py @@ -0,0 +1,191 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2023 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +import asyncio +from concurrent.futures import ThreadPoolExecutor +from unittest.mock import Mock + +import pytest +import pytest_asyncio + +from nautilus_trader.common.executor import ActorExecutor +from nautilus_trader.common.executor import TaskId +from nautilus_trader.core.uuid import UUID4 +from nautilus_trader.test_kit.functions import eventually + + +def test_task_id_creation(): + task_id = TaskId.create() + assert isinstance(task_id, TaskId) + assert isinstance(task_id.value, str) + + +def test_task_id_repr(): + value = str(UUID4()) + task_id = TaskId(value=value) + assert repr(task_id) == f"TaskId('{value}')" + + +@pytest.fixture +def logger(): + return Mock() + + +@pytest_asyncio.fixture(name="actor_executor") +async def fixture_actor_executor(loop, logger): + executor = ActorExecutor( + loop=loop, + executor=ThreadPoolExecutor(), + logger=logger, + ) + yield executor + await executor.shutdown() + + +@pytest.mark.asyncio +async def test_cancel_invalid_task(actor_executor: ActorExecutor) -> None: + # Arrange + invalid_task_id = TaskId.create() + + # Act + actor_executor.cancel_task(invalid_task_id) + + # Assert + assert not actor_executor.has_active_tasks() + assert not actor_executor.has_queued_tasks() + + +@pytest.mark.asyncio +async def test_queue_for_executor(actor_executor: ActorExecutor) -> None: + # Arrange + def func(x): + return x + 1 + + # Act + task_id = actor_executor.queue_for_executor(func, 1) + + # Assert + assert task_id in actor_executor.queued_task_ids() + + +@pytest.mark.asyncio +async def test_run_in_executor(actor_executor: ActorExecutor) -> None: + # Arrange + def func(x): + return x + 1 + + # Act + task_id = actor_executor.run_in_executor(func, 1) + + # Assert + assert task_id in actor_executor.active_task_ids() + + +@pytest.mark.asyncio +async def test_cancel_task(actor_executor: ActorExecutor) -> None: + # Arrange + def func(x): + return x + 1 + + # Act + task_id = actor_executor.queue_for_executor(func, 1) + actor_executor.cancel_task(task_id) + + # Assert + assert task_id not in actor_executor.queued_task_ids() + + +@pytest.mark.asyncio +async def test_cancel_all_tasks(actor_executor: ActorExecutor) -> None: + # Arrange + def func(x): + return x + 1 + + # Act + actor_executor.queue_for_executor(func, 1) + actor_executor.run_in_executor(func, 2) + actor_executor.cancel_all_tasks() + + # Assert + assert not actor_executor.has_active_tasks() + assert not actor_executor.has_queued_tasks() + + +@pytest.mark.asyncio +async def test_run_in_executor_execution(actor_executor: ActorExecutor) -> None: + # Arrange + handler: list[str] = [] + msg = "a" + + # Act + actor_executor.run_in_executor(handler.append, msg) + + # Assert + assert msg in handler + + +@pytest.mark.asyncio +async def test_queue_for_executor_execution(actor_executor: ActorExecutor) -> None: + # Arrange + handler: list[str] = [] + msg = "a" + + # Act + actor_executor.queue_for_executor(handler.append, msg) + await eventually(lambda: bool(handler)) + + # Assert + assert msg in handler + + +@pytest.mark.asyncio +async def test_function_exception( + actor_executor: ActorExecutor, + logger: Mock, +) -> None: + # Arrange + def func(): + raise ValueError("Test Exception") + + # Act + task_id = actor_executor.run_in_executor(func) + future = actor_executor.get_future(task_id) + assert future + with pytest.raises(ValueError): + await future + + # Assert + assert future.exception() is not None + logger.error.assert_called_once() + + +@pytest.mark.asyncio +async def test_run_in_executor_multiple_functions(actor_executor: ActorExecutor) -> None: + # Arrange + def func(x): + return x + 1 + + async def async_func(x): + task_id = actor_executor.run_in_executor(func, x) + future = actor_executor.get_future(task_id) + assert future + return await asyncio.wrap_future(future) + + # Act + tasks = [async_func(i) for i in range(5)] + results = await asyncio.gather(*tasks) + + # Assert + assert results == [1, 2, 3, 4, 5] diff --git a/tests/unit_tests/common/test_queue.py b/tests/unit_tests/common/test_queue.py deleted file mode 100644 index 97741c4fc772..000000000000 --- a/tests/unit_tests/common/test_queue.py +++ /dev/null @@ -1,198 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# Copyright (C) 2015-2023 Nautech Systems Pty Ltd. All rights reserved. -# https://nautechsystems.io -# -# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); -# You may not use this file except in compliance with the License. -# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ------------------------------------------------------------------------------------------------- - -import asyncio - -import pytest - -from nautilus_trader.common.queue import Queue - - -class TestQueue: - def test_queue_instantiation(self): - # Arrange - queue = Queue() - - # Act, Assert - assert queue.maxsize == 0 - assert queue.qsize() == 0 - assert queue.empty() - assert not queue.full() - - def test_put_nowait(self): - # Arrange - queue = Queue() - - # Act - queue.put_nowait("A") - - # Assert - assert queue.qsize() == 1 - assert not queue.empty() - - def test_get_nowait(self): - # Arrange - queue = Queue() - queue.put_nowait("A") - - # Act - item = queue.get_nowait() - - # Assert - assert queue.empty() - assert item == "A" - - def test_put_nowait_multiple_items(self): - # Arrange - queue = Queue() - - # Act - queue.put_nowait("A") - queue.put_nowait("B") - queue.put_nowait("C") - queue.put_nowait("D") - queue.put_nowait("E") - - # Assert - assert queue.qsize() == 5 - assert not queue.empty() - - def test_put_to_maxlen_makes_queue_full(self): - # Arrange - queue = Queue(maxsize=5) - - # Act - queue.put_nowait("A") - queue.put_nowait("B") - queue.put_nowait("C") - queue.put_nowait("D") - queue.put_nowait("E") - - # Assert - assert queue.qsize() == 5 - assert queue.full() - - def test_put_nowait_onto_queue_at_maxsize_raises_queue_full(self): - # Arrange - queue = Queue(maxsize=5) - - # Act - queue.put_nowait("A") - queue.put_nowait("B") - queue.put_nowait("C") - queue.put_nowait("D") - queue.put_nowait("E") - - # Assert - with pytest.raises(asyncio.QueueFull): - queue.put_nowait("F") - - def test_get_nowait_from_empty_queue_raises_queue_empty(self): - # Arrange - queue = Queue() - - # Act, Assert - with pytest.raises(asyncio.QueueEmpty): - queue.get_nowait() - - @pytest.mark.asyncio() - async def test_await_put(self): - # Arrange - queue = Queue() - await queue.put("A") - - # Act - item = queue.get_nowait() - - # Assert - assert queue.empty() - assert item == "A" - - @pytest.mark.asyncio() - async def test_await_get(self): - # Arrange - queue = Queue() - queue.put_nowait("A") - - # Act - item = await queue.get() - - # Assert - assert queue.empty() - assert item == "A" - - def test_peek_when_no_items_returns_none(self): - # Arrange - queue = Queue() - - # Act, Assert - assert queue.peek_back() is None - - def test_peek_front_when_items_returns_expected_front_of_queue(self): - # Arrange - queue = Queue() - queue.put_nowait("A") - queue.put_nowait("B") - queue.put_nowait("C") - - # Act, Assert - assert queue.peek_front() == "A" - - def test_peek_index_when_items_returns_expected_front_of_queue(self): - # Arrange - queue = Queue() - queue.put_nowait("A") - queue.put_nowait("B") - queue.put_nowait("C") - - # Act, Assert - assert queue.peek_index(-1) == "A" - assert queue.peek_index(1) == "B" - assert queue.peek_index(0) == "C" - - def test_peek_back_when_items_returns_expected_front_of_queue(self): - # Arrange - queue = Queue() - queue.put_nowait("A") - queue.put_nowait("B") - queue.put_nowait("C") - - # Act, Assert - assert queue.peek_back() == "C" - - def test_as_list_when_no_items_returns_empty_list(self): - # Arrange - queue = Queue() - - # Act - result = queue.to_list() - - # Assert - assert result == [] - - def test_as_list_when_items_returns_expected_list(self): - # Arrange - queue = Queue() - queue.put_nowait("A") - queue.put_nowait("B") - queue.put_nowait("C") - - # Act - result = queue.to_list() - - # Assert - assert result == ["C", "B", "A"] - assert queue.get_nowait() == "A" - assert result == ["C", "B", "A"] # <-- confirm was copy diff --git a/tests/unit_tests/core/test_message.py b/tests/unit_tests/core/test_message.py index 36b8cb655219..566bc5353da8 100644 --- a/tests/unit_tests/core/test_message.py +++ b/tests/unit_tests/core/test_message.py @@ -17,7 +17,6 @@ from nautilus_trader.core.message import Command from nautilus_trader.core.message import Document -from nautilus_trader.core.message import Event from nautilus_trader.core.message import Request from nautilus_trader.core.message import Response from nautilus_trader.core.uuid import UUID4 @@ -82,48 +81,6 @@ def test_response_message_pickling(self): # Assert assert res == unpickled - def test_event_message_equality(self): - # Arrange - uuid = UUID4() - - event1 = Event( - event_id=uuid, - ts_event=0, - ts_init=0, - ) - - event2 = Event( - event_id=uuid, - ts_event=0, - ts_init=0, - ) - - event3 = Event( - event_id=UUID4(), # Different UUID4 - ts_event=0, - ts_init=0, - ) - - # Act, Assert - assert event1 == event1 - assert event1 == event2 - assert event1 != event3 - - def test_event_message_picking(self): - # Arrange - event = Event( - UUID4(), - 1, - 2, - ) - - # Act - pickled = pickle.dumps(event) - unpickled = pickle.loads(pickled) # noqa S301 (pickle is safe here) - - # Assert - assert event == unpickled - def test_document_message_hash(self): # Arrange message = Document( diff --git a/tests/unit_tests/data/test_aggregation.py b/tests/unit_tests/data/test_aggregation.py index a2bd54fe5a12..64b349ee3ba7 100644 --- a/tests/unit_tests/data/test_aggregation.py +++ b/tests/unit_tests/data/test_aggregation.py @@ -269,8 +269,8 @@ def test_handle_quote_tick_when_count_below_threshold_updates(self): tick1 = QuoteTick( instrument_id=instrument.id, - bid=Price.from_str("1.00001"), - ask=Price.from_str("1.00004"), + bid_price=Price.from_str("1.00001"), + ask_price=Price.from_str("1.00004"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -327,8 +327,8 @@ def test_handle_quote_tick_when_count_at_threshold_sends_bar_to_handler(self): tick1 = QuoteTick( instrument_id=instrument.id, - bid=Price.from_str("1.00001"), - ask=Price.from_str("1.00004"), + bid_price=Price.from_str("1.00001"), + ask_price=Price.from_str("1.00004"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -337,8 +337,8 @@ def test_handle_quote_tick_when_count_at_threshold_sends_bar_to_handler(self): tick2 = QuoteTick( instrument_id=instrument.id, - bid=Price.from_str("1.00002"), - ask=Price.from_str("1.00005"), + bid_price=Price.from_str("1.00002"), + ask_price=Price.from_str("1.00005"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -347,8 +347,8 @@ def test_handle_quote_tick_when_count_at_threshold_sends_bar_to_handler(self): tick3 = QuoteTick( instrument_id=instrument.id, - bid=Price.from_str("1.00000"), - ask=Price.from_str("1.00003"), + bid_price=Price.from_str("1.00000"), + ask_price=Price.from_str("1.00003"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -502,8 +502,8 @@ def test_handle_quote_tick_when_volume_below_threshold_updates(self): tick1 = QuoteTick( instrument_id=instrument.id, - bid=Price.from_str("1.00001"), - ask=Price.from_str("1.00004"), + bid_price=Price.from_str("1.00001"), + ask_price=Price.from_str("1.00004"), bid_size=Quantity.from_int(3_000), ask_size=Quantity.from_int(2_000), ts_event=0, @@ -560,8 +560,8 @@ def test_handle_quote_tick_when_volume_at_threshold_sends_bar_to_handler(self): tick1 = QuoteTick( instrument_id=instrument.id, - bid=Price.from_str("1.00001"), - ask=Price.from_str("1.00004"), + bid_price=Price.from_str("1.00001"), + ask_price=Price.from_str("1.00004"), bid_size=Quantity.from_int(3_000), ask_size=Quantity.from_int(2_000), ts_event=0, @@ -570,8 +570,8 @@ def test_handle_quote_tick_when_volume_at_threshold_sends_bar_to_handler(self): tick2 = QuoteTick( instrument_id=instrument.id, - bid=Price.from_str("1.00002"), - ask=Price.from_str("1.00005"), + bid_price=Price.from_str("1.00002"), + ask_price=Price.from_str("1.00005"), bid_size=Quantity.from_int(4_000), ask_size=Quantity.from_int(2_000), ts_event=0, @@ -580,8 +580,8 @@ def test_handle_quote_tick_when_volume_at_threshold_sends_bar_to_handler(self): tick3 = QuoteTick( instrument_id=instrument.id, - bid=Price.from_str("1.00000"), - ask=Price.from_str("1.00003"), + bid_price=Price.from_str("1.00000"), + ask_price=Price.from_str("1.00003"), bid_size=Quantity.from_int(3_000), ask_size=Quantity.from_int(2_000), ts_event=0, @@ -672,8 +672,8 @@ def test_handle_quote_tick_when_volume_beyond_threshold_sends_bars_to_handler(se tick1 = QuoteTick( instrument_id=instrument.id, - bid=Price.from_str("1.00001"), - ask=Price.from_str("1.00004"), + bid_price=Price.from_str("1.00001"), + ask_price=Price.from_str("1.00004"), bid_size=Quantity.from_int(2_000), ask_size=Quantity.from_int(2_000), ts_event=0, @@ -682,8 +682,8 @@ def test_handle_quote_tick_when_volume_beyond_threshold_sends_bars_to_handler(se tick2 = QuoteTick( instrument_id=instrument.id, - bid=Price.from_str("1.00002"), - ask=Price.from_str("1.00005"), + bid_price=Price.from_str("1.00002"), + ask_price=Price.from_str("1.00005"), bid_size=Quantity.from_int(3_000), ask_size=Quantity.from_int(3_000), ts_event=0, @@ -692,8 +692,8 @@ def test_handle_quote_tick_when_volume_beyond_threshold_sends_bars_to_handler(se tick3 = QuoteTick( instrument_id=instrument.id, - bid=Price.from_str("1.00000"), - ask=Price.from_str("1.00003"), + bid_price=Price.from_str("1.00000"), + ask_price=Price.from_str("1.00003"), bid_size=Quantity.from_int(25_000), ask_size=Quantity.from_int(25_000), ts_event=0, @@ -870,8 +870,8 @@ def test_handle_quote_tick_when_value_below_threshold_updates(self): tick1 = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("1.00001"), - ask=Price.from_str("1.00004"), + bid_price=Price.from_str("1.00001"), + ask_price=Price.from_str("1.00004"), bid_size=Quantity.from_int(3_000), ask_size=Quantity.from_int(2_000), ts_event=0, @@ -930,8 +930,8 @@ def test_handle_quote_tick_when_value_beyond_threshold_sends_bar_to_handler(self tick1 = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("1.00001"), - ask=Price.from_str("1.00004"), + bid_price=Price.from_str("1.00001"), + ask_price=Price.from_str("1.00004"), bid_size=Quantity.from_int(20_000), ask_size=Quantity.from_int(20_000), ts_event=0, @@ -940,8 +940,8 @@ def test_handle_quote_tick_when_value_beyond_threshold_sends_bar_to_handler(self tick2 = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("1.00002"), - ask=Price.from_str("1.00005"), + bid_price=Price.from_str("1.00002"), + ask_price=Price.from_str("1.00005"), bid_size=Quantity.from_int(60_000), ask_size=Quantity.from_int(20_000), ts_event=0, @@ -950,8 +950,8 @@ def test_handle_quote_tick_when_value_beyond_threshold_sends_bar_to_handler(self tick3 = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("1.00000"), - ask=Price.from_str("1.00003"), + bid_price=Price.from_str("1.00000"), + ask_price=Price.from_str("1.00003"), bid_size=Quantity.from_int(30_500), ask_size=Quantity.from_int(20_000), ts_event=0, @@ -1186,8 +1186,8 @@ def test_update_timer_with_test_clock_sends_single_bar_to_handler(self): tick1 = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("1.00001"), - ask=Price.from_str("1.00004"), + bid_price=Price.from_str("1.00001"), + ask_price=Price.from_str("1.00004"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -1196,8 +1196,8 @@ def test_update_timer_with_test_clock_sends_single_bar_to_handler(self): tick2 = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("1.00002"), - ask=Price.from_str("1.00005"), + bid_price=Price.from_str("1.00002"), + ask_price=Price.from_str("1.00005"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -1206,8 +1206,8 @@ def test_update_timer_with_test_clock_sends_single_bar_to_handler(self): tick3 = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("1.00000"), - ask=Price.from_str("1.00003"), + bid_price=Price.from_str("1.00000"), + ask_price=Price.from_str("1.00003"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=1 * 60 * 1_000_000_000, # 1 minute in nanoseconds diff --git a/tests/unit_tests/data/test_engine.py b/tests/unit_tests/data/test_engine.py index ddfc9eb0f872..306a05ca7e9a 100644 --- a/tests/unit_tests/data/test_engine.py +++ b/tests/unit_tests/data/test_engine.py @@ -73,7 +73,6 @@ XBTUSD_BITMEX = TestInstrumentProvider.xbtusd_bitmex() BTCUSDT_BINANCE = TestInstrumentProvider.btcusdt_binance() ETHUSDT_BINANCE = TestInstrumentProvider.ethusdt_binance() -BETFAIR_INSTRUMENT = TestInstrumentProvider.betting_instrument() class TestDataEngine: @@ -1160,7 +1159,7 @@ def test_order_book_delta_creates_book(self): # Arrange self.data_engine.register_client(self.betfair) self.betfair.start() - self.data_engine.process(BETFAIR_INSTRUMENT) # <-- add necessary instrument for test + self.data_engine.process(ETHUSDT_BINANCE) # <-- add necessary instrument for test subscribe = Subscribe( client_id=ClientId(BETFAIR.value), @@ -1168,7 +1167,7 @@ def test_order_book_delta_creates_book(self): data_type=DataType( OrderBookDelta, metadata={ - "instrument_id": BETFAIR_INSTRUMENT.id, + "instrument_id": ETHUSDT_BINANCE.id, "book_type": 2, "depth": 25, "interval_ms": 1000, @@ -1181,17 +1180,17 @@ def test_order_book_delta_creates_book(self): self.data_engine.execute(subscribe) deltas = OrderBookDeltas( - instrument_id=BETFAIR_INSTRUMENT.id, - deltas=[TestDataStubs.order_book_delta(instrument_id=BETFAIR_INSTRUMENT.id)], + instrument_id=ETHUSDT_BINANCE.id, + deltas=[TestDataStubs.order_book_delta(instrument_id=ETHUSDT_BINANCE.id)], ) # Act self.data_engine.process(deltas) # Assert - cached_book = self.cache.order_book(BETFAIR_INSTRUMENT.id) + cached_book = self.cache.order_book(ETHUSDT_BINANCE.id) assert isinstance(cached_book, OrderBook) - assert cached_book.instrument_id == BETFAIR_INSTRUMENT.id + assert cached_book.instrument_id == ETHUSDT_BINANCE.id assert cached_book.best_bid_price() == 100 def test_execute_subscribe_ticker(self): @@ -1624,18 +1623,18 @@ def test_process_quote_tick_when_synthetic_then_sends_to_registered_handlers( tick1 = TestDataStubs.quote_tick( instrument=BTCUSDT_BINANCE, - bid=50_000.0, - ask=50_001.0, + bid_price=50_000.0, + ask_price=50_001.0, ) tick2 = TestDataStubs.quote_tick( instrument=ETHUSDT_BINANCE, - bid=10_000.0, - ask=10_000.0, + bid_price=10_000.0, + ask_price=10_000.0, ) tick3 = TestDataStubs.quote_tick( instrument=BTCUSDT_BINANCE, - bid=50_001.0, - ask=50_002.0, + bid_price=50_001.0, + ask_price=50_002.0, ) # Act @@ -1651,8 +1650,8 @@ def test_process_quote_tick_when_synthetic_then_sends_to_registered_handlers( assert synthetic_tick.to_dict(synthetic_tick) == { "type": "QuoteTick", "instrument_id": "BTC-ETH.SYNTH", - "bid": "30000.50000000", - "ask": "30001.00000000", + "bid_price": "30000.50000000", + "ask_price": "30001.00000000", "bid_size": "1", "ask_size": "1", "ts_event": 0, diff --git a/tests/unit_tests/execution/test_algorithm.py b/tests/unit_tests/execution/test_algorithm.py index b3a34c01f784..7e10fa24cbce 100644 --- a/tests/unit_tests/execution/test_algorithm.py +++ b/tests/unit_tests/execution/test_algorithm.py @@ -16,6 +16,8 @@ from datetime import timedelta from decimal import Decimal +import pytest + from nautilus_trader.backtest.exchange import SimulatedExchange from nautilus_trader.backtest.execution_client import BacktestExecClient from nautilus_trader.backtest.models import FillModel @@ -27,6 +29,7 @@ from nautilus_trader.config import DataEngineConfig from nautilus_trader.config import ExecEngineConfig from nautilus_trader.config import RiskEngineConfig +from nautilus_trader.config.common import ImportableExecAlgorithmConfig from nautilus_trader.core.datetime import secs_to_nanos from nautilus_trader.data.engine import DataEngine from nautilus_trader.examples.algorithms.twap import TWAPExecAlgorithm @@ -47,6 +50,7 @@ from nautilus_trader.model.identifiers import ExecAlgorithmId from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.objects import Money +from nautilus_trader.model.objects import Quantity from nautilus_trader.model.orders.list import OrderList from nautilus_trader.msgbus.bus import MessageBus from nautilus_trader.portfolio.portfolio import Portfolio @@ -184,6 +188,73 @@ def setup(self) -> None: self.emulator.start() self.strategy.start() + def test_exec_algorithm_reset(self) -> None: + # Arrange + exec_algorithm = TWAPExecAlgorithm() + exec_algorithm.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + exec_algorithm.start() + exec_algorithm.stop() + + # Act, Assert + exec_algorithm.reset() + + def test_exec_algorithm_to_importable_config(self) -> None: + # Arrange + exec_algorithm = TWAPExecAlgorithm() + + # Act + config = exec_algorithm.to_importable_config() + + # Assert + assert isinstance(config, ImportableExecAlgorithmConfig) + assert config.dict() == { + "exec_algorithm_path": "nautilus_trader.examples.algorithms.twap:TWAPExecAlgorithm", + "config_path": "nautilus_trader.examples.algorithms.twap:TWAPExecAlgorithmConfig", + "config": {"exec_algorithm_id": "TWAP"}, + } + + def test_exec_algorithm_spawn_market_order_with_quantity_too_high(self) -> None: + """ + Test that an exception is raised when more than the primary quantity attempts to + be spawned. + """ + # Arrange + exec_algorithm = TWAPExecAlgorithm() + exec_algorithm.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + exec_algorithm.start() + + primary_order = self.strategy.order_factory.market( + instrument_id=ETHUSDT_PERP_BINANCE.id, + order_side=OrderSide.BUY, + quantity=ETHUSDT_PERP_BINANCE.make_qty(Decimal("1")), + exec_algorithm_id=ExecAlgorithmId("TWAP"), + exec_algorithm_params={"horizon_secs": 2, "interval_secs": 1}, + ) + + # Act, Assert + with pytest.raises(ValueError): + exec_algorithm.spawn_market( + primary=primary_order, + quantity=ETHUSDT_PERP_BINANCE.make_qty(Decimal("2")), # <-- Greater than primary + time_in_force=TimeInForce.FOK, + reduce_only=True, + tags="EXIT", + ) + def test_exec_algorithm_spawn_market_order(self) -> None: """ Test that the primary order was reduced and the spawned order has the expected @@ -221,6 +292,8 @@ def test_exec_algorithm_spawn_market_order(self) -> None: # Assert assert primary_order.quantity == ETHUSDT_PERP_BINANCE.make_qty(Decimal("0.5")) + assert primary_order.is_active_local + assert spawned_order.is_active_local assert spawned_order.client_order_id.value == primary_order.client_order_id.value + "-E1" assert spawned_order.order_type == OrderType.MARKET assert spawned_order.quantity == spawned_qty @@ -267,6 +340,8 @@ def test_exec_algorithm_spawn_limit_order(self) -> None: # Assert assert primary_order.quantity == ETHUSDT_PERP_BINANCE.make_qty(Decimal("0.5")) + assert primary_order.is_active_local + assert spawned_order.is_active_local assert spawned_order.client_order_id.value == primary_order.client_order_id.value + "-E1" assert spawned_order.order_type == OrderType.LIMIT assert spawned_order.quantity == spawned_qty @@ -317,6 +392,8 @@ def test_exec_algorithm_spawn_market_to_limit_order(self) -> None: # Assert assert primary_order.quantity == ETHUSDT_PERP_BINANCE.make_qty(Decimal("0.5")) + assert primary_order.is_active_local + assert spawned_order.is_active_local assert spawned_order.client_order_id.value == primary_order.client_order_id.value + "-E1" assert spawned_order.order_type == OrderType.MARKET_TO_LIMIT assert spawned_order.quantity == spawned_qty @@ -443,6 +520,7 @@ def test_exec_algorithm_cancel_order(self) -> None: # Assert assert primary_order.status == OrderStatus.CANCELED + assert not primary_order.is_active_local def test_exec_algorithm_on_order(self) -> None: # Arrange @@ -502,16 +580,16 @@ def test_exec_algorithm_on_order_list_emulated_with_entry_exec_algorithm(self) - tick1: QuoteTick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5005.0, - ask=5005.0, + bid_price=5005.0, + ask_price=5005.0, bid_size=10.000, ask_size=10.000, ) tick2: QuoteTick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5000.0, - ask=5000.0, + bid_price=5000.0, + ask_price=5000.0, bid_size=10.000, ask_size=10.000, ) @@ -539,10 +617,12 @@ def test_exec_algorithm_on_order_list_emulated_with_entry_exec_algorithm(self) - sl_order = bracket.orders[1] tp_order = bracket.orders[2] + exec_spawn_id = original_entry_order.client_order_id + # Act self.strategy.submit_order_list(bracket, manage_gtd_expiry=True) - # Trigger order from emulator + # Trigger ENTRY order release self.data_engine.process(tick2) events: list[TimeEventHandler] = self.clock.advance_time(secs_to_nanos(3.0)) @@ -552,7 +632,7 @@ def test_exec_algorithm_on_order_list_emulated_with_entry_exec_algorithm(self) - transformed_entry_order = self.cache.order(original_entry_order.client_order_id) # Assert - spawned_orders = self.cache.orders_for_exec_spawn(original_entry_order.client_order_id) + spawned_orders = self.cache.orders_for_exec_spawn(exec_spawn_id) assert transformed_entry_order.status == OrderStatus.SUBMITTED assert sl_order.status == OrderStatus.INITIALIZED assert tp_order.status == OrderStatus.INITIALIZED @@ -572,3 +652,317 @@ def test_exec_algorithm_on_order_list_emulated_with_entry_exec_algorithm(self) - assert transformed_entry_order.quantity == ETHUSDT_PERP_BINANCE.make_qty(0.004) assert sl_order.quantity == quantity assert tp_order.quantity == quantity + assert self.cache.exec_spawn_total_quantity(exec_spawn_id) == Quantity.from_str("1.000") + assert self.cache.exec_spawn_total_filled_qty(exec_spawn_id) == Quantity.from_str("0.000") + assert self.cache.exec_spawn_total_leaves_qty(exec_spawn_id) == Quantity.from_str("1.000") + + def test_exec_algorithm_on_emulated_bracket_with_exec_algo_entry(self) -> None: + """ + Test that the OTO contingent orders update as the primary order is filled. + """ + # Arrange + exec_algorithm = TWAPExecAlgorithm() + exec_algorithm.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + exec_algorithm.start() + + tick1: QuoteTick = TestDataStubs.quote_tick( + instrument=ETHUSDT_PERP_BINANCE, + bid_price=5005.0, + ask_price=5005.0, + bid_size=10.000, + ask_size=10.000, + ) + + tick2: QuoteTick = TestDataStubs.quote_tick( + instrument=ETHUSDT_PERP_BINANCE, + bid_price=5000.0, + ask_price=5000.0, + bid_size=10.000, + ask_size=10.000, + ) + + self.data_engine.process(tick1) + self.exchange.process_quote_tick(tick1) + + quantity = ETHUSDT_PERP_BINANCE.make_qty(1) + bracket: OrderList = self.strategy.order_factory.bracket( + instrument_id=ETHUSDT_PERP_BINANCE.id, + order_side=OrderSide.BUY, + quantity=quantity, + time_in_force=TimeInForce.GTD, + expire_time=self.clock.utc_now() + timedelta(seconds=30), + entry_trigger_price=ETHUSDT_PERP_BINANCE.make_price(5000.00), + sl_trigger_price=ETHUSDT_PERP_BINANCE.make_price(4090.00), + tp_price=ETHUSDT_PERP_BINANCE.make_price(5010.00), + emulation_trigger=TriggerType.BID_ASK, + entry_order_type=OrderType.MARKET_IF_TOUCHED, + entry_exec_algorithm_id=exec_algorithm.id, + entry_exec_algorithm_params={"horizon_secs": 2, "interval_secs": 0.5}, + ) + + entry_order = bracket.orders[0] + sl_order = bracket.orders[1] + tp_order = bracket.orders[2] + + exec_spawn_id = entry_order.client_order_id + + # Act + self.strategy.submit_order_list(bracket, manage_gtd_expiry=True) + + # Trigger ENTRY order release + self.data_engine.process(tick2) + self.exchange.process(0) + + # Assert + spawned_orders = self.cache.orders_for_exec_spawn(exec_spawn_id) + transformed_entry_order = self.cache.order(entry_order.client_order_id) + assert transformed_entry_order.status == OrderStatus.RELEASED + assert sl_order.status == OrderStatus.EMULATED + assert tp_order.status == OrderStatus.EMULATED + assert sl_order.is_active_local + assert tp_order.is_active_local + assert self.exec_engine.command_count == 1 + assert self.risk_engine.command_count == 1 + assert len(spawned_orders) == 2 + assert [o.client_order_id.value for o in spawned_orders] == [ + "O-19700101-0000-000-None-1", + "O-19700101-0000-000-None-1-E1", + ] + # Assert final scheduled order quantity + assert sl_order.quantity == Quantity.from_str("0.250") + assert tp_order.quantity == Quantity.from_str("0.250") + assert self.cache.exec_spawn_total_quantity(exec_spawn_id) == Quantity.from_str("1.000") + assert self.cache.exec_spawn_total_filled_qty(exec_spawn_id) == Quantity.from_str("0.250") + assert self.cache.exec_spawn_total_leaves_qty(exec_spawn_id) == Quantity.from_str("0.750") + + # Fill more SL size + events: list[TimeEventHandler] = self.clock.advance_time(secs_to_nanos(0.5)) + for event in events: + event.handle() + self.exchange.process(0) + + assert sl_order.quantity == Quantity.from_str("0.500") + assert tp_order.quantity == Quantity.from_str("0.500") + assert self.cache.exec_spawn_total_quantity(exec_spawn_id) == Quantity.from_str("1.000") + assert self.cache.exec_spawn_total_filled_qty(exec_spawn_id) == Quantity.from_str("0.500") + assert self.cache.exec_spawn_total_leaves_qty(exec_spawn_id) == Quantity.from_str("0.500") + assert self.exec_engine.command_count == 2 + + # Fill remaining SL size + events = self.clock.advance_time(secs_to_nanos(2.0)) + for event in events: + event.handle() + self.exchange.process(0) + + assert sl_order.status == OrderStatus.EMULATED + assert tp_order.status == OrderStatus.EMULATED + assert sl_order.quantity == Quantity.from_str("1.000") + assert tp_order.quantity == Quantity.from_str("1.000") + assert self.cache.exec_spawn_total_quantity(exec_spawn_id) == Quantity.from_str("1.000") + assert self.cache.exec_spawn_total_filled_qty(exec_spawn_id) == Quantity.from_str("1.000") + assert self.cache.exec_spawn_total_leaves_qty(exec_spawn_id) == Quantity.from_str("0.000") + assert self.exec_engine.command_count == 4 + + def test_exec_algorithm_on_emulated_bracket_with_partially_multi_filled_sl(self) -> None: + """ + Test that the TP order in an OUO contingent relationship with the SL should have + its size reduced to the total size of the execution spawns leaves quantity. + """ + # Arrange + exec_algorithm = TWAPExecAlgorithm() + exec_algorithm.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + exec_algorithm.start() + + tick1: QuoteTick = TestDataStubs.quote_tick( + instrument=ETHUSDT_PERP_BINANCE, + bid_price=5005.0, + ask_price=5005.0, + bid_size=10.000, + ask_size=10.000, + ) + + tick2: QuoteTick = TestDataStubs.quote_tick( + instrument=ETHUSDT_PERP_BINANCE, + bid_price=5000.0, + ask_price=5000.0, + bid_size=10.000, + ask_size=10.000, + ) + + self.data_engine.process(tick1) + self.exchange.process_quote_tick(tick1) + + quantity = ETHUSDT_PERP_BINANCE.make_qty(1) + bracket: OrderList = self.strategy.order_factory.bracket( + instrument_id=ETHUSDT_PERP_BINANCE.id, + order_side=OrderSide.BUY, + quantity=quantity, + time_in_force=TimeInForce.GTD, + expire_time=self.clock.utc_now() + timedelta(seconds=30), + entry_trigger_price=ETHUSDT_PERP_BINANCE.make_price(5000.00), + sl_trigger_price=ETHUSDT_PERP_BINANCE.make_price(4090.00), + tp_price=ETHUSDT_PERP_BINANCE.make_price(5010.00), + emulation_trigger=TriggerType.BID_ASK, + entry_order_type=OrderType.MARKET_IF_TOUCHED, + sl_exec_algorithm_id=exec_algorithm.id, + tp_exec_algorithm_id=exec_algorithm.id, + sl_exec_algorithm_params={"horizon_secs": 2, "interval_secs": 0.5}, + tp_exec_algorithm_params={"horizon_secs": 2, "interval_secs": 0.5}, + ) + + entry_order = bracket.orders[0] + sl_order = bracket.orders[1] + tp_order = bracket.orders[2] + + # Act + self.strategy.submit_order_list(bracket, manage_gtd_expiry=True) + + # Trigger ENTRY order release + self.data_engine.process(tick2) + self.exchange.process(0) + + tick3: QuoteTick = TestDataStubs.quote_tick( + instrument=ETHUSDT_PERP_BINANCE, + bid_price=4090.0, + ask_price=4090.0, + bid_size=10.000, + ask_size=10.000, + ) + + # Trigger SL order release + self.data_engine.process(tick3) + self.exchange.process(0) + + # Assert + spawned_orders = self.cache.orders_for_exec_spawn(sl_order.exec_spawn_id) + transformed_entry_order = self.cache.order(entry_order.client_order_id) + sl_order = self.cache.order(sl_order.client_order_id) + assert transformed_entry_order.status == OrderStatus.FILLED + assert sl_order.status == OrderStatus.RELEASED + assert tp_order.status == OrderStatus.EMULATED + assert self.exec_engine.command_count == 2 + assert self.risk_engine.command_count == 1 + assert len(spawned_orders) == 2 + assert [o.client_order_id.value for o in spawned_orders] == [ + "O-19700101-0000-000-None-2", + "O-19700101-0000-000-None-2-E1", + ] + # Assert final scheduled order quantity + assert sl_order.quantity == Quantity.from_str("0.750") + assert sl_order.leaves_qty == Quantity.from_str("0.750") + assert tp_order.quantity == Quantity.from_str("0.750") + assert tp_order.leaves_qty == Quantity.from_str("0.750") + assert self.cache.exec_spawn_total_quantity(sl_order.exec_spawn_id) == Quantity.from_str( + "1.000", + ) + assert self.cache.exec_spawn_total_filled_qty(sl_order.exec_spawn_id) == Quantity.from_str( + "0.250", + ) + assert self.cache.exec_spawn_total_leaves_qty(sl_order.exec_spawn_id) == Quantity.from_str( + "0.750", + ) + assert self.cache.exec_spawn_total_quantity(tp_order.exec_spawn_id) == Quantity.from_str( + "0.750", + ) + assert self.cache.exec_spawn_total_filled_qty(tp_order.exec_spawn_id) == Quantity.from_str( + "0.000", + ) + assert self.cache.exec_spawn_total_leaves_qty(tp_order.exec_spawn_id) == Quantity.from_str( + "0.750", + ) + + # Fill more SL size + events: list[TimeEventHandler] = self.clock.advance_time(secs_to_nanos(0.5)) + for event in events: + event.handle() + self.exchange.process(0) + + assert sl_order.quantity == Quantity.from_str("0.500") + assert sl_order.leaves_qty == Quantity.from_str("0.500") + assert tp_order.quantity == Quantity.from_str("0.500") + assert tp_order.leaves_qty == Quantity.from_str("0.500") + assert self.cache.exec_spawn_total_quantity(sl_order.exec_spawn_id) == Quantity.from_str( + "1.000", + ) + assert self.cache.exec_spawn_total_filled_qty(sl_order.exec_spawn_id) == Quantity.from_str( + "0.500", + ) + assert self.cache.exec_spawn_total_leaves_qty(sl_order.exec_spawn_id) == Quantity.from_str( + "0.500", + ) + assert self.cache.exec_spawn_total_quantity(tp_order.exec_spawn_id) == Quantity.from_str( + "0.500", + ) + assert self.cache.exec_spawn_total_filled_qty(tp_order.exec_spawn_id) == Quantity.from_str( + "0.000", + ) + assert self.cache.exec_spawn_total_leaves_qty(tp_order.exec_spawn_id) == Quantity.from_str( + "0.500", + ) + assert self.exec_engine.command_count == 3 + + # Fill remaining SL size + events = self.clock.advance_time(secs_to_nanos(2.0)) + for event in events: + event.handle() + self.exchange.process(0) + + assert sl_order.status == OrderStatus.FILLED + assert tp_order.status == OrderStatus.CANCELED + assert self.cache.exec_spawn_total_quantity(sl_order.exec_spawn_id) == Quantity.from_str( + "1.000", + ) + assert self.cache.exec_spawn_total_filled_qty(sl_order.exec_spawn_id) == Quantity.from_str( + "1.000", + ) + assert self.cache.exec_spawn_total_leaves_qty(sl_order.exec_spawn_id) == Quantity.from_str( + "0.000", + ) + assert self.cache.exec_spawn_total_quantity(tp_order.exec_spawn_id) == Quantity.from_str( + "0.250", + ) + assert self.cache.exec_spawn_total_filled_qty(tp_order.exec_spawn_id) == Quantity.from_str( + "0.000", + ) + assert self.cache.exec_spawn_total_leaves_qty(tp_order.exec_spawn_id) == Quantity.from_str( + "0.250", + ) + assert self.cache.exec_spawn_total_quantity( + sl_order.exec_spawn_id, + active_only=True, + ) == Quantity.from_str("0.000") + assert self.cache.exec_spawn_total_filled_qty( + sl_order.exec_spawn_id, + active_only=True, + ) == Quantity.from_str("0.000") + assert self.cache.exec_spawn_total_leaves_qty( + sl_order.exec_spawn_id, + active_only=True, + ) == Quantity.from_str("0.000") + assert self.cache.exec_spawn_total_quantity( + tp_order.exec_spawn_id, + active_only=True, + ) == Quantity.from_str("0.000") + assert self.cache.exec_spawn_total_filled_qty( + tp_order.exec_spawn_id, + active_only=True, + ) == Quantity.from_str("0.000") + assert self.cache.exec_spawn_total_leaves_qty( + tp_order.exec_spawn_id, + active_only=True, + ) == Quantity.from_str("0.000") + assert self.exec_engine.command_count == 5 diff --git a/tests/unit_tests/execution/test_emulator.py b/tests/unit_tests/execution/test_emulator.py index 3420ec3d71af..a1676f962c03 100644 --- a/tests/unit_tests/execution/test_emulator.py +++ b/tests/unit_tests/execution/test_emulator.py @@ -38,8 +38,9 @@ from nautilus_trader.model.enums import OrderType from nautilus_trader.model.enums import TrailingOffsetType from nautilus_trader.model.enums import TriggerType +from nautilus_trader.model.events import OrderEmulated from nautilus_trader.model.events import OrderInitialized -from nautilus_trader.model.events import OrderTriggered +from nautilus_trader.model.events import OrderReleased from nautilus_trader.model.events import OrderUpdated from nautilus_trader.model.identifiers import AccountId from nautilus_trader.model.identifiers import ClientId @@ -76,7 +77,7 @@ def setup(self) -> None: self.clock = TestClock() self.logger = Logger( clock=TestClock(), - level_stdout=LogLevel.DEBUG, + level_stdout=LogLevel.INFO, bypass=True, ) @@ -196,7 +197,7 @@ def test_create_matching_core_twice_raises_exception(self) -> None: ) # Act, Assert - with pytest.raises(RuntimeError): + with pytest.raises(KeyError): self.emulator.create_matching_core( ETHUSDT_PERP_BINANCE.id, ETHUSDT_PERP_BINANCE.price_increment, @@ -234,8 +235,8 @@ def test_process_quote_tick_when_no_matching_core_setup_logs_and_does_nothing(se # Arrange tick: QuoteTick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5060.0, - ask=5070.0, + bid_price=5060.0, + ask_price=5070.0, bid_size=10.0, ask_size=10.0, ) @@ -377,6 +378,30 @@ def test_submit_order_with_emulation_trigger_last_subscribes_to_data(self) -> No assert len(self.emulator.get_submit_order_commands()) == 1 assert self.emulator.subscribed_trades == [InstrumentId.from_str("ETHUSDT-PERP.BINANCE")] + def test_emulator_restart_reactivates_emulated_orders(self) -> None: + # Arrange + order = self.strategy.order_factory.limit( + instrument_id=ETHUSDT_PERP_BINANCE.id, + order_side=OrderSide.BUY, + quantity=Quantity.from_int(10), + price=ETHUSDT_PERP_BINANCE.make_price(2000), + emulation_trigger=TriggerType.LAST_TRADE, + ) + + self.strategy.submit_order(order) + + # Act + self.emulator.stop() + self.emulator.reset() + self.emulator.start() + + # Assert + matching_core = self.emulator.get_matching_core(ETHUSDT_PERP_BINANCE.id) + assert matching_core is not None + assert order in matching_core.get_orders() + assert len(self.emulator.get_submit_order_commands()) == 1 + assert self.emulator.subscribed_trades == [InstrumentId.from_str("ETHUSDT-PERP.BINANCE")] + def test_cancel_all_with_emulated_order_cancels_order(self) -> None: # Arrange order = self.strategy.order_factory.limit( @@ -421,7 +446,9 @@ def test_cancel_all_buy_orders_with_emulated_orders_cancels_buy_order(self) -> N # Assert assert order1.is_canceled + assert not order1.is_active_local assert not order2.is_canceled + assert order2.is_active_local def test_cancel_all_sell_orders_with_emulated_orders_cancels_sell_order(self) -> None: # Arrange @@ -449,7 +476,9 @@ def test_cancel_all_sell_orders_with_emulated_orders_cancels_sell_order(self) -> # Assert assert not order1.is_canceled + assert order1.is_active_local assert order2.is_canceled + assert not order2.is_active_local @pytest.mark.parametrize( ("order_side", "trigger_price"), @@ -486,9 +515,11 @@ def test_submit_limit_order_last_then_triggered_releases_market_order( order = self.cache.order(order.client_order_id) # Recover transformed order from cache assert order.order_type == OrderType.MARKET assert order.emulation_trigger == TriggerType.NO_TRIGGER - assert len(order.events) == 2 + assert len(order.events) == 4 assert isinstance(order.events[0], OrderInitialized) - assert isinstance(order.events[1], OrderInitialized) + assert isinstance(order.events[1], OrderEmulated) + assert isinstance(order.events[2], OrderInitialized) + assert isinstance(order.events[3], OrderReleased) assert self.exec_client.calls == ["_start", "submit_order"] @pytest.mark.parametrize( @@ -516,8 +547,8 @@ def test_submit_limit_order_bid_ask_then_triggered_releases_market_order( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5000.0, - ask=5000.0, + bid_price=5000.0, + ask_price=5000.0, ) # Act @@ -527,9 +558,12 @@ def test_submit_limit_order_bid_ask_then_triggered_releases_market_order( order = self.cache.order(order.client_order_id) # Recover transformed order from cache assert order.order_type == OrderType.MARKET assert order.emulation_trigger == TriggerType.NO_TRIGGER - assert len(order.events) == 2 + assert order.is_active_local + assert len(order.events) == 4 assert isinstance(order.events[0], OrderInitialized) - assert isinstance(order.events[1], OrderInitialized) + assert isinstance(order.events[1], OrderEmulated) + assert isinstance(order.events[2], OrderInitialized) + assert isinstance(order.events[3], OrderReleased) assert self.exec_client.calls == ["_start", "submit_order"] @pytest.mark.parametrize( @@ -558,8 +592,8 @@ def test_submit_limit_if_touched_then_triggered_releases_limit_order( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5000.0, - ask=5000.0, + bid_price=5000.0, + ask_price=5000.0, ) # Act @@ -569,10 +603,12 @@ def test_submit_limit_if_touched_then_triggered_releases_limit_order( order = self.cache.order(order.client_order_id) # Recover transformed order from cache assert order.order_type == OrderType.LIMIT assert order.emulation_trigger == TriggerType.NO_TRIGGER - assert len(order.events) == 3 + assert order.is_active_local + assert len(order.events) == 4 assert isinstance(order.events[0], OrderInitialized) - assert isinstance(order.events[1], OrderTriggered) + assert isinstance(order.events[1], OrderEmulated) assert isinstance(order.events[2], OrderInitialized) + assert isinstance(order.events[3], OrderReleased) assert self.exec_client.calls == ["_start", "submit_order"] @pytest.mark.parametrize( @@ -601,22 +637,23 @@ def test_submit_stop_limit_order_then_triggered_releases_limit_order( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5000.0, - ask=5000.0, + bid_price=5000.0, + ask_price=5000.0, ) # Act self.data_engine.process(tick) # Assert - assert order.is_triggered order = self.cache.order(order.client_order_id) # Recover transformed order from cache assert order.order_type == OrderType.LIMIT assert order.emulation_trigger == TriggerType.NO_TRIGGER - assert len(order.events) == 3 + assert order.is_active_local + assert len(order.events) == 4 assert isinstance(order.events[0], OrderInitialized) - assert isinstance(order.events[1], OrderTriggered) + assert isinstance(order.events[1], OrderEmulated) assert isinstance(order.events[2], OrderInitialized) + assert isinstance(order.events[3], OrderReleased) assert self.exec_client.calls == ["_start", "submit_order"] @pytest.mark.parametrize( @@ -645,8 +682,8 @@ def test_submit_market_if_touched_order_then_triggered_releases_market_order( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5060.0, - ask=5070.0, + bid_price=5060.0, + ask_price=5070.0, ) # Act @@ -656,9 +693,11 @@ def test_submit_market_if_touched_order_then_triggered_releases_market_order( order = self.cache.order(order.client_order_id) # Recover transformed order from cache assert order.order_type == OrderType.MARKET assert order.emulation_trigger == TriggerType.NO_TRIGGER - assert len(order.events) == 2 + assert len(order.events) == 4 assert isinstance(order.events[0], OrderInitialized) - assert isinstance(order.events[1], OrderInitialized) + assert isinstance(order.events[1], OrderEmulated) + assert isinstance(order.events[2], OrderInitialized) + assert isinstance(order.events[3], OrderReleased) assert self.exec_client.calls == ["_start", "submit_order"] @pytest.mark.parametrize( @@ -687,8 +726,8 @@ def test_submit_stop_market_order_then_triggered_releases_market_order( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5060.0, - ask=5070.0, + bid_price=5060.0, + ask_price=5070.0, ) # Act @@ -698,9 +737,11 @@ def test_submit_stop_market_order_then_triggered_releases_market_order( order = self.cache.order(order.client_order_id) # Recover transformed order from cache assert order.order_type == OrderType.MARKET assert order.emulation_trigger == TriggerType.NO_TRIGGER - assert len(order.events) == 2 + assert len(order.events) == 4 assert isinstance(order.events[0], OrderInitialized) - assert isinstance(order.events[1], OrderInitialized) + assert isinstance(order.events[1], OrderEmulated) + assert isinstance(order.events[2], OrderInitialized) + assert isinstance(order.events[3], OrderReleased) assert self.exec_client.calls == ["_start", "submit_order"] assert order not in self.cache.orders_emulated() @@ -729,8 +770,8 @@ def test_submit_trailing_stop_market_order_with_no_trigger_price_then_updates( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5060.0, - ask=5070.0, + bid_price=5060.0, + ask_price=5070.0, ) self.data_engine.process(tick) @@ -742,9 +783,11 @@ def test_submit_trailing_stop_market_order_with_no_trigger_price_then_updates( order = self.cache.order(order.client_order_id) # Recover transformed order from cache assert order.order_type == OrderType.TRAILING_STOP_MARKET assert order.emulation_trigger == TriggerType.BID_ASK - assert len(order.events) == 2 + assert order.is_active_local + assert len(order.events) == 3 assert isinstance(order.events[0], OrderInitialized) assert isinstance(order.events[1], OrderUpdated) + assert isinstance(order.events[2], OrderEmulated) assert order.trigger_price == expected_trigger_price @pytest.mark.parametrize( @@ -782,8 +825,8 @@ def test_submit_trailing_stop_market_order_with_trigger_price_then_updates( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5060.0, - ask=5070.0, + bid_price=5060.0, + ask_price=5070.0, ) self.data_engine.process(tick) @@ -800,8 +843,8 @@ def test_submit_trailing_stop_market_order_with_trigger_price_then_updates( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5065.0, - ask=5065.0, + bid_price=5065.0, + ask_price=5065.0, ) self.data_engine.process(tick) @@ -809,9 +852,10 @@ def test_submit_trailing_stop_market_order_with_trigger_price_then_updates( order = self.cache.order(order.client_order_id) # Recover transformed order from cache assert order.order_type == OrderType.TRAILING_STOP_MARKET assert order.emulation_trigger == TriggerType.BID_ASK - assert len(order.events) == 2 + assert len(order.events) == 3 assert isinstance(order.events[0], OrderInitialized) - assert isinstance(order.events[1], OrderUpdated) + assert isinstance(order.events[1], OrderEmulated) + assert isinstance(order.events[2], OrderUpdated) assert order.trigger_price == expected_trigger_price @pytest.mark.parametrize( @@ -840,8 +884,8 @@ def test_submit_trailing_stop_market_order_with_trigger_price_then_triggers( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5060.0, - ask=5070.0, + bid_price=5060.0, + ask_price=5070.0, ) self.data_engine.process(tick) @@ -850,8 +894,8 @@ def test_submit_trailing_stop_market_order_with_trigger_price_then_triggers( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5055.0, - ask=5075.0, + bid_price=5055.0, + ask_price=5075.0, ) self.data_engine.process(tick) @@ -859,9 +903,11 @@ def test_submit_trailing_stop_market_order_with_trigger_price_then_triggers( order = self.cache.order(order.client_order_id) # Recover transformed order from cache assert order.order_type == OrderType.MARKET assert order.emulation_trigger == TriggerType.NO_TRIGGER - assert len(order.events) == 2 + assert len(order.events) == 4 assert isinstance(order.events[0], OrderInitialized) - assert isinstance(order.events[1], OrderInitialized) + assert isinstance(order.events[1], OrderEmulated) + assert isinstance(order.events[2], OrderInitialized) + assert isinstance(order.events[3], OrderReleased) assert order not in self.cache.orders_emulated() @pytest.mark.parametrize( @@ -900,8 +946,8 @@ def test_submit_trailing_stop_limit_order_with_no_trigger_price_then_updates( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5060.0, - ask=5070.0, + bid_price=5060.0, + ask_price=5070.0, ) self.data_engine.process(tick) @@ -912,9 +958,10 @@ def test_submit_trailing_stop_limit_order_with_no_trigger_price_then_updates( order = self.cache.order(order.client_order_id) # Recover transformed order from cache assert order.order_type == OrderType.TRAILING_STOP_LIMIT assert order.emulation_trigger == TriggerType.BID_ASK - assert len(order.events) == 2 + assert len(order.events) == 3 assert isinstance(order.events[0], OrderInitialized) assert isinstance(order.events[1], OrderUpdated) + assert isinstance(order.events[2], OrderEmulated) assert order.trigger_price == expected_trigger_price @pytest.mark.parametrize( @@ -957,8 +1004,8 @@ def test_submit_trailing_stop_limit_order_with_trigger_price_then_updates( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5060.0, - ask=5070.0, + bid_price=5060.0, + ask_price=5070.0, ) self.data_engine.process(tick) @@ -968,8 +1015,8 @@ def test_submit_trailing_stop_limit_order_with_trigger_price_then_updates( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5065.0, - ask=5065.0, + bid_price=5065.0, + ask_price=5065.0, ) self.data_engine.process(tick) @@ -977,9 +1024,10 @@ def test_submit_trailing_stop_limit_order_with_trigger_price_then_updates( order = self.cache.order(order.client_order_id) # Recover transformed order from cache assert order.order_type == OrderType.TRAILING_STOP_LIMIT assert order.emulation_trigger == TriggerType.BID_ASK - assert len(order.events) == 2 + assert len(order.events) == 3 assert isinstance(order.events[0], OrderInitialized) - assert isinstance(order.events[1], OrderUpdated) + assert isinstance(order.events[1], OrderEmulated) + assert isinstance(order.events[2], OrderUpdated) assert order.trigger_price == expected_trigger_price @pytest.mark.parametrize( @@ -1010,8 +1058,8 @@ def test_submit_trailing_stop_limit_order_with_trigger_price_then_triggers( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5060.0, - ask=5070.0, + bid_price=5060.0, + ask_price=5070.0, ) self.data_engine.process(tick) @@ -1021,8 +1069,8 @@ def test_submit_trailing_stop_limit_order_with_trigger_price_then_triggers( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5055.0, - ask=5075.0, + bid_price=5055.0, + ask_price=5075.0, ) self.data_engine.process(tick) @@ -1031,10 +1079,11 @@ def test_submit_trailing_stop_limit_order_with_trigger_price_then_triggers( order = self.cache.order(order.client_order_id) # Recover transformed order from cache assert order.order_type == OrderType.LIMIT assert order.emulation_trigger == TriggerType.NO_TRIGGER - assert len(order.events) == 3 + assert len(order.events) == 4 assert isinstance(order.events[0], OrderInitialized) - assert isinstance(order.events[1], OrderTriggered) + assert isinstance(order.events[1], OrderEmulated) assert isinstance(order.events[2], OrderInitialized) + assert isinstance(order.events[3], OrderReleased) assert order not in self.cache.orders_emulated() @pytest.mark.parametrize( @@ -1061,8 +1110,8 @@ def test_submit_limit_if_touched_immediately_triggered_releases_limit_order( # Arrange tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5060.0, - ask=5070.0, + bid_price=5060.0, + ask_price=5070.0, ) self.emulator.create_matching_core( @@ -1090,8 +1139,8 @@ def test_submit_limit_if_touched_immediately_triggered_releases_limit_order( assert order.emulation_trigger == TriggerType.NO_TRIGGER assert len(order.events) == 3 assert isinstance(order.events[0], OrderInitialized) - assert isinstance(order.events[1], OrderTriggered) - assert isinstance(order.events[2], OrderInitialized) + assert isinstance(order.events[1], OrderInitialized) + assert isinstance(order.events[2], OrderReleased) assert self.exec_client.calls == ["_start", "submit_order"] assert order not in self.cache.orders_emulated() @@ -1114,7 +1163,7 @@ def test_submit_limit_order_bid_ask_with_synthetic_instrument_trigger( instrument_id=ETHUSDT_BINANCE.id, order_side=order_side, quantity=Quantity.from_int(10), - price=Price.from_str("30000.00000000"), # <-- Synthetic price + price=Price.from_str("30000.00"), # <-- Synthetic price emulation_trigger=TriggerType.DEFAULT, trigger_instrument_id=synthetic.id, ) @@ -1123,13 +1172,13 @@ def test_submit_limit_order_bid_ask_with_synthetic_instrument_trigger( tick1 = TestDataStubs.quote_tick( instrument=ETHUSDT_BINANCE, - bid=10_000.0, - ask=10_000.0, + bid_price=10_000.0, + ask_price=10_000.0, ) tick2 = TestDataStubs.quote_tick( instrument=BTCUSDT_BINANCE, - bid=50_000.0, - ask=50_000.0, + bid_price=50_000.0, + ask_price=50_000.0, ) # Act @@ -1140,7 +1189,9 @@ def test_submit_limit_order_bid_ask_with_synthetic_instrument_trigger( order = self.cache.order(order.client_order_id) # Recover transformed order from cache assert order.order_type == OrderType.MARKET assert order.emulation_trigger == TriggerType.NO_TRIGGER - assert len(order.events) == 2 + assert len(order.events) == 4 assert isinstance(order.events[0], OrderInitialized) - assert isinstance(order.events[1], OrderInitialized) + assert isinstance(order.events[1], OrderEmulated) + assert isinstance(order.events[2], OrderInitialized) + assert isinstance(order.events[3], OrderReleased) assert self.exec_client.calls == ["_start", "submit_order"] diff --git a/tests/unit_tests/execution/test_emulator_list.py b/tests/unit_tests/execution/test_emulator_list.py index f84c9c073d9a..b7b13c7faea7 100644 --- a/tests/unit_tests/execution/test_emulator_list.py +++ b/tests/unit_tests/execution/test_emulator_list.py @@ -453,8 +453,8 @@ def test_submit_bracket_when_stop_limit_entry_filled_then_emulates_sl_and_tp(sel tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=4990.0, - ask=4990.0, + bid_price=4990.0, + ask_price=4990.0, ) # Act @@ -698,8 +698,8 @@ def test_triggered_sl_submits_market_order( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5100.0, - ask=5100.0, + bid_price=5100.0, + ask_price=5100.0, ) # Act @@ -749,6 +749,11 @@ def test_triggered_stop_limit_tp_submits_limit_order( ) # Act + self.exec_engine.process( + TestEventStubs.order_released( + bracket.first, + ), + ) self.exec_engine.process( TestEventStubs.order_submitted( bracket.first, @@ -765,8 +770,8 @@ def test_triggered_stop_limit_tp_submits_limit_order( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5100.0, - ask=5100.0, + bid_price=5100.0, + ask_price=5100.0, ) # Act @@ -828,8 +833,8 @@ def test_triggered_then_filled_tp_cancels_sl( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5100.0, - ask=5100.0, + bid_price=5100.0, + ask_price=5100.0, ) # Act @@ -897,8 +902,8 @@ def test_triggered_then_partially_filled_oco_sl_cancels_tp(self): tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=4900.0, - ask=4900.0, + bid_price=4900.0, + ask_price=4900.0, ) # Act @@ -930,6 +935,9 @@ def test_triggered_then_partially_filled_oco_sl_cancels_tp(self): assert entry_order.status == OrderStatus.FILLED assert sl_order.status == OrderStatus.PARTIALLY_FILLED assert tp_order.status == OrderStatus.CANCELED + assert not entry_order.is_active_local + assert not sl_order.is_active_local + assert not tp_order.is_active_local assert not matching_core.order_exists(entry_order.client_order_id) assert not matching_core.order_exists(sl_order.client_order_id) assert not matching_core.order_exists(tp_order.client_order_id) @@ -968,8 +976,8 @@ def test_triggered_then_partially_filled_ouo_sl_updated_tp(self): tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=4900.0, - ask=4900.0, + bid_price=4900.0, + ask_price=4900.0, ) # Act @@ -1000,11 +1008,14 @@ def test_triggered_then_partially_filled_ouo_sl_updated_tp(self): assert self.cache.orders_emulated_count() == 1 assert entry_order.status == OrderStatus.FILLED assert sl_order.status == OrderStatus.PARTIALLY_FILLED - assert tp_order.status == OrderStatus.INITIALIZED + assert tp_order.status == OrderStatus.EMULATED assert sl_order.quantity == Quantity.from_int(10) assert sl_order.leaves_qty == Quantity.from_int(5) assert tp_order.quantity == Quantity.from_int(5) assert tp_order.leaves_qty == Quantity.from_int(5) + assert not entry_order.is_active_local + assert not sl_order.is_active_local + assert tp_order.is_active_local assert not matching_core.order_exists(entry_order.client_order_id) assert not matching_core.order_exists(sl_order.client_order_id) assert matching_core.order_exists(tp_order.client_order_id) @@ -1029,8 +1040,8 @@ def test_released_order_with_quote_quantity_sets_contingent_orders_to_base_quant tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid=5000.0, - ask=5000.0, + bid_price=5000.0, + ask_price=5000.0, ) # Act @@ -1047,6 +1058,9 @@ def test_released_order_with_quote_quantity_sets_contingent_orders_to_base_quant assert not entry_order.is_quote_quantity assert not sl_order.is_quote_quantity assert not tp_order.is_quote_quantity + assert entry_order.is_active_local + assert sl_order.is_active_local + assert tp_order.is_active_local assert entry_order.quantity == ETHUSDT_PERP_BINANCE.make_qty(0.002) assert sl_order.quantity == ETHUSDT_PERP_BINANCE.make_qty(0.002) assert tp_order.quantity == ETHUSDT_PERP_BINANCE.make_qty(0.002) diff --git a/tests/unit_tests/execution/test_engine.py b/tests/unit_tests/execution/test_engine.py index b35ef4b82d78..7e28127d350c 100644 --- a/tests/unit_tests/execution/test_engine.py +++ b/tests/unit_tests/execution/test_engine.py @@ -2175,8 +2175,8 @@ def test_submit_order_with_quote_quantity_and_quote_tick_converts_to_base_quanti # Setup market tick = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("0.80000"), - ask=Price.from_str("0.80010"), + bid_price=Price.from_str("0.80000"), + ask_price=Price.from_str("0.80010"), bid_size=Quantity.from_int(10_000_000), ask_size=Quantity.from_int(10_000_000), ts_event=0, @@ -2299,8 +2299,8 @@ def test_submit_bracket_order_with_quote_quantity_and_ticks_converts_expected( quote_tick = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("0.80000"), - ask=Price.from_str("0.80010"), + bid_price=Price.from_str("0.80000"), + ask_price=Price.from_str("0.80010"), bid_size=Quantity.from_int(10_000_000), ask_size=Quantity.from_int(10_000_000), ts_event=0, diff --git a/tests/unit_tests/execution/test_messages.py b/tests/unit_tests/execution/test_messages.py index 13dd475b4523..d5ecf6a1f5d6 100644 --- a/tests/unit_tests/execution/test_messages.py +++ b/tests/unit_tests/execution/test_messages.py @@ -108,11 +108,11 @@ def test_submit_order_command_with_exec_algorithm_from_dict_and_str_repr(self): assert SubmitOrder.from_dict(SubmitOrder.to_dict(command)) == command assert ( str(command) - == "SubmitOrder(order=LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, exec_algorithm_id=VWAP, exec_algorithm_params={'max_percentage': 100.0, 'start': 0, 'end': 1}, tags=None), position_id=P-001)" # noqa + == "SubmitOrder(order=LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, exec_algorithm_id=VWAP, exec_algorithm_params={'max_percentage': 100.0, 'start': 0, 'end': 1}, exec_spawn_id=O-19700101-0000-000-001-1, tags=None), position_id=P-001)" # noqa ) assert ( repr(command) - == f"SubmitOrder(client_id=None, trader_id=TRADER-001, strategy_id=S-001, instrument_id=AUD/USD.SIM, client_order_id=O-19700101-0000-000-001-1, order=LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, exec_algorithm_id=VWAP, exec_algorithm_params={{'max_percentage': 100.0, 'start': 0, 'end': 1}}, tags=None), position_id=P-001, command_id={uuid}, ts_init=0)" # noqa + == f"SubmitOrder(client_id=None, trader_id=TRADER-001, strategy_id=S-001, instrument_id=AUD/USD.SIM, client_order_id=O-19700101-0000-000-001-1, order=LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, exec_algorithm_id=VWAP, exec_algorithm_params={{'max_percentage': 100.0, 'start': 0, 'end': 1}}, exec_spawn_id=O-19700101-0000-000-001-1, tags=None), position_id=P-001, command_id={uuid}, ts_init=0)" # noqa ) def test_submit_bracket_order_command_to_from_dict_and_str_repr(self): @@ -125,6 +125,9 @@ def test_submit_bracket_order_command_to_from_dict_and_str_repr(self): quantity=Quantity.from_int(100_000), sl_trigger_price=Price.from_str("1.00000"), tp_price=Price.from_str("1.00100"), + entry_tags="ENTRY", + tp_tags="TAKE_PROFIT", + sl_tags="STOP_LOSS", ) command = SubmitOrderList( @@ -158,6 +161,9 @@ def test_submit_bracket_order_command_with_exec_algorithm_to_from_dict_and_str_r quantity=Quantity.from_int(100_000), sl_trigger_price=Price.from_str("1.00000"), tp_price=Price.from_str("1.00100"), + entry_tags="ENTRY", + tp_tags="TAKE_PROFIT", + sl_tags="STOP_LOSS", ) command = SubmitOrderList( diff --git a/tests/unit_tests/indicators/test_sma.py b/tests/unit_tests/indicators/test_sma.py index 316c0c321828..fad91efcf8ae 100644 --- a/tests/unit_tests/indicators/test_sma.py +++ b/tests/unit_tests/indicators/test_sma.py @@ -133,8 +133,8 @@ def test_handle_quote_tick_updates_with_expected_value(self): sma_for_ticks3 = SimpleMovingAverage(10, PriceType.BID) tick = TestDataStubs.quote_tick( - bid=1.00001, - ask=1.00003, + bid_price=1.00001, + ask_price=1.00003, ) # Act diff --git a/tests/unit_tests/indicators/test_spread_analyzer.py b/tests/unit_tests/indicators/test_spread_analyzer.py index 01627333de68..12b909fa9344 100644 --- a/tests/unit_tests/indicators/test_spread_analyzer.py +++ b/tests/unit_tests/indicators/test_spread_analyzer.py @@ -55,8 +55,8 @@ def test_update_with_incorrect_tick_raises_exception(self): analyzer = SpreadAnalyzer(AUDUSD_SIM.id, 1000) tick = QuoteTick( instrument_id=USDJPY_SIM.id, - bid=Price.from_str("117.80000"), - ask=Price.from_str("117.80010"), + bid_price=Price.from_str("117.80000"), + ask_price=Price.from_str("117.80010"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -71,8 +71,8 @@ def test_update_correctly_updates_analyzer(self): analyzer = SpreadAnalyzer(AUDUSD_SIM.id, 1000) tick1 = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("0.80000"), - ask=Price.from_str("0.80010"), + bid_price=Price.from_str("0.80000"), + ask_price=Price.from_str("0.80010"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -81,8 +81,8 @@ def test_update_correctly_updates_analyzer(self): tick2 = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("0.80002"), - ask=Price.from_str("0.80008"), + bid_price=Price.from_str("0.80002"), + ask_price=Price.from_str("0.80008"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, diff --git a/tests/unit_tests/model/test_events.py b/tests/unit_tests/model/test_events.py index 084d2a3fd92e..471ece20c150 100644 --- a/tests/unit_tests/model/test_events.py +++ b/tests/unit_tests/model/test_events.py @@ -30,6 +30,7 @@ from nautilus_trader.model.events import OrderCanceled from nautilus_trader.model.events import OrderCancelRejected from nautilus_trader.model.events import OrderDenied +from nautilus_trader.model.events import OrderEmulated from nautilus_trader.model.events import OrderExpired from nautilus_trader.model.events import OrderFilled from nautilus_trader.model.events import OrderInitialized @@ -37,6 +38,7 @@ from nautilus_trader.model.events import OrderPendingCancel from nautilus_trader.model.events import OrderPendingUpdate from nautilus_trader.model.events import OrderRejected +from nautilus_trader.model.events import OrderReleased from nautilus_trader.model.events import OrderSubmitted from nautilus_trader.model.events import OrderTriggered from nautilus_trader.model.events import OrderUpdated @@ -205,6 +207,53 @@ def test_order_denied_event_to_from_dict_and_str_repr(self): == f"OrderDenied(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, reason=Exceeded MAX_ORDER_SUBMIT_RATE, event_id={uuid}, ts_init=0)" # noqa ) + def test_order_emulated_event_to_from_dict_and_str_repr(self): + # Arrange + uuid = UUID4() + event = OrderEmulated( + trader_id=TraderId("TRADER-001"), + strategy_id=StrategyId("SCALPER-001"), + instrument_id=InstrumentId(Symbol("BTCUSDT"), Venue("BINANCE")), + client_order_id=ClientOrderId("O-2020872378423"), + event_id=uuid, + ts_init=0, + ) + + # Act, Assert + assert OrderEmulated.from_dict(OrderEmulated.to_dict(event)) == event + assert ( + str(event) + == "OrderEmulated(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423)" + ) + assert ( + repr(event) + == f"OrderEmulated(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, event_id={uuid}, ts_init=0)" # noqa + ) + + def test_order_released_event_to_from_dict_and_str_repr(self): + # Arrange + uuid = UUID4() + event = OrderReleased( + trader_id=TraderId("TRADER-001"), + strategy_id=StrategyId("SCALPER-001"), + instrument_id=InstrumentId(Symbol("BTCUSDT"), Venue("BINANCE")), + client_order_id=ClientOrderId("O-2020872378423"), + released_price=Price.from_str("50200.10"), + event_id=uuid, + ts_init=0, + ) + + # Act, Assert + assert OrderReleased.from_dict(OrderReleased.to_dict(event)) == event + assert ( + str(event) + == "OrderReleased(instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, released_price=50200.10)" + ) + assert ( + repr(event) + == f"OrderReleased(trader_id=TRADER-001, strategy_id=SCALPER-001, instrument_id=BTCUSDT.BINANCE, client_order_id=O-2020872378423, released_price=50200.10, event_id={uuid}, ts_init=0)" # noqa + ) + def test_order_submitted_event_to_from_dict_and_str_repr(self): # Arrange uuid = UUID4() diff --git a/tests/unit_tests/model/test_instrument.py b/tests/unit_tests/model/test_instrument.py index d7f5a5e77eb6..94131eed6e70 100644 --- a/tests/unit_tests/model/test_instrument.py +++ b/tests/unit_tests/model/test_instrument.py @@ -47,7 +47,6 @@ AAPL_EQUITY = TestInstrumentProvider.aapl_equity() ES_FUTURE = TestInstrumentProvider.es_future() AAPL_OPTION = TestInstrumentProvider.aapl_option() -NFL_INSTRUMENT = TestInstrumentProvider.betting_instrument() class TestInstrument: @@ -449,48 +448,3 @@ def test_next_bid_price(self, instrument, value, n, expected): def test_option_attributes(self): assert AAPL_OPTION.underlying == "AAPL" assert AAPL_OPTION.kind == option_kind_from_str("CALL") - - -class TestBettingInstrument: - def setup(self): - self.instrument = TestInstrumentProvider.betting_instrument() - - def test_notional_value(self): - notional = self.instrument.notional_value( - quantity=Quantity.from_int(100), - price=Price.from_str("0.5"), - use_quote_for_inverse=False, - ).as_decimal() - # We are long 100 at 0.5 probability, aka 2.0 in odds terms - assert notional == Decimal("200.0") - - @pytest.mark.parametrize( - ("value", "n", "expected"), - [ - (101, 0, "110"), - ], - ) - def test_next_ask_price(self, value, n, expected): - result = self.instrument.next_ask_price(value, num_ticks=n) - expected = Price.from_str(expected) - assert result == expected - - @pytest.mark.parametrize( - ("value", "n", "expected"), - [ - (1.999, 0, "1.99"), - ], - ) - def test_next_bid_price(self, value, n, expected): - result = self.instrument.next_bid_price(value, num_ticks=n) - expected = Price.from_str(expected) - assert result == expected - - def test_min_max_price(self): - assert self.instrument.min_price == Price.from_str("1.01") - assert self.instrument.max_price == Price.from_str("1000") - - def test_to_dict(self): - instrument = TestInstrumentProvider.betting_instrument() - data = instrument.to_dict(instrument) - assert data["venue_name"] == "BETFAIR" diff --git a/tests/unit_tests/model/test_objects_money.py b/tests/unit_tests/model/test_objects_money.py index 0f8b368f8ba2..9754ce91793b 100644 --- a/tests/unit_tests/model/test_objects_money.py +++ b/tests/unit_tests/model/test_objects_money.py @@ -13,6 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +import math import pickle from decimal import Decimal @@ -30,6 +31,11 @@ class TestMoney: + def test_instantiate_with_nan_raises_value_error(self): + # Arrange, Act, Assert + with pytest.raises(ValueError): + Money(math.nan, currency=USD) + def test_instantiate_with_none_currency_raises_type_error(self) -> None: # Arrange, Act, Assert with pytest.raises(TypeError): @@ -96,6 +102,7 @@ def test_as_double_returns_expected_result(self) -> None: # Assert assert money.as_double() == 1.0 + assert money.raw == 1_000_000_000 assert str(money) == "1.00" def test_initialized_with_many_decimals_rounds_to_currency_precision(self) -> None: @@ -104,6 +111,8 @@ def test_initialized_with_many_decimals_rounds_to_currency_precision(self) -> No result2 = Money(5005.556666, USD) # Assert + assert result1.raw == 1_000_330_000_000 + assert result2.raw == 5_005_560_000_000 assert result1.to_str() == "1_000.33 USD" assert result2.to_str() == "5_005.56 USD" diff --git a/tests/unit_tests/model/test_objects_price.py b/tests/unit_tests/model/test_objects_price.py index a7348adc951d..408ab91c02a0 100644 --- a/tests/unit_tests/model/test_objects_price.py +++ b/tests/unit_tests/model/test_objects_price.py @@ -13,6 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +import math import pickle from decimal import Decimal @@ -22,11 +23,19 @@ class TestPrice: + def test_instantiate_with_nan_raises_value_error(self): + # Arrange, Act, Assert + with pytest.raises(ValueError): + Price(math.nan, precision=0) + def test_instantiate_with_none_value_raises_type_error(self): # Arrange, Act, Assert with pytest.raises(TypeError): Price(None) + with pytest.raises(TypeError): + Price(None, precision=0) + def test_instantiate_with_negative_precision_raises_overflow_error(self): # Arrange, Act, Assert with pytest.raises(OverflowError): @@ -52,6 +61,7 @@ def test_instantiate_base_decimal_from_int(self): result = Price(1, precision=1) # Assert + assert result.raw == 1_000_000_000 assert str(result) == "1.0" def test_instantiate_base_decimal_from_float(self): @@ -59,6 +69,7 @@ def test_instantiate_base_decimal_from_float(self): result = Price(1.12300, precision=5) # Assert + assert result.raw == 1_123_000_000 assert str(result) == "1.12300" def test_instantiate_base_decimal_from_decimal(self): @@ -113,7 +124,7 @@ def test_abs_with_various_values_returns_expected_decimal(self, value, expected) [ Price(-1, precision=0), Decimal("-1"), - ], # Matches built-in decimal.Decimal behaviour + ], # Matches built-in decimal.Decimal behavior [Price(0, 0), Decimal("0")], ], ) diff --git a/tests/unit_tests/model/test_objects_quantity.py b/tests/unit_tests/model/test_objects_quantity.py index 2b4b180513f3..b0b124a61c28 100644 --- a/tests/unit_tests/model/test_objects_quantity.py +++ b/tests/unit_tests/model/test_objects_quantity.py @@ -13,6 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +import math import pickle from decimal import Decimal @@ -22,11 +23,19 @@ class TestQuantity: + def test_instantiate_with_nan_raises_value_error(self): + # Arrange, Act, Assert + with pytest.raises(ValueError): + Quantity(math.nan, precision=0) + def test_instantiate_with_none_value_raises_type_error(self): # Arrange, Act, Assert with pytest.raises(TypeError): Quantity(None) + with pytest.raises(TypeError): + Quantity(None, precision=0) + def test_instantiate_with_negative_precision_raises_overflow_error(self): # Arrange, Act, Assert with pytest.raises(OverflowError): @@ -51,6 +60,7 @@ def test_instantiate_base_decimal_from_float(self): result = Quantity(1.12300, precision=5) # Assert + assert result.raw == 1_123_000_000 assert str(result) == "1.12300" def test_instantiate_base_decimal_from_decimal(self): @@ -65,6 +75,7 @@ def test_instantiate_base_decimal_from_str(self): result = Quantity.from_str("1.23") # Assert + assert result.raw == 1_230_000_000 assert str(result) == "1.23" @pytest.mark.parametrize( diff --git a/tests/unit_tests/model/test_orderbook.py b/tests/unit_tests/model/test_orderbook.py index 3948064447ac..c6c08a6ee30a 100644 --- a/tests/unit_tests/model/test_orderbook.py +++ b/tests/unit_tests/model/test_orderbook.py @@ -13,6 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +import copy + import msgspec import pandas as pd import pytest @@ -21,6 +23,7 @@ from nautilus_trader.model.enums import BookAction from nautilus_trader.model.enums import BookType from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.objects import Price from nautilus_trader.model.objects import Quantity from nautilus_trader.model.orderbook import OrderBook @@ -40,6 +43,61 @@ def setup(self): ) self.sample_book = self.make_sample_book() + def test_order_book_pickleable(self): + # Arrange + book = OrderBook( + instrument_id=InstrumentId.from_str("1.166564490-237491-0.0.BETFAIR"), + book_type=BookType.L2_MBP, + ) + raw_updates = [ + { + "type": "OrderBookDelta", + "instrument_id": "1.166564490-237491-0.0.BETFAIR", + "action": "CLEAR", + "order": {"side": "NO_ORDER_SIDE", "price": "0", "size": "0", "order_id": 0}, + "flags": 0, + "sequence": 0, + "ts_event": 1576840503572000000, + "ts_init": 1576840503572000000, + }, + { + "type": "OrderBookDelta", + "instrument_id": "1.166564490-237491-0.0.BETFAIR", + "action": "UPDATE", + "order": {"side": "BUY", "price": "2", "size": "77", "order_id": 181}, + "flags": 0, + "sequence": 0, + "ts_event": 1576840503572000000, + "ts_init": 1576840503572000000, + }, + { + "type": "OrderBookDelta", + "instrument_id": "1.166564490-237491-0.0.BETFAIR", + "action": "UPDATE", + "order": {"side": "BUY", "price": "1", "size": "2", "order_id": 103}, + "flags": 0, + "sequence": 0, + "ts_event": 1576840503572000000, + "ts_init": 1576840503572000000, + }, + { + "type": "OrderBookDelta", + "instrument_id": "1.166564490-237491-0.0.BETFAIR", + "action": "UPDATE", + "order": {"side": "BUY", "price": "1", "size": "40", "order_id": 107}, + "flags": 0, + "sequence": 0, + "ts_event": 1576840503572000000, + "ts_init": 1576840503572000000, + }, + ] + updates = [OrderBookDelta.from_dict(upd) for upd in raw_updates] + + # Act, Assert + for update in updates[:2]: + book.apply_delta(update) + copy.deepcopy(book) + def make_sample_book(self): return TestDataStubs.make_book( instrument=self.instrument, diff --git a/tests/unit_tests/model/test_orderbook_data.py b/tests/unit_tests/model/test_orderbook_data.py index 7f562e5b6a9f..bcaa9576d81d 100644 --- a/tests/unit_tests/model/test_orderbook_data.py +++ b/tests/unit_tests/model/test_orderbook_data.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -import pytest +import pickle from nautilus_trader.model.data import NULL_ORDER from nautilus_trader.model.data import BookOrder @@ -29,6 +29,23 @@ AUDUSD = TestIdStubs.audusd_id() +def test_book_order_pickle_round_trip(): + # Arrange + order = BookOrder( + side=OrderSide.BUY, + price=Price.from_str("10.0"), + size=Quantity.from_str("5"), + order_id=1, + ) + + # Act + pickled = pickle.dumps(order) + unpickled = pickle.loads(pickled) # noqa + + # Assert + assert order == unpickled + + class TestOrderBookDelta: def test_fully_qualified_name(self): # Arrange, Act, Assert @@ -37,7 +54,31 @@ def test_fully_qualified_name(self): == "nautilus_trader.model.data.book:OrderBookDelta" ) - @pytest.mark.skip(reason="TBD") + def test_pickle_round_trip(self): + order = BookOrder( + side=OrderSide.BUY, + price=Price.from_str("10.0"), + size=Quantity.from_str("5"), + order_id=1, + ) + + delta = OrderBookDelta( + instrument_id=AUDUSD, + action=BookAction.ADD, + order=order, + flags=0, + sequence=123456789, + ts_event=0, + ts_init=1_000_000_000, + ) + + # Act + pickled = pickle.dumps(delta) + unpickled = pickle.loads(pickled) # noqa + + # Assert + assert delta == unpickled + def test_hash_str_and_repr(self): # Arrange order = BookOrder( @@ -61,11 +102,11 @@ def test_hash_str_and_repr(self): assert isinstance(hash(delta), int) assert ( str(delta) - == f"OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder {{ side: NoOrderSide, price: 0, size: 0, order_id: 0}}), sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa + == f"OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder {{ side: Buy, price: 10.0, size: 5, order_id: 1 }}, flags=0, sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa ) assert ( repr(delta) - == f"OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder {{ side: NoOrderSide, price: 0, size: 0, order_id: 0}}), sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa + == f"OrderBookDelta(instrument_id=AUD/USD.SIM, action=ADD, order=BookOrder {{ side: Buy, price: 10.0, size: 5, order_id: 1 }}, flags=0, sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa ) def test_with_null_book_order(self): @@ -84,11 +125,11 @@ def test_with_null_book_order(self): assert isinstance(hash(delta), int) assert ( str(delta) - == f"OrderBookDelta(instrument_id=AUD/USD.SIM, action=CLEAR, order=BookOrder {{ side: NoOrderSide, price: 0, size: 0, order_id: 0 }}, ts_event=0, ts_init=1000000000, sequence=123456789, flags=32)" # noqa + == f"OrderBookDelta(instrument_id=AUD/USD.SIM, action=CLEAR, order=BookOrder {{ side: NoOrderSide, price: 0, size: 0, order_id: 0 }}, flags=32, sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa ) assert ( repr(delta) - == f"OrderBookDelta(instrument_id=AUD/USD.SIM, action=CLEAR, order=BookOrder {{ side: NoOrderSide, price: 0, size: 0, order_id: 0 }}, ts_event=0, ts_init=1000000000, sequence=123456789, flags=32)" # noqa + == f"OrderBookDelta(instrument_id=AUD/USD.SIM, action=CLEAR, order=BookOrder {{ side: NoOrderSide, price: 0, size: 0, order_id: 0 }}, flags=32, sequence=123456789, ts_event=0, ts_init=1000000000)" # noqa ) def test_to_dict_with_order_returns_expected_dict(self): diff --git a/tests/unit_tests/model/test_orders.py b/tests/unit_tests/model/test_orders.py index 337ade4d0300..b4918fd8144c 100644 --- a/tests/unit_tests/model/test_orders.py +++ b/tests/unit_tests/model/test_orders.py @@ -298,6 +298,8 @@ def test_initialize_buy_market_order(self): assert not order.is_open assert not order.is_closed assert not order.is_inflight + assert not order.is_emulated + assert order.is_active_local assert order.is_buy assert order.is_aggressive assert not order.is_sell @@ -335,6 +337,8 @@ def test_initialize_sell_market_order(self): assert not order.is_closed assert not order.is_inflight assert not order.is_buy + assert not order.is_emulated + assert order.is_active_local assert order.is_sell assert order.ts_last == 0 assert isinstance(order.init_event, OrderInitialized) @@ -436,16 +440,18 @@ def test_initialize_limit_order(self): assert not order.is_open assert not order.is_aggressive assert not order.is_closed + assert not order.is_emulated + assert order.is_active_local assert order.is_primary assert not order.is_spawned assert isinstance(order.init_event, OrderInitialized) assert ( str(order) - == "LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, exec_algorithm_id=TWAP, tags=None)" # noqa + == "LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, exec_algorithm_id=TWAP, exec_spawn_id=O-19700101-0000-000-001-1, tags=None)" # noqa ) assert ( repr(order) - == "LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, exec_algorithm_id=TWAP, tags=None)" # noqa + == "LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, status=INITIALIZED, client_order_id=O-19700101-0000-000-001-1, venue_order_id=None, position_id=None, exec_algorithm_id=TWAP, exec_spawn_id=O-19700101-0000-000-001-1, tags=None)" # noqa ) def test_limit_order_to_dict(self): @@ -497,7 +503,7 @@ def test_limit_order_to_dict(self): "parent_order_id": None, "exec_algorithm_id": "VWAP", "exec_algorithm_params": b'{"period":60}', - "exec_spawn_id": None, + "exec_spawn_id": "O-19700101-0000-000-001-1", "tags": None, "ts_init": 0, "ts_last": 0, @@ -1503,6 +1509,9 @@ def test_order_list_str_and_repr(self): Quantity.from_int(100_000), sl_trigger_price=Price.from_str("0.99990"), tp_price=Price.from_str("1.00010"), + entry_tags="ENTRY", + tp_tags="TAKE_PROFIT", + sl_tags="STOP_LOSS", ) # Assert diff --git a/tests/unit_tests/model/test_position.py b/tests/unit_tests/model/test_position.py index 52411a5a5de0..272159642f45 100644 --- a/tests/unit_tests/model/test_position.py +++ b/tests/unit_tests/model/test_position.py @@ -256,6 +256,105 @@ def test_short_position_to_dict_equity(self) -> None: "commissions": "['0.00 USD']", } + @pytest.mark.parametrize( + ("side1", "side2", "last_px1", "last_px2", "last_qty1", "last_qty2"), + [ + [ + OrderSide.BUY, + OrderSide.SELL, # <--- Different side + Price.from_str("1.00001"), + Price.from_str("1.00001"), + Quantity.from_str("1"), + Quantity.from_str("1"), + ], + [ + OrderSide.BUY, + OrderSide.SELL, # <--- Different side + Price.from_str("1.00001"), + Price.from_str("1.00001"), + Quantity.from_str("1"), + Quantity.from_str("1"), + ], + [ + OrderSide.BUY, + OrderSide.SELL, # <--- Different side + Price.from_str("1.00001"), + Price.from_str("1.00001"), + Quantity.from_str("1"), + Quantity.from_str("1"), + ], + ], + ) + def test_position_filled_with_duplicate_trade_id_different_trade( + self, + side1: OrderSide, + side2: OrderSide, + last_px1: Price, + last_px2: Price, + last_qty1: Quantity, + last_qty2: Quantity, + ) -> None: + # Arrange + order1 = self.order_factory.market( + AUDUSD_SIM.id, + side1, + Quantity.from_int(4), + ) + order2 = self.order_factory.market( + AUDUSD_SIM.id, + side2, + Quantity.from_int(4), + ) + + trade_id = TradeId("1") + + fill1 = TestEventStubs.order_filled( + order1, + instrument=AUDUSD_SIM, + strategy_id=StrategyId("S-001"), + last_px=last_px1, + last_qty=last_qty1, + trade_id=trade_id, + position_id=PositionId("1"), + ) + + fill2 = TestEventStubs.order_filled( + order2, + instrument=AUDUSD_SIM, + strategy_id=StrategyId("S-001"), + last_px=last_px2, + last_qty=last_qty2, + trade_id=trade_id, + position_id=PositionId("1"), + ) + + # Act + position = Position(instrument=AUDUSD_SIM, fill=fill1) + position.apply(fill2) + + def test_position_filled_with_duplicate_trade_id_and_same_trade(self) -> None: + # Arrange + order = self.order_factory.market( + AUDUSD_SIM.id, + OrderSide.BUY, + Quantity.from_int(4), + ) + + fill = TestEventStubs.order_filled( + order, + instrument=AUDUSD_SIM, + strategy_id=StrategyId("S-001"), + last_px=Price.from_str("1.000"), + trade_id=TradeId("1"), + position_id=PositionId("1"), + ) + + position = Position(instrument=AUDUSD_SIM, fill=fill) + + # Act + with pytest.raises(ValueError): + position.apply(fill) + def test_position_filled_with_buy_order(self) -> None: # Arrange order = self.order_factory.market( diff --git a/tests/unit_tests/model/test_tick.py b/tests/unit_tests/model/test_tick.py index 6681ec288ca8..a840a2c4c14f 100644 --- a/tests/unit_tests/model/test_tick.py +++ b/tests/unit_tests/model/test_tick.py @@ -48,8 +48,8 @@ def test_tick_hash_str_and_repr(self): tick = QuoteTick( instrument_id=instrument_id, - bid=Price.from_str("1.00000"), - ask=Price.from_str("1.00001"), + bid_price=Price.from_str("1.00000"), + ask_price=Price.from_str("1.00001"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=3, @@ -65,8 +65,8 @@ def test_extract_price_with_various_price_types_returns_expected_values(self): # Arrange tick = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("1.00000"), - ask=Price.from_str("1.00001"), + bid_price=Price.from_str("1.00000"), + ask_price=Price.from_str("1.00001"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -87,8 +87,8 @@ def test_extract_volume_with_various_price_types_returns_expected_values(self): # Arrange tick = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("1.00000"), - ask=Price.from_str("1.00001"), + bid_price=Price.from_str("1.00000"), + ask_price=Price.from_str("1.00001"), bid_size=Quantity.from_int(500_000), ask_size=Quantity.from_int(800_000), ts_event=0, @@ -109,8 +109,8 @@ def test_to_dict_returns_expected_dict(self): # Arrange tick = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("1.00000"), - ask=Price.from_str("1.00001"), + bid_price=Price.from_str("1.00000"), + ask_price=Price.from_str("1.00001"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=1, @@ -124,8 +124,8 @@ def test_to_dict_returns_expected_dict(self): assert result == { "type": "QuoteTick", "instrument_id": "AUD/USD.SIM", - "bid": "1.00000", - "ask": "1.00001", + "bid_price": "1.00000", + "ask_price": "1.00001", "bid_size": "1", "ask_size": "1", "ts_event": 1, @@ -136,8 +136,8 @@ def test_from_dict_returns_expected_tick(self): # Arrange tick = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("1.00000"), - ask=Price.from_str("1.00001"), + bid_price=Price.from_str("1.00000"), + ask_price=Price.from_str("1.00001"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=1, @@ -168,8 +168,8 @@ def test_from_raw_returns_expected_tick(self): # Assert assert tick.instrument_id == AUDUSD_SIM.id - assert tick.bid == Price.from_str("1.00000") - assert tick.ask == Price.from_str("1.00001") + assert tick.bid_price == Price.from_str("1.00000") + assert tick.ask_price == Price.from_str("1.00001") assert tick.bid_size == Quantity.from_int(1) assert tick.ask_size == Quantity.from_int(2) assert tick.ts_event == 1 @@ -179,8 +179,8 @@ def test_pickling_round_trip_results_in_expected_tick(self): # Arrange tick = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("1.00000"), - ask=Price.from_str("1.00001"), + bid_price=Price.from_str("1.00000"), + ask_price=Price.from_str("1.00001"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=1, diff --git a/tests/unit_tests/model/test_tick_scheme.py b/tests/unit_tests/model/test_tick_scheme.py index e0f299404e98..060481878cd2 100644 --- a/tests/unit_tests/model/test_tick_scheme.py +++ b/tests/unit_tests/model/test_tick_scheme.py @@ -20,7 +20,6 @@ from nautilus_trader.model.tick_scheme.base import round_down from nautilus_trader.model.tick_scheme.base import round_up from nautilus_trader.model.tick_scheme.implementations.fixed import FixedTickScheme -from nautilus_trader.model.tick_scheme.implementations.tiered import TieredTickScheme from nautilus_trader.test_kit.providers import TestInstrumentProvider @@ -97,69 +96,6 @@ def test_next_bid_price(self, value, expected): assert result == expected -class TestBettingTickScheme: - def setup(self) -> None: - self.tick_scheme: TieredTickScheme = get_tick_scheme("BETFAIR") - - def test_attrs(self): - assert self.tick_scheme.min_price == Price.from_str("1.01") - assert self.tick_scheme.max_price == Price.from_str("1000") - - def test_build_ticks(self): - result = self.tick_scheme.ticks[:5].tolist() - expected = [ - Price.from_str("1.01"), - Price.from_str("1.02"), - Price.from_str("1.03"), - Price.from_str("1.04"), - Price.from_str("1.05"), - ] - assert result == expected - - @pytest.mark.parametrize( - ("value", "expected"), - [ - (1.01, 0), - (1.10, 9), - (2.0, 99), - (3.5, 159), - ], - ) - def test_find_tick_idx(self, value, expected): - result = self.tick_scheme.find_tick_index(value) - assert result == expected - - @pytest.mark.parametrize( - ("value", "n", "expected"), - [ - (1.499, 0, "1.50"), - (2.000, 0, "2.0"), - (2.011, 0, "2.02"), - (2.021, 0, "2.04"), - (2.027, 2, "2.08"), - ], - ) - def test_next_ask_price(self, value, n, expected): - result = self.tick_scheme.next_ask_price(value, n=n) - expected = Price.from_str(expected) - assert result == expected - - @pytest.mark.parametrize( - ("value", "n", "expected"), - [ - (1.499, 0, "1.49"), - (2.000, 0, "2.0"), - (2.011, 0, "2.00"), - (2.021, 0, "2.02"), - (2.027, 2, "1.99"), - ], - ) - def test_next_bid_price(self, value, n, expected): - result = self.tick_scheme.next_bid_price(value=value, n=n) - expected = Price.from_str(expected) - assert result == expected - - class TestTopix100TickScheme: def setup(self) -> None: self.tick_scheme = get_tick_scheme("TOPIX100") diff --git a/tests/unit_tests/persistence/external/test_core.py b/tests/unit_tests/persistence/external/test_core.py index 9eef2c0d9198..8b03e70913f6 100644 --- a/tests/unit_tests/persistence/external/test_core.py +++ b/tests/unit_tests/persistence/external/test_core.py @@ -182,8 +182,8 @@ def test_write_parquet_determine_partitions_writes_instrument_id(self): self._load_data_into_catalog() quote = QuoteTick( instrument_id=TestIdStubs.audusd_id(), - bid=Price.from_str("0.80"), - ask=Price.from_str("0.81"), + bid_price=Price.from_str("0.80"), + ask_price=Price.from_str("0.81"), bid_size=Quantity.from_int(1_000), ask_size=Quantity.from_int(1_000), ts_event=0, @@ -540,8 +540,8 @@ def test_write_parquet_rust_quote_ticks_writes_expected(self): objs = [ QuoteTick( instrument_id=instrument.id, - bid=Price.from_str("4507.24000000"), - ask=Price.from_str("4507.25000000"), + bid_price=Price.from_str("4507.24000000"), + ask_price=Price.from_str("4507.25000000"), bid_size=Quantity.from_str("2.35950000"), ask_size=Quantity.from_str("2.84570000"), ts_event=1, @@ -549,8 +549,8 @@ def test_write_parquet_rust_quote_ticks_writes_expected(self): ), QuoteTick( instrument_id=instrument.id, - bid=Price.from_str("4507.24000000"), - ask=Price.from_str("4507.25000000"), + bid_price=Price.from_str("4507.24000000"), + ask_price=Price.from_str("4507.25000000"), bid_size=Quantity.from_str("2.35950000"), ask_size=Quantity.from_str("2.84570000"), ts_event=10, diff --git a/tests/unit_tests/persistence/external/test_parsers.py b/tests/unit_tests/persistence/external/test_parsers.py index dcceff7f206e..a008e134ed28 100644 --- a/tests/unit_tests/persistence/external/test_parsers.py +++ b/tests/unit_tests/persistence/external/test_parsers.py @@ -36,6 +36,7 @@ from tests import TEST_DATA_DIR from tests.integration_tests.adapters.betfair.test_kit import BetfairDataProvider from tests.integration_tests.adapters.betfair.test_kit import BetfairTestStubs +from tests.integration_tests.adapters.betfair.test_kit import betting_instrument pytestmark = pytest.mark.skip(reason="WIP pending catalog refactor") @@ -74,7 +75,7 @@ def block_parser(block: bytes): yield obj.from_dict(values) provider = BetfairInstrumentProvider.from_instruments( - [TestInstrumentProvider.betting_instrument()], + [betting_instrument()], ) block = BetfairDataProvider.badly_formatted_log() reader = ByteReader(block_parser=block_parser, instrument_provider=provider) diff --git a/tests/unit_tests/persistence/test_catalog.py b/tests/unit_tests/persistence/test_catalog.py index 8b8bd725fa9e..735246c48a70 100644 --- a/tests/unit_tests/persistence/test_catalog.py +++ b/tests/unit_tests/persistence/test_catalog.py @@ -398,8 +398,8 @@ def test_partition_key_correctly_remapped(self): instrument = TestInstrumentProvider.default_fx_ccy("AUD/USD") tick = QuoteTick( instrument_id=instrument.id, - bid=Price(10, 1), - ask=Price(11, 1), + bid_price=Price(10, 1), + ask_price=Price(11, 1), bid_size=Quantity(10, 1), ask_size=Quantity(10, 1), ts_init=0, @@ -438,8 +438,8 @@ def test_list_partitions(self): instrument = TestInstrumentProvider.default_fx_ccy("AUD/USD") tick = QuoteTick( instrument_id=instrument.id, - bid=Price(10, 1), - ask=Price(11, 1), + bid_price=Price(10, 1), + ask_price=Price(11, 1), bid_size=Quantity(10, 1), ask_size=Quantity(10, 1), ts_init=0, @@ -652,8 +652,8 @@ def test_catalog_persists_equity(self): quote_tick = QuoteTick( instrument_id=instrument.id, - ask=Price.from_str("2.0"), - bid=Price.from_str("2.1"), + bid_price=Price.from_str("2.1"), + ask_price=Price.from_str("2.0"), bid_size=Quantity.from_int(10), ask_size=Quantity.from_int(10), ts_event=0, diff --git a/tests/unit_tests/persistence/test_transformer.py b/tests/unit_tests/persistence/test_transformer.py index 5066643a1c95..e1f996e76db1 100644 --- a/tests/unit_tests/persistence/test_transformer.py +++ b/tests/unit_tests/persistence/test_transformer.py @@ -18,8 +18,13 @@ import pandas as pd import pyarrow as pa +import pytest from nautilus_trader.core.nautilus_pyo3.persistence import DataTransformer +from nautilus_trader.model.data import Bar +from nautilus_trader.model.data import OrderBookDelta +from nautilus_trader.model.data import QuoteTick +from nautilus_trader.model.data import TradeTick from nautilus_trader.persistence.wranglers import TradeTickDataWrangler from nautilus_trader.persistence.wranglers_v2 import QuoteTickDataWrangler from nautilus_trader.test_kit.providers import TestDataProvider @@ -66,3 +71,73 @@ def test_legacy_trade_ticks_to_record_batch_reader() -> None: assert len(ticks) == 69_806 assert len(reader.read_all()) == len(ticks) reader.close() + + +def test_get_schema_map_with_unsupported_type() -> None: + # Arrange, Act, Assert + with pytest.raises(TypeError): + DataTransformer.get_schema_map(str) + + +@pytest.mark.parametrize( + ("data_type", "expected_map"), + [ + [ + OrderBookDelta, + { + "action": "UInt8", + "flags": "UInt8", + "order_id": "UInt64", + "price": "Int64", + "sequence": "UInt64", + "side": "UInt8", + "size": "UInt64", + "ts_event": "UInt64", + "ts_init": "UInt64", + }, + ], + [ + QuoteTick, + { + "bid_price": "Int64", + "ask_price": "Int64", + "bid_size": "UInt64", + "ask_size": "UInt64", + "ts_event": "UInt64", + "ts_init": "UInt64", + }, + ], + [ + TradeTick, + { + "price": "Int64", + "size": "UInt64", + "aggressor_side": "UInt8", + "trade_id": "Utf8", + "ts_event": "UInt64", + "ts_init": "UInt64", + }, + ], + [ + Bar, + { + "open": "Int64", + "high": "Int64", + "low": "Int64", + "close": "Int64", + "volume": "UInt64", + "ts_event": "UInt64", + "ts_init": "UInt64", + }, + ], + ], +) +def test_get_schema_map_for_all_implemented_types( + data_type: type, + expected_map: dict[str, str], +) -> None: + # Arrange, Act + schema_map = DataTransformer.get_schema_map(data_type) + + # Assert + assert schema_map == expected_map diff --git a/tests/unit_tests/persistence/test_wranglers_v2.py b/tests/unit_tests/persistence/test_wranglers_v2.py index 6e78d81c85e3..54823f9bc1ec 100644 --- a/tests/unit_tests/persistence/test_wranglers_v2.py +++ b/tests/unit_tests/persistence/test_wranglers_v2.py @@ -16,6 +16,8 @@ import pandas as pd from fsspec.utils import pathlib +from nautilus_trader.model.data import QuoteTick +from nautilus_trader.model.data import TradeTick from nautilus_trader.persistence.wranglers_v2 import QuoteTickDataWrangler from nautilus_trader.persistence.wranglers_v2 import TradeTickDataWrangler from nautilus_trader.test_kit.providers import TestInstrumentProvider @@ -36,8 +38,12 @@ def test_quote_tick_data_wrangler() -> None: wrangler = QuoteTickDataWrangler(AUDUSD_SIM) ticks = wrangler.from_pandas(df) + cython_ticks = QuoteTick.from_pyo3(ticks) + # Assert assert len(ticks) == 100_000 + assert len(cython_ticks) == 100_000 + assert isinstance(cython_ticks[0], QuoteTick) assert str(ticks[0]) == "AUD/USD.SIM,0.67067,0.67070,1000000,1000000,1580398089820000000" assert str(ticks[-1]) == "AUD/USD.SIM,0.66934,0.66938,1000000,1000000,1580504394501000000" @@ -51,7 +57,11 @@ def test_trade_tick_data_wrangler() -> None: wrangler = TradeTickDataWrangler(ETHUSDT_BINANCE) ticks = wrangler.from_pandas(df) + cython_ticks = TradeTick.from_pyo3(ticks) + # Assert assert len(ticks) == 69806 + assert len(cython_ticks) == 69806 + assert isinstance(cython_ticks[0], TradeTick) assert str(ticks[0]) == "ETHUSDT.BINANCE,423.76,2.67900,BUYER,148568980,1597399200223000000" assert str(ticks[-1]) == "ETHUSDT.BINANCE,426.89,0.16100,BUYER,148638715,1597417198693000000" diff --git a/tests/unit_tests/portfolio/test_portfolio.py b/tests/unit_tests/portfolio/test_portfolio.py index 6acc136a7e24..936e6364d266 100644 --- a/tests/unit_tests/portfolio/test_portfolio.py +++ b/tests/unit_tests/portfolio/test_portfolio.py @@ -25,7 +25,6 @@ from nautilus_trader.execution.engine import ExecutionEngine from nautilus_trader.model.currencies import BTC from nautilus_trader.model.currencies import ETH -from nautilus_trader.model.currencies import GBP from nautilus_trader.model.currencies import USD from nautilus_trader.model.currencies import USDT from nautilus_trader.model.data import QuoteTick @@ -63,7 +62,6 @@ BTCUSDT_BINANCE = TestInstrumentProvider.btcusdt_binance() BTCUSD_BITMEX = TestInstrumentProvider.xbtusd_bitmex() ETHUSD_BITMEX = TestInstrumentProvider.ethusd_bitmex() -BETTING_INSTRUMENT = TestInstrumentProvider.betting_instrument() class TestPortfolio: @@ -108,7 +106,6 @@ def setup(self): self.cache.add_instrument(BTCUSDT_BINANCE) self.cache.add_instrument(BTCUSD_BITMEX) self.cache.add_instrument(ETHUSD_BITMEX) - self.cache.add_instrument(BETTING_INSTRUMENT) def test_account_when_no_account_returns_none(self): # Arrange, Act, Assert @@ -427,8 +424,8 @@ def test_update_orders_open_margin_account(self): # Update the last quote last = QuoteTick( instrument_id=BTCUSDT_BINANCE.id, - bid=Price.from_str("25001.00"), - ask=Price.from_str("25002.00"), + bid_price=Price.from_str("25001.00"), + ask_price=Price.from_str("25002.00"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -446,16 +443,27 @@ def test_order_accept_updates_margin_init(self): # Arrange AccountFactory.register_calculated_account("BINANCE") + account_id = AccountId("BINANCE-01234") state = AccountState( - account_id=AccountId("BETFAIR-01234"), + account_id=account_id, account_type=AccountType.MARGIN, - base_currency=GBP, + base_currency=None, # Multi-currency account reported=True, balances=[ AccountBalance( - total=Money(1000, GBP), - free=Money(1000, GBP), - locked=Money(0, GBP), + Money(10.00000000, BTC), + Money(0.00000000, BTC), + Money(10.00000000, BTC), + ), + AccountBalance( + Money(20.00000000, ETH), + Money(0.00000000, ETH), + Money(20.00000000, ETH), + ), + AccountBalance( + Money(100000.00000000, USDT), + Money(0.00000000, USDT), + Money(100000.00000000, USDT), ), ], margins=[], @@ -465,13 +473,11 @@ def test_order_accept_updates_margin_init(self): ts_init=0, ) - AccountFactory.register_calculated_account("BETFAIR") - self.portfolio.update_account(state) # Create a limit order order1 = self.order_factory.limit( - BETTING_INSTRUMENT.id, + BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("100"), Price.from_str("0.5"), @@ -489,7 +495,7 @@ def test_order_accept_updates_margin_init(self): self.portfolio.initialize_orders() # Assert - assert self.portfolio.margins_init(BETFAIR)[BETTING_INSTRUMENT.id] == Money(200, GBP) + assert self.portfolio.margins_init(BINANCE)[BTCUSDT_BINANCE.id] == Money(0.1, USDT) def test_update_positions(self): # Arrange @@ -585,8 +591,8 @@ def test_update_positions(self): # Update the last quote last = QuoteTick( instrument_id=BTCUSDT_BINANCE.id, - bid=Price.from_str("25001.00"), - ask=Price.from_str("25002.00"), + bid_price=Price.from_str("25001.00"), + ask_price=Price.from_str("25002.00"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -655,8 +661,8 @@ def test_opening_one_long_position_updates_portfolio(self): last = QuoteTick( instrument_id=BTCUSDT_BINANCE.id, - bid=Price.from_str("10510.00"), - ask=Price.from_str("10511.00"), + bid_price=Price.from_str("10510.00"), + ask_price=Price.from_str("10511.00"), bid_size=Quantity.from_str("1.000000"), ask_size=Quantity.from_str("1.000000"), ts_event=0, @@ -739,8 +745,8 @@ def test_opening_one_short_position_updates_portfolio(self): last = QuoteTick( instrument_id=BTCUSDT_BINANCE.id, - bid=Price.from_str("15510.15"), - ask=Price.from_str("15510.25"), + bid_price=Price.from_str("15510.15"), + ask_price=Price.from_str("15510.25"), bid_size=Quantity.from_str("12.62"), ask_size=Quantity.from_str("3.10"), ts_event=0, @@ -803,8 +809,8 @@ def test_opening_positions_with_multi_asset_account(self): last_ethusd = QuoteTick( instrument_id=ETHUSD_BITMEX.id, - bid=Price.from_str("376.05"), - ask=Price.from_str("377.10"), + bid_price=Price.from_str("376.05"), + ask_price=Price.from_str("377.10"), bid_size=Quantity.from_str("16"), ask_size=Quantity.from_str("25"), ts_event=0, @@ -813,8 +819,8 @@ def test_opening_positions_with_multi_asset_account(self): last_btcusd = QuoteTick( instrument_id=BTCUSD_BITMEX.id, - bid=Price.from_str("10500.05"), - ask=Price.from_str("10501.51"), + bid_price=Price.from_str("10500.05"), + ask_price=Price.from_str("10501.51"), bid_size=Quantity.from_str("2.54"), ask_size=Quantity.from_str("0.91"), ts_event=0, @@ -956,8 +962,8 @@ def test_market_value_when_insufficient_data_for_xrate_returns_none(self): last_ethusd = QuoteTick( instrument_id=ETHUSD_BITMEX.id, - bid=Price.from_str("376.05"), - ask=Price.from_str("377.10"), + bid_price=Price.from_str("376.05"), + ask_price=Price.from_str("377.10"), bid_size=Quantity.from_str("16"), ask_size=Quantity.from_str("25"), ts_event=0, @@ -966,8 +972,8 @@ def test_market_value_when_insufficient_data_for_xrate_returns_none(self): last_xbtusd = QuoteTick( instrument_id=BTCUSD_BITMEX.id, - bid=Price.from_str("50000.00"), - ask=Price.from_str("50000.00"), + bid_price=Price.from_str("50000.00"), + ask_price=Price.from_str("50000.00"), bid_size=Quantity.from_str("1"), ask_size=Quantity.from_str("1"), ts_event=0, @@ -1017,8 +1023,8 @@ def test_opening_several_positions_updates_portfolio(self): last_audusd = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("0.80501"), - ask=Price.from_str("0.80505"), + bid_price=Price.from_str("0.80501"), + ask_price=Price.from_str("0.80505"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -1027,8 +1033,8 @@ def test_opening_several_positions_updates_portfolio(self): last_gbpusd = QuoteTick( instrument_id=GBPUSD_SIM.id, - bid=Price.from_str("1.30315"), - ask=Price.from_str("1.30317"), + bid_price=Price.from_str("1.30315"), + ask_price=Price.from_str("1.30317"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -1133,8 +1139,8 @@ def test_modifying_position_updates_portfolio(self): last_audusd = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("0.80501"), - ask=Price.from_str("0.80505"), + bid_price=Price.from_str("0.80501"), + ask_price=Price.from_str("0.80505"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -1365,8 +1371,8 @@ def test_several_positions_with_different_instruments_updates_portfolio(self): last_audusd = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("0.80501"), - ask=Price.from_str("0.80505"), + bid_price=Price.from_str("0.80501"), + ask_price=Price.from_str("0.80505"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, @@ -1375,8 +1381,8 @@ def test_several_positions_with_different_instruments_updates_portfolio(self): last_gbpusd = QuoteTick( instrument_id=GBPUSD_SIM.id, - bid=Price.from_str("1.30315"), - ask=Price.from_str("1.30317"), + bid_price=Price.from_str("1.30315"), + ask_price=Price.from_str("1.30317"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, diff --git a/tests/unit_tests/risk/test_engine.py b/tests/unit_tests/risk/test_engine.py index 28183036cf62..1c8c4f802a6d 100644 --- a/tests/unit_tests/risk/test_engine.py +++ b/tests/unit_tests/risk/test_engine.py @@ -869,8 +869,8 @@ def test_submit_order_when_sell_market_order_and_over_max_notional_then_denies(s # Initialize market quote = QuoteTick( instrument_id=AUDUSD_SIM.id, - bid=Price.from_str("0.75000"), - ask=Price.from_str("0.75005"), + bid_price=Price.from_str("0.75000"), + ask_price=Price.from_str("0.75005"), bid_size=Quantity.from_int(5_000_000), ask_size=Quantity.from_int(5_000_000), ts_event=0, diff --git a/tests/unit_tests/serialization/test_msgpack.py b/tests/unit_tests/serialization/test_msgpack.py index 743e1e74a2db..e3a114f5250b 100644 --- a/tests/unit_tests/serialization/test_msgpack.py +++ b/tests/unit_tests/serialization/test_msgpack.py @@ -40,6 +40,7 @@ from nautilus_trader.model.events import OrderCanceled from nautilus_trader.model.events import OrderCancelRejected from nautilus_trader.model.events import OrderDenied +from nautilus_trader.model.events import OrderEmulated from nautilus_trader.model.events import OrderExpired from nautilus_trader.model.events import OrderFilled from nautilus_trader.model.events import OrderInitialized @@ -47,6 +48,7 @@ from nautilus_trader.model.events import OrderPendingCancel from nautilus_trader.model.events import OrderPendingUpdate from nautilus_trader.model.events import OrderRejected +from nautilus_trader.model.events import OrderReleased from nautilus_trader.model.events import OrderSubmitted from nautilus_trader.model.events import OrderTriggered from nautilus_trader.model.events import OrderUpdated @@ -841,6 +843,43 @@ def test_serialize_and_deserialize_order_denied_events(self): # Assert assert deserialized == event + def test_serialize_and_deserialize_order_emulated_events(self): + # Arrange + event = OrderEmulated( + self.trader_id, + self.strategy_id, + AUDUSD_SIM.id, + ClientOrderId("O-123456"), + UUID4(), + 0, + ) + + # Act + serialized = self.serializer.serialize(event) + deserialized = self.serializer.deserialize(serialized) + + # Assert + assert deserialized == event + + def test_serialize_and_deserialize_order_released_events(self): + # Arrange + event = OrderReleased( + self.trader_id, + self.strategy_id, + AUDUSD_SIM.id, + ClientOrderId("O-123456"), + Price.from_str("1.00000"), + UUID4(), + 0, + ) + + # Act + serialized = self.serializer.serialize(event) + deserialized = self.serializer.deserialize(serialized) + + # Assert + assert deserialized == event + def test_serialize_and_deserialize_order_submitted_events(self): # Arrange event = OrderSubmitted( diff --git a/tests/unit_tests/trading/test_strategy.py b/tests/unit_tests/trading/test_strategy.py index e4363e5d0b99..f074d303d9d5 100644 --- a/tests/unit_tests/trading/test_strategy.py +++ b/tests/unit_tests/trading/test_strategy.py @@ -173,8 +173,8 @@ def setup(self): self.exchange.process_quote_tick( TestDataStubs.quote_tick( instrument=USDJPY_SIM, - bid=90.001, - ask=90.002, + bid_price=90.001, + ask_price=90.002, ), ) diff --git a/version.json b/version.json index 0c960213615a..653c7a89e8fd 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "", - "message": "v1.176.0", + "message": "v1.177.0", "color": "orange" }