From 1ebce5b14319073fc6676d1b577cd80a2df80c41 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 23 Oct 2023 18:55:37 +1100 Subject: [PATCH 01/78] Fix release workflow --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6f375f16153e..c53bc35350a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -318,9 +318,9 @@ jobs: - name: Set release output id: vars run: | - echo "ASSET_PATH=$(find ./dist -mindepth 1 -print -quit)" >> $ GITHUB_ENV + echo "ASSET_PATH=$(find ./dist -mindepth 1 -print -quit)" >> $GITHUB_ENV cd dist - echo "ASSET_NAME=$(printf '%s\0' * | awk 'BEGIN{RS="\0"} {print; exit}')" >> $ GITHUB_ENV + echo "ASSET_NAME=$(printf '%s\0' * | awk 'BEGIN{RS="\0"} {print; exit}')" >> $GITHUB_ENV - name: Upload release asset id: upload-release-asset-unix From 7ae616a8131653a763d2900fd4f85aba9a010713 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 23 Oct 2023 19:22:08 +1100 Subject: [PATCH 02/78] Bump version and drop support for Python 3.9 --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 4 +- RELEASES.md | 14 + build.py | 2 - nautilus_core/Cargo.lock | 73 +++-- nautilus_core/Cargo.toml | 2 +- nautilus_trader/adapters/_template/data.py | 21 +- .../adapters/_template/execution.py | 27 +- .../adapters/_template/providers.py | 7 +- nautilus_trader/adapters/betfair/client.py | 61 ++-- nautilus_trader/adapters/betfair/config.py | 21 +- nautilus_trader/adapters/betfair/data.py | 7 +- .../adapters/betfair/data_types.py | 11 +- nautilus_trader/adapters/betfair/execution.py | 29 +- nautilus_trader/adapters/betfair/factories.py | 7 +- .../adapters/betfair/parsing/common.py | 3 +- .../adapters/betfair/parsing/core.py | 4 +- .../adapters/betfair/parsing/requests.py | 7 +- .../adapters/betfair/parsing/streaming.py | 33 ++- nautilus_trader/adapters/betfair/providers.py | 35 ++- nautilus_trader/adapters/betfair/sockets.py | 42 +-- .../adapters/binance/common/data.py | 51 ++-- .../adapters/binance/common/execution.py | 51 ++-- .../binance/common/schemas/account.py | 101 ++++--- .../adapters/binance/common/schemas/market.py | 167 ++++++----- .../adapters/binance/common/schemas/symbol.py | 5 +- .../adapters/binance/common/types.py | 22 +- nautilus_trader/adapters/binance/config.py | 21 +- nautilus_trader/adapters/binance/factories.py | 15 +- .../adapters/binance/futures/data.py | 7 +- .../adapters/binance/futures/execution.py | 5 +- .../adapters/binance/futures/http/account.py | 36 +-- .../adapters/binance/futures/http/wallet.py | 5 +- .../adapters/binance/futures/providers.py | 13 +- .../binance/futures/schemas/account.py | 31 +-- .../binance/futures/schemas/market.py | 7 +- .../adapters/binance/futures/schemas/user.py | 23 +- .../adapters/binance/http/account.py | 165 ++++++----- .../adapters/binance/http/client.py | 2 - .../adapters/binance/http/endpoint.py | 4 +- .../adapters/binance/http/market.py | 95 ++++--- nautilus_trader/adapters/binance/http/user.py | 23 +- nautilus_trader/adapters/binance/spot/data.py | 3 +- .../adapters/binance/spot/execution.py | 5 +- .../adapters/binance/spot/http/account.py | 122 ++++---- .../adapters/binance/spot/http/market.py | 15 +- .../adapters/binance/spot/http/wallet.py | 9 +- .../adapters/binance/spot/providers.py | 11 +- .../adapters/binance/spot/schemas/account.py | 5 +- .../adapters/binance/spot/schemas/user.py | 5 +- .../adapters/binance/websocket/client.py | 25 +- .../interactive_brokers/client/client.py | 26 +- .../interactive_brokers/client/common.py | 29 +- .../adapters/interactive_brokers/common.py | 14 +- .../adapters/interactive_brokers/config.py | 26 +- .../adapters/interactive_brokers/data.py | 28 +- .../adapters/interactive_brokers/execution.py | 34 +-- .../adapters/interactive_brokers/factories.py | 3 +- .../adapters/interactive_brokers/gateway.py | 12 +- .../historic/async_actor.py | 16 +- .../interactive_brokers/historic/bar_data.py | 4 +- .../interactive_brokers/parsing/execution.py | 2 +- .../parsing/instruments.py | 3 +- .../adapters/interactive_brokers/providers.py | 11 +- nautilus_trader/adapters/sandbox/execution.py | 28 +- nautilus_trader/adapters/tardis/loaders.py | 2 - nautilus_trader/analysis/analyzer.py | 10 +- nautilus_trader/analysis/reporter.py | 2 - nautilus_trader/analysis/statistic.py | 2 - .../analysis/statistics/expectancy.py | 8 +- .../analysis/statistics/long_ratio.py | 4 +- .../analysis/statistics/loser_avg.py | 4 +- .../analysis/statistics/loser_max.py | 4 +- .../analysis/statistics/loser_min.py | 4 +- .../analysis/statistics/profit_factor.py | 4 +- .../analysis/statistics/returns_avg.py | 4 +- .../analysis/statistics/returns_avg_loss.py | 4 +- .../analysis/statistics/returns_avg_win.py | 4 +- .../analysis/statistics/returns_volatility.py | 4 +- .../analysis/statistics/risk_return_ratio.py | 4 +- .../analysis/statistics/sharpe_ratio.py | 4 +- .../analysis/statistics/sortino_ratio.py | 4 +- .../analysis/statistics/win_rate.py | 4 +- .../analysis/statistics/winner_avg.py | 4 +- .../analysis/statistics/winner_max.py | 4 +- .../analysis/statistics/winner_min.py | 4 +- nautilus_trader/backtest/__main__.py | 5 +- nautilus_trader/backtest/engine.pyx | 3 +- nautilus_trader/backtest/node.py | 2 - nautilus_trader/backtest/results.py | 2 - nautilus_trader/common/clock.pyx | 3 +- nautilus_trader/common/providers.py | 2 - nautilus_trader/common/throttler.pyx | 4 +- nautilus_trader/config/backtest.py | 43 +-- nautilus_trader/config/common.py | 82 +++--- nautilus_trader/config/live.py | 7 +- nautilus_trader/core/message.pyx | 3 +- nautilus_trader/core/nautilus_pyo3.pyi | 1 - nautilus_trader/data/engine.pyx | 3 +- nautilus_trader/data/messages.pyx | 4 +- nautilus_trader/examples/algorithms/blank.py | 3 +- nautilus_trader/examples/algorithms/twap.py | 5 +- .../examples/strategies/ema_cross_bracket.py | 3 +- .../strategies/ema_cross_bracket_algo.py | 16 +- .../strategies/ema_cross_stop_entry.py | 3 +- .../strategies/ema_cross_trailing_stop.py | 5 +- .../examples/strategies/market_maker.py | 9 +- .../strategies/orderbook_imbalance.py | 9 +- .../examples/strategies/signal_strategy.py | 3 +- .../examples/strategies/subscribe.py | 5 +- .../strategies/volatility_market_maker.py | 7 +- nautilus_trader/execution/algorithm.pyx | 3 +- nautilus_trader/execution/matching_core.pyx | 3 +- nautilus_trader/execution/messages.pyx | 3 +- nautilus_trader/execution/reports.py | 2 - nautilus_trader/live/__main__.py | 2 - nautilus_trader/live/data_client.py | 5 +- nautilus_trader/live/data_engine.py | 2 - nautilus_trader/live/execution_client.py | 5 +- nautilus_trader/live/execution_engine.py | 2 - nautilus_trader/live/factories.py | 2 - nautilus_trader/live/node.py | 2 - nautilus_trader/live/node_builder.py | 2 - nautilus_trader/live/risk_engine.py | 2 - nautilus_trader/model/events/order.pyx | 5 +- nautilus_trader/msgbus/bus.pyx | 3 +- nautilus_trader/msgbus/subscription.pyx | 3 +- .../persistence/catalog/parquet.py | 5 +- nautilus_trader/persistence/funcs.py | 4 +- nautilus_trader/persistence/loaders.py | 2 - nautilus_trader/persistence/wranglers_v2.py | 2 - nautilus_trader/persistence/writer.py | 4 +- .../arrow/implementations/account_state.py | 3 +- .../arrow/implementations/position_events.py | 3 +- nautilus_trader/serialization/arrow/schema.py | 2 - .../serialization/arrow/serializer.py | 7 +- nautilus_trader/serialization/base.pyx | 3 +- nautilus_trader/system/kernel.py | 4 +- nautilus_trader/test_kit/functions.py | 3 +- nautilus_trader/test_kit/mocks/actors.py | 4 +- .../test_kit/mocks/cache_database.py | 15 +- nautilus_trader/test_kit/mocks/data.py | 3 +- .../test_kit/mocks/exec_clients.py | 27 +- nautilus_trader/test_kit/mocks/strategies.py | 3 +- nautilus_trader/test_kit/performance.py | 2 +- nautilus_trader/test_kit/providers.py | 14 +- nautilus_trader/test_kit/rust/instruments.py | 3 +- nautilus_trader/test_kit/stubs/commands.py | 21 +- nautilus_trader/test_kit/stubs/component.py | 21 +- nautilus_trader/test_kit/stubs/config.py | 15 +- nautilus_trader/test_kit/stubs/data.py | 2 - nautilus_trader/test_kit/stubs/events.py | 39 ++- nautilus_trader/test_kit/stubs/execution.py | 39 ++- nautilus_trader/trading/controller.py | 2 - nautilus_trader/trading/filters.py | 2 - nautilus_trader/trading/trader.py | 5 +- poetry.lock | 260 +++++++++--------- pyproject.toml | 15 +- tests/acceptance_tests/test_backtest.py | 4 +- .../adapters/betfair/test_betfair_data.py | 4 +- .../betfair/test_betfair_execution.py | 3 +- .../adapters/betfair/test_kit.py | 19 +- tests/integration_tests/adapters/conftest.py | 4 +- .../interactive_brokers/test_parsing.py | 3 +- tests/integration_tests/network/test_http.py | 3 +- tests/unit_tests/backtest/test_engine.py | 5 +- .../unit_tests/persistence/test_streaming.py | 3 +- version.json | 2 +- 168 files changed, 1282 insertions(+), 1365 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ea79e7148c56..b3d9e29cb610 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: matrix: arch: [x64] os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11"] defaults: run: shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c53bc35350a4..bd189b9dbede 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: matrix: arch: [x64] os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11"] defaults: run: shell: bash @@ -245,7 +245,7 @@ jobs: matrix: arch: [x64] os: [ubuntu-20.04, ubuntu-latest, windows-latest] - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11"] defaults: run: shell: bash diff --git a/RELEASES.md b/RELEASES.md index 7b25d83cc3e8..30c218704633 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,17 @@ +# NautilusTrader 1.180.0 Beta + +Released on TBC (UTC). + +### Enhancements +None + +### Breaking Changes +- Dropped support for Python 3.9 + +### Fixes +None + +--- # NautilusTrader 1.179.0 Beta Released on 22nd October 2023 (UTC). diff --git a/build.py b/build.py index 16f852f32b26..2c0a4f25b881 100644 --- a/build.py +++ b/build.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -from __future__ import annotations - import itertools import os import platform diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index e39b6686be72..eebeaa99cddf 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -30,15 +30,16 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "72832d73be48bac96a5d7944568f305d829ed55b0ce3b483647089dfaf6cf704" dependencies = [ "cfg-if", "const-random", "getrandom", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -122,7 +123,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fab9e93ba8ce88a37d5a30dce4b9913b75413dc1ac56cb5d72e5a840543f829" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.4", "arrow-arith", "arrow-array", "arrow-buffer", @@ -160,7 +161,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d02efa7253ede102d45a4e802a129e83bcc3f49884cab795b1ac223918e4318d" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.4", "arrow-buffer", "arrow-data", "arrow-schema", @@ -286,7 +287,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "114a348ab581e7c9b6908fcab23cb39ff9f060eb19e72b13f8fb8eaa37f65d22" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.4", "arrow-array", "arrow-buffer", "arrow-data", @@ -310,7 +311,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5c71e003202e67e9db139e5278c79f5520bb79922261dfe140e4637ee8b6108" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.4", "arrow-array", "arrow-buffer", "arrow-data", @@ -561,9 +562,9 @@ dependencies = [ [[package]] name = "bytecount" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad152d03a2c813c80bb94fedbf3a3f02b28f793e39e7c214c8a0bcc196343de7" +checksum = "d1a12477b7237a01c11a80a51278165f9ba0edd28fa6db00a65ab230320dc58c" [[package]] name = "byteorder" @@ -1006,7 +1007,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7014432223f4d721cb9786cd88bb89e7464e0ba984d4a7f49db7787f5f268674" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.4", "arrow", "arrow-array", "arrow-schema", @@ -1054,7 +1055,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3903ed8f102892f17b48efa437f3542159241d41c564f0d1e78efdc5e663aa" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.4", "arrow", "arrow-array", "arrow-buffer", @@ -1095,7 +1096,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24c382676338d8caba6c027ba0da47260f65ffedab38fda78f6d8043f607557c" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.4", "arrow", "arrow-array", "datafusion-common", @@ -1128,7 +1129,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57b4968e9a998dc0476c4db7a82f280e2026b25f464e4aa0c3bb9807ee63ddfd" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.4", "arrow", "arrow-array", "arrow-buffer", @@ -1162,7 +1163,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd0d1fe54e37a47a2d58a1232c22786f2c28ad35805fdcd08f0253a8b0aaa90" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.4", "arrow", "arrow-array", "arrow-buffer", @@ -1546,7 +1547,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.4", ] [[package]] @@ -1555,7 +1556,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.4", "allocator-api2", ] @@ -2001,7 +2002,7 @@ dependencies = [ [[package]] name = "nautilus-backtest" -version = "0.10.0" +version = "0.11.0" dependencies = [ "cbindgen", "nautilus-common", @@ -2014,7 +2015,7 @@ dependencies = [ [[package]] name = "nautilus-common" -version = "0.10.0" +version = "0.11.0" dependencies = [ "anyhow", "cbindgen", @@ -2032,7 +2033,7 @@ dependencies = [ [[package]] name = "nautilus-core" -version = "0.10.0" +version = "0.11.0" dependencies = [ "anyhow", "cbindgen", @@ -2051,7 +2052,7 @@ dependencies = [ [[package]] name = "nautilus-indicators" -version = "0.10.0" +version = "0.11.0" dependencies = [ "anyhow", "nautilus-core", @@ -2063,7 +2064,7 @@ dependencies = [ [[package]] name = "nautilus-model" -version = "0.10.0" +version = "0.11.0" dependencies = [ "anyhow", "cbindgen", @@ -2092,7 +2093,7 @@ dependencies = [ [[package]] name = "nautilus-network" -version = "0.10.0" +version = "0.11.0" dependencies = [ "anyhow", "criterion", @@ -2115,7 +2116,7 @@ dependencies = [ [[package]] name = "nautilus-persistence" -version = "0.10.0" +version = "0.11.0" dependencies = [ "binary-heap-plus", "chrono", @@ -2136,7 +2137,7 @@ dependencies = [ [[package]] name = "nautilus-pyo3" -version = "0.10.0" +version = "0.11.0" dependencies = [ "nautilus-core", "nautilus-indicators", @@ -2409,7 +2410,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0463cc3b256d5f50408c49a4be3a16674f4c8ceef60941709620a062b1f6bf4d" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.4", "arrow-array", "arrow-buffer", "arrow-cast", @@ -3787,7 +3788,7 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" version = "0.20.1" -source = "git+https://github.com/snapview/tungstenite-rs#8b3ecd3cc0008145ab4bc8d0657c39d09db8c7e2" +source = "git+https://github.com/snapview/tungstenite-rs#3d9fd1e5cb8b78e930eead4604e7ba9debec618e" dependencies = [ "byteorder", "bytes", @@ -3881,7 +3882,7 @@ name = "ustr" version = "0.10.0" source = "git+https://github.com/anderslanglands/ustr#c78ddc25300c4720ffcb5f8ddef6028cef14535f" dependencies = [ - "ahash 0.8.3", + "ahash 0.8.4", "byteorder", "lazy_static", "parking_lot", @@ -4143,6 +4144,26 @@ dependencies = [ "lzma-sys", ] +[[package]] +name = "zerocopy" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c19fae0c8a9efc6a8281f2e623db8af1db9e57852e04cde3e754dd2dc29340f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc56589e9ddd1f1c28d4b4b5c773ce232910a6bb67a70133d61c9e347585efe9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "zstd" version = "0.12.4" diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 49a1d7c3b630..0da7730c61e3 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -14,7 +14,7 @@ members = [ [workspace.package] rust-version = "1.73.0" -version = "0.10.0" +version = "0.11.0" edition = "2021" authors = ["Nautech Systems "] description = "A high-performance algorithmic trading platform and event-driven backtester" diff --git a/nautilus_trader/adapters/_template/data.py b/nautilus_trader/adapters/_template/data.py index ed0973a986dd..fce4829b8095 100644 --- a/nautilus_trader/adapters/_template/data.py +++ b/nautilus_trader/adapters/_template/data.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional import pandas as pd @@ -161,8 +160,8 @@ async def _subscribe_order_book_deltas( self, instrument_id: InstrumentId, book_type: BookType, - depth: Optional[int] = None, - kwargs: Optional[dict] = None, + depth: int | None = None, + kwargs: dict | None = None, ) -> None: raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover @@ -170,8 +169,8 @@ async def _subscribe_order_book_snapshots( self, instrument_id: InstrumentId, book_type: BookType, - depth: Optional[int] = None, - kwargs: Optional[dict] = None, + depth: int | None = None, + kwargs: dict | None = None, ) -> None: raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover @@ -242,8 +241,8 @@ async def _request_quote_ticks( instrument_id: InstrumentId, limit: int, correlation_id: UUID4, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> None: raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover @@ -252,8 +251,8 @@ async def _request_trade_ticks( instrument_id: InstrumentId, limit: int, correlation_id: UUID4, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> None: raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover @@ -262,7 +261,7 @@ async def _request_bars( bar_type: BarType, limit: int, correlation_id: UUID4, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> None: raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover diff --git a/nautilus_trader/adapters/_template/execution.py b/nautilus_trader/adapters/_template/execution.py index 77cba7064c35..0b9861ec40b9 100644 --- a/nautilus_trader/adapters/_template/execution.py +++ b/nautilus_trader/adapters/_template/execution.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional import pandas as pd @@ -85,34 +84,34 @@ def dispose(self) -> None: async def generate_order_status_report( self, instrument_id: InstrumentId, - client_order_id: Optional[ClientOrderId] = None, - venue_order_id: Optional[VenueOrderId] = None, - ) -> Optional[OrderStatusReport]: + client_order_id: ClientOrderId | None = None, + venue_order_id: VenueOrderId | None = None, + ) -> OrderStatusReport | None: raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover async def generate_order_status_reports( self, - instrument_id: Optional[InstrumentId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, open_only: bool = False, ) -> list[OrderStatusReport]: raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover async def generate_trade_reports( self, - instrument_id: Optional[InstrumentId] = None, - venue_order_id: Optional[VenueOrderId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + venue_order_id: VenueOrderId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> list[TradeReport]: raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover async def generate_position_status_reports( self, - instrument_id: Optional[InstrumentId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> list[PositionStatusReport]: raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover diff --git a/nautilus_trader/adapters/_template/providers.py b/nautilus_trader/adapters/_template/providers.py index 88bfd78930e0..d3a881116b02 100644 --- a/nautilus_trader/adapters/_template/providers.py +++ b/nautilus_trader/adapters/_template/providers.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional from nautilus_trader.common.providers import InstrumentProvider from nautilus_trader.model.identifiers import InstrumentId @@ -36,15 +35,15 @@ class TemplateInstrumentProvider(InstrumentProvider): must be implemented for an integration to be complete. """ - async def load_all_async(self, filters: Optional[dict] = None) -> None: + async def load_all_async(self, filters: dict | None = None) -> None: raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover async def load_ids_async( self, instrument_ids: list[InstrumentId], - filters: Optional[dict] = None, + filters: dict | None = None, ) -> None: raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover - async def load_async(self, instrument_id: InstrumentId, filters: Optional[dict] = None): + async def load_async(self, instrument_id: InstrumentId, filters: dict | None = None): raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover diff --git a/nautilus_trader/adapters/betfair/client.py b/nautilus_trader/adapters/betfair/client.py index b1b36b4de632..9579198aaaa0 100644 --- a/nautilus_trader/adapters/betfair/client.py +++ b/nautilus_trader/adapters/betfair/client.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional from betfair_parser.endpoints import ENDPOINTS from betfair_parser.spec.accounts.operations import GetAccountDetails @@ -113,7 +112,7 @@ async def _get(self, request: Request) -> Request.return_type: return request.parse_response(response.body, raise_errors=True) @property - def session_token(self) -> Optional[str]: + def session_token(self) -> str | None: return self._headers.get("X-Authentication") def update_headers(self, login_resp: LoginResponse): @@ -166,10 +165,10 @@ async def list_navigation(self) -> Navigation: async def list_market_catalogue( self, filter_: MarketFilter, - market_projection: Optional[list[MarketProjection]] = None, - sort: Optional[MarketSort] = None, + market_projection: list[MarketProjection] | None = None, + sort: MarketSort | None = None, max_results: int = 1000, - locale: Optional[str] = None, + locale: str | None = None, ) -> list[MarketCatalogue]: """ Return specific data about markets. @@ -189,7 +188,7 @@ async def list_market_catalogue( async def get_account_details(self) -> AccountDetailsResponse: return await self._post(request=GetAccountDetails.with_params()) - async def get_account_funds(self, wallet: Optional[str] = None) -> AccountFundsResponse: + async def get_account_funds(self, wallet: str | None = None) -> AccountFundsResponse: return await self._post(request=GetAccountFunds.with_params(wallet=wallet)) async def place_orders(self, request: PlaceOrders) -> PlaceExecutionReport: @@ -203,17 +202,17 @@ async def cancel_orders(self, request: CancelOrders) -> CancelExecutionReport: async def list_current_orders( self, - bet_ids: Optional[set[BetId]] = None, - market_ids: Optional[set[str]] = None, - order_projection: Optional[OrderProjection] = None, - customer_order_refs: Optional[set[CustomerOrderRef]] = None, - customer_strategy_refs: Optional[set[CustomerStrategyRef]] = None, - date_range: Optional[TimeRange] = None, - order_by: Optional[OrderBy] = None, - sort_dir: Optional[SortDir] = None, - from_record: Optional[int] = None, - record_count: Optional[int] = None, - include_item_description: Optional[bool] = None, + bet_ids: set[BetId] | None = None, + market_ids: set[str] | None = None, + order_projection: OrderProjection | None = None, + customer_order_refs: set[CustomerOrderRef] | None = None, + customer_strategy_refs: set[CustomerStrategyRef] | None = None, + date_range: TimeRange | None = None, + order_by: OrderBy | None = None, + sort_dir: SortDir | None = None, + from_record: int | None = None, + record_count: int | None = None, + include_item_description: bool | None = None, ) -> list[CurrentOrderSummary]: current_orders: list[CurrentOrderSummary] = [] more_available = True @@ -242,20 +241,20 @@ async def list_current_orders( async def list_cleared_orders( self, bet_status: BetStatus, - event_type_ids: Optional[set[EventTypeId]] = None, - event_ids: Optional[set[EventId]] = None, - market_ids: Optional[set[MarketId]] = None, - runner_ids: Optional[set[RunnerId]] = None, - bet_ids: Optional[set[BetId]] = None, - customer_order_refs: Optional[set[CustomerOrderRef]] = None, - customer_strategy_refs: Optional[set[CustomerStrategyRef]] = None, - side: Optional[Side] = None, - settled_date_range: Optional[TimeRange] = None, - group_by: Optional[GroupBy] = None, - include_item_description: Optional[bool] = None, - locale: Optional[str] = None, - from_record: Optional[int] = None, - record_count: Optional[int] = None, + event_type_ids: set[EventTypeId] | None = None, + event_ids: set[EventId] | None = None, + market_ids: set[MarketId] | None = None, + runner_ids: set[RunnerId] | None = None, + bet_ids: set[BetId] | None = None, + customer_order_refs: set[CustomerOrderRef] | None = None, + customer_strategy_refs: set[CustomerStrategyRef] | None = None, + side: Side | None = None, + settled_date_range: TimeRange | None = None, + group_by: GroupBy | None = None, + include_item_description: bool | None = None, + locale: str | None = None, + from_record: int | None = None, + record_count: int | None = None, ) -> list[ClearedOrderSummary]: cleared_orders: list[ClearedOrderSummary] = [] more_available = True diff --git a/nautilus_trader/adapters/betfair/config.py b/nautilus_trader/adapters/betfair/config.py index 46bb29f3cc20..de1631f90e28 100644 --- a/nautilus_trader/adapters/betfair/config.py +++ b/nautilus_trader/adapters/betfair/config.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional from nautilus_trader.adapters.betfair.providers import BetfairInstrumentProviderConfig from nautilus_trader.config import LiveDataClientConfig @@ -38,11 +37,11 @@ class BetfairDataClientConfig(LiveDataClientConfig, kw_only=True, frozen=True): """ account_currency: str - username: Optional[str] = None - password: Optional[str] = None - app_key: Optional[str] = None - cert_dir: Optional[str] = None - instrument_config: Optional[BetfairInstrumentProviderConfig] = None + username: str | None = None + password: str | None = None + app_key: str | None = None + cert_dir: str | None = None + instrument_config: BetfairInstrumentProviderConfig | None = None class BetfairExecClientConfig(LiveExecClientConfig, kw_only=True, frozen=True): @@ -63,8 +62,8 @@ class BetfairExecClientConfig(LiveExecClientConfig, kw_only=True, frozen=True): """ account_currency: str - username: Optional[str] = None - password: Optional[str] = None - app_key: Optional[str] = None - cert_dir: Optional[str] = None - instrument_config: Optional[BetfairInstrumentProviderConfig] = None + username: str | None = None + password: str | None = None + app_key: str | None = None + cert_dir: str | None = None + instrument_config: BetfairInstrumentProviderConfig | None = None diff --git a/nautilus_trader/adapters/betfair/data.py b/nautilus_trader/adapters/betfair/data.py index e63ccc9f6372..cc1202b46fbb 100644 --- a/nautilus_trader/adapters/betfair/data.py +++ b/nautilus_trader/adapters/betfair/data.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- import asyncio -from typing import Optional import msgspec from betfair_parser.spec.streaming import MCM @@ -170,8 +169,8 @@ async def _subscribe_order_book_deltas( self, instrument_id: InstrumentId, book_type: BookType, - depth: Optional[int] = None, - kwargs: Optional[dict] = None, + depth: int | None = None, + kwargs: dict | None = None, ): PyCondition.not_none(instrument_id, "instrument_id") @@ -256,7 +255,7 @@ def _on_market_update(self, mcm: MCM): updates = self.parser.parse(mcm=mcm) for data in updates: self._log.debug(f"{data}") - if isinstance(data, (BetfairStartingPrice, BSPOrderBookDelta)): + if isinstance(data, BetfairStartingPrice | BSPOrderBookDelta): # Not a regular data type generic_data = GenericData( DataType(data.__class__, {"instrument_id": data.instrument_id}), diff --git a/nautilus_trader/adapters/betfair/data_types.py b/nautilus_trader/adapters/betfair/data_types.py index f20825fc7487..9dd99bd49dde 100644 --- a/nautilus_trader/adapters/betfair/data_types.py +++ b/nautilus_trader/adapters/betfair/data_types.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from enum import Enum -from typing import Optional import pyarrow as pa @@ -132,10 +131,10 @@ def __init__( instrument_id: InstrumentId, ts_event: int, ts_init: int, - last_traded_price: Optional[float] = None, - traded_volume: Optional[float] = None, - starting_price_near: Optional[float] = None, - starting_price_far: Optional[float] = None, + last_traded_price: float | None = None, + traded_volume: float | None = None, + starting_price_near: float | None = None, + starting_price_far: float | None = None, ): super().__init__(instrument_id=instrument_id, ts_event=ts_event, ts_init=ts_init) self.last_traded_price = last_traded_price @@ -205,7 +204,7 @@ def __init__( instrument_id: InstrumentId, ts_event: int, ts_init: int, - bsp: Optional[float] = None, + bsp: float | None = None, ): super().__init__() self._ts_event = ts_event diff --git a/nautilus_trader/adapters/betfair/execution.py b/nautilus_trader/adapters/betfair/execution.py index e7c55bda66fc..c3a3e04e693d 100644 --- a/nautilus_trader/adapters/betfair/execution.py +++ b/nautilus_trader/adapters/betfair/execution.py @@ -16,7 +16,6 @@ import asyncio import hashlib from collections import defaultdict -from typing import Optional import msgspec import pandas as pd @@ -208,9 +207,9 @@ async def connection_account_state(self) -> None: async def generate_order_status_report( self, instrument_id: InstrumentId, - client_order_id: Optional[ClientOrderId] = None, - venue_order_id: Optional[VenueOrderId] = None, - ) -> Optional[OrderStatusReport]: + client_order_id: ClientOrderId | None = None, + venue_order_id: VenueOrderId | None = None, + ) -> OrderStatusReport | None: assert venue_order_id is not None, "`venue_order_id` is None" bet_id = BetId(venue_order_id.value) self._log.debug(f"Listing current orders for {venue_order_id=} {bet_id=}") @@ -240,9 +239,9 @@ async def generate_order_status_report( async def generate_order_status_reports( self, - instrument_id: Optional[InstrumentId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, open_only: bool = False, ) -> list[OrderStatusReport]: self._log.warning("Cannot generate `OrderStatusReports`: not yet implemented.") @@ -251,10 +250,10 @@ async def generate_order_status_reports( async def generate_trade_reports( self, - instrument_id: Optional[InstrumentId] = None, - venue_order_id: Optional[VenueOrderId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + venue_order_id: VenueOrderId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> list[TradeReport]: self._log.warning("Cannot generate `TradeReports`: not yet implemented.") @@ -262,9 +261,9 @@ async def generate_trade_reports( async def generate_position_status_reports( self, - instrument_id: Optional[InstrumentId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> list[PositionStatusReport]: self._log.warning("Cannot generate `PositionStatusReports`: not yet implemented.") @@ -796,7 +795,7 @@ async def wait_for_order( self, venue_order_id: VenueOrderId, timeout_seconds=10.0, - ) -> Optional[ClientOrderId]: + ) -> ClientOrderId | None: """ We may get an order update from the socket before our submit_order response has come back (with our bet_id). diff --git a/nautilus_trader/adapters/betfair/factories.py b/nautilus_trader/adapters/betfair/factories.py index 897178c53e3b..00f21690a241 100644 --- a/nautilus_trader/adapters/betfair/factories.py +++ b/nautilus_trader/adapters/betfair/factories.py @@ -16,7 +16,6 @@ import asyncio import os from functools import lru_cache -from typing import Optional from nautilus_trader.adapters.betfair.client import BetfairHttpClient from nautilus_trader.adapters.betfair.config import BetfairDataClientConfig @@ -42,9 +41,9 @@ @lru_cache(1) def get_cached_betfair_client( logger: Logger, - username: Optional[str] = None, - password: Optional[str] = None, - app_key: Optional[str] = None, + username: str | None = None, + password: str | None = None, + app_key: str | None = None, ) -> BetfairHttpClient: """ Cache and return a Betfair HTTP client with the given credentials. diff --git a/nautilus_trader/adapters/betfair/parsing/common.py b/nautilus_trader/adapters/betfair/parsing/common.py index 621fd2ed28d2..c864f49bfc40 100644 --- a/nautilus_trader/adapters/betfair/parsing/common.py +++ b/nautilus_trader/adapters/betfair/parsing/common.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from functools import lru_cache -from typing import Optional from nautilus_trader.adapters.betfair.constants import BETFAIR_VENUE from nautilus_trader.core.correctness import PyCondition @@ -30,7 +29,7 @@ def hash_market_trade(timestamp: int, price: float, volume: float): def betfair_instrument_id( market_id: str, selection_id: str, - selection_handicap: Optional[str], + selection_handicap: str | None, ) -> InstrumentId: """ Create an instrument ID from betfair fields. diff --git a/nautilus_trader/adapters/betfair/parsing/core.py b/nautilus_trader/adapters/betfair/parsing/core.py index cded310e30f6..f13042b5cc36 100644 --- a/nautilus_trader/adapters/betfair/parsing/core.py +++ b/nautilus_trader/adapters/betfair/parsing/core.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - from collections.abc import Generator from os import PathLike from typing import BinaryIO @@ -48,7 +46,7 @@ def __init__(self, currency: str) -> None: self.traded_volumes: dict[InstrumentId, dict[float, float]] = {} def parse(self, mcm: MCM, ts_init: int | None = None) -> list[PARSE_TYPES]: - if isinstance(mcm, (Status, Connection, OCM)): + if isinstance(mcm, Status | Connection | OCM): return [] if mcm.is_heartbeat: return [] diff --git a/nautilus_trader/adapters/betfair/parsing/requests.py b/nautilus_trader/adapters/betfair/parsing/requests.py index f3864f93da7c..8d7a64e0e0d6 100644 --- a/nautilus_trader/adapters/betfair/parsing/requests.py +++ b/nautilus_trader/adapters/betfair/parsing/requests.py @@ -15,7 +15,6 @@ from datetime import datetime from functools import lru_cache -from typing import Optional import pandas as pd from betfair_parser.spec.accounts.type_definitions import AccountDetailsResponse @@ -307,7 +306,7 @@ async def generate_trades_list( self, venue_order_id: VenueOrderId, symbol: Symbol, - since: Optional[datetime] = None, + since: datetime | None = None, ) -> list[TradeReport]: filled = self.client().betting.list_cleared_orders( bet_ids=[venue_order_id], @@ -338,13 +337,13 @@ async def generate_trades_list( @lru_cache(None) -def parse_handicap(x) -> Optional[str]: +def parse_handicap(x) -> str | None: """ Ensure consistent parsing of the various handicap sources we get. """ if x in (None, ""): return "0.0" - if isinstance(x, (int, str)): + if isinstance(x, int | str): return str(float(x)) elif isinstance(x, float): return str(x) diff --git a/nautilus_trader/adapters/betfair/parsing/streaming.py b/nautilus_trader/adapters/betfair/parsing/streaming.py index e307806d9088..65dc213dcdc0 100644 --- a/nautilus_trader/adapters/betfair/parsing/streaming.py +++ b/nautilus_trader/adapters/betfair/parsing/streaming.py @@ -16,7 +16,6 @@ import math from collections import defaultdict from datetime import datetime -from typing import Optional, Union import pandas as pd from betfair_parser.spec.betting.type_definitions import ClearedOrderSummary @@ -63,15 +62,15 @@ from nautilus_trader.model.objects import Price -PARSE_TYPES = Union[ - InstrumentStatus, - InstrumentClose, - OrderBookDeltas, - TradeTick, - BetfairTicker, - BSPOrderBookDelta, - BetfairStartingPrice, -] +PARSE_TYPES = ( + InstrumentStatus + | InstrumentClose + | OrderBookDeltas + | TradeTick + | BetfairTicker + | BSPOrderBookDelta + | BetfairStartingPrice +) def market_change_to_updates( # noqa: C901 @@ -79,8 +78,8 @@ def market_change_to_updates( # noqa: C901 traded_volumes: dict[InstrumentId, dict[float, float]], ts_event: int, ts_init: int, -) -> list[PARSE_TYPES]: - updates: list[PARSE_TYPES] = [] +) -> list[PARSE_TYPES]: # type: ignore + updates: list[PARSE_TYPES] = [] # type: ignore # Handle instrument status and close updates first if mc.market_definition is not None: @@ -233,7 +232,7 @@ def runner_to_instrument_close( market_id: str, ts_event: int, ts_init: int, -) -> Optional[InstrumentClose]: +) -> InstrumentClose | None: instrument_id: InstrumentId = betfair_instrument_id( market_id=market_id, selection_id=str(runner.id), @@ -281,7 +280,7 @@ def runner_to_betfair_starting_price( market_id: str, ts_event: int, ts_init: int, -) -> Optional[BetfairStartingPrice]: +) -> BetfairStartingPrice | None: if runner.bsp is not None: instrument_id = betfair_instrument_id( market_id=market_id, @@ -406,7 +405,7 @@ def runner_change_to_order_book_deltas( instrument_id: InstrumentId, ts_event: int, ts_init: int, -) -> Optional[OrderBookDeltas]: +) -> OrderBookDeltas | None: """ Convert a RunnerChange to a list of OrderBookDeltas. """ @@ -488,7 +487,7 @@ def runner_change_to_bsp_order_book_deltas( instrument_id: InstrumentId, ts_event: int, ts_init: int, -) -> Optional[list[BSPOrderBookDelta]]: +) -> list[BSPOrderBookDelta] | None: if not (rc.spb or rc.spl): return None bsp_instrument_id = make_bsp_instrument_id(instrument_id) @@ -540,7 +539,7 @@ async def generate_trades_list( self, venue_order_id: VenueOrderId, symbol: Symbol, - since: Optional[datetime] = None, + since: datetime | None = None, ) -> list[TradeReport]: filled: list[ClearedOrderSummary] = self.client().betting.list_cleared_orders( bet_ids=[venue_order_id], diff --git a/nautilus_trader/adapters/betfair/providers.py b/nautilus_trader/adapters/betfair/providers.py index cb9dad01e0c8..76343a443486 100644 --- a/nautilus_trader/adapters/betfair/providers.py +++ b/nautilus_trader/adapters/betfair/providers.py @@ -15,7 +15,6 @@ import time from collections.abc import Iterable -from typing import Optional, Union import msgspec.json import pandas as pd @@ -42,12 +41,12 @@ class BetfairInstrumentProviderConfig(InstrumentProviderConfig, frozen=True): - event_type_ids: Optional[list[str]] = None - event_ids: Optional[list[str]] = None - market_ids: Optional[list[str]] = None - country_codes: Optional[list[str]] = None - market_types: Optional[list[str]] = None - event_type_names: Optional[list[str]] = None + event_type_ids: list[str] | None = None + event_ids: list[str] | None = None + market_ids: list[str] | None = None + country_codes: list[str] | None = None + market_types: list[str] | None = None + event_type_names: list[str] | None = None class BetfairInstrumentProvider(InstrumentProvider): @@ -67,7 +66,7 @@ class BetfairInstrumentProvider(InstrumentProvider): def __init__( self, - client: Optional[BetfairHttpClient], + client: BetfairHttpClient | None, logger: Logger, config: BetfairInstrumentProviderConfig, ): @@ -84,18 +83,18 @@ def __init__( async def load_ids_async( self, instrument_ids: list[InstrumentId], - filters: Optional[dict] = None, + filters: dict | None = None, ) -> None: raise NotImplementedError async def load_async( self, instrument_id: InstrumentId, - filters: Optional[dict] = None, + filters: dict | None = None, ): raise NotImplementedError - async def load_all_async(self, filters: Optional[dict] = None): + async def load_all_async(self, filters: dict | None = None): currency = await self.get_account_currency() filters = filters or {} @@ -207,7 +206,7 @@ def market_definition_to_instruments( def make_instruments( - market: Union[MarketCatalogue, MarketDefinition], + market: MarketCatalogue | MarketDefinition, currency: str, ) -> list[BettingInstrument]: if isinstance(market, MarketCatalogue): @@ -241,12 +240,12 @@ def check_market_filter_keys(keys: Iterable[str]) -> None: async def load_markets( client: BetfairHttpClient, - event_type_ids: Optional[list[str]] = None, - event_ids: Optional[list[str]] = None, - market_ids: Optional[list[str]] = None, - event_country_codes: Optional[list[str]] = None, - market_market_types: Optional[list[str]] = None, - event_type_names: Optional[list[str]] = None, + event_type_ids: list[str] | None = None, + event_ids: list[str] | None = None, + market_ids: list[str] | None = None, + event_country_codes: list[str] | None = None, + market_market_types: list[str] | None = None, + event_type_names: list[str] | None = None, ) -> list[FlattenedMarket]: market_filter = { "event_type_id": event_type_ids, diff --git a/nautilus_trader/adapters/betfair/sockets.py b/nautilus_trader/adapters/betfair/sockets.py index 8c6377d620c0..f348f4404202 100644 --- a/nautilus_trader/adapters/betfair/sockets.py +++ b/nautilus_trader/adapters/betfair/sockets.py @@ -15,7 +15,7 @@ import asyncio import itertools -from typing import Callable, Optional +from collections.abc import Callable import msgspec @@ -45,10 +45,10 @@ def __init__( http_client: BetfairHttpClient, logger_adapter: LoggerAdapter, message_handler: Callable[[bytes], None], - host: Optional[str] = HOST, - port: Optional[int] = None, - crlf: Optional[bytes] = None, - encoding: Optional[str] = None, + host: str | None = HOST, + port: int | None = None, + crlf: bytes | None = None, + encoding: str | None = None, ) -> None: self._http_client = http_client self._log = logger_adapter @@ -58,7 +58,7 @@ def __init__( self.crlf = crlf or CRLF self.use_ssl = USE_SSL self.encoding = encoding or ENCODING - self._client: Optional[SocketClient] = None + self._client: SocketClient | None = None self.unique_id = next(UNIQUE_ID) self.is_connected: bool = False self.disconnecting: bool = False @@ -148,8 +148,8 @@ def __init__( logger: Logger, message_handler, partition_matched_by_strategy_ref: bool = True, - include_overall_position: Optional[str] = None, - customer_strategy_refs: Optional[str] = None, + include_overall_position: str | None = None, + customer_strategy_refs: str | None = None, **kwargs, ): super().__init__( @@ -202,19 +202,19 @@ def __init__( # TODO - Add support for initial_clk/clk reconnection async def send_subscription_message( self, - market_ids: Optional[list] = None, - betting_types: Optional[list] = None, - event_type_ids: Optional[list] = None, - event_ids: Optional[list] = None, - turn_in_play_enabled: Optional[bool] = None, - market_types: Optional[list] = None, - venues: Optional[list] = None, - country_codes: Optional[list] = None, - race_types: Optional[list] = None, - initial_clk: Optional[str] = None, - clk: Optional[str] = None, - conflate_ms: Optional[int] = None, - heartbeat_ms: Optional[int] = None, + market_ids: list | None = None, + betting_types: list | None = None, + event_type_ids: list | None = None, + event_ids: list | None = None, + turn_in_play_enabled: bool | None = None, + market_types: list | None = None, + venues: list | None = None, + country_codes: list | None = None, + race_types: list | None = None, + initial_clk: str | None = None, + clk: str | None = None, + conflate_ms: int | None = None, + heartbeat_ms: int | None = None, segmentation_enabled: bool = True, subscribe_book_updates=True, subscribe_trade_updates=True, diff --git a/nautilus_trader/adapters/binance/common/data.py b/nautilus_trader/adapters/binance/common/data.py index b2d49b2542dd..6ad1ccef4c2f 100644 --- a/nautilus_trader/adapters/binance/common/data.py +++ b/nautilus_trader/adapters/binance/common/data.py @@ -16,7 +16,6 @@ import asyncio import decimal from decimal import Decimal -from typing import Optional, Union import msgspec import pandas as pd @@ -145,10 +144,10 @@ def __init__( self._log.info(f"{config.use_agg_trade_ticks=}", LogColor.BLUE) self._update_instrument_interval: int = 60 * 60 # Once per hour (hardcode) - self._update_instruments_task: Optional[asyncio.Task] = None + self._update_instruments_task: asyncio.Task | None = None self._connect_websockets_delay: float = 0.0 # Delay for bulk subscriptions to come in - self._connect_websockets_task: Optional[asyncio.Task] = None + self._connect_websockets_task: asyncio.Task | None = None # HTTP API self._http_client = client @@ -170,7 +169,7 @@ def __init__( self._instrument_ids: dict[str, InstrumentId] = {} self._book_buffer: dict[ InstrumentId, - list[Union[OrderBookDelta, OrderBookDeltas]], + list[OrderBookDelta | OrderBookDeltas], ] = {} self._log.info(f"Base URL HTTP {self._http_client.base_url}.", LogColor.BLUE) @@ -279,8 +278,8 @@ async def _subscribe_order_book_deltas( self, instrument_id: InstrumentId, book_type: BookType, - depth: Optional[int] = None, - kwargs: Optional[dict] = None, + depth: int | None = None, + kwargs: dict | None = None, ) -> None: update_speed = None if kwargs is not None: @@ -296,8 +295,8 @@ async def _subscribe_order_book_snapshots( self, instrument_id: InstrumentId, book_type: BookType, - depth: Optional[int] = None, - kwargs: Optional[dict] = None, + depth: int | None = None, + kwargs: dict | None = None, ) -> None: update_speed = None if kwargs is not None: @@ -313,8 +312,8 @@ async def _subscribe_order_book( # noqa (too complex) self, instrument_id: InstrumentId, book_type: BookType, - update_speed: Optional[int] = None, - depth: Optional[int] = None, + update_speed: int | None = None, + depth: int | None = None, ) -> None: if book_type == BookType.L3_MBO: self._log.error( @@ -346,7 +345,7 @@ async def _subscribe_order_book( # noqa (too complex) # Add delta stream buffer self._book_buffer[instrument_id] = [] - snapshot: Optional[OrderBookDeltas] = None + snapshot: OrderBookDeltas | None = None if 0 < depth <= 20: if depth not in (5, 10, 20): self._log.error( @@ -473,7 +472,7 @@ async def _unsubscribe_bars(self, bar_type: BarType) -> None: # -- REQUESTS --------------------------------------------------------------------------------- async def _request_instrument(self, instrument_id: InstrumentId, correlation_id: UUID4) -> None: - instrument: Optional[Instrument] = self._instrument_provider.find(instrument_id) + instrument: Instrument | None = self._instrument_provider.find(instrument_id) if instrument is None: self._log.error(f"Cannot find instrument for {instrument_id}.") return @@ -494,8 +493,8 @@ async def _request_quote_ticks( instrument_id: InstrumentId, limit: int, correlation_id: UUID4, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> None: self._log.error( "Cannot request historical quote ticks: not published by Binance.", @@ -506,8 +505,8 @@ async def _request_trade_ticks( instrument_id: InstrumentId, limit: int, correlation_id: UUID4, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> None: if limit == 0 or limit > 1000: limit = 1000 @@ -547,8 +546,8 @@ async def _request_bars( # (too complex) bar_type: BarType, limit: int, correlation_id: UUID4, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> None: if bar_type.spec.price_type != PriceType.LAST: self._log.error( @@ -623,9 +622,9 @@ async def _request_bars( # (too complex) async def _aggregate_internal_from_minute_bars( self, bar_type: BarType, - start_time_ms: Optional[int], - end_time_ms: Optional[int], - limit: Optional[int], + start_time_ms: int | None, + end_time_ms: int | None, + limit: int | None, ) -> list[Bar]: instrument = self._instrument_provider.find(bar_type.instrument_id) if instrument is None: @@ -768,9 +767,9 @@ def _aggregate_bar_to_trade_ticks( async def _aggregate_internal_from_agg_trade_ticks( self, bar_type: BarType, - start_time_ms: Optional[int], - end_time_ms: Optional[int], - limit: Optional[int], + start_time_ms: int | None, + end_time_ms: int | None, + limit: int | None, ) -> list[Bar]: instrument = self._instrument_provider.find(bar_type.instrument_id) if instrument is None: @@ -844,7 +843,7 @@ def _get_cached_instrument_id(self, symbol: str) -> InstrumentId: nautilus_symbol: str = binance_symbol.parse_as_nautilus( self._binance_account_type, ) - instrument_id: Optional[InstrumentId] = self._instrument_ids.get(nautilus_symbol) + instrument_id: InstrumentId | None = self._instrument_ids.get(nautilus_symbol) if not instrument_id: instrument_id = InstrumentId(Symbol(nautilus_symbol), BINANCE_VENUE) self._instrument_ids[nautilus_symbol] = instrument_id @@ -879,7 +878,7 @@ def _handle_book_diff_update(self, raw: bytes) -> None: instrument_id=instrument_id, ts_init=self._clock.timestamp_ns(), ) - book_buffer: Optional[list[Union[OrderBookDelta, OrderBookDeltas]]] = self._book_buffer.get( + book_buffer: list[OrderBookDelta | OrderBookDeltas] | None = self._book_buffer.get( instrument_id, ) if book_buffer is not None: diff --git a/nautilus_trader/adapters/binance/common/execution.py b/nautilus_trader/adapters/binance/common/execution.py index af773495eddc..85a85bb0a950 100644 --- a/nautilus_trader/adapters/binance/common/execution.py +++ b/nautilus_trader/adapters/binance/common/execution.py @@ -15,7 +15,6 @@ import asyncio from decimal import Decimal -from typing import Optional import pandas as pd @@ -178,8 +177,8 @@ def __init__( # Listen keys self._ping_listen_keys_interval: int = 60 * 5 # Once every 5 mins (hardcode) - self._ping_listen_keys_task: Optional[asyncio.Task] = None - self._listen_key: Optional[str] = None + self._ping_listen_keys_task: asyncio.Task | None = None + self._listen_key: str | None = None # WebSocket API self._ws_client = BinanceWebSocketClient( @@ -312,9 +311,9 @@ async def _disconnect(self) -> None: async def generate_order_status_report( self, instrument_id: InstrumentId, - client_order_id: Optional[ClientOrderId] = None, - venue_order_id: Optional[VenueOrderId] = None, - ) -> Optional[OrderStatusReport]: + client_order_id: ClientOrderId | None = None, + venue_order_id: VenueOrderId | None = None, + ) -> OrderStatusReport | None: PyCondition.false( client_order_id is None and venue_order_id is None, "both `client_order_id` and `venue_order_id` were `None`", @@ -357,7 +356,7 @@ async def generate_order_status_report( if not client_order_id: self._log.warning("Cannot retry without a client order ID.") else: - order: Optional[Order] = self._cache.order(client_order_id) + order: Order | None = self._cache.order(client_order_id) if order is None: self._log.warning("Order not found in cache.") return None @@ -410,23 +409,23 @@ def _get_cache_active_symbols(self) -> set[str]: async def _get_binance_position_status_reports( self, - symbol: Optional[str] = None, + symbol: str | None = None, ) -> list[PositionStatusReport]: # Implement in child class raise NotImplementedError async def _get_binance_active_position_symbols( self, - symbol: Optional[str] = None, + symbol: str | None = None, ) -> set[str]: # Implement in child class raise NotImplementedError async def generate_order_status_reports( self, - instrument_id: Optional[InstrumentId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, open_only: bool = False, ) -> list[OrderStatusReport]: self._log.info("Requesting OrderStatusReports...") @@ -475,10 +474,10 @@ async def generate_order_status_reports( async def generate_trade_reports( self, - instrument_id: Optional[InstrumentId] = None, - venue_order_id: Optional[VenueOrderId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + venue_order_id: VenueOrderId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> list[TradeReport]: self._log.info("Requesting TradeReports...") @@ -526,9 +525,9 @@ async def generate_trade_reports( async def generate_position_status_reports( self, - instrument_id: Optional[InstrumentId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> list[PositionStatusReport]: self._log.info("Requesting PositionStatusReports...") @@ -570,7 +569,7 @@ def _determine_time_in_force(self, order: Order) -> BinanceTimeInForce: ) return time_in_force - def _determine_good_till_date(self, order: Order) -> Optional[int]: + def _determine_good_till_date(self, order: Order) -> int | None: good_till_date = nanos_to_millis(order.expire_time_ns) if order.expire_time_ns else None if self._binance_account_type.is_spot_or_margin: good_till_date = None @@ -580,7 +579,7 @@ def _determine_good_till_date(self, order: Order) -> Optional[int]: def _determine_reduce_only(self, order: Order) -> bool: return order.is_reduce_only if self._use_reduce_only else False - def _determine_reduce_only_str(self, order: Order) -> Optional[str]: + def _determine_reduce_only_str(self, order: Order) -> str | None: if self._binance_account_type.is_futures: return str(self._determine_reduce_only(order)) return None @@ -759,7 +758,7 @@ async def _submit_trailing_stop_market_order(self, order: TrailingStopMarketOrde return # Ensure activation price - activation_price: Optional[Price] = order.trigger_price + activation_price: Price | None = order.trigger_price if not activation_price: quote = self._cache.quote_tick(order.instrument_id) trade = self._cache.trade_tick(order.instrument_id) @@ -796,7 +795,7 @@ def _get_cached_instrument_id(self, symbol: str) -> InstrumentId: nautilus_symbol: str = BinanceSymbol(symbol).parse_as_nautilus( self._binance_account_type, ) - instrument_id: Optional[InstrumentId] = self._instrument_ids.get(nautilus_symbol) + instrument_id: InstrumentId | None = self._instrument_ids.get(nautilus_symbol) if not instrument_id: instrument_id = InstrumentId(Symbol(nautilus_symbol), BINANCE_VENUE) self._instrument_ids[nautilus_symbol] = instrument_id @@ -809,7 +808,7 @@ async def _modify_order(self, command: ModifyOrder) -> None: ) return - order: Optional[Order] = self._cache.order(command.client_order_id) + order: Order | None = self._cache.order(command.client_order_id) if order is None: self._log.error(f"{command.client_order_id!r} not found to modify.") return @@ -911,9 +910,9 @@ async def _cancel_order_single( self, instrument_id: InstrumentId, client_order_id: ClientOrderId, - venue_order_id: Optional[VenueOrderId], + venue_order_id: VenueOrderId | None, ) -> None: - order: Optional[Order] = self._cache.order(client_order_id) + order: Order | None = self._cache.order(client_order_id) if order is None: self._log.error(f"{client_order_id!r} not found to cancel.") return diff --git a/nautilus_trader/adapters/binance/common/schemas/account.py b/nautilus_trader/adapters/binance/common/schemas/account.py index 3b8ba66c9ff1..17a730931f31 100644 --- a/nautilus_trader/adapters/binance/common/schemas/account.py +++ b/nautilus_trader/adapters/binance/common/schemas/account.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Optional import msgspec @@ -64,27 +63,27 @@ class BinanceUserTrade(msgspec.Struct, frozen=True): qty: str # Parameters not present in 'fills' list (see FULL response of BinanceOrder) - symbol: Optional[str] = None - id: Optional[int] = None - orderId: Optional[int] = None - time: Optional[int] = None - quoteQty: Optional[str] = None # SPOT/MARGIN & USD-M FUTURES only + symbol: str | None = None + id: int | None = None + orderId: int | None = None + time: int | None = None + quoteQty: str | None = None # SPOT/MARGIN & USD-M FUTURES only # Parameters in SPOT/MARGIN only: - orderListId: Optional[int] = None # unless OCO, the value will always be -1 - isBuyer: Optional[bool] = None - isMaker: Optional[bool] = None - isBestMatch: Optional[bool] = None - tradeId: Optional[int] = None # only in BinanceOrder FULL response + orderListId: int | None = None # unless OCO, the value will always be -1 + isBuyer: bool | None = None + isMaker: bool | None = None + isBestMatch: bool | None = None + tradeId: int | None = None # only in BinanceOrder FULL response # Parameters in FUTURES only: - buyer: Optional[bool] = None - maker: Optional[bool] = None - realizedPnl: Optional[str] = None - side: Optional[BinanceOrderSide] = None - positionSide: Optional[str] = None - baseQty: Optional[str] = None # COIN-M FUTURES only - pair: Optional[str] = None # COIN-M FUTURES only + buyer: bool | None = None + maker: bool | None = None + realizedPnl: str | None = None + side: BinanceOrderSide | None = None + positionSide: str | None = None + baseQty: str | None = None # COIN-M FUTURES only + pair: str | None = None # COIN-M FUTURES only def parse_to_trade_report( self, @@ -94,7 +93,7 @@ def parse_to_trade_report( ts_init: int, use_position_ids: bool = True, ) -> TradeReport: - venue_position_id: Optional[PositionId] = None + venue_position_id: PositionId | None = None if self.positionSide is not None and use_position_ids: venue_position_id = PositionId(f"{instrument_id}-{self.positionSide}") @@ -130,42 +129,42 @@ class BinanceOrder(msgspec.Struct, frozen=True): clientOrderId: str # Parameters not in ACK response: - price: Optional[str] = None - origQty: Optional[str] = None - executedQty: Optional[str] = None - status: Optional[BinanceOrderStatus] = None - timeInForce: Optional[BinanceTimeInForce] = None - goodTillDate: Optional[int] = None - type: Optional[BinanceOrderType] = None - side: Optional[BinanceOrderSide] = None - stopPrice: Optional[str] = None # please ignore when order type is TRAILING_STOP_MARKET - time: Optional[int] = None - updateTime: Optional[int] = None + price: str | None = None + origQty: str | None = None + executedQty: str | None = None + status: BinanceOrderStatus | None = None + timeInForce: BinanceTimeInForce | None = None + goodTillDate: int | None = None + type: BinanceOrderType | None = None + side: BinanceOrderSide | None = None + stopPrice: str | None = None # please ignore when order type is TRAILING_STOP_MARKET + time: int | None = None + updateTime: int | None = None # Parameters in SPOT/MARGIN only: - orderListId: Optional[int] = None # Unless OCO, the value will always be -1 - cumulativeQuoteQty: Optional[str] = None # cumulative quote qty - icebergQty: Optional[str] = None - isWorking: Optional[bool] = None - workingTime: Optional[int] = None - origQuoteOrderQty: Optional[str] = None - selfTradePreventionMode: Optional[str] = None - transactTime: Optional[int] = None # POST & DELETE methods only - fills: Optional[list[BinanceUserTrade]] = None # FULL response only + orderListId: int | None = None # Unless OCO, the value will always be -1 + cumulativeQuoteQty: str | None = None # cumulative quote qty + icebergQty: str | None = None + isWorking: bool | None = None + workingTime: int | None = None + origQuoteOrderQty: str | None = None + selfTradePreventionMode: str | None = None + transactTime: int | None = None # POST & DELETE methods only + fills: list[BinanceUserTrade] | None = None # FULL response only # Parameters in FUTURES only: - avgPrice: Optional[str] = None - origType: Optional[BinanceOrderType] = None - reduceOnly: Optional[bool] = None - positionSide: Optional[str] = None - closePosition: Optional[bool] = None - activatePrice: Optional[str] = None # activation price, only for TRAILING_STOP_MARKET order - priceRate: Optional[str] = None # callback rate, only for TRAILING_STOP_MARKET order - workingType: Optional[str] = None - priceProtect: Optional[bool] = None # if conditional order trigger is protected - cumQuote: Optional[str] = None # USD-M FUTURES only - cumBase: Optional[str] = None # COIN-M FUTURES only - pair: Optional[str] = None # COIN-M FUTURES only + avgPrice: str | None = None + origType: BinanceOrderType | None = None + reduceOnly: bool | None = None + positionSide: str | None = None + closePosition: bool | None = None + activatePrice: str | None = None # activation price, only for TRAILING_STOP_MARKET order + priceRate: str | None = None # callback rate, only for TRAILING_STOP_MARKET order + workingType: str | None = None + priceProtect: bool | None = None # if conditional order trigger is protected + cumQuote: str | None = None # USD-M FUTURES only + cumBase: str | None = None # COIN-M FUTURES only + pair: str | None = None # COIN-M FUTURES only def parse_to_order_status_report( self, diff --git a/nautilus_trader/adapters/binance/common/schemas/market.py b/nautilus_trader/adapters/binance/common/schemas/market.py index 00a269a858d8..39ff11c65057 100644 --- a/nautilus_trader/adapters/binance/common/schemas/market.py +++ b/nautilus_trader/adapters/binance/common/schemas/market.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Optional import msgspec @@ -62,8 +61,8 @@ class BinanceExchangeFilter(msgspec.Struct): """ filterType: BinanceExchangeFilterType - maxNumOrders: Optional[int] = None - maxNumAlgoOrders: Optional[int] = None + maxNumOrders: int | None = None + maxNumAlgoOrders: int | None = None class BinanceRateLimit(msgspec.Struct): @@ -75,7 +74,7 @@ class BinanceRateLimit(msgspec.Struct): interval: BinanceRateLimitInterval intervalNum: int limit: int - count: Optional[int] = None # SPOT/MARGIN rateLimit/order response only + count: int | None = None # SPOT/MARGIN rateLimit/order response only class BinanceSymbolFilter(msgspec.Struct): @@ -84,36 +83,36 @@ class BinanceSymbolFilter(msgspec.Struct): """ filterType: BinanceSymbolFilterType - minPrice: Optional[str] = None - maxPrice: Optional[str] = None - tickSize: Optional[str] = None - multiplierUp: Optional[str] = None - multiplierDown: Optional[str] = None - multiplierDecimal: Optional[str] = None - avgPriceMins: Optional[int] = None - minQty: Optional[str] = None - maxQty: Optional[str] = None - stepSize: Optional[str] = None - limit: Optional[int] = None - maxNumOrders: Optional[int] = None - - notional: Optional[str] = None # SPOT/MARGIN & USD-M FUTURES only - minNotional: Optional[str] = None # SPOT/MARGIN & USD-M FUTURES only - maxNumAlgoOrders: Optional[int] = None # SPOT/MARGIN & USD-M FUTURES only - - bidMultiplierUp: Optional[str] = None # SPOT/MARGIN only - bidMultiplierDown: Optional[str] = None # SPOT/MARGIN only - askMultiplierUp: Optional[str] = None # SPOT/MARGIN only - askMultiplierDown: Optional[str] = None # SPOT/MARGIN only - applyMinToMarket: Optional[bool] = None # SPOT/MARGIN only - maxNotional: Optional[str] = None # SPOT/MARGIN only - applyMaxToMarket: Optional[bool] = None # SPOT/MARGIN only - maxNumIcebergOrders: Optional[int] = None # SPOT/MARGIN only - maxPosition: Optional[str] = None # SPOT/MARGIN only - minTrailingAboveDelta: Optional[int] = None # SPOT/MARGIN only - maxTrailingAboveDelta: Optional[int] = None # SPOT/MARGIN only - minTrailingBelowDelta: Optional[int] = None # SPOT/MARGIN only - maxTrailingBelowDelta: Optional[int] = None # SPOT/MARGIN only + minPrice: str | None = None + maxPrice: str | None = None + tickSize: str | None = None + multiplierUp: str | None = None + multiplierDown: str | None = None + multiplierDecimal: str | None = None + avgPriceMins: int | None = None + minQty: str | None = None + maxQty: str | None = None + stepSize: str | None = None + limit: int | None = None + maxNumOrders: int | None = None + + notional: str | None = None # SPOT/MARGIN & USD-M FUTURES only + minNotional: str | None = None # SPOT/MARGIN & USD-M FUTURES only + maxNumAlgoOrders: int | None = None # SPOT/MARGIN & USD-M FUTURES only + + bidMultiplierUp: str | None = None # SPOT/MARGIN only + bidMultiplierDown: str | None = None # SPOT/MARGIN only + askMultiplierUp: str | None = None # SPOT/MARGIN only + askMultiplierDown: str | None = None # SPOT/MARGIN only + applyMinToMarket: bool | None = None # SPOT/MARGIN only + maxNotional: str | None = None # SPOT/MARGIN only + applyMaxToMarket: bool | None = None # SPOT/MARGIN only + maxNumIcebergOrders: int | None = None # SPOT/MARGIN only + maxPosition: str | None = None # SPOT/MARGIN only + minTrailingAboveDelta: int | None = None # SPOT/MARGIN only + maxTrailingAboveDelta: int | None = None # SPOT/MARGIN only + minTrailingBelowDelta: int | None = None # SPOT/MARGIN only + maxTrailingBelowDelta: int | None = None # SPOT/MARGIN only class BinanceDepth(msgspec.Struct, frozen=True): @@ -128,11 +127,11 @@ class BinanceDepth(msgspec.Struct, frozen=True): bids: list[tuple[str, str]] asks: list[tuple[str, str]] - symbol: Optional[str] = None # COIN-M FUTURES only - pair: Optional[str] = None # COIN-M FUTURES only + symbol: str | None = None # COIN-M FUTURES only + pair: str | None = None # COIN-M FUTURES only - E: Optional[int] = None # FUTURES only, Message output time - T: Optional[int] = None # FUTURES only, Transaction time + E: int | None = None # FUTURES only, Message output time + T: int | None = None # FUTURES only, Transaction time def parse_to_order_book_snapshot( self, @@ -174,7 +173,7 @@ class BinanceTrade(msgspec.Struct, frozen=True): quoteQty: str time: int isBuyerMaker: bool - isBestMatch: Optional[bool] = None # SPOT/MARGIN only + isBestMatch: bool | None = None # SPOT/MARGIN only def parse_to_trade_tick( self, @@ -207,7 +206,7 @@ class BinanceAggTrade(msgspec.Struct, frozen=True): l: int # Last tradeId T: int # Timestamp m: bool # Was the buyer the maker? - M: Optional[bool] = None # SPOT/MARGIN only, was the trade the best price match? + M: bool | None = None # SPOT/MARGIN only, was the trade the best price match? def parse_to_trade_tick( self, @@ -275,33 +274,33 @@ class BinanceTicker24hr(msgspec.Struct, frozen=True): Schema of single Binance 24hr ticker (FULL/MINI). """ - symbol: Optional[str] - lastPrice: Optional[str] - openPrice: Optional[str] - highPrice: Optional[str] - lowPrice: Optional[str] - volume: Optional[str] - openTime: Optional[int] - closeTime: Optional[int] - firstId: Optional[int] - lastId: Optional[int] - count: Optional[int] + symbol: str | None + lastPrice: str | None + openPrice: str | None + highPrice: str | None + lowPrice: str | None + volume: str | None + openTime: int | None + closeTime: int | None + firstId: int | None + lastId: int | None + count: int | None - priceChange: Optional[str] = None # FULL response only (SPOT/MARGIN) - priceChangePercent: Optional[str] = None # FULL response only (SPOT/MARGIN) - weightedAvgPrice: Optional[str] = None # FULL response only (SPOT/MARGIN) - lastQty: Optional[str] = None # FULL response only (SPOT/MARGIN) + priceChange: str | None = None # FULL response only (SPOT/MARGIN) + priceChangePercent: str | None = None # FULL response only (SPOT/MARGIN) + weightedAvgPrice: str | None = None # FULL response only (SPOT/MARGIN) + lastQty: str | None = None # FULL response only (SPOT/MARGIN) - prevClosePrice: Optional[str] = None # SPOT/MARGIN only - bidPrice: Optional[str] = None # SPOT/MARGIN only - bidQty: Optional[str] = None # SPOT/MARGIN only - askPrice: Optional[str] = None # SPOT/MARGIN only - askQty: Optional[str] = None # SPOT/MARGIN only + prevClosePrice: str | None = None # SPOT/MARGIN only + bidPrice: str | None = None # SPOT/MARGIN only + bidQty: str | None = None # SPOT/MARGIN only + askPrice: str | None = None # SPOT/MARGIN only + askQty: str | None = None # SPOT/MARGIN only - pair: Optional[str] = None # COIN-M FUTURES only - baseVolume: Optional[str] = None # COIN-M FUTURES only + pair: str | None = None # COIN-M FUTURES only + baseVolume: str | None = None # COIN-M FUTURES only - quoteVolume: Optional[str] = None # SPOT/MARGIN & USD-M FUTURES only + quoteVolume: str | None = None # SPOT/MARGIN & USD-M FUTURES only class BinanceTickerPrice(msgspec.Struct, frozen=True): @@ -309,10 +308,10 @@ class BinanceTickerPrice(msgspec.Struct, frozen=True): Schema of single Binance Price Ticker. """ - symbol: Optional[str] - price: Optional[str] - time: Optional[int] = None # FUTURES only - ps: Optional[str] = None # COIN-M FUTURES only, pair + symbol: str | None + price: str | None + time: int | None = None # FUTURES only + ps: str | None = None # COIN-M FUTURES only, pair class BinanceTickerBook(msgspec.Struct, frozen=True): @@ -320,13 +319,13 @@ class BinanceTickerBook(msgspec.Struct, frozen=True): Schema of a single Binance Order Book Ticker. """ - symbol: Optional[str] - bidPrice: Optional[str] - bidQty: Optional[str] - askPrice: Optional[str] - askQty: Optional[str] - pair: Optional[str] = None # USD-M FUTURES only - time: Optional[int] = None # FUTURES only, transaction time + symbol: str | None + bidPrice: str | None + bidQty: str | None + askPrice: str | None + askQty: str | None + pair: str | None = None # USD-M FUTURES only + time: int | None = None # FUTURES only, transaction time ################################################################################ @@ -339,8 +338,8 @@ class BinanceDataMsgWrapper(msgspec.Struct): Provides a wrapper for data WebSocket messages from `Binance`. """ - stream: Optional[str] = None - id: Optional[int] = None + stream: str | None = None + id: int | None = None class BinanceOrderBookDelta(msgspec.Struct, array_like=True): @@ -394,9 +393,9 @@ class BinanceOrderBookData(msgspec.Struct, frozen=True): b: list[BinanceOrderBookDelta] # Bids to be updated a: list[BinanceOrderBookDelta] # Asks to be updated - T: Optional[int] = None # FUTURES only, transaction time - pu: Optional[int] = None # FUTURES only, previous final update ID - ps: Optional[str] = None # COIN-M FUTURES only, pair + T: int | None = None # FUTURES only, transaction time + pu: int | None = None # FUTURES only, previous final update ID + ps: str | None = None # COIN-M FUTURES only, pair def parse_to_order_book_deltas( self, @@ -578,13 +577,13 @@ class BinanceTickerData(msgspec.Struct, kw_only=True, frozen=True): p: str # Price change P: str # Price change percent w: str # Weighted average price - x: Optional[str] = None # First trade(F)-1 price (first trade before the 24hr rolling window) + x: str | None = None # First trade(F)-1 price (first trade before the 24hr rolling window) c: str # Last price Q: str # Last quantity - b: Optional[str] = None # Best bid price - B: Optional[str] = None # Best bid quantity - a: Optional[str] = None # Best ask price - A: Optional[str] = None # Best ask quantity + b: str | None = None # Best bid price + B: str | None = None # Best bid quantity + a: str | None = None # Best ask price + A: str | None = None # Best ask quantity o: str # Open price h: str # High price l: str # Low price diff --git a/nautilus_trader/adapters/binance/common/schemas/symbol.py b/nautilus_trader/adapters/binance/common/schemas/symbol.py index 0e14cb716410..6e38621f158f 100644 --- a/nautilus_trader/adapters/binance/common/schemas/symbol.py +++ b/nautilus_trader/adapters/binance/common/schemas/symbol.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- import json -from typing import Optional from nautilus_trader.adapters.binance.common.enums import BinanceAccountType @@ -29,7 +28,7 @@ class BinanceSymbol(str): Binance compatible symbol. """ - def __new__(cls, symbol: Optional[str]): + def __new__(cls, symbol: str | None): if symbol is not None: # Format the string on construction to be Binance compatible return super().__new__( @@ -55,7 +54,7 @@ class BinanceSymbols(str): Binance compatible list of symbols. """ - def __new__(cls, symbols: Optional[list[str]]): + def __new__(cls, symbols: list[str] | None): if symbols is not None: binance_symbols: list[BinanceSymbol] = [BinanceSymbol(symbol) for symbol in symbols] return super().__new__(cls, json.dumps(binance_symbols).replace(" ", "")) diff --git a/nautilus_trader/adapters/binance/common/types.py b/nautilus_trader/adapters/binance/common/types.py index d0b8845dcc84..984e8907b446 100644 --- a/nautilus_trader/adapters/binance/common/types.py +++ b/nautilus_trader/adapters/binance/common/types.py @@ -14,7 +14,7 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Any, Optional +from typing import Any from nautilus_trader.model.data import Bar from nautilus_trader.model.data import BarType @@ -275,11 +275,11 @@ def __init__( count: int, ts_event: int, ts_init: int, - prev_close_price: Optional[Decimal] = None, - bid_price: Optional[Decimal] = None, - bid_qty: Optional[Decimal] = None, - ask_price: Optional[Decimal] = None, - ask_qty: Optional[Decimal] = None, + prev_close_price: Decimal | None = None, + bid_price: Decimal | None = None, + bid_qty: Decimal | None = None, + ask_price: Decimal | None = None, + ask_qty: Decimal | None = None, ): super().__init__( instrument_id=instrument_id, @@ -351,11 +351,11 @@ def from_dict(values: dict[str, Any]) -> "BinanceTicker": BinanceTicker """ - prev_close_str: Optional[str] = values.get("prev_close") - bid_price_str: Optional[str] = values.get("bid_price") - bid_qty_str: Optional[str] = values.get("bid_qty") - ask_price_str: Optional[str] = values.get("ask_price") - ask_qty_str: Optional[str] = values.get("ask_qty") + prev_close_str: str | None = values.get("prev_close") + bid_price_str: str | None = values.get("bid_price") + bid_qty_str: str | None = values.get("bid_qty") + ask_price_str: str | None = values.get("ask_price") + ask_qty_str: str | None = values.get("ask_qty") return BinanceTicker( instrument_id=InstrumentId.from_str(values["instrument_id"]), price_change=Decimal(values["price_change"]), diff --git a/nautilus_trader/adapters/binance/config.py b/nautilus_trader/adapters/binance/config.py index 37b40cff875d..356e055eade9 100644 --- a/nautilus_trader/adapters/binance/config.py +++ b/nautilus_trader/adapters/binance/config.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional from nautilus_trader.adapters.binance.common.enums import BinanceAccountType from nautilus_trader.config import LiveDataClientConfig @@ -52,11 +51,11 @@ class BinanceDataClientConfig(LiveDataClientConfig, frozen=True): """ - api_key: Optional[str] = None - api_secret: Optional[str] = None + api_key: str | None = None + api_secret: str | None = None account_type: BinanceAccountType = BinanceAccountType.SPOT - base_url_http: Optional[str] = None - base_url_ws: Optional[str] = None + base_url_http: str | None = None + base_url_ws: str | None = None us: bool = False testnet: bool = False use_agg_trade_ticks: bool = False @@ -108,11 +107,11 @@ class BinanceExecClientConfig(LiveExecClientConfig, frozen=True): """ - api_key: Optional[str] = None - api_secret: Optional[str] = None + api_key: str | None = None + api_secret: str | None = None account_type: BinanceAccountType = BinanceAccountType.SPOT - base_url_http: Optional[str] = None - base_url_ws: Optional[str] = None + base_url_http: str | None = None + base_url_ws: str | None = None us: bool = False testnet: bool = False clock_sync_interval_secs: int = 0 @@ -120,5 +119,5 @@ class BinanceExecClientConfig(LiveExecClientConfig, frozen=True): use_reduce_only: bool = True use_position_ids: bool = True treat_expired_as_canceled: bool = False - max_retries: Optional[PositiveInt] = None - retry_delay: Optional[PositiveFloat] = None + max_retries: PositiveInt | None = None + retry_delay: PositiveFloat | None = None diff --git a/nautilus_trader/adapters/binance/factories.py b/nautilus_trader/adapters/binance/factories.py index d325c1e6f0d8..9360a5b0125f 100644 --- a/nautilus_trader/adapters/binance/factories.py +++ b/nautilus_trader/adapters/binance/factories.py @@ -16,7 +16,6 @@ import asyncio import os from functools import lru_cache -from typing import Optional, Union from nautilus_trader.adapters.binance.common.enums import BinanceAccountType from nautilus_trader.adapters.binance.config import BinanceDataClientConfig @@ -46,9 +45,9 @@ def get_cached_binance_http_client( clock: LiveClock, logger: Logger, account_type: BinanceAccountType, - key: Optional[str] = None, - secret: Optional[str] = None, - base_url: Optional[str] = None, + key: str | None = None, + secret: str | None = None, + base_url: str | None = None, is_testnet: bool = False, is_us: bool = False, ) -> BinanceHttpClient: @@ -217,7 +216,7 @@ def create( # type: ignore cache: Cache, clock: LiveClock, logger: Logger, - ) -> Union[BinanceSpotDataClient, BinanceFuturesDataClient]: + ) -> BinanceSpotDataClient | BinanceFuturesDataClient: """ Create a new Binance data client. @@ -266,7 +265,7 @@ def create( # type: ignore is_us=config.us, ) - provider: Union[BinanceSpotInstrumentProvider, BinanceFuturesInstrumentProvider] + provider: BinanceSpotInstrumentProvider | BinanceFuturesInstrumentProvider if config.account_type.is_spot_or_margin: # Get instrument provider singleton provider = get_cached_binance_spot_instrument_provider( @@ -330,7 +329,7 @@ def create( # type: ignore cache: Cache, clock: LiveClock, logger: Logger, - ) -> Union[BinanceSpotExecutionClient, BinanceFuturesExecutionClient]: + ) -> BinanceSpotExecutionClient | BinanceFuturesExecutionClient: """ Create a new Binance execution client. @@ -379,7 +378,7 @@ def create( # type: ignore is_us=config.us, ) - provider: Union[BinanceSpotInstrumentProvider, BinanceFuturesInstrumentProvider] + provider: BinanceSpotInstrumentProvider | BinanceFuturesInstrumentProvider if config.account_type.is_spot or config.account_type.is_margin: # Get instrument provider singleton provider = get_cached_binance_spot_instrument_provider( diff --git a/nautilus_trader/adapters/binance/futures/data.py b/nautilus_trader/adapters/binance/futures/data.py index 9d206a78e7e8..001df6ae8cbf 100644 --- a/nautilus_trader/adapters/binance/futures/data.py +++ b/nautilus_trader/adapters/binance/futures/data.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- import asyncio -from typing import Optional, Union import msgspec @@ -127,7 +126,7 @@ async def _subscribe(self, data_type: DataType) -> None: f"for {self._binance_account_type.value} account types.", ) return - instrument_id: Optional[InstrumentId] = data_type.metadata.get("instrument_id") + instrument_id: InstrumentId | None = data_type.metadata.get("instrument_id") if instrument_id is None: self._log.error( "Cannot subscribe to `BinanceFuturesMarkPriceUpdate` " @@ -148,7 +147,7 @@ async def _unsubscribe(self, data_type: DataType) -> None: f"for {self._binance_account_type.value} account types.", ) return - instrument_id: Optional[InstrumentId] = data_type.metadata.get("instrument_id") + instrument_id: InstrumentId | None = data_type.metadata.get("instrument_id") if instrument_id is None: self._log.error( "Cannot subscribe to `BinanceFuturesMarkPriceUpdate` no instrument ID in `data_type` metadata.", @@ -169,7 +168,7 @@ def _handle_book_partial_update(self, raw: bytes) -> None: ts_init=self._clock.timestamp_ns(), ) # Check if book buffer active - book_buffer: Optional[list[Union[OrderBookDelta, OrderBookDeltas]]] = self._book_buffer.get( + book_buffer: list[OrderBookDelta | OrderBookDeltas] | None = self._book_buffer.get( instrument_id, ) if book_buffer is not None: diff --git a/nautilus_trader/adapters/binance/futures/execution.py b/nautilus_trader/adapters/binance/futures/execution.py index 1c7740d837d5..ed1229c20b9f 100644 --- a/nautilus_trader/adapters/binance/futures/execution.py +++ b/nautilus_trader/adapters/binance/futures/execution.py @@ -15,7 +15,6 @@ import asyncio from decimal import Decimal -from typing import Optional import msgspec @@ -175,7 +174,7 @@ async def _update_account_state(self) -> None: async def _get_binance_position_status_reports( self, - symbol: Optional[str] = None, + symbol: str | None = None, ) -> list[PositionStatusReport]: reports: list[PositionStatusReport] = [] # Check Binance for all active positions @@ -197,7 +196,7 @@ async def _get_binance_position_status_reports( async def _get_binance_active_position_symbols( self, - symbol: Optional[str] = None, + symbol: str | None = None, ) -> set[str]: # Check Binance for all active positions active_symbols: set[str] = set() diff --git a/nautilus_trader/adapters/binance/futures/http/account.py b/nautilus_trader/adapters/binance/futures/http/account.py index 74127f974852..ca5b054da728 100644 --- a/nautilus_trader/adapters/binance/futures/http/account.py +++ b/nautilus_trader/adapters/binance/futures/http/account.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional, Union +from typing import Any import msgspec @@ -81,7 +81,7 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ timestamp: str - recvWindow: Optional[str] = None + recvWindow: str | None = None class PostParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ @@ -101,7 +101,7 @@ class PostParameters(msgspec.Struct, omit_defaults=True, frozen=True): timestamp: str dualSidePosition: str - recvWindow: Optional[str] = None + recvWindow: str | None = None async def get(self, parameters: GetParameters) -> BinanceFuturesDualSidePosition: method_type = HttpMethod.GET @@ -161,7 +161,7 @@ class DeleteParameters(msgspec.Struct, omit_defaults=True, frozen=True): timestamp: str symbol: BinanceSymbol - recvWindow: Optional[str] = None + recvWindow: str | None = None async def delete(self, parameters: DeleteParameters) -> BinanceStatusCode: method_type = HttpMethod.DELETE @@ -198,7 +198,7 @@ def __init__( url_path, ) self._delete_resp_decoder = msgspec.json.Decoder( - Union[list[BinanceOrder], dict[str, Any]], + list[BinanceOrder] | dict[str, Any], strict=False, ) @@ -219,9 +219,9 @@ class DeleteParameters(msgspec.Struct, omit_defaults=True, frozen=True): timestamp: str symbol: BinanceSymbol - orderIdList: Optional[str] = None - origClientOrderIdList: Optional[str] = None - recvWindow: Optional[str] = None + orderIdList: str | None = None + origClientOrderIdList: str | None = None + recvWindow: str | None = None async def delete(self, parameters: DeleteParameters) -> list[BinanceOrder]: method_type = HttpMethod.DELETE @@ -273,7 +273,7 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ timestamp: str - recvWindow: Optional[str] = None + recvWindow: str | None = None async def get(self, parameters: GetParameters) -> BinanceFuturesAccountInfo: method_type = HttpMethod.GET @@ -327,8 +327,8 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ timestamp: str - symbol: Optional[BinanceSymbol] = None - recvWindow: Optional[str] = None + symbol: BinanceSymbol | None = None + recvWindow: str | None = None async def get(self, parameters: GetParameters) -> list[BinanceFuturesPositionRisk]: method_type = HttpMethod.GET @@ -389,7 +389,7 @@ def __init__( async def query_futures_hedge_mode( self, - recv_window: Optional[str] = None, + recv_window: str | None = None, ) -> BinanceFuturesDualSidePosition: """ Check Binance Futures hedge mode (dualSidePosition). @@ -404,7 +404,7 @@ async def query_futures_hedge_mode( async def set_futures_hedge_mode( self, dual_side_position: bool, - recv_window: Optional[str] = None, + recv_window: str | None = None, ) -> BinanceStatusCode: """ Set Binance Futures hedge mode (dualSidePosition). @@ -420,7 +420,7 @@ async def set_futures_hedge_mode( async def cancel_all_open_orders( self, symbol: str, - recv_window: Optional[str] = None, + recv_window: str | None = None, ) -> bool: """ Delete all Futures open orders. @@ -441,7 +441,7 @@ async def cancel_multiple_orders( self, symbol: str, client_order_ids: list[str], - recv_window: Optional[str] = None, + recv_window: str | None = None, ) -> bool: """ Delete multiple Futures orders. @@ -462,7 +462,7 @@ async def cancel_multiple_orders( async def query_futures_account_info( self, - recv_window: Optional[str] = None, + recv_window: str | None = None, ) -> BinanceFuturesAccountInfo: """ Check Binance Futures account information. @@ -476,8 +476,8 @@ async def query_futures_account_info( async def query_futures_position_risk( self, - symbol: Optional[str] = None, - recv_window: Optional[str] = None, + symbol: str | None = None, + recv_window: str | None = None, ) -> list[BinanceFuturesPositionRisk]: """ Check all Futures position's info for a symbol. diff --git a/nautilus_trader/adapters/binance/futures/http/wallet.py b/nautilus_trader/adapters/binance/futures/http/wallet.py index 62ab803faf56..76f1ebca7b39 100644 --- a/nautilus_trader/adapters/binance/futures/http/wallet.py +++ b/nautilus_trader/adapters/binance/futures/http/wallet.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional import msgspec @@ -73,7 +72,7 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): timestamp: str symbol: BinanceSymbol - recvWindow: Optional[str] = None + recvWindow: str | None = None async def get(self, parameters: GetParameters) -> BinanceFuturesCommissionRate: method_type = HttpMethod.GET @@ -125,7 +124,7 @@ def _timestamp(self) -> str: async def query_futures_commission_rate( self, symbol: str, - recv_window: Optional[str] = None, + recv_window: str | None = None, ) -> BinanceFuturesCommissionRate: """ Get Futures commission rates for a given symbol. diff --git a/nautilus_trader/adapters/binance/futures/providers.py b/nautilus_trader/adapters/binance/futures/providers.py index 0ef850080164..4b572ec5b296 100644 --- a/nautilus_trader/adapters/binance/futures/providers.py +++ b/nautilus_trader/adapters/binance/futures/providers.py @@ -15,7 +15,6 @@ from datetime import datetime as dt from decimal import Decimal -from typing import Optional import msgspec @@ -74,7 +73,7 @@ def __init__( logger: Logger, clock: LiveClock, account_type: BinanceAccountType = BinanceAccountType.USDT_FUTURE, - config: Optional[InstrumentProviderConfig] = None, + config: InstrumentProviderConfig | None = None, ): super().__init__( venue=BINANCE_VENUE, @@ -121,7 +120,7 @@ def __init__( 9: BinanceFuturesFeeRates(feeTier=9, maker="0.000000", taker="0.000170"), } - async def load_all_async(self, filters: Optional[dict] = None) -> None: + async def load_all_async(self, filters: dict | None = None) -> None: filters_str = "..." if not filters else f" with filters {filters}..." self._log.info(f"Loading all instruments{filters_str}") @@ -146,7 +145,7 @@ async def load_all_async(self, filters: Optional[dict] = None) -> None: async def load_ids_async( self, instrument_ids: list[InstrumentId], - filters: Optional[dict] = None, + filters: dict | None = None, ) -> None: if not instrument_ids: self._log.info("No instrument IDs given for loading.") @@ -191,7 +190,7 @@ async def load_ids_async( position_risk=position_risk[symbol], ) - async def load_async(self, instrument_id: InstrumentId, filters: Optional[dict] = None) -> None: + async def load_async(self, instrument_id: InstrumentId, filters: dict | None = None) -> None: PyCondition.not_none(instrument_id, "instrument_id") PyCondition.equal(instrument_id.venue, self.venue, "instrument_id.venue", "self.venue") @@ -224,8 +223,8 @@ def _parse_instrument( self, symbol_info: BinanceFuturesSymbolInfo, ts_event: int, - position_risk: Optional[BinanceFuturesPositionRisk] = None, - fee: Optional[BinanceFuturesCommissionRate] = None, + position_risk: BinanceFuturesPositionRisk | None = None, + fee: BinanceFuturesCommissionRate | None = None, ) -> None: contract_type_str = symbol_info.contractType diff --git a/nautilus_trader/adapters/binance/futures/schemas/account.py b/nautilus_trader/adapters/binance/futures/schemas/account.py index c9fc4a9ef37a..9ecb30937782 100644 --- a/nautilus_trader/adapters/binance/futures/schemas/account.py +++ b/nautilus_trader/adapters/binance/futures/schemas/account.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Optional import msgspec @@ -55,8 +54,8 @@ class BinanceFuturesBalanceInfo(msgspec.Struct, frozen=True): availableBalance: str # available balance maxWithdrawAmount: str # maximum amount for transfer out # whether the asset can be used as margin in Multi - Assets mode - marginAvailable: Optional[bool] = None - updateTime: Optional[int] = None # last update time + marginAvailable: bool | None = None + updateTime: int | None = None # last update time def parse_to_account_balance(self) -> AccountBalance: currency = Currency.from_str(self.asset) @@ -87,22 +86,20 @@ class BinanceFuturesAccountInfo(msgspec.Struct, kw_only=True, frozen=True): canDeposit: bool # if can transfer in asset canWithdraw: bool # if can transfer out asset updateTime: int - totalInitialMargin: Optional[ - str - ] = None # total initial margin required with current mark price (useless with isolated positions), only for USDT asset - totalMaintMargin: Optional[str] = None # total maintenance margin required, only for USDT asset - totalWalletBalance: Optional[str] = None # total wallet balance, only for USDT asset - totalUnrealizedProfit: Optional[str] = None # total unrealized profit, only for USDT asset - totalMarginBalance: Optional[str] = None # total margin balance, only for USDT asset + totalInitialMargin: str | None = None # total initial margin required with current mark price (useless with isolated positions), only for USDT + totalMaintMargin: str | None = None # total maintenance margin required, only for USDT asset + totalWalletBalance: str | None = None # total wallet balance, only for USDT asset + totalUnrealizedProfit: str | None = None # total unrealized profit, only for USDT asset + totalMarginBalance: str | None = None # total margin balance, only for USDT asset # initial margin required for positions with current mark price, only for USDT asset - totalPositionInitialMargin: Optional[str] = None + totalPositionInitialMargin: str | None = None # initial margin required for open orders with current mark price, only for USDT asset - totalOpenOrderInitialMargin: Optional[str] = None - totalCrossWalletBalance: Optional[str] = None # crossed wallet balance, only for USDT asset + totalOpenOrderInitialMargin: str | None = None + totalCrossWalletBalance: str | None = None # crossed wallet balance, only for USDT asset # unrealized profit of crossed positions, only for USDT asset - totalCrossUnPnl: Optional[str] = None - availableBalance: Optional[str] = None # available balance, only for USDT asset - maxWithdrawAmount: Optional[str] = None # maximum amount for transfer out, only for USDT asset + totalCrossUnPnl: str | None = None + availableBalance: str | None = None # available balance, only for USDT asset + maxWithdrawAmount: str | None = None # maximum amount for transfer out, only for USDT asset assets: list[BinanceFuturesBalanceInfo] def parse_to_account_balances(self) -> list[AccountBalance]: @@ -124,7 +121,7 @@ class BinanceFuturesPositionRisk(msgspec.Struct, kw_only=True, frozen=True): leverage: str liquidationPrice: str markPrice: str - maxNotionalValue: Optional[str] = None + maxNotionalValue: str | None = None positionAmt: str symbol: str unRealizedProfit: str diff --git a/nautilus_trader/adapters/binance/futures/schemas/market.py b/nautilus_trader/adapters/binance/futures/schemas/market.py index e964b1031eb2..7004af2f3aec 100644 --- a/nautilus_trader/adapters/binance/futures/schemas/market.py +++ b/nautilus_trader/adapters/binance/futures/schemas/market.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Optional import msgspec @@ -61,7 +60,7 @@ class BinanceFuturesSymbolInfo(msgspec.Struct, kw_only=True, frozen=True): contractType: str # Can be '' empty string deliveryDate: int onboardDate: int - status: Optional[BinanceFuturesContractStatus] = None + status: BinanceFuturesContractStatus | None = None maintMarginPercent: str requiredMarginPercent: str baseAsset: str @@ -73,7 +72,7 @@ class BinanceFuturesSymbolInfo(msgspec.Struct, kw_only=True, frozen=True): quotePrecision: int underlyingType: str underlyingSubType: list[str] - settlePlan: Optional[int] = None + settlePlan: int | None = None triggerProtect: str liquidationFee: str marketTakeBound: str @@ -109,7 +108,7 @@ class BinanceFuturesExchangeInfo(msgspec.Struct, kw_only=True, frozen=True): serverTime: int rateLimits: list[BinanceRateLimit] exchangeFilters: list[BinanceExchangeFilter] - assets: Optional[list[BinanceFuturesAsset]] = None + assets: list[BinanceFuturesAsset] | None = None symbols: list[BinanceFuturesSymbolInfo] diff --git a/nautilus_trader/adapters/binance/futures/schemas/user.py b/nautilus_trader/adapters/binance/futures/schemas/user.py index 9062ac2e2209..402522acfb2a 100644 --- a/nautilus_trader/adapters/binance/futures/schemas/user.py +++ b/nautilus_trader/adapters/binance/futures/schemas/user.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Optional import msgspec @@ -68,8 +67,8 @@ class BinanceFuturesUserMsgWrapper(msgspec.Struct, frozen=True): Provides a wrapper for execution WebSocket messages from `Binance`. """ - data: Optional[BinanceFuturesUserMsgData] = None - stream: Optional[str] = None + data: BinanceFuturesUserMsgData | None = None + stream: str | None = None class MarginCallPosition(msgspec.Struct, frozen=True): @@ -198,15 +197,15 @@ class BinanceFuturesOrderData(msgspec.Struct, kw_only=True, frozen=True): q: str # Original Quantity p: str # Original Price ap: str # Average Price - sp: Optional[str] = None # Stop Price. Ignore with TRAILING_STOP_MARKET order + sp: str | None = None # Stop Price. Ignore with TRAILING_STOP_MARKET order x: BinanceExecutionType X: BinanceOrderStatus i: int # Order ID l: str # Order Last Filled Quantity z: str # Order Filled Accumulated Quantity L: str # Last Filled Price - N: Optional[str] = None # Commission Asset, will not push if no commission - n: Optional[str] = None # Commission, will not push if no commission + N: str | None = None # Commission Asset, will not push if no commission + n: str | None = None # Commission, will not push if no commission T: int # Order Trade Time t: int # Trade ID b: str # Bids Notional @@ -216,9 +215,9 @@ class BinanceFuturesOrderData(msgspec.Struct, kw_only=True, frozen=True): wt: BinanceFuturesWorkingType ot: BinanceOrderType ps: BinanceFuturesPositionSide - cp: Optional[bool] = None # If Close-All, pushed with conditional order - AP: Optional[str] = None # Activation Price, only pushed with TRAILING_STOP_MARKET order - cr: Optional[str] = None # Callback Rate, only pushed with TRAILING_STOP_MARKET order + cp: bool | None = None # If Close-All, pushed with conditional order + AP: str | None = None # Activation Price, only pushed with TRAILING_STOP_MARKET order + cr: str | None = None # Callback Rate, only pushed with TRAILING_STOP_MARKET order pP: bool # ignore si: int # ignore ss: int # ignore @@ -305,15 +304,15 @@ def handle_order_trade_update( # noqa: C901 (too complex) raise ValueError(f"Cannot handle trade: instrument {instrument_id} not found") # Determine commission - commission_asset: Optional[str] = self.N - commission_amount: Optional[str] = self.n + commission_asset: str | None = self.N + commission_amount: str | None = self.n if commission_asset is not None: commission = Money.from_str(f"{commission_amount} {commission_asset}") else: # Commission in margin collateral currency commission = Money(0, instrument.quote_currency) - venue_position_id: Optional[PositionId] = None + venue_position_id: PositionId | None = None if exec_client.use_position_ids: venue_position_id = PositionId(f"{instrument_id}-{self.ps.value}") diff --git a/nautilus_trader/adapters/binance/http/account.py b/nautilus_trader/adapters/binance/http/account.py index b437626d07d9..2c450d3bf1f5 100644 --- a/nautilus_trader/adapters/binance/http/account.py +++ b/nautilus_trader/adapters/binance/http/account.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional import msgspec @@ -71,7 +70,7 @@ def __init__( self, client: BinanceHttpClient, base_endpoint: str, - testing_endpoint: Optional[bool] = False, + testing_endpoint: bool | None = False, ): methods = { HttpMethod.GET: BinanceSecurityType.USER_DATA, @@ -117,9 +116,9 @@ class GetDeleteParameters(msgspec.Struct, omit_defaults=True, frozen=True): symbol: BinanceSymbol timestamp: str - orderId: Optional[int] = None - origClientOrderId: Optional[str] = None - recvWindow: Optional[str] = None + orderId: int | None = None + origClientOrderId: str | None = None + recvWindow: str | None = None class PostParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ @@ -215,25 +214,25 @@ class PostParameters(msgspec.Struct, omit_defaults=True, frozen=True): timestamp: str side: BinanceOrderSide type: BinanceOrderType - timeInForce: Optional[BinanceTimeInForce] = None - quantity: Optional[str] = None - quoteOrderQty: Optional[str] = None - price: Optional[str] = None - newClientOrderId: Optional[str] = None - strategyId: Optional[int] = None - strategyType: Optional[int] = None - stopPrice: Optional[str] = None - trailingDelta: Optional[str] = None - icebergQty: Optional[str] = None - reduceOnly: Optional[str] = None - closePosition: Optional[str] = None - activationPrice: Optional[str] = None - callbackRate: Optional[str] = None - workingType: Optional[str] = None - priceProtect: Optional[str] = None - newOrderRespType: Optional[BinanceNewOrderRespType] = None - goodTillDate: Optional[int] = None - recvWindow: Optional[str] = None + timeInForce: BinanceTimeInForce | None = None + quantity: str | None = None + quoteOrderQty: str | None = None + price: str | None = None + newClientOrderId: str | None = None + strategyId: int | None = None + strategyType: int | None = None + stopPrice: str | None = None + trailingDelta: str | None = None + icebergQty: str | None = None + reduceOnly: str | None = None + closePosition: str | None = None + activationPrice: str | None = None + callbackRate: str | None = None + workingType: str | None = None + priceProtect: str | None = None + newOrderRespType: BinanceNewOrderRespType | None = None + goodTillDate: int | None = None + recvWindow: str | None = None class PutParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ @@ -266,9 +265,9 @@ class PutParameters(msgspec.Struct, omit_defaults=True, frozen=True): quantity: str price: str timestamp: str - orderId: Optional[int] = None - origClientOrderId: Optional[str] = None - recvWindow: Optional[str] = None + orderId: int | None = None + origClientOrderId: str | None = None + recvWindow: str | None = None async def get(self, parameters: GetDeleteParameters) -> BinanceOrder: method_type = HttpMethod.GET @@ -350,11 +349,11 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): symbol: BinanceSymbol timestamp: str - orderId: Optional[int] = None - startTime: Optional[int] = None - endTime: Optional[int] = None - limit: Optional[int] = None - recvWindow: Optional[str] = None + orderId: int | None = None + startTime: int | None = None + endTime: int | None = None + limit: int | None = None + recvWindow: str | None = None async def get(self, parameters: GetParameters) -> list[BinanceOrder]: method_type = HttpMethod.GET @@ -387,7 +386,7 @@ def __init__( self, client: BinanceHttpClient, base_endpoint: str, - methods: Optional[dict[HttpMethod, BinanceSecurityType]] = None, + methods: dict[HttpMethod, BinanceSecurityType] | None = None, ): if methods is None: methods = { @@ -417,8 +416,8 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ timestamp: str - symbol: Optional[BinanceSymbol] = None - recvWindow: Optional[str] = None + symbol: BinanceSymbol | None = None + recvWindow: str | None = None async def get(self, parameters: GetParameters) -> list[BinanceOrder]: method_type = HttpMethod.GET @@ -486,12 +485,12 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): symbol: BinanceSymbol timestamp: str - orderId: Optional[int] = None - startTime: Optional[int] = None - endTime: Optional[int] = None - fromId: Optional[int] = None - limit: Optional[int] = None - recvWindow: Optional[str] = None + orderId: int | None = None + startTime: int | None = None + endTime: int | None = None + fromId: int | None = None + limit: int | None = None + recvWindow: str | None = None async def _get(self, parameters: GetParameters) -> list[BinanceUserTrade]: method_type = HttpMethod.GET @@ -555,9 +554,9 @@ def _timestamp(self) -> str: async def query_order( self, symbol: str, - order_id: Optional[int] = None, - orig_client_order_id: Optional[str] = None, - recv_window: Optional[str] = None, + order_id: int | None = None, + orig_client_order_id: str | None = None, + recv_window: str | None = None, ) -> BinanceOrder: """ Check an order status. @@ -580,7 +579,7 @@ async def query_order( async def cancel_all_open_orders( self, symbol: str, - recv_window: Optional[str] = None, + recv_window: str | None = None, ) -> bool: # Implement in child class raise NotImplementedError @@ -588,9 +587,9 @@ async def cancel_all_open_orders( async def cancel_order( self, symbol: str, - order_id: Optional[int] = None, - orig_client_order_id: Optional[str] = None, - recv_window: Optional[str] = None, + order_id: int | None = None, + orig_client_order_id: str | None = None, + recv_window: str | None = None, ) -> BinanceOrder: """ Cancel an active order. @@ -615,25 +614,25 @@ async def new_order( symbol: str, side: BinanceOrderSide, order_type: BinanceOrderType, - time_in_force: Optional[BinanceTimeInForce] = None, - quantity: Optional[str] = None, - quote_order_qty: Optional[str] = None, - price: Optional[str] = None, - new_client_order_id: Optional[str] = None, - strategy_id: Optional[int] = None, - strategy_type: Optional[int] = None, - stop_price: Optional[str] = None, - trailing_delta: Optional[str] = None, - iceberg_qty: Optional[str] = None, - reduce_only: Optional[str] = None, - close_position: Optional[str] = None, - activation_price: Optional[str] = None, - callback_rate: Optional[str] = None, - working_type: Optional[str] = None, - price_protect: Optional[str] = None, - good_till_date: Optional[int] = None, - new_order_resp_type: Optional[BinanceNewOrderRespType] = None, - recv_window: Optional[str] = None, + time_in_force: BinanceTimeInForce | None = None, + quantity: str | None = None, + quote_order_qty: str | None = None, + price: str | None = None, + new_client_order_id: str | None = None, + strategy_id: int | None = None, + strategy_type: int | None = None, + stop_price: str | None = None, + trailing_delta: str | None = None, + iceberg_qty: str | None = None, + reduce_only: str | None = None, + close_position: str | None = None, + activation_price: str | None = None, + callback_rate: str | None = None, + working_type: str | None = None, + price_protect: str | None = None, + good_till_date: int | None = None, + new_order_resp_type: BinanceNewOrderRespType | None = None, + recv_window: str | None = None, ) -> BinanceOrder: """ Send in a new order to Binance. @@ -673,9 +672,9 @@ async def modify_order( side: BinanceOrderSide, quantity: str, price: str, - order_id: Optional[int] = None, - orig_client_order_id: Optional[str] = None, - recv_window: Optional[str] = None, + order_id: int | None = None, + orig_client_order_id: str | None = None, + recv_window: str | None = None, ) -> BinanceOrder: """ Modify a LIMIT order with Binance. @@ -697,11 +696,11 @@ async def modify_order( async def query_all_orders( self, symbol: str, - order_id: Optional[int] = None, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - limit: Optional[int] = None, - recv_window: Optional[str] = None, + order_id: int | None = None, + start_time: int | None = None, + end_time: int | None = None, + limit: int | None = None, + recv_window: str | None = None, ) -> list[BinanceOrder]: """ Query all orders, active or filled. @@ -720,8 +719,8 @@ async def query_all_orders( async def query_open_orders( self, - symbol: Optional[str] = None, - recv_window: Optional[str] = None, + symbol: str | None = None, + recv_window: str | None = None, ) -> list[BinanceOrder]: """ Query open orders. @@ -737,12 +736,12 @@ async def query_open_orders( async def query_user_trades( self, symbol: str, - order_id: Optional[int] = None, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - from_id: Optional[int] = None, - limit: Optional[int] = None, - recv_window: Optional[str] = None, + order_id: int | None = None, + start_time: int | None = None, + end_time: int | None = None, + from_id: int | None = None, + limit: int | None = None, + recv_window: str | None = None, ) -> list[BinanceUserTrade]: """ Query user's trade history for a symbol, with provided filters. diff --git a/nautilus_trader/adapters/binance/http/client.py b/nautilus_trader/adapters/binance/http/client.py index 32a548110d5f..2b3700e23c92 100644 --- a/nautilus_trader/adapters/binance/http/client.py +++ b/nautilus_trader/adapters/binance/http/client.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import hashlib import hmac import urllib.parse diff --git a/nautilus_trader/adapters/binance/http/endpoint.py b/nautilus_trader/adapters/binance/http/endpoint.py index 897fa08bd0c4..4df2a39b8a2c 100644 --- a/nautilus_trader/adapters/binance/http/endpoint.py +++ b/nautilus_trader/adapters/binance/http/endpoint.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import msgspec @@ -69,7 +69,7 @@ async def _method( self, method_type: HttpMethod, parameters: Any, - ratelimiter_keys: Optional[list[str]] = None, + ratelimiter_keys: list[str] | None = None, ) -> bytes: payload: dict = self.decoder.decode(self.encoder.encode(parameters)) if self.methods_desc[method_type] is None: diff --git a/nautilus_trader/adapters/binance/http/market.py b/nautilus_trader/adapters/binance/http/market.py index 63373b3c9f3f..58109b435292 100644 --- a/nautilus_trader/adapters/binance/http/market.py +++ b/nautilus_trader/adapters/binance/http/market.py @@ -15,7 +15,6 @@ import sys import time -from typing import Optional import msgspec @@ -167,7 +166,7 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ symbol: BinanceSymbol - limit: Optional[int] = None + limit: int | None = None async def get(self, parameters: GetParameters) -> BinanceDepth: method_type = HttpMethod.GET @@ -221,7 +220,7 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ symbol: BinanceSymbol - limit: Optional[int] = None + limit: int | None = None async def get(self, parameters: GetParameters) -> list[BinanceTrade]: method_type = HttpMethod.GET @@ -277,8 +276,8 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ symbol: BinanceSymbol - limit: Optional[int] = None - fromId: Optional[int] = None + limit: int | None = None + fromId: int | None = None async def get(self, parameters: GetParameters) -> list[BinanceTrade]: method_type = HttpMethod.GET @@ -339,10 +338,10 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ symbol: BinanceSymbol - limit: Optional[int] = None - fromId: Optional[int] = None - startTime: Optional[int] = None - endTime: Optional[int] = None + limit: int | None = None + fromId: int | None = None + startTime: int | None = None + endTime: int | None = None async def get(self, parameters: GetParameters) -> list[BinanceAggTrade]: method_type = HttpMethod.GET @@ -404,9 +403,9 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): symbol: BinanceSymbol interval: BinanceKlineInterval - limit: Optional[int] = None - startTime: Optional[int] = None - endTime: Optional[int] = None + limit: int | None = None + startTime: int | None = None + endTime: int | None = None async def get(self, parameters: GetParameters) -> list[BinanceKline]: method_type = HttpMethod.GET @@ -470,9 +469,9 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ - symbol: Optional[BinanceSymbol] = None - symbols: Optional[BinanceSymbols] = None # SPOT/MARGIN only - type: Optional[str] = None # SPOT/MARIN only + symbol: BinanceSymbol | None = None + symbols: BinanceSymbols | None = None # SPOT/MARGIN only + type: str | None = None # SPOT/MARIN only async def _get(self, parameters: GetParameters) -> list[BinanceTicker24hr]: method_type = HttpMethod.GET @@ -531,8 +530,8 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ - symbol: Optional[BinanceSymbol] = None - symbols: Optional[BinanceSymbols] = None # SPOT/MARGIN only + symbol: BinanceSymbol | None = None + symbols: BinanceSymbols | None = None # SPOT/MARGIN only async def _get(self, parameters: GetParameters) -> list[BinanceTickerPrice]: method_type = HttpMethod.GET @@ -591,8 +590,8 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ - symbol: Optional[BinanceSymbol] = None - symbols: Optional[BinanceSymbols] = None # SPOT/MARGIN only + symbol: BinanceSymbol | None = None + symbols: BinanceSymbols | None = None # SPOT/MARGIN only async def _get(self, parameters: GetParameters) -> list[BinanceTickerBook]: method_type = HttpMethod.GET @@ -667,7 +666,7 @@ async def request_server_time(self) -> int: async def query_depth( self, symbol: str, - limit: Optional[int] = None, + limit: int | None = None, ) -> BinanceDepth: """ Query order book depth for a symbol. @@ -683,7 +682,7 @@ async def request_order_book_snapshot( self, instrument_id: InstrumentId, ts_init: int, - limit: Optional[int] = None, + limit: int | None = None, ) -> OrderBookDeltas: """ Request snapshot of order book depth. @@ -697,7 +696,7 @@ async def request_order_book_snapshot( async def query_trades( self, symbol: str, - limit: Optional[int] = None, + limit: int | None = None, ) -> list[BinanceTrade]: """ Query trades for symbol. @@ -713,7 +712,7 @@ async def request_trade_ticks( self, instrument_id: InstrumentId, ts_init: int, - limit: Optional[int] = None, + limit: int | None = None, ) -> list[TradeTick]: """ Request TradeTicks from Binance. @@ -730,10 +729,10 @@ async def request_trade_ticks( async def query_agg_trades( self, symbol: str, - limit: Optional[int] = None, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - from_id: Optional[int] = None, + limit: int | None = None, + start_time: int | None = None, + end_time: int | None = None, + from_id: int | None = None, ) -> list[BinanceAggTrade]: """ Query aggregated trades for symbol. @@ -752,10 +751,10 @@ async def request_agg_trade_ticks( self, instrument_id: InstrumentId, ts_init: int, - limit: Optional[int] = 1000, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - from_id: Optional[int] = None, + limit: int | None = 1000, + start_time: int | None = None, + end_time: int | None = None, + from_id: int | None = None, ) -> list[TradeTick]: """ Request TradeTicks from Binance aggregated trades. @@ -835,8 +834,8 @@ def _calculate_next_end_time(start_time: int, end_time: int) -> tuple[int, bool] async def query_historical_trades( self, symbol: str, - limit: Optional[int] = None, - from_id: Optional[int] = None, + limit: int | None = None, + from_id: int | None = None, ) -> list[BinanceTrade]: """ Query historical trades for symbol. @@ -853,8 +852,8 @@ async def request_historical_trade_ticks( self, instrument_id: InstrumentId, ts_init: int, - limit: Optional[int] = None, - from_id: Optional[int] = None, + limit: int | None = None, + from_id: int | None = None, ) -> list[TradeTick]: """ Request historical TradeTicks from Binance. @@ -876,9 +875,9 @@ async def query_klines( self, symbol: str, interval: BinanceKlineInterval, - limit: Optional[int] = None, - start_time: Optional[int] = None, - end_time: Optional[int] = None, + limit: int | None = None, + start_time: int | None = None, + end_time: int | None = None, ) -> list[BinanceKline]: """ Query klines for a symbol over an interval. @@ -898,9 +897,9 @@ async def request_binance_bars( bar_type: BarType, ts_init: int, interval: BinanceKlineInterval, - limit: Optional[int] = None, - start_time: Optional[int] = None, - end_time: Optional[int] = None, + limit: int | None = None, + start_time: int | None = None, + end_time: int | None = None, ) -> list[BinanceBar]: """ Request Binance Bars from Klines. @@ -937,9 +936,9 @@ async def request_binance_bars( async def query_ticker_24hr( self, - symbol: Optional[str] = None, - symbols: Optional[list[str]] = None, - response_type: Optional[str] = None, + symbol: str | None = None, + symbols: list[str] | None = None, + response_type: str | None = None, ) -> list[BinanceTicker24hr]: """ Query 24hr ticker for symbol or symbols. @@ -958,8 +957,8 @@ async def query_ticker_24hr( async def query_ticker_price( self, - symbol: Optional[str] = None, - symbols: Optional[list[str]] = None, + symbol: str | None = None, + symbols: list[str] | None = None, ) -> list[BinanceTickerPrice]: """ Query price ticker for symbol or symbols. @@ -977,8 +976,8 @@ async def query_ticker_price( async def query_ticker_book( self, - symbol: Optional[str] = None, - symbols: Optional[list[str]] = None, + symbol: str | None = None, + symbols: list[str] | None = None, ) -> list[BinanceTickerBook]: """ Query book ticker for symbol or symbols. diff --git a/nautilus_trader/adapters/binance/http/user.py b/nautilus_trader/adapters/binance/http/user.py index 8546a8555643..957a218ddb71 100644 --- a/nautilus_trader/adapters/binance/http/user.py +++ b/nautilus_trader/adapters/binance/http/user.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional import msgspec @@ -88,7 +87,7 @@ class PostParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ - symbol: Optional[BinanceSymbol] = None # MARGIN_ISOLATED only, mandatory + symbol: BinanceSymbol | None = None # MARGIN_ISOLATED only, mandatory class PutDeleteParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ @@ -103,20 +102,20 @@ class PutDeleteParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ - symbol: Optional[BinanceSymbol] = None # MARGIN_ISOLATED only, mandatory - listenKey: Optional[str] = None # SPOT/MARGIN only, mandatory + symbol: BinanceSymbol | None = None # MARGIN_ISOLATED only, mandatory + listenKey: str | None = None # SPOT/MARGIN only, mandatory - async def _post(self, parameters: Optional[PostParameters] = None) -> BinanceListenKey: + async def _post(self, parameters: PostParameters | None = None) -> BinanceListenKey: method_type = HttpMethod.POST raw = await self._method(method_type, parameters) return self._post_resp_decoder.decode(raw) - async def _put(self, parameters: Optional[PutDeleteParameters] = None) -> dict: + async def _put(self, parameters: PutDeleteParameters | None = None) -> dict: method_type = HttpMethod.PUT raw = await self._method(method_type, parameters) return self._put_resp_decoder.decode(raw) - async def _delete(self, parameters: Optional[PutDeleteParameters] = None) -> dict: + async def _delete(self, parameters: PutDeleteParameters | None = None) -> dict: method_type = HttpMethod.DELETE raw = await self._method(method_type, parameters) return self._delete_resp_decoder.decode(raw) @@ -172,7 +171,7 @@ def __init__( async def create_listen_key( self, - symbol: Optional[str] = None, + symbol: str | None = None, ) -> BinanceListenKey: """ Create Binance ListenKey. @@ -186,8 +185,8 @@ async def create_listen_key( async def keepalive_listen_key( self, - symbol: Optional[str] = None, - listen_key: Optional[str] = None, + symbol: str | None = None, + listen_key: str | None = None, ): """ Ping/Keepalive Binance ListenKey. @@ -201,8 +200,8 @@ async def keepalive_listen_key( async def delete_listen_key( self, - symbol: Optional[str] = None, - listen_key: Optional[str] = None, + symbol: str | None = None, + listen_key: str | None = None, ): """ Delete Binance ListenKey. diff --git a/nautilus_trader/adapters/binance/spot/data.py b/nautilus_trader/adapters/binance/spot/data.py index 629649133c45..288721b085e7 100644 --- a/nautilus_trader/adapters/binance/spot/data.py +++ b/nautilus_trader/adapters/binance/spot/data.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- import asyncio -from typing import Optional, Union import msgspec @@ -124,7 +123,7 @@ def _handle_book_partial_update(self, raw: bytes) -> None: ts_init=self._clock.timestamp_ns(), ) # Check if book buffer active - book_buffer: Optional[list[Union[OrderBookDelta, OrderBookDeltas]]] = self._book_buffer.get( + book_buffer: list[OrderBookDelta | OrderBookDeltas] | None = self._book_buffer.get( instrument_id, ) if book_buffer is not None: diff --git a/nautilus_trader/adapters/binance/spot/execution.py b/nautilus_trader/adapters/binance/spot/execution.py index 9b0c030ee90e..df791989e1f2 100644 --- a/nautilus_trader/adapters/binance/spot/execution.py +++ b/nautilus_trader/adapters/binance/spot/execution.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- import asyncio -from typing import Optional import msgspec @@ -161,14 +160,14 @@ async def _update_account_state(self) -> None: async def _get_binance_position_status_reports( self, - symbol: Optional[str] = None, + symbol: str | None = None, ) -> list[PositionStatusReport]: # Never cash positions return [] async def _get_binance_active_position_symbols( self, - symbol: Optional[str] = None, + symbol: str | None = None, ) -> set[str]: # Never cash positions return set() diff --git a/nautilus_trader/adapters/binance/spot/http/account.py b/nautilus_trader/adapters/binance/spot/http/account.py index 264e4b279ddb..032ca25744fd 100644 --- a/nautilus_trader/adapters/binance/spot/http/account.py +++ b/nautilus_trader/adapters/binance/spot/http/account.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import msgspec @@ -87,7 +87,7 @@ class DeleteParameters(msgspec.Struct, omit_defaults=True, frozen=True): timestamp: str symbol: BinanceSymbol - recvWindow: Optional[str] = None + recvWindow: str | None = None async def _delete(self, parameters: DeleteParameters) -> list[dict[str, Any]]: method_type = HttpMethod.DELETE @@ -184,20 +184,20 @@ class PostParameters(msgspec.Struct, omit_defaults=True, frozen=True): quantity: str price: str stopPrice: str - listClientOrderId: Optional[str] = None - limitClientOrderId: Optional[str] = None - limitStrategyId: Optional[int] = None - limitStrategyType: Optional[int] = None - limitIcebergQty: Optional[str] = None - trailingDelta: Optional[str] = None - stopClientOrderId: Optional[str] = None - stopStrategyId: Optional[int] = None - stopStrategyType: Optional[int] = None - stopLimitPrice: Optional[str] = None - stopIcebergQty: Optional[str] = None - stopLimitTimeInForce: Optional[BinanceTimeInForce] = None - newOrderRespType: Optional[BinanceNewOrderRespType] = None - recvWindow: Optional[str] = None + listClientOrderId: str | None = None + limitClientOrderId: str | None = None + limitStrategyId: int | None = None + limitStrategyType: int | None = None + limitIcebergQty: str | None = None + trailingDelta: str | None = None + stopClientOrderId: str | None = None + stopStrategyId: int | None = None + stopStrategyType: int | None = None + stopLimitPrice: str | None = None + stopIcebergQty: str | None = None + stopLimitTimeInForce: BinanceTimeInForce | None = None + newOrderRespType: BinanceNewOrderRespType | None = None + recvWindow: str | None = None async def _post(self, parameters: PostParameters) -> BinanceSpotOrderOco: method_type = HttpMethod.POST @@ -257,9 +257,9 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ timestamp: str - orderListId: Optional[str] = None - origClientOrderId: Optional[str] = None - recvWindow: Optional[str] = None + orderListId: str | None = None + origClientOrderId: str | None = None + recvWindow: str | None = None class DeleteParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ @@ -288,10 +288,10 @@ class DeleteParameters(msgspec.Struct, omit_defaults=True, frozen=True): timestamp: str symbol: BinanceSymbol - orderListId: Optional[str] = None - listClientOrderId: Optional[str] = None - newClientOrderId: Optional[str] = None - recvWindow: Optional[str] = None + orderListId: str | None = None + listClientOrderId: str | None = None + newClientOrderId: str | None = None + recvWindow: str | None = None async def get(self, parameters: GetParameters) -> BinanceSpotOrderOco: method_type = HttpMethod.GET @@ -360,11 +360,11 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ timestamp: str - fromId: Optional[int] = None - startTime: Optional[int] = None - endTime: Optional[int] = None - limit: Optional[int] = None - recvWindow: Optional[str] = None + fromId: int | None = None + startTime: int | None = None + endTime: int | None = None + limit: int | None = None + recvWindow: str | None = None async def get(self, parameters: GetParameters) -> list[BinanceSpotOrderOco]: method_type = HttpMethod.GET @@ -414,7 +414,7 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ timestamp: str - recvWindow: Optional[str] = None + recvWindow: str | None = None async def get(self, parameters: GetParameters) -> list[BinanceSpotOrderOco]: method_type = HttpMethod.GET @@ -464,7 +464,7 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ timestamp: str - recvWindow: Optional[str] = None + recvWindow: str | None = None async def get(self, parameters: GetParameters) -> BinanceSpotAccountInfo: method_type = HttpMethod.GET @@ -514,7 +514,7 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ timestamp: str - recvWindow: Optional[str] = None + recvWindow: str | None = None async def get(self, parameters: GetParameters) -> list[BinanceRateLimit]: method_type = HttpMethod.GET @@ -576,20 +576,20 @@ async def new_spot_oco( quantity: str, price: str, stop_price: str, - list_client_order_id: Optional[str] = None, - limit_client_order_id: Optional[str] = None, - limit_strategy_id: Optional[int] = None, - limit_strategy_type: Optional[int] = None, - limit_iceberg_qty: Optional[str] = None, - trailing_delta: Optional[str] = None, - stop_client_order_id: Optional[str] = None, - stop_strategy_id: Optional[int] = None, - stop_strategy_type: Optional[int] = None, - stop_limit_price: Optional[str] = None, - stop_iceberg_qty: Optional[str] = None, - stop_limit_time_in_force: Optional[BinanceTimeInForce] = None, - new_order_resp_type: Optional[BinanceNewOrderRespType] = None, - recv_window: Optional[str] = None, + list_client_order_id: str | None = None, + limit_client_order_id: str | None = None, + limit_strategy_id: int | None = None, + limit_strategy_type: int | None = None, + limit_iceberg_qty: str | None = None, + trailing_delta: str | None = None, + stop_client_order_id: str | None = None, + stop_strategy_id: int | None = None, + stop_strategy_type: int | None = None, + stop_limit_price: str | None = None, + stop_iceberg_qty: str | None = None, + stop_limit_time_in_force: BinanceTimeInForce | None = None, + new_order_resp_type: BinanceNewOrderRespType | None = None, + recv_window: str | None = None, ) -> BinanceSpotOrderOco: """ Send in a new spot OCO order to Binance. @@ -629,9 +629,9 @@ async def new_spot_oco( async def query_spot_oco( self, - order_list_id: Optional[str] = None, - orig_client_order_id: Optional[str] = None, - recv_window: Optional[str] = None, + order_list_id: str | None = None, + orig_client_order_id: str | None = None, + recv_window: str | None = None, ) -> BinanceSpotOrderOco: """ Check single spot OCO order information. @@ -652,7 +652,7 @@ async def query_spot_oco( async def cancel_all_open_orders( self, symbol: str, - recv_window: Optional[str] = None, + recv_window: str | None = None, ) -> bool: """ Cancel all active orders on a symbol, including OCO. @@ -672,10 +672,10 @@ async def cancel_all_open_orders( async def cancel_spot_oco( self, symbol: str, - order_list_id: Optional[str] = None, - list_client_order_id: Optional[str] = None, - new_client_order_id: Optional[str] = None, - recv_window: Optional[str] = None, + order_list_id: str | None = None, + list_client_order_id: str | None = None, + new_client_order_id: str | None = None, + recv_window: str | None = None, ) -> BinanceSpotOrderOco: """ Delete spot OCO order from Binance. @@ -697,11 +697,11 @@ async def cancel_spot_oco( async def query_spot_all_oco( self, - from_id: Optional[int] = None, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - limit: Optional[int] = None, - recv_window: Optional[str] = None, + from_id: int | None = None, + start_time: int | None = None, + end_time: int | None = None, + limit: int | None = None, + recv_window: str | None = None, ) -> list[BinanceSpotOrderOco]: """ Check all spot OCO orders' information, matching provided filter parameters. @@ -723,7 +723,7 @@ async def query_spot_all_oco( async def query_spot_all_open_oco( self, - recv_window: Optional[str] = None, + recv_window: str | None = None, ) -> list[BinanceSpotOrderOco]: """ Check all OPEN spot OCO orders' information. @@ -737,7 +737,7 @@ async def query_spot_all_open_oco( async def query_spot_account_info( self, - recv_window: Optional[str] = None, + recv_window: str | None = None, ) -> BinanceSpotAccountInfo: """ Check SPOT/MARGIN Binance account information. @@ -751,7 +751,7 @@ async def query_spot_account_info( async def query_spot_order_rate_limit( self, - recv_window: Optional[str] = None, + recv_window: str | None = None, ) -> list[BinanceRateLimit]: """ Check SPOT/MARGIN order count/rateLimit. diff --git a/nautilus_trader/adapters/binance/spot/http/market.py b/nautilus_trader/adapters/binance/spot/http/market.py index ca06e8e5144d..185171a164f2 100644 --- a/nautilus_trader/adapters/binance/spot/http/market.py +++ b/nautilus_trader/adapters/binance/spot/http/market.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional import msgspec @@ -73,11 +72,11 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ - symbol: Optional[BinanceSymbol] = None - symbols: Optional[BinanceSymbols] = None - permissions: Optional[BinanceSpotPermissions] = None + symbol: BinanceSymbol | None = None + symbols: BinanceSymbols | None = None + permissions: BinanceSpotPermissions | None = None - async def get(self, parameters: Optional[GetParameters] = None) -> BinanceSpotExchangeInfo: + async def get(self, parameters: GetParameters | None = None) -> BinanceSpotExchangeInfo: method_type = HttpMethod.GET raw = await self._method(method_type, parameters) return self._get_resp_decoder.decode(raw) @@ -163,9 +162,9 @@ def __init__( async def query_spot_exchange_info( self, - symbol: Optional[str] = None, - symbols: Optional[list[str]] = None, - permissions: Optional[BinanceSpotPermissions] = None, + symbol: str | None = None, + symbols: list[str] | None = None, + permissions: BinanceSpotPermissions | None = None, ) -> BinanceSpotExchangeInfo: """ Check Binance Spot exchange information. diff --git a/nautilus_trader/adapters/binance/spot/http/wallet.py b/nautilus_trader/adapters/binance/spot/http/wallet.py index 46a36a4691d4..7352a320ee07 100644 --- a/nautilus_trader/adapters/binance/spot/http/wallet.py +++ b/nautilus_trader/adapters/binance/spot/http/wallet.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional import msgspec @@ -71,8 +70,8 @@ class GetParameters(msgspec.Struct, omit_defaults=True, frozen=True): """ timestamp: str - symbol: Optional[BinanceSymbol] = None - recvWindow: Optional[str] = None + symbol: BinanceSymbol | None = None + recvWindow: str | None = None async def get(self, parameters: GetParameters) -> list[BinanceSpotTradeFee]: method_type = HttpMethod.GET @@ -119,8 +118,8 @@ def _timestamp(self) -> str: async def query_spot_trade_fees( self, - symbol: Optional[str] = None, - recv_window: Optional[str] = None, + symbol: str | None = None, + recv_window: str | None = None, ) -> list[BinanceSpotTradeFee]: fees = await self._endpoint_spot_trade_fee.get( parameters=self._endpoint_spot_trade_fee.GetParameters( diff --git a/nautilus_trader/adapters/binance/spot/providers.py b/nautilus_trader/adapters/binance/spot/providers.py index ae40e33033dc..1d753423eea0 100644 --- a/nautilus_trader/adapters/binance/spot/providers.py +++ b/nautilus_trader/adapters/binance/spot/providers.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Optional import msgspec @@ -75,7 +74,7 @@ def __init__( clock: LiveClock, account_type: BinanceAccountType = BinanceAccountType.SPOT, is_testnet: bool = False, - config: Optional[InstrumentProviderConfig] = None, + config: InstrumentProviderConfig | None = None, ): super().__init__( venue=BINANCE_VENUE, @@ -100,7 +99,7 @@ def __init__( self._decoder = msgspec.json.Decoder() self._encoder = msgspec.json.Encoder() - async def load_all_async(self, filters: Optional[dict] = None) -> None: + async def load_all_async(self, filters: dict | None = None) -> None: filters_str = "..." if not filters else f" with filters {filters}..." self._log.info(f"Loading all instruments{filters_str}") @@ -134,7 +133,7 @@ async def load_all_async(self, filters: Optional[dict] = None) -> None: async def load_ids_async( self, instrument_ids: list[InstrumentId], - filters: Optional[dict] = None, + filters: dict | None = None, ) -> None: if not instrument_ids: self._log.info("No instrument IDs given for loading.") @@ -182,7 +181,7 @@ async def load_ids_async( ts_event=millis_to_nanos(exchange_info.serverTime), ) - async def load_async(self, instrument_id: InstrumentId, filters: Optional[dict] = None) -> None: + async def load_async(self, instrument_id: InstrumentId, filters: dict | None = None) -> None: PyCondition.not_none(instrument_id, "instrument_id") PyCondition.equal(instrument_id.venue, self.venue, "instrument_id.venue", "self.venue") @@ -224,7 +223,7 @@ async def load_async(self, instrument_id: InstrumentId, filters: Optional[dict] def _parse_instrument( self, symbol_info: BinanceSpotSymbolInfo, - fee: Optional[BinanceSpotTradeFee], + fee: BinanceSpotTradeFee | None, ts_event: int, ) -> None: ts_init = self._clock.timestamp_ns() diff --git a/nautilus_trader/adapters/binance/spot/schemas/account.py b/nautilus_trader/adapters/binance/spot/schemas/account.py index 09fda50e7361..074cfeafd694 100644 --- a/nautilus_trader/adapters/binance/spot/schemas/account.py +++ b/nautilus_trader/adapters/binance/spot/schemas/account.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Optional import msgspec @@ -89,5 +88,5 @@ class BinanceSpotOrderOco(msgspec.Struct, frozen=True): listClientOrderId: str transactionTime: int symbol: str - orders: Optional[list[BinanceOrder]] = None # Included for ACK response type - orderReports: Optional[list[BinanceOrder]] = None # Included for FULL & RESPONSE types + orders: list[BinanceOrder] | None = None # Included for ACK response type + orderReports: list[BinanceOrder] | None = None # Included for FULL & RESPONSE types diff --git a/nautilus_trader/adapters/binance/spot/schemas/user.py b/nautilus_trader/adapters/binance/spot/schemas/user.py index cbad9c403d33..8d6c1ba43a31 100644 --- a/nautilus_trader/adapters/binance/spot/schemas/user.py +++ b/nautilus_trader/adapters/binance/spot/schemas/user.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Optional import msgspec @@ -148,8 +147,8 @@ class BinanceSpotOrderUpdateData(msgspec.Struct, kw_only=True): l: str # Order Last Filled Quantity z: str # Order Filled Accumulated Quantity L: str # Last Filled Price - n: Optional[str] = None # Commission, will not push if no commission - N: Optional[str] = None # Commission Asset, will not push if no commission + n: str | None = None # Commission, will not push if no commission + N: str | None = None # Commission Asset, will not push if no commission T: int # Order Trade Time t: int # Trade ID I: int # Ignore diff --git a/nautilus_trader/adapters/binance/websocket/client.py b/nautilus_trader/adapters/binance/websocket/client.py index 87a4a3e84448..19deb4e3271a 100644 --- a/nautilus_trader/adapters/binance/websocket/client.py +++ b/nautilus_trader/adapters/binance/websocket/client.py @@ -15,7 +15,8 @@ import asyncio import json -from typing import Any, Callable, Optional +from collections.abc import Callable +from typing import Any from nautilus_trader.adapters.binance.common.schemas.symbol import BinanceSymbol from nautilus_trader.common.clock import LiveClock @@ -65,7 +66,7 @@ def __init__( self._loop = loop self._streams: list[str] = [] - self._inner: Optional[WebSocketClient] = None + self._inner: WebSocketClient | None = None self._is_connecting = False self._msg_id: int = 0 @@ -253,7 +254,7 @@ async def unsubscribe_bars( async def subscribe_mini_ticker( self, - symbol: Optional[str] = None, + symbol: str | None = None, ) -> None: """ Subscribe to individual symbol or all symbols mini ticker stream. @@ -273,7 +274,7 @@ async def subscribe_mini_ticker( async def unsubscribe_mini_ticker( self, - symbol: Optional[str] = None, + symbol: str | None = None, ) -> None: """ Unsubscribe to individual symbol or all symbols mini ticker stream. @@ -286,7 +287,7 @@ async def unsubscribe_mini_ticker( async def subscribe_ticker( self, - symbol: Optional[str] = None, + symbol: str | None = None, ) -> None: """ Subscribe to individual symbol or all symbols ticker stream. @@ -306,7 +307,7 @@ async def subscribe_ticker( async def unsubscribe_ticker( self, - symbol: Optional[str] = None, + symbol: str | None = None, ) -> None: """ Unsubscribe from individual symbol or all symbols ticker stream. @@ -319,7 +320,7 @@ async def unsubscribe_ticker( async def subscribe_book_ticker( self, - symbol: Optional[str] = None, + symbol: str | None = None, ) -> None: """ Subscribe to individual symbol or all book tickers stream. @@ -338,7 +339,7 @@ async def subscribe_book_ticker( async def unsubscribe_book_ticker( self, - symbol: Optional[str] = None, + symbol: str | None = None, ) -> None: """ Unsubscribe from individual symbol or all book tickers. @@ -407,8 +408,8 @@ async def unsubscribe_diff_book_depth( async def subscribe_mark_price( self, - symbol: Optional[str] = None, - speed: Optional[int] = None, + symbol: str | None = None, + speed: int | None = None, ) -> None: """ Subscribe to aggregate mark price stream. @@ -423,8 +424,8 @@ async def subscribe_mark_price( async def unsubscribe_mark_price( self, - symbol: Optional[str] = None, - speed: Optional[int] = None, + symbol: str | None = None, + speed: int | None = None, ) -> None: """ Unsubscribe from aggregate mark price stream. diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index 12faab83e582..2f5780d09ed8 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -15,10 +15,10 @@ import asyncio import functools +from collections.abc import Callable from collections.abc import Coroutine from decimal import Decimal from inspect import iscoroutinefunction -from typing import Callable, Optional, Union # fmt: off import pandas as pd @@ -132,16 +132,16 @@ def __init__( self._incoming_msg_queue: asyncio.Queue = asyncio.Queue() # Tasks - self._watch_dog_task: Optional[asyncio.Task] = None - self._incoming_msg_reader_task: Optional[asyncio.Task] = None - self._incoming_msg_queue_task: Optional[asyncio.Task] = None + self._watch_dog_task: asyncio.Task | None = None + self._incoming_msg_reader_task: asyncio.Task | None = None + self._incoming_msg_queue_task: asyncio.Task | None = None # Event Flags self.is_ready: asyncio.Event = asyncio.Event() # Client is fully functional self.is_ib_ready: asyncio.Event = asyncio.Event() # Connectivity between IB and TWS # Hot caches - self._bar_type_to_last_bar: dict[str, Union[BarData, None]] = {} + self._bar_type_to_last_bar: dict[str, BarData | None] = {} self.registered_nautilus_clients: set = set() self._event_subscriptions: dict[str, Callable] = {} self._order_id_to_order_ref: dict[int, AccountOrderRef] = {} @@ -149,7 +149,7 @@ def __init__( # Temporary caches self._exec_id_details: dict[ str, - dict[str, Union[Execution, CommissionReport, str]], + dict[str, Execution | (CommissionReport | str)], ] = {} # Reset @@ -210,9 +210,9 @@ async def is_running_async(self, timeout: int = 300): def create_task( self, coro: Coroutine, - log_msg: Optional[str] = None, - actions: Optional[Callable] = None, - success: Optional[str] = None, + log_msg: str | None = None, + actions: Callable | None = None, + success: str | None = None, ) -> asyncio.Task: """ Run the given coroutine with error handling and optional callback actions when @@ -251,8 +251,8 @@ def create_task( def _on_task_completed( self, - actions: Optional[Callable], - success: Optional[str], + actions: Callable | None, + success: str | None, task: asyncio.Task, ) -> None: if task.exception(): @@ -1226,8 +1226,8 @@ def _process_bar_data( bar_type_str: str, bar: BarData, handle_revised_bars: bool, - historical: Optional[bool] = False, - ) -> Optional[Bar]: + historical: bool | None = False, + ) -> Bar | None: previous_bar = self._bar_type_to_last_bar.get(bar_type_str) previous_ts = 0 if not previous_bar else int(previous_bar.date) current_ts = int(bar.date) diff --git a/nautilus_trader/adapters/interactive_brokers/client/common.py b/nautilus_trader/adapters/interactive_brokers/client/common.py index 1c58e70d2350..a97081166988 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/common.py +++ b/nautilus_trader/adapters/interactive_brokers/client/common.py @@ -14,8 +14,9 @@ # ------------------------------------------------------------------------------------------------- import asyncio +from collections.abc import Callable from decimal import Decimal -from typing import Annotated, Any, Callable, NamedTuple, Optional, Union +from typing import Annotated, Any, NamedTuple import msgspec @@ -46,7 +47,7 @@ class Request(msgspec.Struct, frozen=True): """ req_id: Annotated[int, msgspec.Meta(gt=0)] - name: Union[str, tuple] + name: str | tuple handle: Callable cancel: Callable future: asyncio.Future @@ -62,7 +63,7 @@ class Subscription(msgspec.Struct, frozen=True): """ req_id: Annotated[int, msgspec.Meta(gt=0)] - name: Union[str, tuple] + name: str | tuple handle: Callable cancel: Callable last: Any @@ -77,7 +78,7 @@ class Base: """ def __init__(self): - self._req_id_to_name: dict[int, Union[str, tuple]] = {} # type: ignore + self._req_id_to_name: dict[int, str | tuple] = {} # type: ignore self._req_id_to_handle: dict[int, Callable] = {} # type: ignore self._req_id_to_cancel: dict[int, Callable] = {} # type: ignore @@ -102,8 +103,8 @@ def _validation_check(self, req_id: int, name: Any): def remove( self, - req_id: Optional[int] = None, - name: Optional[Union[InstrumentId, BarType, str]] = None, + req_id: int | None = None, + name: InstrumentId | (BarType | str) | None = None, ): if not req_id: req_id = self._name_to_req_id(name) @@ -118,8 +119,8 @@ def get_all(self): def get( self, - req_id: Optional[int] = None, - name: Optional[Union[str, tuple]] = None, + req_id: int | None = None, + name: str | tuple | None = None, ): raise NotImplementedError("method must be implemented in the subclass") @@ -136,7 +137,7 @@ def __init__(self): def add( self, req_id: int, - name: Union[str, tuple], + name: str | tuple, handle: Callable, cancel: Callable = lambda: None, ): @@ -149,8 +150,8 @@ def add( def get( self, - req_id: Optional[int] = None, - name: Optional[Union[str, tuple]] = None, + req_id: int | None = None, + name: str | tuple | None = None, ): if not req_id: req_id = self._name_to_req_id(name) @@ -184,7 +185,7 @@ def get_futures(self): def add( self, req_id: int, - name: Union[str, tuple], + name: str | tuple, handle: Callable, cancel: Callable = lambda: None, ): @@ -198,8 +199,8 @@ def add( def get( self, - req_id: Optional[int] = None, - name: Optional[Union[str, tuple]] = None, + req_id: int | None = None, + name: str | tuple | None = None, ): if not req_id: req_id = self._name_to_req_id(name) diff --git a/nautilus_trader/adapters/interactive_brokers/common.py b/nautilus_trader/adapters/interactive_brokers/common.py index 965b4a1fdab3..2e48c2329759 100644 --- a/nautilus_trader/adapters/interactive_brokers/common.py +++ b/nautilus_trader/adapters/interactive_brokers/common.py @@ -14,7 +14,7 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Literal, Optional +from typing import Literal from ibapi.common import UNSET_DECIMAL @@ -130,13 +130,13 @@ class IBContract(NautilusConfig, frozen=True, repr_omit_defaults=True): # combos comboLegsDescrip: str = "" comboLegs: list[ComboLeg] = None - deltaNeutralContract: Optional[DeltaNeutralContract] = None + deltaNeutralContract: DeltaNeutralContract | None = None # nautilus specific parameters - build_futures_chain: Optional[bool] = None - build_options_chain: Optional[bool] = None - min_expiry_days: Optional[int] = None - max_expiry_days: Optional[int] = None + build_futures_chain: bool | None = None + build_options_chain: bool | None = None + min_expiry_days: int | None = None + max_expiry_days: int | None = None class IBOrderTags(NautilusConfig, frozen=True, repr_omit_defaults=True): @@ -200,7 +200,7 @@ class IBContractDetails(NautilusConfig, frozen=True, repr_omit_defaults=True): underSymbol: str = "" underSecType: str = "" marketRuleIds: str = "" - secIdList: Optional[list] = None + secIdList: list | None = None realExpirationDate: str = "" lastTradeTime: str = "" stockType: str = "" diff --git a/nautilus_trader/adapters/interactive_brokers/config.py b/nautilus_trader/adapters/interactive_brokers/config.py index 5b851241b8b4..43f762479335 100644 --- a/nautilus_trader/adapters/interactive_brokers/config.py +++ b/nautilus_trader/adapters/interactive_brokers/config.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Literal, Optional +from typing import Literal from ibapi.common import MarketDataTypeEnum as IBMarketDataTypeEnum @@ -47,8 +47,8 @@ class InteractiveBrokersGatewayConfig(NautilusConfig, frozen=True): """ - username: Optional[str] = None - password: Optional[str] = None + username: str | None = None + password: str | None = None trading_mode: Literal["paper", "live"] = "paper" start: bool = False read_only_api: bool = True @@ -102,14 +102,14 @@ def __hash__(self): ), ) - load_contracts: Optional[frozenset[IBContract]] = None - build_options_chain: Optional[bool] = None - build_futures_chain: Optional[bool] = None - min_expiry_days: Optional[int] = None - max_expiry_days: Optional[int] = None + load_contracts: frozenset[IBContract] | None = None + build_options_chain: bool | None = None + build_futures_chain: bool | None = None + min_expiry_days: int | None = None + max_expiry_days: int | None = None - cache_validity_days: Optional[int] = None - pickle_path: Optional[str] = None + cache_validity_days: int | None = None + pickle_path: str | None = None class InteractiveBrokersDataClientConfig(LiveDataClientConfig, frozen=True): @@ -140,7 +140,7 @@ class InteractiveBrokersDataClientConfig(LiveDataClientConfig, frozen=True): ) ibg_host: str = "127.0.0.1" - ibg_port: Optional[int] = None + ibg_port: int | None = None ibg_client_id: int = 1 gateway: InteractiveBrokersGatewayConfig = InteractiveBrokersGatewayConfig() use_regular_trading_hours: bool = True @@ -169,9 +169,9 @@ class InteractiveBrokersExecClientConfig(LiveExecClientConfig, frozen=True): InteractiveBrokersInstrumentProviderConfig() ) ibg_host: str = "127.0.0.1" - ibg_port: Optional[int] = None + ibg_port: int | None = None ibg_client_id: int = 1 gateway: InteractiveBrokersGatewayConfig = InteractiveBrokersGatewayConfig() - account_id: Optional[str] = None + account_id: str | None = None # trade_outside_regular_hours (possible to set flag in order) diff --git a/nautilus_trader/adapters/interactive_brokers/data.py b/nautilus_trader/adapters/interactive_brokers/data.py index 3ccbd37d3700..afe66ae0c816 100644 --- a/nautilus_trader/adapters/interactive_brokers/data.py +++ b/nautilus_trader/adapters/interactive_brokers/data.py @@ -15,7 +15,7 @@ import asyncio from operator import attrgetter -from typing import Any, Optional, Union +from typing import Any import pandas as pd @@ -149,8 +149,8 @@ async def _subscribe_order_book_deltas( self, instrument_id: InstrumentId, book_type: BookType, - depth: Optional[int] = None, - kwargs: Optional[dict[str, Any]] = None, + depth: int | None = None, + kwargs: dict[str, Any] | None = None, ) -> None: raise NotImplementedError( # pragma: no cover "implement the `_subscribe_order_book_deltas` coroutine", # pragma: no cover @@ -160,8 +160,8 @@ async def _subscribe_order_book_snapshots( self, instrument_id: InstrumentId, book_type: BookType, - depth: Optional[int] = None, - kwargs: Optional[dict[str, Any]] = None, + depth: int | None = None, + kwargs: dict[str, Any] | None = None, ) -> None: raise NotImplementedError( # pragma: no cover "implement the `_subscribe_order_book_snapshots` coroutine", # pragma: no cover @@ -301,8 +301,8 @@ async def _request_quote_ticks( instrument_id: InstrumentId, limit: int, correlation_id: UUID4, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> None: if not (instrument := self._cache.instrument(instrument_id)): self._log.error( @@ -328,8 +328,8 @@ async def _request_trade_ticks( instrument_id: InstrumentId, limit: int, correlation_id: UUID4, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> None: if not (instrument := self._cache.instrument(instrument_id)): self._log.error( @@ -361,8 +361,8 @@ async def _handle_ticks_request( contract: IBContract, tick_type: str, limit: int, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ): if not start: limit = self._cache.tick_capacity @@ -370,7 +370,7 @@ async def _handle_ticks_request( if not end: end = pd.Timestamp.utcnow() - ticks: list[Union[QuoteTick, TradeTick]] = [] + ticks: list[QuoteTick | TradeTick] = [] while (start and end > start) or (len(ticks) < limit > 0): await self._client.is_running_async() ticks_part = await self._client.get_historical_ticks( @@ -392,8 +392,8 @@ async def _request_bars( bar_type: BarType, limit: int, correlation_id: UUID4, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> None: if not (instrument := self._cache.instrument(bar_type.instrument_id)): self._log.error( diff --git a/nautilus_trader/adapters/interactive_brokers/execution.py b/nautilus_trader/adapters/interactive_brokers/execution.py index 104cf9983e98..bfa5ed5ba905 100644 --- a/nautilus_trader/adapters/interactive_brokers/execution.py +++ b/nautilus_trader/adapters/interactive_brokers/execution.py @@ -16,7 +16,7 @@ import asyncio import json from decimal import Decimal -from typing import Any, Optional +from typing import Any import pandas as pd from ibapi.commission_report import CommissionReport @@ -210,9 +210,9 @@ async def _disconnect(self): async def generate_order_status_report( self, instrument_id: InstrumentId, - client_order_id: Optional[ClientOrderId] = None, - venue_order_id: Optional[VenueOrderId] = None, - ) -> Optional[OrderStatusReport]: + client_order_id: ClientOrderId | None = None, + venue_order_id: VenueOrderId | None = None, + ) -> OrderStatusReport | None: """ Generate an `OrderStatusReport` for the given order identifier parameter(s). If the order is not found, or an error occurs, then logs and returns ``None``. @@ -324,9 +324,9 @@ async def _parse_ib_order_to_order_status_report(self, ib_order: IBOrder): async def generate_order_status_reports( self, - instrument_id: Optional[InstrumentId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, open_only: bool = False, ) -> list[OrderStatusReport]: """ @@ -400,10 +400,10 @@ async def generate_order_status_reports( async def generate_trade_reports( self, - instrument_id: Optional[InstrumentId] = None, - venue_order_id: Optional[VenueOrderId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + venue_order_id: VenueOrderId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> list[TradeReport]: """ Generate a list of `TradeReport`s with optional query filters. The returned list @@ -431,9 +431,9 @@ async def generate_trade_reports( async def generate_position_status_reports( self, - instrument_id: Optional[InstrumentId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> list[PositionStatusReport]: """ Generate a list of `PositionStatusReport`s with optional query filters. The @@ -490,7 +490,7 @@ def _transform_order(self, order: Order) -> IBOrder: if value := getattr(order, key, None): setattr(ib_order, field, fn(value)) - if isinstance(order, (TrailingStopLimitOrder, TrailingStopMarketOrder)): + if isinstance(order, TrailingStopLimitOrder | TrailingStopMarketOrder): ib_order.auxPrice = float(order.trailing_offset) if order.trigger_price: ib_order.trailStopPrice = order.trigger_price.as_double() @@ -498,7 +498,7 @@ def _transform_order(self, order: Order) -> IBOrder: elif ( isinstance( order, - (MarketIfTouchedOrder, LimitIfTouchedOrder, StopLimitOrder, StopMarketOrder), + MarketIfTouchedOrder | LimitIfTouchedOrder | StopLimitOrder | StopMarketOrder, ) ) and order.trigger_price: ib_order.auxPrice = order.trigger_price.as_double() @@ -690,7 +690,7 @@ def _handle_order_event( self, status: OrderStatus, order: Order, - order_id: Optional[int] = None, + order_id: int | None = None, reason: str = "", ): if status == OrderStatus.SUBMITTED: diff --git a/nautilus_trader/adapters/interactive_brokers/factories.py b/nautilus_trader/adapters/interactive_brokers/factories.py index 747c0ade99e7..dc6c0a882aeb 100644 --- a/nautilus_trader/adapters/interactive_brokers/factories.py +++ b/nautilus_trader/adapters/interactive_brokers/factories.py @@ -16,7 +16,6 @@ import asyncio import os from functools import lru_cache -from typing import Optional # fmt: off from nautilus_trader.adapters.interactive_brokers.client import InteractiveBrokersClient @@ -51,7 +50,7 @@ def get_cached_ib_client( clock: LiveClock, logger: Logger, host: str = "127.0.0.1", - port: Optional[int] = None, + port: int | None = None, client_id: int = 1, gateway: InteractiveBrokersGatewayConfig = InteractiveBrokersGatewayConfig(), ) -> InteractiveBrokersClient: diff --git a/nautilus_trader/adapters/interactive_brokers/gateway.py b/nautilus_trader/adapters/interactive_brokers/gateway.py index eb13067d6881..375b609afd93 100644 --- a/nautilus_trader/adapters/interactive_brokers/gateway.py +++ b/nautilus_trader/adapters/interactive_brokers/gateway.py @@ -18,7 +18,7 @@ import warnings from enum import IntEnum from time import sleep -from typing import ClassVar, Optional +from typing import ClassVar try: @@ -53,13 +53,13 @@ def __init__( self, username: str, password: str, - host: Optional[str] = "localhost", - port: Optional[int] = None, - trading_mode: Optional[str] = "paper", + host: str | None = "localhost", + port: int | None = None, + trading_mode: str | None = "paper", start: bool = False, read_only_api: bool = True, timeout: int = 90, - logger: Optional[logging.Logger] = None, + logger: logging.Logger | None = None, ): username = username if username is not None else os.environ["TWS_USERNAME"] password = password if password is not None else os.environ["TWS_PASSWORD"] @@ -116,7 +116,7 @@ def is_logged_in(container) -> bool: return False return any(b"Forking :::" in line for line in logs.split(b"\n")) - def start(self, wait: Optional[int] = 90): + def start(self, wait: int | None = 90): """ Start the gateway. diff --git a/nautilus_trader/adapters/interactive_brokers/historic/async_actor.py b/nautilus_trader/adapters/interactive_brokers/historic/async_actor.py index af12449de364..86515d7e91cf 100644 --- a/nautilus_trader/adapters/interactive_brokers/historic/async_actor.py +++ b/nautilus_trader/adapters/interactive_brokers/historic/async_actor.py @@ -16,10 +16,10 @@ import asyncio import functools +from collections.abc import Callable # fmt: off from collections.abc import Coroutine -from typing import Callable, Optional import async_timeout @@ -38,13 +38,13 @@ class AsyncActor(Actor): def __init__(self, config: ActorConfig): super().__init__(config) - self.environment: Optional[Environment] = Environment.BACKTEST + self.environment: Environment | None = Environment.BACKTEST # Hot Cache self._pending_async_requests: dict[UUID4, asyncio.Event] = {} # Initialized in on_start - self._loop: Optional[asyncio.AbstractEventLoop] = None + self._loop: asyncio.AbstractEventLoop | None = None def on_start(self): if isinstance(self.clock, LiveClock): @@ -78,9 +78,9 @@ async def await_request(self, request_id: UUID4, timeout: int = 30): def create_task( self, coro: Coroutine, - log_msg: Optional[str] = None, - actions: Optional[Callable] = None, - success: Optional[str] = None, + log_msg: str | None = None, + actions: Callable | None = None, + success: str | None = None, ) -> asyncio.Task: """ Run the given coroutine with error handling and optional callback actions when @@ -119,8 +119,8 @@ def create_task( def _on_task_completed( self, - actions: Optional[Callable], - success: Optional[str], + actions: Callable | None, + success: str | None, task: asyncio.Task, ) -> None: if task.exception(): diff --git a/nautilus_trader/adapters/interactive_brokers/historic/bar_data.py b/nautilus_trader/adapters/interactive_brokers/historic/bar_data.py index 75a9d724f5ad..ec920980f4ed 100644 --- a/nautilus_trader/adapters/interactive_brokers/historic/bar_data.py +++ b/nautilus_trader/adapters/interactive_brokers/historic/bar_data.py @@ -14,7 +14,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Callable, Optional +from collections.abc import Callable import pandas as pd @@ -60,7 +60,7 @@ def __init__(self, config: BarDataDownloaderConfig): for bar_type in config.bar_types: self.bar_types.append(BarType.from_str(bar_type)) - self.handler: Optional[Callable] = config.handler + self.handler: Callable | None = config.handler self.freq: str = config.freq async def _on_start(self): diff --git a/nautilus_trader/adapters/interactive_brokers/parsing/execution.py b/nautilus_trader/adapters/interactive_brokers/parsing/execution.py index 678965259419..ca43441a5dbf 100644 --- a/nautilus_trader/adapters/interactive_brokers/parsing/execution.py +++ b/nautilus_trader/adapters/interactive_brokers/parsing/execution.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Callable +from collections.abc import Callable import pandas as pd diff --git a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py index 41a424e30882..caeb4091787d 100644 --- a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py +++ b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py @@ -17,7 +17,6 @@ import re import time from decimal import Decimal -from typing import Union import msgspec @@ -102,7 +101,7 @@ def _extract_isin(details: IBContractDetails): raise ValueError("No ISIN found") -def _tick_size_to_precision(tick_size: Union[float, Decimal]) -> int: +def _tick_size_to_precision(tick_size: float | Decimal) -> int: tick_size_str = f"{tick_size:.10f}" return len(tick_size_str.partition(".")[2].rstrip("0")) diff --git a/nautilus_trader/adapters/interactive_brokers/providers.py b/nautilus_trader/adapters/interactive_brokers/providers.py index f52dba35c367..f60b6a8c4cc4 100644 --- a/nautilus_trader/adapters/interactive_brokers/providers.py +++ b/nautilus_trader/adapters/interactive_brokers/providers.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- import copy -from typing import Optional, Union import pandas as pd from ibapi.contract import ContractDetails @@ -83,13 +82,13 @@ def __init__( self.contract_details: dict[str, IBContractDetails] = {} self.contract_id_to_instrument_id: dict[int, InstrumentId] = {} - async def load_all_async(self, filters: Optional[dict] = None) -> None: + async def load_all_async(self, filters: dict | None = None) -> None: await self.load_ids_async([]) async def load_ids_async( self, instrument_ids: list[InstrumentId], - filters: Optional[dict] = None, + filters: dict | None = None, ) -> None: # Parse and load InstrumentIds if self._load_ids_on_start: @@ -194,7 +193,7 @@ async def get_option_chain_details( min_expiry: pd.Timestamp, max_expiry: pd.Timestamp, last_trading_date: str, - exchange: Optional[str] = None, + exchange: str | None = None, ) -> list[ContractDetails]: if last_trading_date: expirations = [last_trading_date] @@ -236,8 +235,8 @@ async def get_option_chain_details( async def load_async( self, - instrument_id: Union[InstrumentId, IBContract], - filters: Optional[dict] = None, + instrument_id: InstrumentId | IBContract, + filters: dict | None = None, ): """ Search and load the instrument for the given IBContract. It is important that diff --git a/nautilus_trader/adapters/sandbox/execution.py b/nautilus_trader/adapters/sandbox/execution.py index 5a28028a7142..8c539b1a7bfd 100644 --- a/nautilus_trader/adapters/sandbox/execution.py +++ b/nautilus_trader/adapters/sandbox/execution.py @@ -15,7 +15,7 @@ import asyncio from decimal import Decimal -from typing import ClassVar, Optional +from typing import ClassVar import pandas as pd @@ -154,34 +154,34 @@ def disconnect(self) -> None: async def generate_order_status_report( self, instrument_id: InstrumentId, - client_order_id: Optional[ClientOrderId] = None, - venue_order_id: Optional[VenueOrderId] = None, - ) -> Optional[OrderStatusReport]: + client_order_id: ClientOrderId | None = None, + venue_order_id: VenueOrderId | None = None, + ) -> OrderStatusReport | None: return None async def generate_order_status_reports( self, - instrument_id: Optional[InstrumentId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, open_only: bool = False, ) -> list[OrderStatusReport]: return [] async def generate_trade_reports( self, - instrument_id: Optional[InstrumentId] = None, - venue_order_id: Optional[VenueOrderId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + venue_order_id: VenueOrderId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> list[TradeReport]: return [] async def generate_position_status_reports( self, - instrument_id: Optional[InstrumentId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> list[PositionStatusReport]: return [] diff --git a/nautilus_trader/adapters/tardis/loaders.py b/nautilus_trader/adapters/tardis/loaders.py index b5b81b15c9fd..9db2cbbbe18f 100644 --- a/nautilus_trader/adapters/tardis/loaders.py +++ b/nautilus_trader/adapters/tardis/loaders.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - from datetime import datetime from os import PathLike diff --git a/nautilus_trader/analysis/analyzer.py b/nautilus_trader/analysis/analyzer.py index 7aff1e394d4d..8ee545d892a2 100644 --- a/nautilus_trader/analysis/analyzer.py +++ b/nautilus_trader/analysis/analyzer.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - from datetime import datetime from decimal import Decimal from typing import Any @@ -372,7 +370,7 @@ def get_performance_stats_pnls( value = stat.calculate_from_realized_pnls(realized_pnls) if value is None: continue # Not implemented - if not isinstance(value, (int, float, str, bool)): + if not isinstance(value, int | float | str | bool): value = str(value) output[name] = value @@ -392,7 +390,7 @@ def get_performance_stats_returns(self) -> dict[str, Any]: value = stat.calculate_from_returns(self._returns) if value is None: continue # Not implemented - if not isinstance(value, (int, float, str, bool)): + if not isinstance(value, int | float | str | bool): value = str(value) output[name] = value @@ -413,7 +411,7 @@ def get_performance_stats_general(self) -> dict[str, Any]: value = stat.calculate_from_positions(self._positions) if value is None: continue # Not implemented - if not isinstance(value, (int, float, str, bool)): + if not isinstance(value, int | float | str | bool): value = str(value) output[name] = value @@ -486,7 +484,7 @@ def get_stats_general_formatted(self) -> list[str]: output = [] for k, v in stats.items(): padding = max_length - len(k) + 1 - v_formatted = f"{v:_}" if isinstance(v, (int, float, Decimal)) else str(v) + v_formatted = f"{v:_}" if isinstance(v, int | float | Decimal) else str(v) output.append(f"{k}: {' ' * padding}{v_formatted}") return output diff --git a/nautilus_trader/analysis/reporter.py b/nautilus_trader/analysis/reporter.py index 21f3ac2ebf3c..0e820ee5cf95 100644 --- a/nautilus_trader/analysis/reporter.py +++ b/nautilus_trader/analysis/reporter.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import msgspec import pandas as pd diff --git a/nautilus_trader/analysis/statistic.py b/nautilus_trader/analysis/statistic.py index 7e71b134a523..e12a65099218 100644 --- a/nautilus_trader/analysis/statistic.py +++ b/nautilus_trader/analysis/statistic.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import re from typing import Any diff --git a/nautilus_trader/analysis/statistics/expectancy.py b/nautilus_trader/analysis/statistics/expectancy.py index fe6da336b65b..281bba039e30 100644 --- a/nautilus_trader/analysis/statistics/expectancy.py +++ b/nautilus_trader/analysis/statistics/expectancy.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import pandas as pd @@ -27,14 +27,14 @@ class Expectancy(PortfolioStatistic): Calculates the expectancy from a realized PnLs series. """ - def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Optional[Any]: + def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Any | None: # Preconditions if realized_pnls is None or realized_pnls.empty: return 0.0 # Calculate statistic - avg_winner: Optional[float] = AvgWinner().calculate_from_realized_pnls(realized_pnls) - avg_loser: Optional[float] = AvgLoser().calculate_from_realized_pnls(realized_pnls) + avg_winner: float | None = AvgWinner().calculate_from_realized_pnls(realized_pnls) + avg_loser: float | None = AvgLoser().calculate_from_realized_pnls(realized_pnls) if avg_winner is None or avg_loser is None: return 0.0 diff --git a/nautilus_trader/analysis/statistics/long_ratio.py b/nautilus_trader/analysis/statistics/long_ratio.py index b23d42c2a82d..d15841e09eea 100644 --- a/nautilus_trader/analysis/statistics/long_ratio.py +++ b/nautilus_trader/analysis/statistics/long_ratio.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any from nautilus_trader.analysis.statistic import PortfolioStatistic from nautilus_trader.model.enums import OrderSide @@ -34,7 +34,7 @@ class LongRatio(PortfolioStatistic): def __init__(self, precision: int = 2): self.precision = precision - def calculate_from_positions(self, positions: list[Position]) -> Optional[Any]: + def calculate_from_positions(self, positions: list[Position]) -> Any | None: # Preconditions if not positions: return None diff --git a/nautilus_trader/analysis/statistics/loser_avg.py b/nautilus_trader/analysis/statistics/loser_avg.py index 481927fa334d..d56a7804e712 100644 --- a/nautilus_trader/analysis/statistics/loser_avg.py +++ b/nautilus_trader/analysis/statistics/loser_avg.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import pandas as pd @@ -25,7 +25,7 @@ class AvgLoser(PortfolioStatistic): Calculates the average loser from a series of PnLs. """ - def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Optional[Any]: + def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Any | None: # Preconditions if realized_pnls is None or realized_pnls.empty: return 0.0 diff --git a/nautilus_trader/analysis/statistics/loser_max.py b/nautilus_trader/analysis/statistics/loser_max.py index 93358947fcc7..e71337304756 100644 --- a/nautilus_trader/analysis/statistics/loser_max.py +++ b/nautilus_trader/analysis/statistics/loser_max.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import numpy as np import pandas as pd @@ -26,7 +26,7 @@ class MaxLoser(PortfolioStatistic): Calculates the maximum loser from a series of PnLs. """ - def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Optional[Any]: + def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Any | None: # Preconditions if realized_pnls is None or realized_pnls.empty: return 0.0 diff --git a/nautilus_trader/analysis/statistics/loser_min.py b/nautilus_trader/analysis/statistics/loser_min.py index 94ad39bce964..f87201592ce6 100644 --- a/nautilus_trader/analysis/statistics/loser_min.py +++ b/nautilus_trader/analysis/statistics/loser_min.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import numpy as np import pandas as pd @@ -26,7 +26,7 @@ class MinLoser(PortfolioStatistic): Calculates the minimum loser from a series of PnLs. """ - def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Optional[Any]: + def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Any | None: # Preconditions if realized_pnls is None or realized_pnls.empty: return 0.0 diff --git a/nautilus_trader/analysis/statistics/profit_factor.py b/nautilus_trader/analysis/statistics/profit_factor.py index 9ba3f96721c7..46d7d1c034a2 100644 --- a/nautilus_trader/analysis/statistics/profit_factor.py +++ b/nautilus_trader/analysis/statistics/profit_factor.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import numpy as np import pandas as pd @@ -26,7 +26,7 @@ class ProfitFactor(PortfolioStatistic): Calculates the annualized profit factor or ratio (wins/loss). """ - def calculate_from_returns(self, returns: pd.Series) -> Optional[Any]: + def calculate_from_returns(self, returns: pd.Series) -> Any | None: # Preconditions if not self._check_valid_returns(returns): return np.nan diff --git a/nautilus_trader/analysis/statistics/returns_avg.py b/nautilus_trader/analysis/statistics/returns_avg.py index 748d96a812d1..8dd681fe44f7 100644 --- a/nautilus_trader/analysis/statistics/returns_avg.py +++ b/nautilus_trader/analysis/statistics/returns_avg.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import numpy as np import pandas as pd @@ -30,7 +30,7 @@ class ReturnsAverage(PortfolioStatistic): def name(self) -> str: return "Average (Return)" - def calculate_from_returns(self, returns: pd.Series) -> Optional[Any]: + def calculate_from_returns(self, returns: pd.Series) -> Any | None: # Preconditions if not self._check_valid_returns(returns): return np.nan diff --git a/nautilus_trader/analysis/statistics/returns_avg_loss.py b/nautilus_trader/analysis/statistics/returns_avg_loss.py index 24ca646870c4..1d4cb0d5a52f 100644 --- a/nautilus_trader/analysis/statistics/returns_avg_loss.py +++ b/nautilus_trader/analysis/statistics/returns_avg_loss.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import numpy as np import pandas as pd @@ -30,7 +30,7 @@ class ReturnsAverageLoss(PortfolioStatistic): def name(self) -> str: return "Average Loss (Return)" - def calculate_from_returns(self, returns: pd.Series) -> Optional[Any]: + def calculate_from_returns(self, returns: pd.Series) -> Any | None: # Preconditions if not self._check_valid_returns(returns): return np.nan diff --git a/nautilus_trader/analysis/statistics/returns_avg_win.py b/nautilus_trader/analysis/statistics/returns_avg_win.py index 14d9fc16a6aa..6a4c8a34938a 100644 --- a/nautilus_trader/analysis/statistics/returns_avg_win.py +++ b/nautilus_trader/analysis/statistics/returns_avg_win.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import numpy as np import pandas as pd @@ -30,7 +30,7 @@ class ReturnsAverageWin(PortfolioStatistic): def name(self) -> str: return "Average Win (Return)" - def calculate_from_returns(self, returns: pd.Series) -> Optional[Any]: + def calculate_from_returns(self, returns: pd.Series) -> Any | None: # Preconditions if not self._check_valid_returns(returns): return np.nan diff --git a/nautilus_trader/analysis/statistics/returns_volatility.py b/nautilus_trader/analysis/statistics/returns_volatility.py index 5432e5cc7416..29d91999ed99 100644 --- a/nautilus_trader/analysis/statistics/returns_volatility.py +++ b/nautilus_trader/analysis/statistics/returns_volatility.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import numpy as np import pandas as pd @@ -41,7 +41,7 @@ def __init__(self, period: int = 252): def name(self) -> str: return f"Returns Volatility ({self.period} days)" - def calculate_from_returns(self, returns: pd.Series) -> Optional[Any]: + def calculate_from_returns(self, returns: pd.Series) -> Any | None: # Preconditions if not self._check_valid_returns(returns): return np.nan diff --git a/nautilus_trader/analysis/statistics/risk_return_ratio.py b/nautilus_trader/analysis/statistics/risk_return_ratio.py index b58f47743585..cc1c74c50ca9 100644 --- a/nautilus_trader/analysis/statistics/risk_return_ratio.py +++ b/nautilus_trader/analysis/statistics/risk_return_ratio.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import numpy as np import pandas as pd @@ -26,7 +26,7 @@ class RiskReturnRatio(PortfolioStatistic): Calculates the return on risk ratio. """ - def calculate_from_returns(self, returns: pd.Series) -> Optional[Any]: + def calculate_from_returns(self, returns: pd.Series) -> Any | None: # Preconditions if not self._check_valid_returns(returns): return np.nan diff --git a/nautilus_trader/analysis/statistics/sharpe_ratio.py b/nautilus_trader/analysis/statistics/sharpe_ratio.py index 503eb83fad6f..62890ef326d9 100644 --- a/nautilus_trader/analysis/statistics/sharpe_ratio.py +++ b/nautilus_trader/analysis/statistics/sharpe_ratio.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import numpy as np import pandas as pd @@ -41,7 +41,7 @@ def __init__(self, period: int = 252): def name(self) -> str: return f"Sharpe Ratio ({self.period} days)" - def calculate_from_returns(self, returns: pd.Series) -> Optional[Any]: + def calculate_from_returns(self, returns: pd.Series) -> Any | None: # Preconditions if not self._check_valid_returns(returns): return np.nan diff --git a/nautilus_trader/analysis/statistics/sortino_ratio.py b/nautilus_trader/analysis/statistics/sortino_ratio.py index 8b5423ff36b5..8f7809ee4594 100644 --- a/nautilus_trader/analysis/statistics/sortino_ratio.py +++ b/nautilus_trader/analysis/statistics/sortino_ratio.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import numpy as np import pandas as pd @@ -41,7 +41,7 @@ def __init__(self, period: int = 252): def name(self) -> str: return f"Sortino Ratio ({self.period} days)" - def calculate_from_returns(self, returns: pd.Series) -> Optional[Any]: + def calculate_from_returns(self, returns: pd.Series) -> Any | None: # Preconditions if not self._check_valid_returns(returns): return np.nan diff --git a/nautilus_trader/analysis/statistics/win_rate.py b/nautilus_trader/analysis/statistics/win_rate.py index b5407f903b26..c9ed4c0d903d 100644 --- a/nautilus_trader/analysis/statistics/win_rate.py +++ b/nautilus_trader/analysis/statistics/win_rate.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import pandas as pd @@ -25,7 +25,7 @@ class WinRate(PortfolioStatistic): Calculates the win rate from a realized PnLs series. """ - def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Optional[Any]: + def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Any | None: # Preconditions if realized_pnls is None or realized_pnls.empty: return 0.0 diff --git a/nautilus_trader/analysis/statistics/winner_avg.py b/nautilus_trader/analysis/statistics/winner_avg.py index 3269fc7c645f..a0171bfb2d88 100644 --- a/nautilus_trader/analysis/statistics/winner_avg.py +++ b/nautilus_trader/analysis/statistics/winner_avg.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import pandas as pd @@ -25,7 +25,7 @@ class AvgWinner(PortfolioStatistic): Calculates the average winner from a series of PnLs. """ - def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Optional[Any]: + def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Any | None: # Preconditions if realized_pnls is None or realized_pnls.empty: return 0.0 diff --git a/nautilus_trader/analysis/statistics/winner_max.py b/nautilus_trader/analysis/statistics/winner_max.py index 91ac4cc3aa2c..2511dc91e203 100644 --- a/nautilus_trader/analysis/statistics/winner_max.py +++ b/nautilus_trader/analysis/statistics/winner_max.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import pandas as pd @@ -25,7 +25,7 @@ class MaxWinner(PortfolioStatistic): Calculates the maximum winner from a series of PnLs. """ - def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Optional[Any]: + def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Any | None: # Preconditions if realized_pnls is None or realized_pnls.empty: return 0.0 diff --git a/nautilus_trader/analysis/statistics/winner_min.py b/nautilus_trader/analysis/statistics/winner_min.py index 634dd012697d..b77f5c0a88cc 100644 --- a/nautilus_trader/analysis/statistics/winner_min.py +++ b/nautilus_trader/analysis/statistics/winner_min.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import numpy as np import pandas as pd @@ -26,7 +26,7 @@ class MinWinner(PortfolioStatistic): Calculates the minimum winner from a series of PnLs. """ - def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Optional[Any]: + def calculate_from_realized_pnls(self, realized_pnls: pd.Series) -> Any | None: # Preconditions if realized_pnls is None or realized_pnls.empty: return 0.0 diff --git a/nautilus_trader/backtest/__main__.py b/nautilus_trader/backtest/__main__.py index 5a9ae3d37815..adf36c96b92b 100644 --- a/nautilus_trader/backtest/__main__.py +++ b/nautilus_trader/backtest/__main__.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional import click import fsspec @@ -27,8 +26,8 @@ @click.option("--raw", help="A raw string configs list") @click.option("--fsspec-url", help="A fsspec url to read a list of configs from") def main( - raw: Optional[str] = None, - fsspec_url: Optional[str] = None, + raw: str | None = None, + fsspec_url: str | None = None, ): assert raw is not None or fsspec_url is not None, "Must pass one of `raw` or `fsspec_url`" if fsspec_url and raw is None: diff --git a/nautilus_trader/backtest/engine.pyx b/nautilus_trader/backtest/engine.pyx index efb4d16a1f74..11527662a3e6 100644 --- a/nautilus_trader/backtest/engine.pyx +++ b/nautilus_trader/backtest/engine.pyx @@ -15,7 +15,8 @@ import pickle from decimal import Decimal -from typing import Optional, Union +from typing import Optional +from typing import Union import pandas as pd diff --git a/nautilus_trader/backtest/node.py b/nautilus_trader/backtest/node.py index ee2877bd6d4d..54f506cebb78 100644 --- a/nautilus_trader/backtest/node.py +++ b/nautilus_trader/backtest/node.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - from decimal import Decimal import pandas as pd diff --git a/nautilus_trader/backtest/results.py b/nautilus_trader/backtest/results.py index a3cafcfe10b9..f30a6beaeeb4 100644 --- a/nautilus_trader/backtest/results.py +++ b/nautilus_trader/backtest/results.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - from dataclasses import dataclass diff --git a/nautilus_trader/common/clock.pyx b/nautilus_trader/common/clock.pyx index 27840445e90c..a88df5cb4a04 100644 --- a/nautilus_trader/common/clock.pyx +++ b/nautilus_trader/common/clock.pyx @@ -14,7 +14,8 @@ # ------------------------------------------------------------------------------------------------- import asyncio -from typing import Callable, Optional +from typing import Callable +from typing import Optional import cython import numpy as np diff --git a/nautilus_trader/common/providers.py b/nautilus_trader/common/providers.py index bb67100c32b6..cee93dad81ad 100644 --- a/nautilus_trader/common/providers.py +++ b/nautilus_trader/common/providers.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import asyncio from nautilus_trader.common.logging import Logger diff --git a/nautilus_trader/common/throttler.pyx b/nautilus_trader/common/throttler.pyx index abdb7b6a27fb..3cfdcbd47da0 100644 --- a/nautilus_trader/common/throttler.pyx +++ b/nautilus_trader/common/throttler.pyx @@ -13,7 +13,9 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Callable, Optional +from typing import Any +from typing import Callable +from typing import Optional from cpython.datetime cimport timedelta from libc.stdint cimport int64_t diff --git a/nautilus_trader/config/backtest.py b/nautilus_trader/config/backtest.py index e12e078fcb1a..1557c4947512 100644 --- a/nautilus_trader/config/backtest.py +++ b/nautilus_trader/config/backtest.py @@ -16,8 +16,9 @@ import hashlib import importlib import sys +from collections.abc import Callable from decimal import Decimal -from typing import Any, Callable, Optional, Union +from typing import Any import msgspec import pandas as pd @@ -45,9 +46,9 @@ class BacktestVenueConfig(NautilusConfig, frozen=True): oms_type: str account_type: str starting_balances: list[str] - base_currency: Optional[str] = None + base_currency: str | None = None default_leverage: float = 1.0 - leverages: Optional[dict[str, float]] = None + leverages: dict[str, float] | None = None book_type: str = "L1_MBP" routing: bool = False frozen_account: bool = False @@ -58,7 +59,7 @@ class BacktestVenueConfig(NautilusConfig, frozen=True): use_random_ids: bool = False use_reduce_only: bool = True # fill_model: Optional[FillModel] = None # TODO(cs): Implement - modules: Optional[list[ImportableConfig]] = None + modules: list[ImportableConfig] | None = None class BacktestDataConfig(NautilusConfig, frozen=True): @@ -68,16 +69,16 @@ class BacktestDataConfig(NautilusConfig, frozen=True): catalog_path: str data_cls: str - catalog_fs_protocol: Optional[str] = None - catalog_fs_storage_options: Optional[dict] = None - instrument_id: Optional[str] = None - start_time: Optional[Union[str, int]] = None - end_time: Optional[Union[str, int]] = None - filter_expr: Optional[str] = None - client_id: Optional[str] = None - metadata: Optional[dict] = None - bar_spec: Optional[str] = None - batch_size: Optional[int] = 10_000 + catalog_fs_protocol: str | None = None + catalog_fs_storage_options: dict | None = None + instrument_id: str | None = None + start_time: str | int | None = None + end_time: str | int | None = None + filter_expr: str | None = None + client_id: str | None = None + metadata: dict | None = None + bar_spec: str | None = None + batch_size: int | None = 10_000 @property def data_type(self) -> type: @@ -92,7 +93,7 @@ def data_type(self) -> type: def query(self) -> dict[str, Any]: if self.data_cls is Bar and self.bar_spec: bar_type = f"{self.instrument_id}-{self.bar_spec}-EXTERNAL" - filter_expr: Optional[str] = f'field("bar_type") == "{bar_type}"' + filter_expr: str | None = f'field("bar_type") == "{bar_type}"' else: filter_expr = self.filter_expr @@ -128,8 +129,8 @@ def catalog(self) -> ParquetDataCatalog: def load( self, - start_time: Optional[pd.Timestamp] = None, - end_time: Optional[pd.Timestamp] = None, + start_time: pd.Timestamp | None = None, + end_time: pd.Timestamp | None = None, ) -> CatalogDataResult: query = self.query query.update( @@ -229,15 +230,15 @@ class BacktestRunConfig(NautilusConfig, frozen=True): venues: list[BacktestVenueConfig] data: list[BacktestDataConfig] - engine: Optional[BacktestEngineConfig] = None - batch_size_bytes: Optional[int] = None + engine: BacktestEngineConfig | None = None + batch_size_bytes: int | None = None @property def id(self): return tokenize_config(self.dict()) -def parse_filters_expr(s: Optional[str]): +def parse_filters_expr(s: str | None): # TODO (bm) - could we do this better, probably requires writing our own parser? """ Parse a pyarrow.dataset filter expression from a string. @@ -274,7 +275,7 @@ def safer_eval(input_string): def json_encoder(x): - if isinstance(x, (str, Decimal)): + if isinstance(x, str | Decimal): return str(x) elif isinstance(x, type) and hasattr(x, "fully_qualified_name"): return x.fully_qualified_name() diff --git a/nautilus_trader/config/common.py b/nautilus_trader/config/common.py index 4efe534d7dce..667bb82363c3 100644 --- a/nautilus_trader/config/common.py +++ b/nautilus_trader/config/common.py @@ -15,7 +15,7 @@ import importlib import importlib.util -from typing import Any, Optional +from typing import Any import fsspec import msgspec @@ -138,7 +138,7 @@ class CacheConfig(NautilusConfig, frozen=True): bar_capacity: PositiveInt = 10_000 snapshot_orders: bool = False snapshot_positions: bool = False - snapshot_positions_interval: Optional[PositiveFloat] = None + snapshot_positions_interval: PositiveFloat | None = None class CacheDatabaseConfig(NautilusConfig, frozen=True): @@ -169,9 +169,9 @@ class CacheDatabaseConfig(NautilusConfig, frozen=True): type: str = "in-memory" host: str = "localhost" - port: Optional[int] = None - username: Optional[str] = None - password: Optional[str] = None + port: int | None = None + username: str | None = None + password: str | None = None ssl: bool = False flush_on_start: bool = False timestamps_as_iso8601: bool = False @@ -208,9 +208,9 @@ def __hash__(self): return hash((self.load_all, self.load_ids, self.filters)) load_all: bool = False - load_ids: Optional[frozenset[str]] = None - filters: Optional[dict[str, Any]] = None - filter_callable: Optional[str] = None + load_ids: frozenset[str] | None = None + filters: dict[str, Any] | None = None + filter_callable: str | None = None log_warnings: bool = True @@ -319,11 +319,11 @@ class StreamingConfig(NautilusConfig, frozen=True): """ catalog_path: str - fs_protocol: Optional[str] = None - fs_storage_options: Optional[dict] = None - flush_interval_ms: Optional[int] = None + fs_protocol: str | None = None + fs_storage_options: dict | None = None + flush_interval_ms: int | None = None replace_existing: bool = False - include_types: Optional[list[str]] = None + include_types: list[str] | None = None @property def fs(self): @@ -353,8 +353,8 @@ class DataCatalogConfig(NautilusConfig, frozen=True): """ path: str - fs_protocol: Optional[str] = None - fs_storage_options: Optional[dict] = None + fs_protocol: str | None = None + fs_storage_options: dict | None = None class ActorConfig(NautilusConfig, kw_only=True, frozen=True): @@ -369,7 +369,7 @@ class ActorConfig(NautilusConfig, kw_only=True, frozen=True): """ - component_id: Optional[str] = None + component_id: str | None = None class ImportableActorConfig(NautilusConfig, frozen=True): @@ -446,10 +446,10 @@ class StrategyConfig(NautilusConfig, kw_only=True, frozen=True): """ - strategy_id: Optional[str] = None - order_id_tag: Optional[str] = None - oms_type: Optional[str] = None - external_order_claims: Optional[list[str]] = None + strategy_id: str | None = None + order_id_tag: str | None = None + oms_type: str | None = None + external_order_claims: list[str] | None = None manage_gtd_expiry: bool = False @@ -565,7 +565,7 @@ class ExecAlgorithmConfig(NautilusConfig, kw_only=True, frozen=True): """ - exec_algorithm_id: Optional[str] = None + exec_algorithm_id: str | None = None class ImportableExecAlgorithmConfig(NautilusConfig, frozen=True): @@ -643,9 +643,9 @@ class TracingConfig(NautilusConfig, frozen=True): """ - stdout_level: Optional[str] = None - stderr_level: Optional[str] = None - file_level: Optional[tuple[str, str, str]] = None + stdout_level: str | None = None + stderr_level: str | None = None + file_level: tuple[str, str, str] | None = None class LoggingConfig(NautilusConfig, frozen=True): @@ -678,11 +678,11 @@ class LoggingConfig(NautilusConfig, frozen=True): """ log_level: str = "INFO" - log_level_file: Optional[str] = None - log_directory: Optional[str] = None - log_file_name: Optional[str] = None - log_file_format: Optional[str] = None - log_component_levels: Optional[dict[str, str]] = None + log_level_file: str | None = None + log_directory: str | None = None + log_file_name: str | None = None + log_file_format: str | None = None + log_component_levels: dict[str, str] | None = None bypass_logging: bool = False @@ -741,24 +741,24 @@ class NautilusKernelConfig(NautilusConfig, frozen=True): environment: Environment trader_id: str - instance_id: Optional[str] = None - cache: Optional[CacheConfig] = None - cache_database: Optional[CacheDatabaseConfig] = None - 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 + instance_id: str | None = None + cache: CacheConfig | None = None + cache_database: CacheDatabaseConfig | None = None + data_engine: DataEngineConfig | None = None + risk_engine: RiskEngineConfig | None = None + exec_engine: ExecEngineConfig | None = None + emulator: OrderEmulatorConfig | None = None + streaming: StreamingConfig | None = None + catalog: DataCatalogConfig | None = None actors: list[ImportableActorConfig] = [] strategies: list[ImportableStrategyConfig] = [] exec_algorithms: list[ImportableExecAlgorithmConfig] = [] - controller: Optional[ImportableControllerConfig] = None + controller: ImportableControllerConfig | None = None load_state: bool = False save_state: bool = False loop_debug: bool = False - logging: Optional[LoggingConfig] = None - tracing: Optional[TracingConfig] = None + logging: LoggingConfig | None = None + tracing: TracingConfig | None = None timeout_connection: PositiveFloat = 10.0 timeout_reconciliation: PositiveFloat = 10.0 timeout_portfolio: PositiveFloat = 10.0 @@ -786,7 +786,7 @@ class ImportableConfig(NautilusConfig, frozen=True): path: str config: dict = {} - factory: Optional[ImportableFactoryConfig] = None + factory: ImportableFactoryConfig | None = None @staticmethod def is_importable(data: dict): diff --git a/nautilus_trader/config/live.py b/nautilus_trader/config/live.py index 0535ce7c7fc0..7ef016db53c5 100644 --- a/nautilus_trader/config/live.py +++ b/nautilus_trader/config/live.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional from nautilus_trader.common import Environment from nautilus_trader.config.common import DataEngineConfig @@ -91,7 +90,7 @@ class LiveExecEngineConfig(ExecEngineConfig, frozen=True): """ reconciliation: bool = True - reconciliation_lookback_mins: Optional[NonNegativeInt] = None + reconciliation_lookback_mins: NonNegativeInt | None = None filter_unclaimed_external_orders: bool = False filter_position_reports: bool = False inflight_check_interval_ms: NonNegativeInt = 2_000 @@ -114,7 +113,7 @@ class RoutingConfig(NautilusConfig, frozen=True): """ default: bool = False - venues: Optional[frozenset[str]] = None + venues: frozenset[str] | None = None class LiveDataClientConfig(NautilusConfig, frozen=True): @@ -190,4 +189,4 @@ class TradingNodeConfig(NautilusKernelConfig, frozen=True): exec_engine: LiveExecEngineConfig = LiveExecEngineConfig() data_clients: dict[str, LiveDataClientConfig] = {} exec_clients: dict[str, LiveExecClientConfig] = {} - heartbeat_interval: Optional[PositiveFloat] = None + heartbeat_interval: PositiveFloat | None = None diff --git a/nautilus_trader/core/message.pyx b/nautilus_trader/core/message.pyx index 3e56bbc5b487..a5dfa4e9be82 100644 --- a/nautilus_trader/core/message.pyx +++ b/nautilus_trader/core/message.pyx @@ -13,7 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Callable +from typing import Any +from typing import Callable import cython diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index f6e060e99ebe..4a9290be714c 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -1,6 +1,5 @@ # ruff: noqa: UP007 PYI021 PYI044 PYI053 # fmt: off -from __future__ import annotations import datetime as dt from collections.abc import Awaitable diff --git a/nautilus_trader/data/engine.pyx b/nautilus_trader/data/engine.pyx index c8f162e9babe..ae8871373bc6 100644 --- a/nautilus_trader/data/engine.pyx +++ b/nautilus_trader/data/engine.pyx @@ -29,7 +29,8 @@ Alternative implementations can be written on top of the generic engine - which just need to override the `execute`, `process`, `send` and `receive` methods. """ -from typing import Callable, Optional +from typing import Callable +from typing import Optional from nautilus_trader.common.enums import LogColor from nautilus_trader.config import DataEngineConfig diff --git a/nautilus_trader/data/messages.pyx b/nautilus_trader/data/messages.pyx index 08722333a567..029ad41e4d86 100644 --- a/nautilus_trader/data/messages.pyx +++ b/nautilus_trader/data/messages.pyx @@ -13,7 +13,9 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Callable, Optional +from typing import Any +from typing import Callable +from typing import Optional from libc.stdint cimport uint64_t diff --git a/nautilus_trader/examples/algorithms/blank.py b/nautilus_trader/examples/algorithms/blank.py index 5dcacad73380..99abba0621dc 100644 --- a/nautilus_trader/examples/algorithms/blank.py +++ b/nautilus_trader/examples/algorithms/blank.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional from nautilus_trader.common.logging import LogColor from nautilus_trader.config.common import ExecAlgorithmConfig @@ -33,7 +32,7 @@ class MyExecAlgorithmConfig(ExecAlgorithmConfig, frozen=True): """ - exec_algorithm_id: Optional[str] = None + exec_algorithm_id: str | None = None class MyExecAlgorithm(ExecAlgorithm): diff --git a/nautilus_trader/examples/algorithms/twap.py b/nautilus_trader/examples/algorithms/twap.py index ec760a03e460..1a1c84af186e 100644 --- a/nautilus_trader/examples/algorithms/twap.py +++ b/nautilus_trader/examples/algorithms/twap.py @@ -17,7 +17,6 @@ from datetime import timedelta from decimal import ROUND_DOWN from decimal import Decimal -from typing import Optional from nautilus_trader.common.enums import LogColor from nautilus_trader.common.timer import TimeEvent @@ -47,7 +46,7 @@ class TWAPExecAlgorithmConfig(ExecAlgorithmConfig, frozen=True): """ - exec_algorithm_id: Optional[str] = "TWAP" + exec_algorithm_id: str | None = "TWAP" class TWAPExecAlgorithm(ExecAlgorithm): @@ -72,7 +71,7 @@ class TWAPExecAlgorithm(ExecAlgorithm): """ - def __init__(self, config: Optional[TWAPExecAlgorithmConfig] = None) -> None: + def __init__(self, config: TWAPExecAlgorithmConfig | None = None) -> None: if config is None: config = TWAPExecAlgorithmConfig() super().__init__(config) diff --git a/nautilus_trader/examples/strategies/ema_cross_bracket.py b/nautilus_trader/examples/strategies/ema_cross_bracket.py index c28b2bb364a4..b67fb402ac65 100644 --- a/nautilus_trader/examples/strategies/ema_cross_bracket.py +++ b/nautilus_trader/examples/strategies/ema_cross_bracket.py @@ -15,7 +15,6 @@ from datetime import timedelta from decimal import Decimal -from typing import Optional from nautilus_trader.common.enums import LogColor from nautilus_trader.config import StrategyConfig @@ -125,7 +124,7 @@ def __init__(self, config: EMACrossBracketConfig) -> None: self.fast_ema = ExponentialMovingAverage(config.fast_ema_period) self.slow_ema = ExponentialMovingAverage(config.slow_ema_period) - self.instrument: Optional[Instrument] = None # Initialized in on_start + self.instrument: Instrument | None = None # Initialized in on_start def on_start(self) -> None: """ diff --git a/nautilus_trader/examples/strategies/ema_cross_bracket_algo.py b/nautilus_trader/examples/strategies/ema_cross_bracket_algo.py index 30869a952604..7b2e85d5668d 100644 --- a/nautilus_trader/examples/strategies/ema_cross_bracket_algo.py +++ b/nautilus_trader/examples/strategies/ema_cross_bracket_algo.py @@ -15,7 +15,7 @@ from datetime import timedelta from decimal import Decimal -from typing import Any, Optional +from typing import Any from nautilus_trader.common.enums import LogColor from nautilus_trader.config import StrategyConfig @@ -99,12 +99,12 @@ class EMACrossBracketAlgoConfig(StrategyConfig, frozen=True): slow_ema_period: int = 20 bracket_distance_atr: float = 3.0 emulation_trigger: str = "NO_TRIGGER" - entry_exec_algorithm_id: Optional[str] = None - entry_exec_algorithm_params: Optional[dict[str, Any]] = None - sl_exec_algorithm_id: Optional[str] = None - sl_exec_algorithm_params: Optional[dict[str, Any]] = None - tp_exec_algorithm_id: Optional[str] = None - tp_exec_algorithm_params: Optional[dict[str, Any]] = None + entry_exec_algorithm_id: str | None = None + entry_exec_algorithm_params: dict[str, Any] | None = None + sl_exec_algorithm_id: str | None = None + sl_exec_algorithm_params: dict[str, Any] | None = None + tp_exec_algorithm_id: str | None = None + tp_exec_algorithm_params: dict[str, Any] | None = None close_positions_on_stop: bool = True @@ -171,7 +171,7 @@ def __init__(self, config: EMACrossBracketAlgoConfig) -> None: self.tp_exec_algorithm_params = config.tp_exec_algorithm_params self.close_positions_on_stop = config.close_positions_on_stop - self.instrument: Optional[Instrument] = None # Initialized in on_start + self.instrument: Instrument | None = None # Initialized in on_start def on_start(self) -> None: """ diff --git a/nautilus_trader/examples/strategies/ema_cross_stop_entry.py b/nautilus_trader/examples/strategies/ema_cross_stop_entry.py index 76a9afa80eac..de6858a85d45 100644 --- a/nautilus_trader/examples/strategies/ema_cross_stop_entry.py +++ b/nautilus_trader/examples/strategies/ema_cross_stop_entry.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Optional from nautilus_trader.common.enums import LogColor from nautilus_trader.config import StrategyConfig @@ -143,7 +142,7 @@ def __init__(self, config: EMACrossStopEntryConfig) -> None: self.slow_ema = ExponentialMovingAverage(config.slow_ema_period) self.atr = AverageTrueRange(config.atr_period) - self.instrument: Optional[Instrument] = None # Initialized in `on_start()` + self.instrument: Instrument | None = None # Initialized in `on_start()` self.tick_size = None # Initialized in `on_start()` # Users order management variables diff --git a/nautilus_trader/examples/strategies/ema_cross_trailing_stop.py b/nautilus_trader/examples/strategies/ema_cross_trailing_stop.py index e3e0d0b6df17..ba4d86a1e7c1 100644 --- a/nautilus_trader/examples/strategies/ema_cross_trailing_stop.py +++ b/nautilus_trader/examples/strategies/ema_cross_trailing_stop.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Optional from nautilus_trader.common.enums import LogColor from nautilus_trader.config import StrategyConfig @@ -141,7 +140,7 @@ def __init__(self, config: EMACrossTrailingStopConfig) -> None: self.slow_ema = ExponentialMovingAverage(config.slow_ema_period) self.atr = AverageTrueRange(config.atr_period) - self.instrument: Optional[Instrument] = None # Initialized in on_start + self.instrument: Instrument | None = None # Initialized in on_start self.tick_size = None # Initialized in on_start # Users order management variables @@ -370,7 +369,7 @@ def on_event(self, event: Event) -> None: if isinstance(event, OrderFilled): if self.trailing_stop and event.client_order_id == self.trailing_stop.client_order_id: self.trailing_stop = None - elif isinstance(event, (PositionOpened, PositionChanged)): + elif isinstance(event, PositionOpened | PositionChanged): if self.entry and event.opening_order_id == self.entry.client_order_id: if event.entry == OrderSide.BUY: self.position_id = event.position_id diff --git a/nautilus_trader/examples/strategies/market_maker.py b/nautilus_trader/examples/strategies/market_maker.py index 8595a24a61d0..7151b78d552f 100644 --- a/nautilus_trader/examples/strategies/market_maker.py +++ b/nautilus_trader/examples/strategies/market_maker.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Optional from nautilus_trader.core.message import Event from nautilus_trader.model.data import OrderBookDeltas @@ -59,9 +58,9 @@ def __init__( self.trade_size = trade_size self.max_size = max_size - self.instrument: Optional[Instrument] = None # Initialized in on_start - self._book: Optional[OrderBook] = None - self._mid: Optional[Decimal] = None + self.instrument: Instrument | None = None # Initialized in on_start + self._book: OrderBook | None = None + self._mid: Decimal | None = None self._adj = Decimal(0) def on_start(self) -> None: @@ -98,7 +97,7 @@ def on_order_book_deltas(self, deltas: OrderBookDeltas) -> None: self.sell(price=val * Decimal(0.99)) def on_event(self, event: Event) -> None: - if isinstance(event, (PositionOpened, PositionChanged)): + if isinstance(event, PositionOpened | PositionChanged): signed_qty = event.quantity.as_decimal() if event.side == PositionSide.SHORT: signed_qty = -signed_qty diff --git a/nautilus_trader/examples/strategies/orderbook_imbalance.py b/nautilus_trader/examples/strategies/orderbook_imbalance.py index 575a6237e6c3..b67cdc03a5ff 100644 --- a/nautilus_trader/examples/strategies/orderbook_imbalance.py +++ b/nautilus_trader/examples/strategies/orderbook_imbalance.py @@ -15,7 +15,6 @@ import datetime from decimal import Decimal -from typing import Optional from nautilus_trader.config import StrategyConfig from nautilus_trader.model.data import OrderBookDeltas @@ -103,8 +102,8 @@ def __init__(self, config: OrderBookImbalanceConfig) -> None: self.trigger_min_size = config.trigger_min_size self.trigger_imbalance_ratio = config.trigger_imbalance_ratio self.min_seconds_between_triggers = config.min_seconds_between_triggers - self._last_trigger_timestamp: Optional[datetime.datetime] = None - self.instrument: Optional[Instrument] = None + self._last_trigger_timestamp: datetime.datetime | None = None + self.instrument: Instrument | None = None if self.config.use_quote_ticks: assert self.config.book_type == "L1_MBP" self.book_type: BookType = book_type_from_str(self.config.book_type) @@ -169,8 +168,8 @@ def check_trigger(self) -> None: # Uncomment for debugging # self.log.info("\n" + book.pprint()) - bid_size: Optional[Quantity] = book.best_bid_size() - ask_size: Optional[Quantity] = book.best_ask_size() + bid_size: Quantity | None = book.best_bid_size() + ask_size: Quantity | None = book.best_ask_size() if (bid_size is None or bid_size <= 0) or (ask_size is None or ask_size <= 0): self.log.warning("No market yet.") return diff --git a/nautilus_trader/examples/strategies/signal_strategy.py b/nautilus_trader/examples/strategies/signal_strategy.py index fd5d3381f073..07068b821c61 100644 --- a/nautilus_trader/examples/strategies/signal_strategy.py +++ b/nautilus_trader/examples/strategies/signal_strategy.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional from nautilus_trader.config import StrategyConfig from nautilus_trader.model.data import QuoteTick @@ -48,7 +47,7 @@ class SignalStrategy(Strategy): def __init__(self, config: SignalStrategyConfig) -> None: super().__init__(config) self.instrument_id = InstrumentId.from_str(self.config.instrument_id) - self.instrument: Optional[Instrument] = None + self.instrument: Instrument | None = None self.counter = 0 def on_start(self) -> None: diff --git a/nautilus_trader/examples/strategies/subscribe.py b/nautilus_trader/examples/strategies/subscribe.py index e852719033bb..291a1db0473e 100644 --- a/nautilus_trader/examples/strategies/subscribe.py +++ b/nautilus_trader/examples/strategies/subscribe.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional from nautilus_trader.config import StrategyConfig from nautilus_trader.model.data import Bar @@ -40,7 +39,7 @@ class SubscribeStrategyConfig(StrategyConfig, frozen=True): """ instrument_id: str - book_type: Optional[BookType] = None + book_type: BookType | None = None snapshots: bool = False trade_ticks: bool = False quote_ticks: bool = False @@ -62,7 +61,7 @@ class SubscribeStrategy(Strategy): def __init__(self, config: SubscribeStrategyConfig) -> None: super().__init__(config) self.instrument_id = InstrumentId.from_str(self.config.instrument_id) - self.book: Optional[OrderBook] = None + self.book: OrderBook | None = None def on_start(self) -> None: """ diff --git a/nautilus_trader/examples/strategies/volatility_market_maker.py b/nautilus_trader/examples/strategies/volatility_market_maker.py index 83d66b499dfd..b58aff0b475b 100644 --- a/nautilus_trader/examples/strategies/volatility_market_maker.py +++ b/nautilus_trader/examples/strategies/volatility_market_maker.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Optional, Union import pandas as pd @@ -107,11 +106,11 @@ def __init__(self, config: VolatilityMarketMakerConfig) -> None: # Create the indicators for the strategy self.atr = AverageTrueRange(config.atr_period) - self.instrument: Optional[Instrument] = None # Initialized in on_start + self.instrument: Instrument | None = None # Initialized in on_start # Users order management variables - self.buy_order: Union[LimitOrder, None] = None - self.sell_order: Union[LimitOrder, None] = None + self.buy_order: LimitOrder | None = None + self.sell_order: LimitOrder | None = None def on_start(self) -> None: """ diff --git a/nautilus_trader/execution/algorithm.pyx b/nautilus_trader/execution/algorithm.pyx index b1aedc9249bb..e63ee00eaaa4 100644 --- a/nautilus_trader/execution/algorithm.pyx +++ b/nautilus_trader/execution/algorithm.pyx @@ -13,7 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any +from typing import Optional from nautilus_trader.config import ExecAlgorithmConfig from nautilus_trader.config import ImportableExecAlgorithmConfig diff --git a/nautilus_trader/execution/matching_core.pyx b/nautilus_trader/execution/matching_core.pyx index 76145bd9d0c1..2602c9245770 100644 --- a/nautilus_trader/execution/matching_core.pyx +++ b/nautilus_trader/execution/matching_core.pyx @@ -13,7 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Callable, Optional +from typing import Callable +from typing import Optional from libc.stdint cimport uint64_t diff --git a/nautilus_trader/execution/messages.pyx b/nautilus_trader/execution/messages.pyx index a18c1759ee01..c6b8d2a029eb 100644 --- a/nautilus_trader/execution/messages.pyx +++ b/nautilus_trader/execution/messages.pyx @@ -13,7 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any +from typing import Optional import msgspec diff --git a/nautilus_trader/execution/reports.py b/nautilus_trader/execution/reports.py index dde4a83969ab..7a67b9b48089 100644 --- a/nautilus_trader/execution/reports.py +++ b/nautilus_trader/execution/reports.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - from datetime import datetime from decimal import Decimal diff --git a/nautilus_trader/live/__main__.py b/nautilus_trader/live/__main__.py index 6eece6d392a6..9552a9a4cffd 100644 --- a/nautilus_trader/live/__main__.py +++ b/nautilus_trader/live/__main__.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import click import fsspec import msgspec.json diff --git a/nautilus_trader/live/data_client.py b/nautilus_trader/live/data_client.py index 6b49c8a7166a..9dece1686cd2 100644 --- a/nautilus_trader/live/data_client.py +++ b/nautilus_trader/live/data_client.py @@ -20,14 +20,13 @@ """ -from __future__ import annotations - import asyncio import functools import traceback from asyncio import Task +from collections.abc import Callable from collections.abc import Coroutine -from typing import Any, Callable +from typing import Any import pandas as pd diff --git a/nautilus_trader/live/data_engine.py b/nautilus_trader/live/data_engine.py index a9892d8949e1..c95f9c214b32 100644 --- a/nautilus_trader/live/data_engine.py +++ b/nautilus_trader/live/data_engine.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import asyncio from asyncio import Queue diff --git a/nautilus_trader/live/execution_client.py b/nautilus_trader/live/execution_client.py index c4b4ad2d1e8b..e7cba2fd04b0 100644 --- a/nautilus_trader/live/execution_client.py +++ b/nautilus_trader/live/execution_client.py @@ -17,15 +17,14 @@ which may be presented directly by an exchange, or broker intermediary. """ -from __future__ import annotations - import asyncio import functools import traceback from asyncio import Task +from collections.abc import Callable from collections.abc import Coroutine from datetime import timedelta -from typing import Any, Callable +from typing import Any import pandas as pd diff --git a/nautilus_trader/live/execution_engine.py b/nautilus_trader/live/execution_engine.py index 4f92d97244e3..5e958b785337 100644 --- a/nautilus_trader/live/execution_engine.py +++ b/nautilus_trader/live/execution_engine.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import asyncio import math from asyncio import Queue diff --git a/nautilus_trader/live/factories.py b/nautilus_trader/live/factories.py index 5affb419161e..b0744e87a63a 100644 --- a/nautilus_trader/live/factories.py +++ b/nautilus_trader/live/factories.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import asyncio from nautilus_trader.cache.cache import Cache diff --git a/nautilus_trader/live/node.py b/nautilus_trader/live/node.py index a646ba816e12..493d54202aac 100644 --- a/nautilus_trader/live/node.py +++ b/nautilus_trader/live/node.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import asyncio import signal import time diff --git a/nautilus_trader/live/node_builder.py b/nautilus_trader/live/node_builder.py index df3bd13f3d46..abc6fc0885f1 100644 --- a/nautilus_trader/live/node_builder.py +++ b/nautilus_trader/live/node_builder.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import asyncio from nautilus_trader.cache.cache import Cache diff --git a/nautilus_trader/live/risk_engine.py b/nautilus_trader/live/risk_engine.py index 3ca2e672479e..889a65d9ff94 100644 --- a/nautilus_trader/live/risk_engine.py +++ b/nautilus_trader/live/risk_engine.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import asyncio from asyncio import Queue diff --git a/nautilus_trader/model/events/order.pyx b/nautilus_trader/model/events/order.pyx index eed0fdfb520a..8cadbd75fa39 100644 --- a/nautilus_trader/model/events/order.pyx +++ b/nautilus_trader/model/events/order.pyx @@ -13,10 +13,9 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import json -from typing import Any, Optional +from typing import Any +from typing import Optional import msgspec diff --git a/nautilus_trader/msgbus/bus.pyx b/nautilus_trader/msgbus/bus.pyx index 4e1bbfe327db..faddbb32d7ea 100644 --- a/nautilus_trader/msgbus/bus.pyx +++ b/nautilus_trader/msgbus/bus.pyx @@ -13,7 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Callable +from typing import Any +from typing import Callable import cython import numpy as np diff --git a/nautilus_trader/msgbus/subscription.pyx b/nautilus_trader/msgbus/subscription.pyx index 5fab9ccd24cb..d82a4d15627a 100644 --- a/nautilus_trader/msgbus/subscription.pyx +++ b/nautilus_trader/msgbus/subscription.pyx @@ -13,7 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Callable +from typing import Any +from typing import Callable from nautilus_trader.core.correctness cimport Condition diff --git a/nautilus_trader/persistence/catalog/parquet.py b/nautilus_trader/persistence/catalog/parquet.py index eb50e8bf981a..cddbc4e7c08d 100644 --- a/nautilus_trader/persistence/catalog/parquet.py +++ b/nautilus_trader/persistence/catalog/parquet.py @@ -19,11 +19,12 @@ import pathlib import platform from collections import defaultdict +from collections.abc import Callable from collections.abc import Generator from itertools import groupby from os import PathLike from pathlib import Path -from typing import Any, Callable, NamedTuple, Union +from typing import Any, NamedTuple import fsspec import pandas as pd @@ -59,7 +60,7 @@ from nautilus_trader.serialization.arrow.serializer import list_schemas -TimestampLike = Union[int, str, float] +TimestampLike = int | str | float class FeatherFile(NamedTuple): diff --git a/nautilus_trader/persistence/funcs.py b/nautilus_trader/persistence/funcs.py index 23c5e2648ab7..52d2d4055672 100644 --- a/nautilus_trader/persistence/funcs.py +++ b/nautilus_trader/persistence/funcs.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - from nautilus_trader.core.inspect import is_nautilus_class from nautilus_trader.core.nautilus_pyo3 import convert_to_snake_case @@ -43,7 +41,7 @@ def parse_bytes(s: float | str) -> int: - if isinstance(s, (int, float)): + if isinstance(s, int | float): return int(s) s = s.replace(" ", "") if not any(char.isdigit() for char in s): diff --git a/nautilus_trader/persistence/loaders.py b/nautilus_trader/persistence/loaders.py index aabb36c4e798..06dfd4f05015 100644 --- a/nautilus_trader/persistence/loaders.py +++ b/nautilus_trader/persistence/loaders.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - from os import PathLike import pandas as pd diff --git a/nautilus_trader/persistence/wranglers_v2.py b/nautilus_trader/persistence/wranglers_v2.py index 9e437f48101b..8847ee71a388 100644 --- a/nautilus_trader/persistence/wranglers_v2.py +++ b/nautilus_trader/persistence/wranglers_v2.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import abc from typing import Any, ClassVar diff --git a/nautilus_trader/persistence/writer.py b/nautilus_trader/persistence/writer.py index 16e492ab132e..f81730d0f0e6 100644 --- a/nautilus_trader/persistence/writer.py +++ b/nautilus_trader/persistence/writer.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import datetime from io import TextIOWrapper from typing import Any, BinaryIO @@ -175,7 +173,7 @@ def _extract_obj_metadata( b"size_precision": str(instrument.size_precision).encode(), }, ) - elif isinstance(obj, (QuoteTick, TradeTick)): + elif isinstance(obj, QuoteTick | TradeTick): metadata.update( { b"price_precision": str(instrument.price_precision).encode(), diff --git a/nautilus_trader/serialization/arrow/implementations/account_state.py b/nautilus_trader/serialization/arrow/implementations/account_state.py index 72bbb97cbc26..d530bf617510 100644 --- a/nautilus_trader/serialization/arrow/implementations/account_state.py +++ b/nautilus_trader/serialization/arrow/implementations/account_state.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional import msgspec import pandas as pd @@ -26,7 +25,7 @@ def serialize(state: AccountState) -> RecordBatch: - result: dict[tuple[Currency, Optional[InstrumentId]], dict] = {} + result: dict[tuple[Currency, InstrumentId | None], dict] = {} base = state.to_dict(state) del base["balances"] diff --git a/nautilus_trader/serialization/arrow/implementations/position_events.py b/nautilus_trader/serialization/arrow/implementations/position_events.py index 248e10f71eae..929ca5e49d13 100644 --- a/nautilus_trader/serialization/arrow/implementations/position_events.py +++ b/nautilus_trader/serialization/arrow/implementations/position_events.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Union import pyarrow as pa @@ -53,7 +52,7 @@ def serialize(event: PositionEvent): def deserialize(cls): - def inner(batch: pa.RecordBatch) -> Union[PositionOpened, PositionChanged, PositionClosed]: + def inner(batch: pa.RecordBatch) -> PositionOpened | (PositionChanged | PositionClosed): def parse(data): for k in ("quantity", "last_qty", "peak_qty", "last_px"): if k in data: diff --git a/nautilus_trader/serialization/arrow/schema.py b/nautilus_trader/serialization/arrow/schema.py index 7a52b734a43b..5c4828f9ea74 100644 --- a/nautilus_trader/serialization/arrow/schema.py +++ b/nautilus_trader/serialization/arrow/schema.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import msgspec import pyarrow as pa diff --git a/nautilus_trader/serialization/arrow/serializer.py b/nautilus_trader/serialization/arrow/serializer.py index 20e04848940a..bae94ec0f488 100644 --- a/nautilus_trader/serialization/arrow/serializer.py +++ b/nautilus_trader/serialization/arrow/serializer.py @@ -13,10 +13,9 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - +from collections.abc import Callable from io import BytesIO -from typing import Any, Callable +from typing import Any import pyarrow as pa @@ -227,7 +226,7 @@ def inner(data: list[Data | Event]) -> pa.RecordBatch: def make_dict_deserializer(data_cls): def inner(table: pa.Table) -> list[Data | Event]: - assert isinstance(table, (pa.Table, pa.RecordBatch)) + assert isinstance(table, pa.Table | pa.RecordBatch) return [data_cls.from_dict(d) for d in table.to_pylist()] return inner diff --git a/nautilus_trader/serialization/base.pyx b/nautilus_trader/serialization/base.pyx index b91cfa524c68..82a94c41c3e3 100644 --- a/nautilus_trader/serialization/base.pyx +++ b/nautilus_trader/serialization/base.pyx @@ -13,7 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Callable +from typing import Any +from typing import Callable from nautilus_trader.adapters.binance.common.types import BinanceBar from nautilus_trader.adapters.binance.common.types import BinanceTicker diff --git a/nautilus_trader/system/kernel.py b/nautilus_trader/system/kernel.py index ef6255329110..0dab2cae4ced 100644 --- a/nautilus_trader/system/kernel.py +++ b/nautilus_trader/system/kernel.py @@ -13,17 +13,15 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import asyncio import concurrent.futures import platform import signal import socket import time +from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor from datetime import timedelta -from typing import Callable import msgspec diff --git a/nautilus_trader/test_kit/functions.py b/nautilus_trader/test_kit/functions.py index 6ef6ba0aed71..37fc4b51c433 100644 --- a/nautilus_trader/test_kit/functions.py +++ b/nautilus_trader/test_kit/functions.py @@ -14,7 +14,8 @@ # ------------------------------------------------------------------------------------------------- import asyncio -from typing import Callable, TypeVar +from collections.abc import Callable +from typing import TypeVar T = TypeVar("T") diff --git a/nautilus_trader/test_kit/mocks/actors.py b/nautilus_trader/test_kit/mocks/actors.py index d48ed3494e9e..1704f41c5d0d 100644 --- a/nautilus_trader/test_kit/mocks/actors.py +++ b/nautilus_trader/test_kit/mocks/actors.py @@ -14,7 +14,7 @@ # ------------------------------------------------------------------------------------------------- import inspect -from typing import Any, Optional +from typing import Any from nautilus_trader.common.actor import Actor from nautilus_trader.config import ActorConfig @@ -40,7 +40,7 @@ class MockActor(Actor): Provides a mock actor for testing. """ - def __init__(self, config: Optional[ActorConfig] = None): + def __init__(self, config: ActorConfig | None = None): super().__init__(config) self.store: list[object] = [] diff --git a/nautilus_trader/test_kit/mocks/cache_database.py b/nautilus_trader/test_kit/mocks/cache_database.py index 753699e292ea..3912a6756c14 100644 --- a/nautilus_trader/test_kit/mocks/cache_database.py +++ b/nautilus_trader/test_kit/mocks/cache_database.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional from nautilus_trader.accounting.accounts.base import Account from nautilus_trader.cache.database import CacheDatabase @@ -91,16 +90,16 @@ def load_positions(self) -> dict: def load_currency(self, code: str) -> Currency: return self.currencies.get(code) - def load_instrument(self, instrument_id: InstrumentId) -> Optional[Instrument]: + def load_instrument(self, instrument_id: InstrumentId) -> Instrument | None: return self.instruments.get(instrument_id) - def load_synthetic(self, instrument_id: InstrumentId) -> Optional[SyntheticInstrument]: + def load_synthetic(self, instrument_id: InstrumentId) -> SyntheticInstrument | None: return self.synthetics.get(instrument_id) - def load_account(self, account_id: AccountId) -> Optional[Account]: + def load_account(self, account_id: AccountId) -> Account | None: return self.accounts.get(account_id) - def load_order(self, client_order_id: ClientOrderId) -> Optional[Order]: + def load_order(self, client_order_id: ClientOrderId) -> Order | None: return self.orders.get(client_order_id) def load_index_order_position(self) -> dict[ClientOrderId, PositionId]: @@ -109,7 +108,7 @@ def load_index_order_position(self) -> dict[ClientOrderId, PositionId]: def load_index_order_client(self) -> dict[ClientOrderId, ClientId]: return self._index_order_client - def load_position(self, position_id: PositionId) -> Optional[Position]: + def load_position(self, position_id: PositionId) -> Position | None: return self.positions.get(position_id) def load_strategy(self, strategy_id: StrategyId) -> dict: @@ -133,8 +132,8 @@ def add_account(self, account: Account) -> None: def add_order( self, order: Order, - position_id: Optional[PositionId] = None, - client_id: Optional[ClientId] = None, + position_id: PositionId | None = None, + client_id: ClientId | None = None, ) -> None: self.orders[order.client_order_id] = order self._index_order_position[order.client_order_id] = position_id diff --git a/nautilus_trader/test_kit/mocks/data.py b/nautilus_trader/test_kit/mocks/data.py index e15692e04f4b..69d8b703d91e 100644 --- a/nautilus_trader/test_kit/mocks/data.py +++ b/nautilus_trader/test_kit/mocks/data.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from pathlib import Path -from typing import Optional, Union from nautilus_trader.common.clock import TestClock from nautilus_trader.common.logging import Logger @@ -39,7 +38,7 @@ class NewsEventData(NewsEvent): def data_catalog_setup( protocol: str, - path: Optional[Union[str, Path]] = None, + path: str | Path | None = None, ) -> ParquetDataCatalog: if protocol not in ("memory", "file"): raise ValueError("`protocol` should only be one of `memory` or `file` for testing") diff --git a/nautilus_trader/test_kit/mocks/exec_clients.py b/nautilus_trader/test_kit/mocks/exec_clients.py index f0837884597d..c75fbca64952 100644 --- a/nautilus_trader/test_kit/mocks/exec_clients.py +++ b/nautilus_trader/test_kit/mocks/exec_clients.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- import inspect -from typing import Optional import pandas as pd @@ -287,9 +286,9 @@ def query_order(self, command) -> None: async def generate_order_status_report( self, instrument_id: InstrumentId, - client_order_id: Optional[ClientOrderId] = None, - venue_order_id: Optional[VenueOrderId] = None, - ) -> Optional[OrderStatusReport]: + client_order_id: ClientOrderId | None = None, + venue_order_id: VenueOrderId | None = None, + ) -> OrderStatusReport | None: current_frame = inspect.currentframe() if current_frame: self.calls.append(current_frame.f_code.co_name) @@ -298,9 +297,9 @@ async def generate_order_status_report( async def generate_order_status_reports( self, - instrument_id: Optional[InstrumentId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, open_only: bool = False, ) -> list[OrderStatusReport]: current_frame = inspect.currentframe() @@ -324,10 +323,10 @@ async def generate_order_status_reports( async def generate_trade_reports( self, - instrument_id: Optional[InstrumentId] = None, - venue_order_id: Optional[VenueOrderId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + venue_order_id: VenueOrderId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> list[TradeReport]: current_frame = inspect.currentframe() if current_frame: @@ -353,9 +352,9 @@ async def generate_trade_reports( async def generate_position_status_reports( self, - instrument_id: Optional[InstrumentId] = None, - start: Optional[pd.Timestamp] = None, - end: Optional[pd.Timestamp] = None, + instrument_id: InstrumentId | None = None, + start: pd.Timestamp | None = None, + end: pd.Timestamp | None = None, ) -> list[PositionStatusReport]: current_frame = inspect.currentframe() if current_frame: diff --git a/nautilus_trader/test_kit/mocks/strategies.py b/nautilus_trader/test_kit/mocks/strategies.py index d0bbd95b1ce3..6bb33bbfa0ca 100644 --- a/nautilus_trader/test_kit/mocks/strategies.py +++ b/nautilus_trader/test_kit/mocks/strategies.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- import inspect -from typing import Optional from nautilus_trader.indicators.average.ema import ExponentialMovingAverage from nautilus_trader.model.data import BarType @@ -43,7 +42,7 @@ def __init__(self, bar_type: BarType) -> None: self.ema1 = ExponentialMovingAverage(10) self.ema2 = ExponentialMovingAverage(20) - self.position_id: Optional[PositionId] = None + self.position_id: PositionId | None = None self.calls: list[str] = [] diff --git a/nautilus_trader/test_kit/performance.py b/nautilus_trader/test_kit/performance.py index c2ca97eb7dd1..a50fcabcb033 100644 --- a/nautilus_trader/test_kit/performance.py +++ b/nautilus_trader/test_kit/performance.py @@ -15,7 +15,7 @@ import inspect import timeit -from typing import Callable +from collections.abc import Callable import pytest diff --git a/nautilus_trader/test_kit/providers.py b/nautilus_trader/test_kit/providers.py index 3aa72ce69897..50287b6a6074 100644 --- a/nautilus_trader/test_kit/providers.py +++ b/nautilus_trader/test_kit/providers.py @@ -17,7 +17,7 @@ import random from datetime import date from decimal import Decimal -from typing import Any, Optional +from typing import Any import fsspec import numpy as np @@ -254,7 +254,7 @@ def ethusdt_perp_binance() -> CryptoPerpetual: ) @staticmethod - def btcusdt_future_binance(expiry: Optional[date] = None) -> CryptoFuture: + def btcusdt_future_binance(expiry: date | None = None) -> CryptoFuture: """ Return the Binance Futures BTCUSDT instrument for backtesting. @@ -375,7 +375,7 @@ def ethusd_bitmex() -> CryptoPerpetual: ) @staticmethod - def default_fx_ccy(symbol: str, venue: Optional[Venue] = None) -> CurrencyPair: + def default_fx_ccy(symbol: str, venue: Venue | None = None) -> CurrencyPair: """ Return a default FX currency pair instrument from the given symbol and venue. @@ -512,7 +512,7 @@ def synthetic_instrument() -> SyntheticInstrument: ) @staticmethod - def betting_instrument(venue: Optional[str] = None) -> BettingInstrument: + def betting_instrument(venue: str | None = None) -> BettingInstrument: return BettingInstrument( venue_name=venue or "BETFAIR", betting_type="ODDS", @@ -550,13 +550,13 @@ class TestDataProvider: """ def __init__(self, branch: str = "develop") -> None: - self.fs: Optional[fsspec.AbstractFileSystem] = None - self.root: Optional[str] = None + self.fs: fsspec.AbstractFileSystem | None = None + self.root: str | None = None self._determine_filesystem() self.branch = branch @staticmethod - def _test_data_directory() -> Optional[str]: + def _test_data_directory() -> str | None: # Determine if the test data directory exists (i.e. this is a checkout of the source code). source_root = pathlib.Path(__file__).parent.parent assert source_root.stem == "nautilus_trader" diff --git a/nautilus_trader/test_kit/rust/instruments.py b/nautilus_trader/test_kit/rust/instruments.py index 8f7d55411900..47280fe7c32d 100644 --- a/nautilus_trader/test_kit/rust/instruments.py +++ b/nautilus_trader/test_kit/rust/instruments.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from datetime import datetime -from typing import Optional import pandas as pd import pytz @@ -56,7 +55,7 @@ def ethusdt_perp_binance() -> CryptoPerpetual: ) @staticmethod - def btcusdt_future_binance(expiry: Optional[pd.Timestamp] = None) -> CryptoFuture: + def btcusdt_future_binance(expiry: pd.Timestamp | None = None) -> CryptoFuture: if expiry is None: expiry = pd.Timestamp(datetime(2022, 3, 25), tz=pytz.UTC) nanos_expiry = int(expiry.timestamp() * 1e9) diff --git a/nautilus_trader/test_kit/stubs/commands.py b/nautilus_trader/test_kit/stubs/commands.py index cb1b0f7b7bcf..b5c430cc48e5 100644 --- a/nautilus_trader/test_kit/stubs/commands.py +++ b/nautilus_trader/test_kit/stubs/commands.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional from nautilus_trader.execution.messages import CancelOrder from nautilus_trader.execution.messages import ModifyOrder @@ -54,12 +53,12 @@ def submit_order_list_command(order_list: OrderList) -> SubmitOrderList: @staticmethod def modify_order_command( - price: Optional[Price] = None, - quantity: Optional[Quantity] = None, - instrument_id: Optional[InstrumentId] = None, - client_order_id: Optional[ClientOrderId] = None, - venue_order_id: Optional[VenueOrderId] = None, - order: Optional[Order] = None, + price: Price | None = None, + quantity: Quantity | None = None, + instrument_id: InstrumentId | None = None, + client_order_id: ClientOrderId | None = None, + venue_order_id: VenueOrderId | None = None, + order: Order | None = None, ) -> ModifyOrder: assert price or quantity if order is not None: @@ -91,10 +90,10 @@ def modify_order_command( @staticmethod def cancel_order_command( - instrument_id: Optional[InstrumentId] = None, - client_order_id: Optional[ClientOrderId] = None, - venue_order_id: Optional[VenueOrderId] = None, - order: Optional[Order] = None, + instrument_id: InstrumentId | None = None, + client_order_id: ClientOrderId | None = None, + venue_order_id: VenueOrderId | None = None, + order: Order | None = None, ) -> CancelOrder: if order is not None: return CancelOrder( diff --git a/nautilus_trader/test_kit/stubs/component.py b/nautilus_trader/test_kit/stubs/component.py index 82a2f45a112d..b2f080526ad0 100644 --- a/nautilus_trader/test_kit/stubs/component.py +++ b/nautilus_trader/test_kit/stubs/component.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- import asyncio -from typing import Optional from nautilus_trader.backtest.engine import BacktestEngine from nautilus_trader.backtest.engine import BacktestEngineConfig @@ -67,7 +66,7 @@ def msgbus() -> MessageBus: ) @staticmethod - def cache(logger: Optional[Logger] = None) -> Cache: + def cache(logger: Logger | None = None) -> Cache: return Cache( database=None, logger=logger or TestComponentStubs.logger(), @@ -145,15 +144,15 @@ def backtest_node( @staticmethod def backtest_engine( - config: Optional[BacktestEngineConfig] = None, - instrument: Optional[Instrument] = None, - ticks: Optional[list[Data]] = None, - venue: Optional[Venue] = None, - oms_type: Optional[OmsType] = None, - account_type: Optional[AccountType] = None, - base_currency: Optional[Currency] = None, - starting_balances: Optional[list[Money]] = None, - fill_model: Optional[FillModel] = None, + config: BacktestEngineConfig | None = None, + instrument: Instrument | None = None, + ticks: list[Data] | None = None, + venue: Venue | None = None, + oms_type: OmsType | None = None, + account_type: AccountType | None = None, + base_currency: Currency | None = None, + starting_balances: list[Money] | None = None, + fill_model: FillModel | None = None, ) -> BacktestEngine: engine = BacktestEngine(config=config) engine.add_venue( diff --git a/nautilus_trader/test_kit/stubs/config.py b/nautilus_trader/test_kit/stubs/config.py index 7e8603e0aadd..be8e875ca902 100644 --- a/nautilus_trader/test_kit/stubs/config.py +++ b/nautilus_trader/test_kit/stubs/config.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional from nautilus_trader.config import BacktestDataConfig from nautilus_trader.config import BacktestEngineConfig @@ -58,7 +57,7 @@ def backtest_venue_config() -> BacktestVenueConfig: @staticmethod def order_book_imbalance( - instrument_id: Optional[InstrumentId] = None, + instrument_id: InstrumentId | None = None, ) -> ImportableStrategyConfig: return ImportableStrategyConfig( strategy_path="nautilus_trader.examples.strategies.orderbook_imbalance:OrderBookImbalance", @@ -102,8 +101,8 @@ def backtest_engine_config( bypass_risk: bool = False, allow_cash_position: bool = True, persist: bool = False, - catalog: Optional[ParquetDataCatalog] = None, - strategies: Optional[list[ImportableStrategyConfig]] = None, + catalog: ParquetDataCatalog | None = None, + strategies: list[ImportableStrategyConfig] | None = None, ) -> BacktestEngineConfig: if persist: assert catalog is not None, "If `persist=True`, must pass `catalog`" @@ -129,7 +128,7 @@ def venue_config() -> BacktestVenueConfig: def backtest_data_config( catalog: ParquetDataCatalog, data_cls: Data = QuoteTick, - instrument_id: Optional[str] = None, + instrument_id: str | None = None, ) -> BacktestDataConfig: return BacktestDataConfig( data_cls=data_cls.fully_qualified_name(), @@ -141,10 +140,10 @@ def backtest_data_config( @staticmethod def backtest_run_config( catalog: ParquetDataCatalog, - config: Optional[BacktestEngineConfig] = None, - instrument_ids: Optional[list[str]] = None, + config: BacktestEngineConfig | None = None, + instrument_ids: list[str] | None = None, data_types: tuple[Data, ...] = (QuoteTick,), - venues: Optional[list[BacktestVenueConfig]] = None, + venues: list[BacktestVenueConfig] | None = None, ) -> BacktestRunConfig: instrument_ids = instrument_ids or [TestIdStubs.betting_instrument_id().value] run_config = BacktestRunConfig( diff --git a/nautilus_trader/test_kit/stubs/data.py b/nautilus_trader/test_kit/stubs/data.py index 20ca02e8e636..a2f885bd4e79 100644 --- a/nautilus_trader/test_kit/stubs/data.py +++ b/nautilus_trader/test_kit/stubs/data.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - import json from datetime import datetime from os import PathLike diff --git a/nautilus_trader/test_kit/stubs/events.py b/nautilus_trader/test_kit/stubs/events.py index 48d4328fb57a..cf2071100578 100644 --- a/nautilus_trader/test_kit/stubs/events.py +++ b/nautilus_trader/test_kit/stubs/events.py @@ -14,7 +14,6 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Optional from nautilus_trader.accounting.accounts.base import Account from nautilus_trader.common.enums import ComponentState @@ -85,7 +84,7 @@ def trading_state_changed() -> TradingStateChanged: ) @staticmethod - def cash_account_state(account_id: Optional[AccountId] = None) -> AccountState: + def cash_account_state(account_id: AccountId | None = None) -> AccountState: return AccountState( account_id=account_id or TestIdStubs.account_id(), account_type=AccountType.CASH, @@ -106,7 +105,7 @@ def cash_account_state(account_id: Optional[AccountId] = None) -> AccountState: ) @staticmethod - def margin_account_state(account_id: Optional[AccountId] = None) -> AccountState: + def margin_account_state(account_id: AccountId | None = None) -> AccountState: return AccountState( account_id=account_id or TestIdStubs.account_id(), account_type=AccountType.MARGIN, @@ -136,7 +135,7 @@ def margin_account_state(account_id: Optional[AccountId] = None) -> AccountState def betting_account_state( balance: float = 1_000, currency: Currency = GBP, - account_id: Optional[AccountId] = None, + account_id: AccountId | None = None, ) -> AccountState: return AccountState( account_id=account_id or TestIdStubs.account_id(), @@ -160,7 +159,7 @@ def betting_account_state( @staticmethod def order_released( order: Order, - released_price: Optional[Price] = None, + released_price: Price | None = None, ) -> OrderReleased: return OrderReleased( trader_id=order.trader_id, @@ -175,7 +174,7 @@ def order_released( @staticmethod def order_submitted( order: Order, - account_id: Optional[AccountId] = None, + account_id: AccountId | None = None, ) -> OrderSubmitted: return OrderSubmitted( trader_id=order.trader_id, @@ -191,8 +190,8 @@ def order_submitted( @staticmethod def order_accepted( order: Order, - account_id: Optional[AccountId] = None, - venue_order_id: Optional[VenueOrderId] = None, + account_id: AccountId | None = None, + venue_order_id: VenueOrderId | None = None, ) -> OrderAccepted: return OrderAccepted( trader_id=order.trader_id, @@ -209,7 +208,7 @@ def order_accepted( @staticmethod def order_rejected( order: Order, - account_id: Optional[AccountId] = None, + account_id: AccountId | None = None, ) -> OrderRejected: return OrderRejected( trader_id=order.trader_id, @@ -240,9 +239,9 @@ def order_pending_update(order: Order) -> OrderPendingUpdate: @staticmethod def order_updated( order: Order, - quantity: Optional[Quantity] = None, - price: Optional[Price] = None, - trigger_price: Optional[Price] = None, + quantity: Quantity | None = None, + price: Price | None = None, + trigger_price: Price | None = None, ) -> OrderUpdated: return OrderUpdated( trader_id=order.trader_id, @@ -277,16 +276,16 @@ def order_pending_cancel(order: Order) -> OrderPendingCancel: def order_filled( order: Order, instrument: Instrument, - strategy_id: Optional[StrategyId] = None, - account_id: Optional[AccountId] = None, - venue_order_id: Optional[VenueOrderId] = None, - trade_id: Optional[TradeId] = None, - position_id: Optional[PositionId] = None, - last_qty: Optional[Quantity] = None, - last_px: Optional[Price] = None, + strategy_id: StrategyId | None = None, + account_id: AccountId | None = None, + venue_order_id: VenueOrderId | None = None, + trade_id: TradeId | None = None, + position_id: PositionId | None = None, + last_qty: Quantity | None = None, + last_px: Price | None = None, liquidity_side: LiquiditySide = LiquiditySide.TAKER, ts_filled_ns: int = 0, - account: Optional[Account] = None, + account: Account | None = None, ) -> OrderFilled: if strategy_id is None: strategy_id = order.strategy_id diff --git a/nautilus_trader/test_kit/stubs/execution.py b/nautilus_trader/test_kit/stubs/execution.py index aea40d8e6bf0..9e094e7488fb 100644 --- a/nautilus_trader/test_kit/stubs/execution.py +++ b/nautilus_trader/test_kit/stubs/execution.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Optional from nautilus_trader.accounting.accounts.betting import BettingAccount from nautilus_trader.accounting.accounts.cash import CashAccount @@ -46,19 +45,19 @@ class TestExecStubs: @staticmethod - def cash_account(account_id: Optional[AccountId] = None) -> CashAccount: + def cash_account(account_id: AccountId | None = None) -> CashAccount: return AccountFactory.create( TestEventStubs.cash_account_state(account_id=account_id or TestIdStubs.account_id()), ) @staticmethod - def margin_account(account_id: Optional[AccountId] = None) -> MarginAccount: + def margin_account(account_id: AccountId | None = None) -> MarginAccount: return AccountFactory.create( TestEventStubs.margin_account_state(account_id=account_id or TestIdStubs.account_id()), ) @staticmethod - def betting_account(account_id: Optional[AccountId] = None) -> BettingAccount: + def betting_account(account_id: AccountId | None = None) -> BettingAccount: return AccountFactory.create( TestEventStubs.betting_account_state(account_id=account_id or TestIdStubs.account_id()), ) @@ -70,9 +69,9 @@ def limit_order( price=None, quantity=None, time_in_force=None, - trader_id: Optional[TradeId] = None, - strategy_id: Optional[StrategyId] = None, - client_order_id: Optional[ClientOrderId] = None, + trader_id: TradeId | None = None, + strategy_id: StrategyId | None = None, + client_order_id: ClientOrderId | None = None, expire_time=None, tags=None, ) -> LimitOrder: @@ -105,11 +104,11 @@ def limit_with_stop_market( price=None, quantity=None, time_in_force=None, - trader_id: Optional[TradeId] = None, - strategy_id: Optional[StrategyId] = None, - order_list_id: Optional[OrderListId] = None, - entry_client_order_id: Optional[ClientOrderId] = None, - sl_client_order_id: Optional[ClientOrderId] = None, + trader_id: TradeId | None = None, + strategy_id: StrategyId | None = None, + order_list_id: OrderListId | None = None, + entry_client_order_id: ClientOrderId | None = None, + sl_client_order_id: ClientOrderId | None = None, sl_trigger_price=None, expire_time=None, tags=None, @@ -158,9 +157,9 @@ def market_order( instrument_id=None, order_side=None, quantity=None, - trader_id: Optional[TradeId] = None, - strategy_id: Optional[StrategyId] = None, - client_order_id: Optional[ClientOrderId] = None, + trader_id: TradeId | None = None, + strategy_id: StrategyId | None = None, + client_order_id: ClientOrderId | None = None, time_in_force=None, ) -> MarketOrder: return MarketOrder( @@ -183,7 +182,7 @@ def market_order( @staticmethod def make_submitted_order( - order: Optional[Order] = None, + order: Order | None = None, instrument_id=None, **order_kwargs, ) -> Order: @@ -195,10 +194,10 @@ def make_submitted_order( @staticmethod def make_accepted_order( - order: Optional[Order] = None, - instrument_id: Optional[InstrumentId] = None, - account_id: Optional[AccountId] = None, - venue_order_id: Optional[VenueOrderId] = None, + order: Order | None = None, + instrument_id: InstrumentId | None = None, + account_id: AccountId | None = None, + venue_order_id: VenueOrderId | None = None, **order_kwargs, ) -> Order: order = order or TestExecStubs.limit_order(instrument_id=instrument_id, **order_kwargs) diff --git a/nautilus_trader/trading/controller.py b/nautilus_trader/trading/controller.py index bafc4bc25761..d8ecac43e62b 100644 --- a/nautilus_trader/trading/controller.py +++ b/nautilus_trader/trading/controller.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - from nautilus_trader.common.actor import Actor from nautilus_trader.config.common import ActorConfig from nautilus_trader.core.correctness import PyCondition diff --git a/nautilus_trader/trading/filters.py b/nautilus_trader/trading/filters.py index 736b4b602df2..33e7a7fa70f1 100644 --- a/nautilus_trader/trading/filters.py +++ b/nautilus_trader/trading/filters.py @@ -13,8 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from __future__ import annotations - from datetime import datetime from datetime import timedelta from enum import Enum diff --git a/nautilus_trader/trading/trader.py b/nautilus_trader/trading/trader.py index 3221c35c88c1..19d536de9f1e 100644 --- a/nautilus_trader/trading/trader.py +++ b/nautilus_trader/trading/trader.py @@ -21,10 +21,9 @@ """ -from __future__ import annotations - import asyncio -from typing import Any, Callable +from collections.abc import Callable +from typing import Any import pandas as pd diff --git a/poetry.lock b/poetry.lock index 374f8d0d1b13..267469bde970 100644 --- a/poetry.lock +++ b/poetry.lock @@ -338,101 +338,101 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.3.0" +version = "3.3.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"}, - {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"}, - {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"}, - {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"}, - {file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"}, - {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"}, - {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"}, - {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"}, + {file = "charset-normalizer-3.3.1.tar.gz", hash = "sha256:d9137a876020661972ca6eec0766d81aef8a5627df628b664b234b73396e727e"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8aee051c89e13565c6bd366813c386939f8e928af93c29fda4af86d25b73d8f8"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:352a88c3df0d1fa886562384b86f9a9e27563d4704ee0e9d56ec6fcd270ea690"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:223b4d54561c01048f657fa6ce41461d5ad8ff128b9678cfe8b2ecd951e3f8a2"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f861d94c2a450b974b86093c6c027888627b8082f1299dfd5a4bae8e2292821"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1171ef1fc5ab4693c5d151ae0fdad7f7349920eabbaca6271f95969fa0756c2d"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28f512b9a33235545fbbdac6a330a510b63be278a50071a336afc1b78781b147"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e842112fe3f1a4ffcf64b06dc4c61a88441c2f02f373367f7b4c1aa9be2ad5"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f9bc2ce123637a60ebe819f9fccc614da1bcc05798bbbaf2dd4ec91f3e08846"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f194cce575e59ffe442c10a360182a986535fd90b57f7debfaa5c845c409ecc3"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a74041ba0bfa9bc9b9bb2cd3238a6ab3b7618e759b41bd15b5f6ad958d17605"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b578cbe580e3b41ad17b1c428f382c814b32a6ce90f2d8e39e2e635d49e498d1"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6db3cfb9b4fcecb4390db154e75b49578c87a3b9979b40cdf90d7e4b945656e1"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:debb633f3f7856f95ad957d9b9c781f8e2c6303ef21724ec94bea2ce2fcbd056"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-win32.whl", hash = "sha256:87071618d3d8ec8b186d53cb6e66955ef2a0e4fa63ccd3709c0c90ac5a43520f"}, + {file = "charset_normalizer-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:e372d7dfd154009142631de2d316adad3cc1c36c32a38b16a4751ba78da2a397"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae4070f741f8d809075ef697877fd350ecf0b7c5837ed68738607ee0a2c572cf"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58e875eb7016fd014c0eea46c6fa92b87b62c0cb31b9feae25cbbe62c919f54d"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbd95e300367aa0827496fe75a1766d198d34385a58f97683fe6e07f89ca3e3c"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de0b4caa1c8a21394e8ce971997614a17648f94e1cd0640fbd6b4d14cab13a72"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:985c7965f62f6f32bf432e2681173db41336a9c2611693247069288bcb0c7f8b"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a15c1fe6d26e83fd2e5972425a772cca158eae58b05d4a25a4e474c221053e2d"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae55d592b02c4349525b6ed8f74c692509e5adffa842e582c0f861751701a673"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4d9c2770044a59715eb57c1144dedea7c5d5ae80c68fb9959515037cde2008"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:851cf693fb3aaef71031237cd68699dded198657ec1e76a76eb8be58c03a5d1f"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:31bbaba7218904d2eabecf4feec0d07469284e952a27400f23b6628439439fa7"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:871d045d6ccc181fd863a3cd66ee8e395523ebfbc57f85f91f035f50cee8e3d4"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:501adc5eb6cd5f40a6f77fbd90e5ab915c8fd6e8c614af2db5561e16c600d6f3"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f5fb672c396d826ca16a022ac04c9dce74e00a1c344f6ad1a0fdc1ba1f332213"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-win32.whl", hash = "sha256:bb06098d019766ca16fc915ecaa455c1f1cd594204e7f840cd6258237b5079a8"}, + {file = "charset_normalizer-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:8af5a8917b8af42295e86b64903156b4f110a30dca5f3b5aedea123fbd638bff"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7ae8e5142dcc7a49168f4055255dbcced01dc1714a90a21f87448dc8d90617d1"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5b70bab78accbc672f50e878a5b73ca692f45f5b5e25c8066d748c09405e6a55"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ceca5876032362ae73b83347be8b5dbd2d1faf3358deb38c9c88776779b2e2f"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d95638ff3613849f473afc33f65c401a89f3b9528d0d213c7037c398a51296"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edbe6a5bf8b56a4a84533ba2b2f489d0046e755c29616ef8830f9e7d9cf5728"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6a02a3c7950cafaadcd46a226ad9e12fc9744652cc69f9e5534f98b47f3bbcf"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10b8dd31e10f32410751b3430996f9807fc4d1587ca69772e2aa940a82ab571a"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edc0202099ea1d82844316604e17d2b175044f9bcb6b398aab781eba957224bd"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b891a2f68e09c5ef989007fac11476ed33c5c9994449a4e2c3386529d703dc8b"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:71ef3b9be10070360f289aea4838c784f8b851be3ba58cf796262b57775c2f14"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:55602981b2dbf8184c098bc10287e8c245e351cd4fdcad050bd7199d5a8bf514"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:46fb9970aa5eeca547d7aa0de5d4b124a288b42eaefac677bde805013c95725c"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:520b7a142d2524f999447b3a0cf95115df81c4f33003c51a6ab637cbda9d0bf4"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-win32.whl", hash = "sha256:8ec8ef42c6cd5856a7613dcd1eaf21e5573b2185263d87d27c8edcae33b62a61"}, + {file = "charset_normalizer-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:baec8148d6b8bd5cee1ae138ba658c71f5b03e0d69d5907703e3e1df96db5e41"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63a6f59e2d01310f754c270e4a257426fe5a591dc487f1983b3bbe793cf6bac6"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d6bfc32a68bc0933819cfdfe45f9abc3cae3877e1d90aac7259d57e6e0f85b1"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f3100d86dcd03c03f7e9c3fdb23d92e32abbca07e7c13ebd7ddfbcb06f5991f"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39b70a6f88eebe239fa775190796d55a33cfb6d36b9ffdd37843f7c4c1b5dc67"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e12f8ee80aa35e746230a2af83e81bd6b52daa92a8afaef4fea4a2ce9b9f4fa"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b6cefa579e1237ce198619b76eaa148b71894fb0d6bcf9024460f9bf30fd228"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:61f1e3fb621f5420523abb71f5771a204b33c21d31e7d9d86881b2cffe92c47c"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4f6e2a839f83a6a76854d12dbebde50e4b1afa63e27761549d006fa53e9aa80e"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:1ec937546cad86d0dce5396748bf392bb7b62a9eeb8c66efac60e947697f0e58"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:82ca51ff0fc5b641a2d4e1cc8c5ff108699b7a56d7f3ad6f6da9dbb6f0145b48"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:633968254f8d421e70f91c6ebe71ed0ab140220469cf87a9857e21c16687c034"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-win32.whl", hash = "sha256:c0c72d34e7de5604df0fde3644cc079feee5e55464967d10b24b1de268deceb9"}, + {file = "charset_normalizer-3.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:63accd11149c0f9a99e3bc095bbdb5a464862d77a7e309ad5938fbc8721235ae"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5a3580a4fdc4ac05f9e53c57f965e3594b2f99796231380adb2baaab96e22761"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2465aa50c9299d615d757c1c888bc6fef384b7c4aec81c05a0172b4400f98557"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb7cd68814308aade9d0c93c5bd2ade9f9441666f8ba5aa9c2d4b389cb5e2a45"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e43805ccafa0a91831f9cd5443aa34528c0c3f2cc48c4cb3d9a7721053874b"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:854cc74367180beb327ab9d00f964f6d91da06450b0855cbbb09187bcdb02de5"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c15070ebf11b8b7fd1bfff7217e9324963c82dbdf6182ff7050519e350e7ad9f"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4c99f98fc3a1835af8179dcc9013f93594d0670e2fa80c83aa36346ee763d2"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb765362688821404ad6cf86772fc54993ec11577cd5a92ac44b4c2ba52155b"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dced27917823df984fe0c80a5c4ad75cf58df0fbfae890bc08004cd3888922a2"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a66bcdf19c1a523e41b8e9d53d0cedbfbac2e93c649a2e9502cb26c014d0980c"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ecd26be9f112c4f96718290c10f4caea6cc798459a3a76636b817a0ed7874e42"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f70fd716855cd3b855316b226a1ac8bdb3caf4f7ea96edcccc6f484217c9597"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:17a866d61259c7de1bdadef418a37755050ddb4b922df8b356503234fff7932c"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-win32.whl", hash = "sha256:548eefad783ed787b38cb6f9a574bd8664468cc76d1538215d510a3cd41406cb"}, + {file = "charset_normalizer-3.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:45f053a0ece92c734d874861ffe6e3cc92150e32136dd59ab1fb070575189c97"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bc791ec3fd0c4309a753f95bb6c749ef0d8ea3aea91f07ee1cf06b7b02118f2f"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8c61fb505c7dad1d251c284e712d4e0372cef3b067f7ddf82a7fa82e1e9a93"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2c092be3885a1b7899cd85ce24acedc1034199d6fca1483fa2c3a35c86e43041"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2000c54c395d9e5e44c99dc7c20a64dc371f777faf8bae4919ad3e99ce5253e"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cb50a0335382aac15c31b61d8531bc9bb657cfd848b1d7158009472189f3d62"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c30187840d36d0ba2893bc3271a36a517a717f9fd383a98e2697ee890a37c273"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe81b35c33772e56f4b6cf62cf4aedc1762ef7162a31e6ac7fe5e40d0149eb67"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0bf89afcbcf4d1bb2652f6580e5e55a840fdf87384f6063c4a4f0c95e378656"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:06cf46bdff72f58645434d467bf5228080801298fbba19fe268a01b4534467f5"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3c66df3f41abee950d6638adc7eac4730a306b022570f71dd0bd6ba53503ab57"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd805513198304026bd379d1d516afbf6c3c13f4382134a2c526b8b854da1c2e"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:9505dc359edb6a330efcd2be825fdb73ee3e628d9010597aa1aee5aa63442e97"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:31445f38053476a0c4e6d12b047b08ced81e2c7c712e5a1ad97bc913256f91b2"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-win32.whl", hash = "sha256:bd28b31730f0e982ace8663d108e01199098432a30a4c410d06fe08fdb9e93f4"}, + {file = "charset_normalizer-3.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:555fe186da0068d3354cdf4bbcbc609b0ecae4d04c921cc13e209eece7720727"}, + {file = "charset_normalizer-3.3.1-py3-none-any.whl", hash = "sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708"}, ] [[package]] @@ -2256,7 +2256,6 @@ babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.14,<0.20" imagesize = ">=1.3" -importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" Pygments = ">=2.12" @@ -2685,47 +2684,42 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvloop" -version = "0.18.0" +version = "0.19.0" description = "Fast implementation of asyncio event loop on top of libuv" optional = false -python-versions = ">=3.7.0" -files = [ - {file = "uvloop-0.18.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1f354d669586fca96a9a688c585b6257706d216177ac457c92e15709acaece10"}, - {file = "uvloop-0.18.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:280904236a5b333a273292b3bcdcbfe173690f69901365b973fa35be302d7781"}, - {file = "uvloop-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad79cd30c7e7484bdf6e315f3296f564b3ee2f453134a23ffc80d00e63b3b59e"}, - {file = "uvloop-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99deae0504547d04990cc5acf631d9f490108c3709479d90c1dcd14d6e7af24d"}, - {file = "uvloop-0.18.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:edbb4de38535f42f020da1e3ae7c60f2f65402d027a08a8c60dc8569464873a6"}, - {file = "uvloop-0.18.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:54b211c46facb466726b227f350792770fc96593c4ecdfaafe20dc00f3209aef"}, - {file = "uvloop-0.18.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:25b714f07c68dcdaad6994414f6ec0f2a3b9565524fba181dcbfd7d9598a3e73"}, - {file = "uvloop-0.18.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1121087dfeb46e9e65920b20d1f46322ba299b8d93f7cb61d76c94b5a1adc20c"}, - {file = "uvloop-0.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74020ef8061678e01a40c49f1716b4f4d1cc71190d40633f08a5ef8a7448a5c6"}, - {file = "uvloop-0.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f4a549cd747e6f4f8446f4b4c8cb79504a8372d5d3a9b4fc20e25daf8e76c05"}, - {file = "uvloop-0.18.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6132318e1ab84a626639b252137aa8d031a6c0550250460644c32ed997604088"}, - {file = "uvloop-0.18.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:585b7281f9ea25c4a5fa993b1acca4ad3d8bc3f3fe2e393f0ef51b6c1bcd2fe6"}, - {file = "uvloop-0.18.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:61151cc207cf5fc88863e50de3d04f64ee0fdbb979d0b97caf21cae29130ed78"}, - {file = "uvloop-0.18.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c65585ae03571b73907b8089473419d8c0aff1e3826b3bce153776de56cbc687"}, - {file = "uvloop-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3d301e23984dcbc92d0e42253e0e0571915f0763f1eeaf68631348745f2dccc"}, - {file = "uvloop-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:680da98f12a7587f76f6f639a8aa7708936a5d17c5e7db0bf9c9d9cbcb616593"}, - {file = "uvloop-0.18.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:75baba0bfdd385c886804970ae03f0172e0d51e51ebd191e4df09b929771b71e"}, - {file = "uvloop-0.18.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ed3c28337d2fefc0bac5705b9c66b2702dc392f2e9a69badb1d606e7e7f773bb"}, - {file = "uvloop-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8849b8ef861431543c07112ad8436903e243cdfa783290cbee3df4ce86d8dd48"}, - {file = "uvloop-0.18.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:211ce38d84118ae282a91408f61b85cf28e2e65a0a8966b9a97e0e9d67c48722"}, - {file = "uvloop-0.18.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0a8f706b943c198dcedf1f2fb84899002c195c24745e47eeb8f2fb340f7dfc3"}, - {file = "uvloop-0.18.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:58e44650cbc8607a218caeece5a689f0a2d10be084a69fc32f7db2e8f364927c"}, - {file = "uvloop-0.18.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b8b7cf7806bdc745917f84d833f2144fabcc38e9cd854e6bc49755e3af2b53e"}, - {file = "uvloop-0.18.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:56c1026a6b0d12b378425e16250acb7d453abaefe7a2f5977143898db6cfe5bd"}, - {file = "uvloop-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:12af0d2e1b16780051d27c12de7e419b9daeb3516c503ab3e98d364cc55303bb"}, - {file = "uvloop-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b028776faf9b7a6d0a325664f899e4c670b2ae430265189eb8d76bd4a57d8a6e"}, - {file = "uvloop-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53aca21735eee3859e8c11265445925911ffe410974f13304edb0447f9f58420"}, - {file = "uvloop-0.18.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:847f2ed0887047c63da9ad788d54755579fa23f0784db7e752c7cf14cf2e7506"}, - {file = "uvloop-0.18.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6e20bb765fcac07879cd6767b6dca58127ba5a456149717e0e3b1f00d8eab51c"}, - {file = "uvloop-0.18.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e14de8800765b9916d051707f62e18a304cde661fa2b98a58816ca38d2b94029"}, - {file = "uvloop-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f3b18663efe0012bc4c315f1b64020e44596f5fabc281f5b0d9bc9465288559c"}, - {file = "uvloop-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6d341bc109fb8ea69025b3ec281fcb155d6824a8ebf5486c989ff7748351a37"}, - {file = "uvloop-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:895a1e3aca2504638a802d0bec2759acc2f43a0291a1dff886d69f8b7baff399"}, - {file = "uvloop-0.18.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d90858f32a852988d33987d608bcfba92a1874eb9f183995def59a34229f30d"}, - {file = "uvloop-0.18.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db1fcbad5deb9551e011ca589c5e7258b5afa78598174ac37a5f15ddcfb4ac7b"}, - {file = "uvloop-0.18.0.tar.gz", hash = "sha256:d5d1135beffe9cd95d0350f19e2716bc38be47d5df296d7cc46e3b7557c0d1ff"}, +python-versions = ">=3.8.0" +files = [ + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, + {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, ] [package.extras] @@ -2892,5 +2886,5 @@ redis = ["hiredis", "redis"] [metadata] lock-version = "2.0" -python-versions = ">=3.9,<3.12" -content-hash = "ca12f95db24382e2dc4c783b04e23622770c9582976ede06da7ef03a023afcea" +python-versions = ">=3.10,<3.12" +content-hash = "87d14ae282ab35ab342bedfdbb1a4a76c649ed0b495d7199d5e0805fc9ed8294" diff --git a/pyproject.toml b/pyproject.toml index f240fe268258..23b343611701 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautilus_trader" -version = "1.179.0" +version = "1.180.0" description = "A high-performance algorithmic trading platform and event-driven backtester" authors = ["Nautech Systems "] license = "LGPL-3.0-or-later" @@ -50,7 +50,7 @@ script = "build.py" generate-setup-file = false [tool.poetry.dependencies] -python = ">=3.9,<3.12" +python = ">=3.10,<3.12" cython = "==3.0.4" # Build dependency (pinned for stability) numpy = "^1.26.1" # Build dependency toml = "^0.10.2" # Build dependency @@ -64,7 +64,7 @@ psutil = "^5.9.6" pyarrow = ">=12.0.1" # Minimum version set one major version behind the latest for compatibility pytz = "^2023.3.0" tqdm = "^4.66.1" -uvloop = {version = "^0.18.0", markers = "sys_platform != 'win32'"} +uvloop = {version = "^0.19.0", markers = "sys_platform != 'win32'"} hiredis = {version = "^2.2.3", optional = true} redis = {version = "^5.0.1", optional = true} docker = {version = "^6.1.3", optional = true} @@ -118,13 +118,12 @@ sphinx-material = "^0.0.35" sphinx_togglebutton = "^0.3.0" [tool.isort] -py_version = "39" +py_version = "310" skip_glob = ["**/core/rust/*"] combine_as_imports = true line_length = 100 ensure_newline_before_comments = true force_single_line = true -single_line_exclusions = ["typing"] include_trailing_comma = true multi_line_output = 3 lines_after_imports = 2 @@ -132,7 +131,7 @@ use_parentheses = true filter_files = true [tool.black] -target_version = ["py39", "py310", "py311"] +target_version = ["py310", "py311"] line_length = 100 [tool.docformatter] @@ -144,7 +143,7 @@ recursive = true in-place = true [tool.ruff] -target-version = "py39" +target-version = "py310" line-length = 150 # Reduce to 100 select = [ "C4", @@ -254,7 +253,7 @@ lines-after-imports = 2 max-complexity = 10 [tool.mypy] -python_version = "3.9" +python_version = "3.10" disallow_incomplete_defs = true explicit_package_bases = true ignore_missing_imports = true diff --git a/tests/acceptance_tests/test_backtest.py b/tests/acceptance_tests/test_backtest.py index caa1050d1a79..8edfc4420bff 100644 --- a/tests/acceptance_tests/test_backtest.py +++ b/tests/acceptance_tests/test_backtest.py @@ -727,7 +727,7 @@ def setup(self): order_book_deltas = [ d for d in data - if isinstance(d, (OrderBookDelta, OrderBookDeltas)) + if isinstance(d, OrderBookDelta | OrderBookDeltas) and d.instrument_id == instrument.id ] self.engine.add_instrument(instrument) @@ -785,7 +785,7 @@ def setup(self): order_book_deltas = [ d for d in data - if isinstance(d, (OrderBookDelta, OrderBookDeltas)) + if isinstance(d, OrderBookDelta | OrderBookDeltas) and d.instrument_id == instrument.id ] self.engine.add_instrument(instrument) diff --git a/tests/integration_tests/adapters/betfair/test_betfair_data.py b/tests/integration_tests/adapters/betfair/test_betfair_data.py index d2c0fecbb015..beec3297f773 100644 --- a/tests/integration_tests/adapters/betfair/test_betfair_data.py +++ b/tests/integration_tests/adapters/betfair/test_betfair_data.py @@ -443,7 +443,7 @@ def test_betfair_orderbook(data_client, parser) -> None: # Act, Assert for update in BetfairDataProvider.market_updates(): for message in parser.parse(update): - if isinstance(message, (BettingInstrument, VenueStatus)): + if isinstance(message, BettingInstrument | VenueStatus): continue if message.instrument_id not in books: books[message.instrument_id] = create_betfair_order_book( @@ -456,7 +456,7 @@ def test_betfair_orderbook(data_client, parser) -> None: book.apply_delta(message) elif isinstance( message, - (Ticker, TradeTick, InstrumentStatus, InstrumentClose), + Ticker | TradeTick | InstrumentStatus | InstrumentClose, ): pass else: diff --git a/tests/integration_tests/adapters/betfair/test_betfair_execution.py b/tests/integration_tests/adapters/betfair/test_betfair_execution.py index bc78380ed7b3..4864afb81347 100644 --- a/tests/integration_tests/adapters/betfair/test_betfair_execution.py +++ b/tests/integration_tests/adapters/betfair/test_betfair_execution.py @@ -15,7 +15,6 @@ import asyncio from functools import partial -from typing import Optional from unittest.mock import MagicMock from unittest.mock import patch @@ -205,7 +204,7 @@ def fill_order( exec_client, venue_order_id: VenueOrderId, quote_currency: Currency, - trade_id: Optional[str] = None, + trade_id: str | None = None, ): return partial( _fill_order, diff --git a/tests/integration_tests/adapters/betfair/test_kit.py b/tests/integration_tests/adapters/betfair/test_kit.py index 18e96e13c7f3..e3c968a1091f 100644 --- a/tests/integration_tests/adapters/betfair/test_kit.py +++ b/tests/integration_tests/adapters/betfair/test_kit.py @@ -17,7 +17,6 @@ import gzip import pathlib from asyncio import Future -from typing import Optional, Union from unittest.mock import MagicMock import msgspec @@ -88,7 +87,7 @@ def trader_id() -> TraderId: @staticmethod def instrument_provider( betfair_client, - config: Optional[BetfairInstrumentProviderConfig] = None, + config: BetfairInstrumentProviderConfig | None = None, ) -> BetfairInstrumentProvider: return BetfairInstrumentProvider( client=betfair_client, @@ -196,7 +195,7 @@ def betfair_venue_config(name="BETFAIR") -> BacktestVenueConfig: def streaming_config( catalog_path: str, catalog_fs_protocol: str = "memory", - flush_interval_ms: Optional[int] = None, + flush_interval_ms: int | None = None, ) -> StreamingConfig: return StreamingConfig( catalog_path=catalog_path, @@ -212,7 +211,7 @@ def betfair_backtest_run_config( persist=True, add_strategy=True, bypass_risk=False, - flush_interval_ms: Optional[int] = None, + flush_interval_ms: int | None = None, bypass_logging: bool = True, log_level: str = "WARNING", venue_name: str = "BETFAIR", @@ -400,7 +399,7 @@ def list_current_orders_custom( return raw @staticmethod - def betting_list_market_catalogue(filter_: Optional[MarketFilter] = None): + def betting_list_market_catalogue(filter_: MarketFilter | None = None): result = BetfairResponses.load("betting_list_market_catalogue.json") if filter_: result = [r for r in result if r["marketId"] in filter_.market_ids] @@ -427,7 +426,7 @@ def decode(raw: bytes, iterate: bool = False): return stream_decode(raw) @staticmethod - def load(filename, iterate: bool = False) -> Union[bytes, list[bytes]]: + def load(filename, iterate: bool = False) -> bytes | list[bytes]: raw = (RESOURCES_PATH / "streaming" / filename).read_bytes() message = BetfairStreaming.decode(raw=raw, iterate=iterate) if iterate: @@ -571,8 +570,8 @@ def generate_order_change_message( avp=0, order_id: int = 248485109136, client_order_id: str = "", - mb: Optional[list[MatchedOrder]] = None, - ml: Optional[list[MatchedOrder]] = None, + mb: list[MatchedOrder] | None = None, + ml: list[MatchedOrder] | None = None, ) -> OCM: assert side in ("B", "L"), "`side` should be 'B' or 'L'" assert isinstance(order_id, int) @@ -623,7 +622,7 @@ class BetfairDataProvider: def betting_instrument( market_id: str = "1.179082386", selection_id: str = "50214", - handicap: Optional[str] = None, + handicap: str | None = None, ) -> BettingInstrument: return BettingInstrument( venue_name=BETFAIR_VENUE.value, @@ -782,7 +781,7 @@ def badly_formatted_log(): def betting_instrument( market_id: str = "1.179082386", selection_id: str = "50214", - selection_handicap: Optional[str] = None, + selection_handicap: str | None = None, ) -> BettingInstrument: return BettingInstrument( venue_name=BETFAIR_VENUE.value, diff --git a/tests/integration_tests/adapters/conftest.py b/tests/integration_tests/adapters/conftest.py index 2d7cba5a527b..01e1343313a3 100644 --- a/tests/integration_tests/adapters/conftest.py +++ b/tests/integration_tests/adapters/conftest.py @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Optional +from typing import Any import pytest from pytest_mock import MockerFixture @@ -224,7 +224,7 @@ def components(data_engine, exec_engine, risk_engine, strategy): return -def _collect_events(msgbus, filter_types: Optional[tuple[type, ...]] = None): +def _collect_events(msgbus, filter_types: tuple[type, ...] | None = None): events = [] def handler(event: Event) -> None: diff --git a/tests/integration_tests/adapters/interactive_brokers/test_parsing.py b/tests/integration_tests/adapters/interactive_brokers/test_parsing.py index 79cb1d26c493..891898ae332d 100644 --- a/tests/integration_tests/adapters/interactive_brokers/test_parsing.py +++ b/tests/integration_tests/adapters/interactive_brokers/test_parsing.py @@ -15,7 +15,6 @@ import datetime from decimal import Decimal -from typing import Union import pytest @@ -316,7 +315,7 @@ def test_timedelta_to_duration_str(timedelta, expected): (Decimal("1E-8"), 8), ], ) -def test_tick_size_to_precision(tick_size: Union[float, Decimal], expected: int): +def test_tick_size_to_precision(tick_size: float | Decimal, expected: int): # Arrange, Act result = _tick_size_to_precision(tick_size) diff --git a/tests/integration_tests/network/test_http.py b/tests/integration_tests/network/test_http.py index 1d50537e62bf..160a8abb9774 100644 --- a/tests/integration_tests/network/test_http.py +++ b/tests/integration_tests/network/test_http.py @@ -13,8 +13,9 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from collections.abc import Callable from collections.abc import Coroutine -from typing import Any, Callable +from typing import Any import msgspec import pytest diff --git a/tests/unit_tests/backtest/test_engine.py b/tests/unit_tests/backtest/test_engine.py index 5a910e1c815e..139d6bb842f7 100644 --- a/tests/unit_tests/backtest/test_engine.py +++ b/tests/unit_tests/backtest/test_engine.py @@ -16,7 +16,6 @@ import sys import tempfile from decimal import Decimal -from typing import Optional import pandas as pd import pytest @@ -85,7 +84,7 @@ def setup(self): BacktestEngineConfig(logging=LoggingConfig(bypass_logging=True)), ) - def create_engine(self, config: Optional[BacktestEngineConfig] = None) -> BacktestEngine: + def create_engine(self, config: BacktestEngineConfig | None = None) -> BacktestEngine: engine = BacktestEngine(config) engine.add_venue( venue=Venue("SIM"), @@ -275,7 +274,7 @@ def setup(self) -> None: BacktestEngineConfig(logging=LoggingConfig(bypass_logging=True)), ) - def create_engine(self, config: Optional[BacktestEngineConfig] = None) -> BacktestEngine: + def create_engine(self, config: BacktestEngineConfig | None = None) -> BacktestEngine: engine = BacktestEngine(config) engine.add_venue( venue=Venue("SIM"), diff --git a/tests/unit_tests/persistence/test_streaming.py b/tests/unit_tests/persistence/test_streaming.py index b1388ea47010..70d422d19bf7 100644 --- a/tests/unit_tests/persistence/test_streaming.py +++ b/tests/unit_tests/persistence/test_streaming.py @@ -16,7 +16,6 @@ import copy import sys from collections import Counter -from typing import Optional import msgspec.json import pytest @@ -44,7 +43,7 @@ @pytest.mark.skipif(sys.platform == "win32", reason="failing on Windows") class TestPersistenceStreaming: def setup(self) -> None: - self.catalog: Optional[ParquetDataCatalog] = None + self.catalog: ParquetDataCatalog | None = None def _run_default_backtest(self, betfair_catalog): self.catalog = betfair_catalog diff --git a/version.json b/version.json index 52abaf358a97..4efd59ceaf8b 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "", - "message": "v1.179.0", + "message": "v1.180.0", "color": "orange" } From 1ac4da29fc3eb2f40929b65e46a1a7252e6364a2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 23 Oct 2023 20:35:09 +1100 Subject: [PATCH 03/78] Update docs dropping Python 3.9 --- README.md | 8 ++++---- docs/getting_started/installation.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6485a9b32807..87e2b6517305 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ | Platform | Rust | Python | | :----------------- | :------ | :----- | -| `Linux (x86_64)` | 1.73.0+ | 3.9+ | -| `macOS (x86_64)` | 1.73.0+ | 3.9+ | -| `macOS (arm64)` | 1.73.0+ | 3.9+ | -| `Windows (x86_64)` | 1.73.0+ | 3.9+ | +| `Linux (x86_64)` | 1.73.0+ | 3.10+ | +| `macOS (x86_64)` | 1.73.0+ | 3.10+ | +| `macOS (arm64)` | 1.73.0+ | 3.10+ | +| `Windows (x86_64)` | 1.73.0+ | 3.10+ | - **Website:** https://nautilustrader.io - **Docs:** https://docs.nautilustrader.io diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md index 9028842992f8..6e2d8e3d5952 100644 --- a/docs/getting_started/installation.md +++ b/docs/getting_started/installation.md @@ -1,6 +1,6 @@ # Installation -NautilusTrader is tested and supported for Python 3.9-3.11 on the following 64-bit platforms: +NautilusTrader is tested and supported for Python 3.10-3.11 on the following 64-bit platforms: | Operating System | Supported Versions | CPU Architecture | |------------------------|-----------------------|-------------------| From 773b9d7c4965e2a9719b2882834ee56300ab108b Mon Sep 17 00:00:00 2001 From: Ishan Bhanuka Date: Mon, 23 Oct 2023 19:19:15 +0800 Subject: [PATCH 04/78] Pass headers in websocket connection (#1299) Co-authored-by: ruthvik125 --- nautilus_core/network/src/lib.rs | 3 +- nautilus_core/network/src/websocket.rs | 170 +++++++++++++----- nautilus_trader/core/nautilus_pyo3.pyi | 15 +- .../network/test_websocket.py | 26 +-- 4 files changed, 147 insertions(+), 67 deletions(-) diff --git a/nautilus_core/network/src/lib.rs b/nautilus_core/network/src/lib.rs index 9809435ab975..6ee52dd2ec00 100644 --- a/nautilus_core/network/src/lib.rs +++ b/nautilus_core/network/src/lib.rs @@ -23,7 +23,7 @@ use http::{HttpClient, HttpMethod, HttpResponse}; use pyo3::prelude::*; use ratelimiter::quota::Quota; use socket::{SocketClient, SocketConfig}; -use websocket::WebSocketClient; +use websocket::{WebSocketClient, WebSocketConfig}; /// Loaded as nautilus_pyo3.network #[pymodule] @@ -33,6 +33,7 @@ pub fn network(_: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; Ok(()) diff --git a/nautilus_core/network/src/websocket.rs b/nautilus_core/network/src/websocket.rs index 4d7d1348db81..a25d2a20ba86 100644 --- a/nautilus_core/network/src/websocket.rs +++ b/nautilus_core/network/src/websocket.rs @@ -13,18 +13,19 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::{sync::Arc, time::Duration}; +use std::{str::FromStr, sync::Arc, time::Duration}; use futures_util::{ stream::{SplitSink, SplitStream}, SinkExt, StreamExt, }; +use hyper::header::HeaderName; use nautilus_core::python::to_pyruntime_err; use pyo3::{exceptions::PyException, prelude::*, types::PyBytes, PyObject, Python}; use tokio::{net::TcpStream, sync::Mutex, task, time::sleep}; use tokio_tungstenite::{ connect_async, - tungstenite::{Error, Message}, + tungstenite::{client::IntoClientRequest, http::HeaderValue, Error, Message}, MaybeTlsStream, WebSocketStream, }; use tracing::{debug, error}; @@ -34,6 +35,36 @@ type SharedMessageWriter = Arc>, Message>>>; type MessageReader = SplitStream>>; +#[derive(Debug, Clone)] +#[cfg_attr( + feature = "python", + pyclass(module = "nautilus_trader.core.nautilus_pyo3.network") +)] +pub struct WebSocketConfig { + url: String, + handler: PyObject, + headers: Vec<(String, String)>, + heartbeat: Option, +} + +#[pymethods] +impl WebSocketConfig { + #[new] + fn new( + url: String, + handler: PyObject, + headers: Vec<(String, String)>, + heartbeat: Option, + ) -> Self { + Self { + url, + handler, + headers, + heartbeat, + } + } +} + /// `WebSocketClient` connects to a websocket server to read and send messages. /// /// The client is opinionated about how messages are read and written. It @@ -50,44 +81,53 @@ type MessageReader = SplitStream>>; /// It's preferable to set the duration slightly lower - heartbeat more /// frequently - than the required amount. struct WebSocketClientInner { + config: WebSocketConfig, read_task: task::JoinHandle<()>, heartbeat_task: Option>, writer: SharedMessageWriter, - url: String, - handler: PyObject, - heartbeat: Option, } impl WebSocketClientInner { /// Create an inner websocket client. - pub async fn connect_url( - url: &str, - handler: PyObject, - heartbeat: Option, - ) -> Result { - let (writer, reader) = Self::connect_with_server(url).await?; + pub async fn connect_url(config: WebSocketConfig) -> Result { + let WebSocketConfig { + url, + handler, + heartbeat, + headers, + } = &config; + let (writer, reader) = Self::connect_with_server(url, headers.clone()).await?; let writer = Arc::new(Mutex::new(writer)); - let handler_clone = handler.clone(); // Keep receiving messages from socket and pass them as arguments to handler - let read_task = Self::spawn_read_task(reader, handler); + let read_task = Self::spawn_read_task(reader, handler.clone()); - let heartbeat_task = Self::spawn_heartbeat_task(heartbeat, writer.clone()); + let heartbeat_task = Self::spawn_heartbeat_task(*heartbeat, writer.clone()); Ok(Self { + config, read_task, heartbeat_task, writer, - url: url.to_string(), - handler: handler_clone, - heartbeat, }) } /// Connects with the server creating a tokio-tungstenite websocket stream. #[inline] - pub async fn connect_with_server(url: &str) -> Result<(MessageWriter, MessageReader), Error> { - connect_async(url).await.map(|resp| resp.0.split()) + pub async fn connect_with_server( + url: &str, + headers: Vec<(String, String)>, + ) -> Result<(MessageWriter, MessageReader), Error> { + let mut request = url.into_client_request()?; + let req_headers = request.headers_mut(); + + headers.into_iter().for_each(|(key, val)| { + let header_value = HeaderValue::from_str(&val).unwrap(); + let header_name = HeaderName::from_str(&key).unwrap(); + req_headers.insert(header_name, header_value); + }); + + connect_async(request).await.map(|resp| resp.0.split()) } /// Optionally spawn a hearbeat task to periodically ping the server. @@ -188,13 +228,15 @@ impl WebSocketClientInner { /// Make a new connection with server. Use the new read and write halves /// to update self writer and read and heartbeat tasks. pub async fn reconnect(&mut self) -> Result<(), Error> { - let (new_writer, reader) = Self::connect_with_server(&self.url).await?; + let (new_writer, reader) = + Self::connect_with_server(&self.config.url, self.config.headers.clone()).await?; let mut guard = self.writer.lock().await; *guard = new_writer; drop(guard); - self.read_task = Self::spawn_read_task(reader, self.handler.clone()); - self.heartbeat_task = Self::spawn_heartbeat_task(self.heartbeat, self.writer.clone()); + self.read_task = Self::spawn_read_task(reader, self.config.handler.clone()); + self.heartbeat_task = + Self::spawn_heartbeat_task(self.config.heartbeat, self.writer.clone()); Ok(()) } @@ -242,14 +284,12 @@ impl WebSocketClient { /// Creates an inner client and controller task to reconnect or disconnect /// the client. Also assumes ownership of writer from inner client pub async fn connect( - url: &str, - handler: PyObject, - heartbeat: Option, + config: WebSocketConfig, post_connection: Option, post_reconnection: Option, post_disconnection: Option, ) -> Result { - let inner = WebSocketClientInner::connect_url(url, handler, heartbeat).await?; + let inner = WebSocketClientInner::connect_url(config).await?; let writer = inner.writer.clone(); let disconnect_mode = Arc::new(Mutex::new(false)); let controller_task = Self::spawn_controller_task( @@ -264,7 +304,7 @@ impl WebSocketClient { Ok(_) => debug!("Called post_connection handler"), Err(e) => error!("Error calling post_connection handler: {e}"), }); - } + }; Ok(Self { writer, @@ -364,9 +404,7 @@ impl WebSocketClient { #[staticmethod] #[pyo3(name = "connect")] fn py_connect( - url: String, - handler: PyObject, - heartbeat: Option, + config: WebSocketConfig, post_connection: Option, post_reconnection: Option, post_disconnection: Option, @@ -374,9 +412,7 @@ impl WebSocketClient { ) -> PyResult<&PyAny> { pyo3_asyncio::tokio::future_into_py(py, async move { Self::connect( - &url, - handler, - heartbeat, + config, post_connection, post_reconnection, post_disconnection, @@ -472,28 +508,69 @@ mod tests { task::{self, JoinHandle}, time::{sleep, Duration}, }; - use tokio_tungstenite::accept_async; + use tokio_tungstenite::{ + accept_hdr_async, + tungstenite::{ + handshake::server::{self, Callback}, + http::HeaderValue, + }, + }; use tracing::debug; use tracing_test::traced_test; - use crate::websocket::WebSocketClient; + use crate::websocket::{WebSocketClient, WebSocketConfig}; struct TestServer { task: JoinHandle<()>, port: u16, } + #[derive(Debug, Clone)] + struct TestCallback { + key: String, + value: HeaderValue, + } + + impl Callback for TestCallback { + fn on_request( + self, + request: &server::Request, + response: server::Response, + ) -> Result { + let _ = response; + let value = request.headers().get(&self.key); + assert!(value.is_some()); + + match request.headers().get(&self.key) { + Some(value) => { + assert_eq!(value, self.value); + () + } + _ => (), + } + + Ok(response) + } + } + impl TestServer { - async fn setup() -> Self { + async fn setup(key: String, value: String) -> Self { let server = TcpListener::bind("127.0.0.1:0").await.unwrap(); let port = TcpListener::local_addr(&server).unwrap().port(); + let test_call_back = TestCallback { + key, + value: HeaderValue::from_str(&value).unwrap(), + }; + // Setup test server let task = task::spawn(async move { // keep accepting connections loop { let (conn, _) = server.accept().await.unwrap(); - let mut websocket = accept_async(conn).await.unwrap(); + let mut websocket = accept_hdr_async(conn, test_call_back.clone()) + .await + .unwrap(); task::spawn(async move { loop { @@ -529,9 +606,11 @@ mod tests { const N: usize = 10; let mut success_count = 0; + let header_key = "hello-custom-key".to_string(); + let header_value = "hello-custom-value".to_string(); // Initialize test server - let server = TestServer::setup().await; + let server = TestServer::setup(header_key.clone(), header_value.clone()).await; // Create counter class and handler that increments it let (counter, handler) = Python::with_gil(|py| { @@ -561,16 +640,15 @@ counter = Counter()", (counter, handler) }); - let client = WebSocketClient::connect( - &format!("ws://127.0.0.1:{}", server.port), + let config = WebSocketConfig::new( + format!("ws://127.0.0.1:{}", server.port), handler.clone(), + vec![(header_key, header_value)], None, - None, - None, - None, - ) - .await - .unwrap(); + ); + let client = WebSocketClient::connect(config, None, None, None) + .await + .unwrap(); // Send messages that increment the count for _ in 0..N { diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 4a9290be714c..c4c39eb853d9 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -14,8 +14,8 @@ from nautilus_trader.core.data import Data # Python Interface typing: -# We will eventually separate these into separate .pyi files per module, for now this at least -# provides import resolution as well as docstring. +# We will eventually separate these into a .pyi file per module, for now this at least +# provides import resolution as well as docstrings. ################################################################################################### # Core @@ -639,12 +639,21 @@ class Quota: @classmethod def rate_per_hour(cls, max_burst: int) -> Quota: ... +class WebSocketConfig: + def __init__( + self, + url: str, + handler: Callable[..., Any], + headers: list[tuple[str, str]], + heartbeat: int | None = None, + ) -> None: ... + class WebSocketClient: @classmethod def connect( cls, url: str, - handler: Callable[[Any], Any], + handler: Callable[..., Any], heartbeat: int | None = None, post_connection: Callable[..., None] | None = None, post_reconnection: Callable[..., None] | None = None, diff --git a/tests/integration_tests/network/test_websocket.py b/tests/integration_tests/network/test_websocket.py index 42e636bb01eb..861cd74d414e 100644 --- a/tests/integration_tests/network/test_websocket.py +++ b/tests/integration_tests/network/test_websocket.py @@ -20,6 +20,7 @@ from aiohttp.test_utils import TestServer from nautilus_trader.core.nautilus_pyo3 import WebSocketClient +from nautilus_trader.core.nautilus_pyo3 import WebSocketConfig from nautilus_trader.test_kit.functions import eventually @@ -31,11 +32,8 @@ def _server_url(server: TestServer) -> str: async def test_connect_and_disconnect(websocket_server): # Arrange store = [] - - client = await WebSocketClient.connect( - url=_server_url(websocket_server), - handler=store.append, - ) + config = WebSocketConfig(_server_url(websocket_server), store.append, []) + client = await WebSocketClient.connect(config) # Act, Assert await eventually(lambda: client.is_alive) @@ -47,10 +45,8 @@ async def test_connect_and_disconnect(websocket_server): async def test_client_send_recv(websocket_server): # Arrange store = [] - client = await WebSocketClient.connect( - url=_server_url(websocket_server), - handler=store.append, - ) + config = WebSocketConfig(_server_url(websocket_server), store.append, []) + client = await WebSocketClient.connect(config) await eventually(lambda: client.is_alive) # Act @@ -69,10 +65,8 @@ async def test_client_send_recv(websocket_server): async def test_client_send_recv_json(websocket_server): # Arrange store = [] - client = await WebSocketClient.connect( - url=_server_url(websocket_server), - handler=store.append, - ) + config = WebSocketConfig(_server_url(websocket_server), store.append, []) + client = await WebSocketClient.connect(config) await eventually(lambda: client.is_alive) # Act @@ -92,10 +86,8 @@ async def test_client_send_recv_json(websocket_server): async def test_reconnect_after_close(websocket_server): # Arrange store = [] - client = await WebSocketClient.connect( - url=_server_url(websocket_server), - handler=store.append, - ) + config = WebSocketConfig(_server_url(websocket_server), store.append, []) + client = await WebSocketClient.connect(config) await eventually(lambda: client.is_alive) # Act From 69244654266c9ed6a0c9bee871918cc6020d5f6e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 23 Oct 2023 22:32:51 +1100 Subject: [PATCH 05/78] Fix BinanceWebSocketClient config and typing --- nautilus_trader/adapters/binance/websocket/client.py | 9 ++++++++- nautilus_trader/core/nautilus_pyo3.pyi | 4 +--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/nautilus_trader/adapters/binance/websocket/client.py b/nautilus_trader/adapters/binance/websocket/client.py index 19deb4e3271a..7a18a1ee7a30 100644 --- a/nautilus_trader/adapters/binance/websocket/client.py +++ b/nautilus_trader/adapters/binance/websocket/client.py @@ -24,6 +24,7 @@ from nautilus_trader.common.logging import Logger from nautilus_trader.common.logging import LoggerAdapter from nautilus_trader.core.nautilus_pyo3 import WebSocketClient +from nautilus_trader.core.nautilus_pyo3 import WebSocketConfig class BinanceWebSocketClient: @@ -120,10 +121,16 @@ async def connect(self) -> None: self._log.debug(f"Connecting to {ws_url}...") self._is_connecting = True - self._inner = await WebSocketClient.connect( + + config = WebSocketConfig( url=ws_url, handler=self._handler, heartbeat=60, + headers=[], + ) + + self._inner = await WebSocketClient.connect( + config=config, post_reconnection=self.reconnect, ) self._is_connecting = False diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index c4c39eb853d9..1d6d53327bbe 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -652,9 +652,7 @@ class WebSocketClient: @classmethod def connect( cls, - url: str, - handler: Callable[..., Any], - heartbeat: int | None = None, + config: WebSocketConfig, post_connection: Callable[..., None] | None = None, post_reconnection: Callable[..., None] | None = None, post_disconnection: Callable[..., None] | None = None, From 035b4fa34d4f5e2ed268140eff983b5e642d0222 Mon Sep 17 00:00:00 2001 From: rsmb7z <105105941+rsmb7z@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:55:13 +0300 Subject: [PATCH 06/78] IB bug fixes (#1302) - Fix data fetch with `PriceType.LAST` for Crypto - Add `async-timeout` package - Make `request_timeout` configurable --- .../interactive_brokers/client/client.py | 6 +++--- .../interactive_brokers/historic/bar_data.py | 5 +++-- .../interactive_brokers/parsing/data.py | 19 +++++++++++++------ pyproject.toml | 3 ++- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index 2f5780d09ed8..7c00587c7a13 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -1107,7 +1107,7 @@ async def get_historical_bars( endDateTime=end_date_time, durationStr=duration, barSizeSetting=bar_size_setting, - whatToShow=what_to_show[bar_type.spec.price_type], + whatToShow=what_to_show(bar_type), useRTH=use_rth, formatDate=2, keepUpToDate=False, @@ -1192,7 +1192,7 @@ async def subscribe_historical_bars( endDateTime="", durationStr=timedelta_to_duration_str(duration), barSizeSetting=bar_size_setting, - whatToShow=what_to_show[bar_type.spec.price_type], + whatToShow=what_to_show(bar_type), useRTH=use_rth, formatDate=2, keepUpToDate=True, @@ -1395,7 +1395,7 @@ async def subscribe_realtime_bars( reqId=req_id, contract=contract, barSize=bar_type.spec.step, - whatToShow=what_to_show[bar_type.spec.price_type], + whatToShow=what_to_show(bar_type), useRTH=use_rth, realTimeBarsOptions=[], ), diff --git a/nautilus_trader/adapters/interactive_brokers/historic/bar_data.py b/nautilus_trader/adapters/interactive_brokers/historic/bar_data.py index ec920980f4ed..a3d095bfb25a 100644 --- a/nautilus_trader/adapters/interactive_brokers/historic/bar_data.py +++ b/nautilus_trader/adapters/interactive_brokers/historic/bar_data.py @@ -39,6 +39,7 @@ class BarDataDownloaderConfig(ActorConfig): bar_types: list[str] handler: Callable freq: str = "1W" + request_timeout: int = 30 class BarDataDownloader(AsyncActor): @@ -67,7 +68,7 @@ async def _on_start(self): instrument_ids = {bar_type.instrument_id for bar_type in self.bar_types} for instrument_id in instrument_ids: request_id = self.request_instrument(instrument_id) - await self.await_request(request_id) + await self.await_request(request_id, timeout=self.config.request_timeout) request_dates = list(pd.date_range(self.start_time, self.end_time, freq=self.freq)) @@ -78,7 +79,7 @@ async def _on_start(self): start=request_date, end=request_date + pd.Timedelta(self.freq), ) - await self.await_request(request_id) + await self.await_request(request_id, timeout=self.config.request_timeout) self.stop() diff --git a/nautilus_trader/adapters/interactive_brokers/parsing/data.py b/nautilus_trader/adapters/interactive_brokers/parsing/data.py index 8c19803da786..3b9f5fcdb99b 100644 --- a/nautilus_trader/adapters/interactive_brokers/parsing/data.py +++ b/nautilus_trader/adapters/interactive_brokers/parsing/data.py @@ -20,6 +20,7 @@ from nautilus_trader.core.datetime import nanos_to_secs from nautilus_trader.model.data import BarAggregation from nautilus_trader.model.data import BarSpecification +from nautilus_trader.model.data import BarType from nautilus_trader.model.enums import BookAction from nautilus_trader.model.enums import OrderSide from nautilus_trader.model.enums import PriceType @@ -44,12 +45,18 @@ 4: "MidPoint", } -what_to_show = { - PriceType.ASK: "ASK", - PriceType.BID: "BID", - PriceType.LAST: "TRADES", - PriceType.MID: "MIDPOINT", -} + +def what_to_show(bar_type: BarType) -> str: + mapping = { + PriceType.ASK: "ASK", + PriceType.BID: "BID", + PriceType.LAST: "TRADES", + PriceType.MID: "MIDPOINT", + } + if str(bar_type.instrument_id.venue) == "PAXOS" and bar_type.spec.price_type == PriceType.LAST: + return "AGGTRADES" + else: + return mapping[bar_type.spec.price_type] def generate_trade_id(ts_event: int, price: float, size: Decimal) -> TradeId: diff --git a/pyproject.toml b/pyproject.toml index 23b343611701..8376d56cd747 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,13 +68,14 @@ uvloop = {version = "^0.19.0", markers = "sys_platform != 'win32'"} hiredis = {version = "^2.2.3", optional = true} redis = {version = "^5.0.1", optional = true} docker = {version = "^6.1.3", optional = true} +async-timeout = "^4.0.3" nautilus_ibapi = {version = "==1019.1", optional = true} # Pinned for stability betfair_parser = {version = "==0.7.1", optional = true} # Pinned for stability [tool.poetry.extras] betfair = ["betfair_parser"] docker = ["docker"] -ib = ["nautilus_ibapi"] +ib = ["nautilus_ibapi", "async-timeout"] redis = ["hiredis", "redis"] [tool.poetry.group.dev] From 28808db2ca322a1f3c22d871b8ea0d37592c504a Mon Sep 17 00:00:00 2001 From: Brad Date: Tue, 24 Oct 2023 17:59:30 +1000 Subject: [PATCH 07/78] Allow passing basename_template to catalog.write_data (#1303) --- nautilus_trader/persistence/catalog/parquet.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nautilus_trader/persistence/catalog/parquet.py b/nautilus_trader/persistence/catalog/parquet.py index cddbc4e7c08d..f082c00b6d5d 100644 --- a/nautilus_trader/persistence/catalog/parquet.py +++ b/nautilus_trader/persistence/catalog/parquet.py @@ -254,11 +254,13 @@ def _fast_write( table: pa.Table, path: str, fs: fsspec.AbstractFileSystem, + basename_template: str = "part-{i}", ) -> None: + name = basename_template.format(i=0) fs.mkdirs(path, exist_ok=True) pq.write_table( table, - where=f"{path}/part-0.parquet", + where=f"{path}/{name}.parquet", filesystem=fs, row_group_size=self.max_rows_per_group, ) From f3f03f6eb94b2d87c522b95560ffd3eff64046ac Mon Sep 17 00:00:00 2001 From: r3k4mn14r <8102483+r3k4mn14r@users.noreply.github.com> Date: Tue, 24 Oct 2023 03:00:50 -0500 Subject: [PATCH 08/78] Add fill report to Trader (#1301) --- nautilus_trader/trading/trader.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nautilus_trader/trading/trader.py b/nautilus_trader/trading/trader.py index 19d536de9f1e..389ad558adb4 100644 --- a/nautilus_trader/trading/trader.py +++ b/nautilus_trader/trading/trader.py @@ -807,6 +807,17 @@ def generate_order_fills_report(self) -> pd.DataFrame: """ return ReportProvider.generate_order_fills_report(self._cache.orders()) + def generate_fills_report(self) -> pd.DataFrame: + """ + Generate a fills report. + + Returns + ------- + pd.DataFrame + + """ + return ReportProvider.generate_fills_report(self._cache.orders()) + def generate_positions_report(self) -> pd.DataFrame: """ Generate a positions report. From 2ff65be22e5e1cfbca014230629f644de54510ba Mon Sep 17 00:00:00 2001 From: Benjamin Singleton Date: Tue, 24 Oct 2023 04:09:12 -0400 Subject: [PATCH 09/78] Add imports to Python snippets in documentation (#1298) --- docs/concepts/adapters.md | 6 +++ docs/concepts/data.md | 10 ++++ docs/concepts/execution.md | 2 + docs/concepts/instruments.md | 9 +++- docs/concepts/logging.md | 3 ++ docs/concepts/orders.md | 75 ++++++++++++++++++++++++++++ docs/concepts/strategies.md | 58 ++++++++++++++++++++- docs/getting_started/installation.md | 1 + docs/integrations/betfair.md | 6 +++ docs/integrations/binance.md | 14 ++++++ docs/integrations/ib.md | 13 ++++- 11 files changed, 194 insertions(+), 3 deletions(-) diff --git a/docs/concepts/adapters.md b/docs/concepts/adapters.md index 3ba5e3e9b1ab..c1d11ab5b416 100644 --- a/docs/concepts/adapters.md +++ b/docs/concepts/adapters.md @@ -62,6 +62,8 @@ as configured: - All instruments are automatically loaded on start: ```python +from nautilus_trader.config import InstrumentProviderConfig + InstrumentProviderConfig(load_all=True) ``` @@ -124,6 +126,10 @@ cpdef void request_instrument(self, InstrumentId instrument_id, ClientId client_ The handler on the `ExecutionClient`: ```python +from nautilus_trader.core.uuid import UUID4 +from nautilus_trader.model.data import DataType +from nautilus_trader.model.identifiers import InstrumentId + # nautilus_trader/adapters/binance/spot/data.py def request_instrument(self, instrument_id: InstrumentId, correlation_id: UUID4): instrument: Optional[Instrument] = self._instrument_provider.find(instrument_id) diff --git a/docs/concepts/data.md b/docs/concepts/data.md index d8bb8f328448..0a70447ff902 100644 --- a/docs/concepts/data.md +++ b/docs/concepts/data.md @@ -130,6 +130,10 @@ The data catalog can be initialized from a `NAUTILUS_PATH` environment variable, The following example shows how to initialize a data catalog where there is pre-existing data already written to disk at the given path. ```python +import os +from nautilus_trader.persistence.catalog import ParquetDataCatalog + + CATALOG_PATH = os.getcwd() + "/catalog" # Create a new catalog instance @@ -159,6 +163,9 @@ Rust Arrow schema implementations and available for the follow data types (enhan ### Reading data Any stored data can then we read back into memory: ```python +from nautilus_trader.core.datetime import dt_to_unix_nanos +import pandas as pd + start = dt_to_unix_nanos(pd.Timestamp("2020-01-03", tz=pytz.utc)) end = dt_to_unix_nanos(pd.Timestamp("2020-01-04", tz=pytz.utc)) @@ -170,6 +177,9 @@ When running backtests in streaming mode with a `BacktestNode`, the data catalog The following example shows how to achieve this by initializing a `BacktestDataConfig` configuration object: ```python +from nautilus_trader.config import BacktestDataConfig +from nautilus_trader.model.data import OrderBookDelta + data_config = BacktestDataConfig( catalog_path=str(catalog.path), data_cls=OrderBookDelta, diff --git a/docs/concepts/execution.md b/docs/concepts/execution.md index 8ebf0f533ecc..e4462a63e319 100644 --- a/docs/concepts/execution.md +++ b/docs/concepts/execution.md @@ -159,6 +159,8 @@ Received orders will arrive via the following `on_order(...)` method. These rece know as "primary" (original) orders when being handled by an execution algorithm. ```python +from nautilus_trader.model.orders.base import Order + def on_order(self, order: Order) -> None: # noqa (too complex) """ Actions to be performed when running and receives an order. diff --git a/docs/concepts/instruments.md b/docs/concepts/instruments.md index e520fd78b3b9..2b95546e2b90 100644 --- a/docs/concepts/instruments.md +++ b/docs/concepts/instruments.md @@ -26,13 +26,18 @@ An incorrectly specified instrument may truncate data or otherwise produce surpr Generic test instruments can be instantiated through the `TestInstrumentProvider`: ```python +from nautilus_trader.test_kit.providers import TestInstrumentProvider + audusd = TestInstrumentProvider.default_fx_ccy("AUD/USD") ``` Exchange specific instruments can be discovered from live exchange data using an adapters `InstrumentProvider`: ```python -provider = BinanceInstrumentProvider( +from nautilus_trader.adapters.binance.spot.providers import BinanceSpotInstrumentProvider +from nautilus_trader.model.identifiers import InstrumentId + +provider = BinanceSpotInstrumentProvider( client=binance_http_client, logger=live_logger, ) @@ -45,6 +50,8 @@ instrument = provider.find(btcusdt) Or flexibly defined by the user through an `Instrument` constructor, or one of its more specific subclasses: ```python +from nautilus_trader.model.instruments import Instrument + instrument = Instrument(...) # <-- provide all necessary parameters ``` See the full instrument [API Reference](../api_reference/model/instruments.md). diff --git a/docs/concepts/logging.md b/docs/concepts/logging.md index 9ae31dbab046..caced0e9183e 100644 --- a/docs/concepts/logging.md +++ b/docs/concepts/logging.md @@ -68,6 +68,9 @@ The input value should be a dictionary of component ID strings to log level stri Below is an example of a trading node logging configuration that includes some of the options mentioned above: ```python +from nautilus_trader.config import LoggingConfig +from nautilus_trader.config import TradingNodeConfig + config_node = TradingNodeConfig( trader_id="TESTER-001", logging=LoggingConfig( diff --git a/docs/concepts/orders.md b/docs/concepts/orders.md index bd59817c05a7..e1a0672a13f6 100644 --- a/docs/concepts/orders.md +++ b/docs/concepts/orders.md @@ -133,6 +133,12 @@ In the following example we create a _Market_ order on the Interactive Brokers [ to BUY 100,000 AUD using USD: ```python +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import TimeInForce +from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.objects import Quantity +from nautilus_trader.model.orders import MarketOrder + order: MarketOrder = self.order_factory.market( instrument_id=InstrumentId.from_str("AUD/USD.IDEALPRO"), order_side=OrderSide.BUY, @@ -152,6 +158,13 @@ In the following example we create a _Limit_ order on the Binance Futures Crypto contracts at a limit price of 5000 USDT, as a market maker. ```python +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import TimeInForce +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.orders import LimitOrder + order: LimitOrder = self.order_factory.limit( instrument_id=InstrumentId.from_str("ETHUSDT-PERP.BINANCE"), order_side=OrderSide.SELL, @@ -176,6 +189,14 @@ In the following example we create a _Stop-Market_ order on the Binance Spot/Mar to SELL 1 BTC at a trigger price of 100,000 USDT, active until further notice: ```python +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import TimeInForce +from nautilus_trader.model.enums import TriggerType +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.orders import StopMarketOrder + order: StopMarketOrder = self.order_factory.stop_market( instrument_id=InstrumentId.from_str("BTCUSDT.BINANCE"), order_side=OrderSide.SELL, @@ -198,6 +219,15 @@ In the following example we create a _Stop-Limit_ order on the Currenex FX ECN t once the market hits the trigger price of 1.30010 USD, active until midday 6th June, 2022 (UTC): ```python +import pandas as pd +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import TimeInForce +from nautilus_trader.model.enums import TriggerType +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.orders import StopLimitOrder + order: StopLimitOrder = self.order_factory.stop_limit( instrument_id=InstrumentId.from_str("GBP/USD.CURRENEX"), order_side=OrderSide.BUY, @@ -223,6 +253,12 @@ In the following example we create a _Market-To-Limit_ order on the Interactive to BUY 200,000 USD using JPY: ```python +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import TimeInForce +from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.objects import Quantity +from nautilus_trader.model.orders import MarketToLimitOrder + order: MarketToLimitOrder = self.order_factory.market_to_limit( instrument_id=InstrumentId.from_str("USD/JPY.IDEALPRO"), order_side=OrderSide.BUY, @@ -246,6 +282,14 @@ In the following example we create a _Market-If-Touched_ order on the Binance Fu to SELL 10 ETHUSDT-PERP Perpetual Futures contracts at a trigger price of 10,000 USDT, active until further notice: ```python +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import TimeInForce +from nautilus_trader.model.enums import TriggerType +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.orders import MarketIfTouchedOrder + order: MarketIfTouchedOrder = self.order_factory.market_if_touched( instrument_id=InstrumentId.from_str("ETHUSDT-PERP.BINANCE"), order_side=OrderSide.SELL, @@ -270,6 +314,15 @@ Binance Futures exchange at a limit price of 30_100 USDT (once the market hits t active until midday 6th June, 2022 (UTC): ```python +import pandas as pd +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import TimeInForce +from nautilus_trader.model.enums import TriggerType +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.orders import StopLimitOrder + order: StopLimitOrder = self.order_factory.limit_if_touched( instrument_id=InstrumentId.from_str("BTCUSDT-PERP.BINANCE"), order_side=OrderSide.BUY, @@ -296,6 +349,17 @@ In the following example we create a _Trailing-Stop-Market_ order on the Binance Perpetual Futures Contracts activating at a trigger price of 5000 USD, then trailing at an offset of 1% (in basis points) away from the current last traded price: ```python +import pandas as pd +from decimal import Decimal +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import TimeInForce +from nautilus_trader.model.enums import TriggerType +from nautilus_trader.model.enums import TrailingOffsetType +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.orders import TrailingStopMarketOrder + order: TrailingStopMarketOrder = self.order_factory.trailing_stop_market( instrument_id=InstrumentId.from_str("ETHUSD-PERP.BINANCE"), order_side=OrderSide.SELL, @@ -323,6 +387,17 @@ at a limit price of 0.72000 USD, activating at 0.71000 USD then trailing at a st away from the current ask price, active until further notice: ```python +import pandas as pd +from decimal import Decimal +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import TimeInForce +from nautilus_trader.model.enums import TriggerType +from nautilus_trader.model.enums import TrailingOffsetType +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.orders import TrailingStopLimitOrder + order: TrailingStopLimitOrder = self.order_factory.trailing_stop_limit( instrument_id=InstrumentId.from_str("AUD/USD.CURRENEX"), order_side=OrderSide.BUY, diff --git a/docs/concepts/strategies.md b/docs/concepts/strategies.md index 83c78dbc5f3b..6a8ce60e0e77 100644 --- a/docs/concepts/strategies.md +++ b/docs/concepts/strategies.md @@ -32,6 +32,8 @@ Since a trading strategy is a class which inherits from `Strategy`, you must def a constructor where you can handle initialization. Minimally the base/super class needs to be initialized: ```python +from nautilus_trader.trading.strategy import Strategy + class MyStrategy(Strategy): def __init__(self) -> None: super().__init__() # <-- the super class must be called to initialize the strategy @@ -75,6 +77,18 @@ These handlers deal with market data updates. You can use these handlers to define actions upon receiving new market data. ```python +from nautilus_trader.core.data import Data +from nautilus_trader.model.data import Bar +from nautilus_trader.model.data import QuoteTick +from nautilus_trader.model.data import TradeTick +from nautilus_trader.model.data.book import OrderBookDeltas +from nautilus_trader.model.data.status import InstrumentClose +from nautilus_trader.model.data.status import InstrumentStatus +from nautilus_trader.model.data.status import VenueStatus +from nautilus_trader.model.data.ticker import Ticker +from nautilus_trader.model.instruments import Instrument +from nautilus_trader.model.orderbook import OrderBook + def on_order_book_deltas(self, deltas: OrderBookDeltas) -> None: def on_order_book(self, order_book: OrderBook) -> None: def on_ticker(self, ticker: Ticker) -> None: @@ -99,6 +113,24 @@ Handlers in this category are triggered by events related to orders. 3. `on_event(...)` ```python +from nautilus_trader.model.events import OrderAccepted +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 OrderEvent +from nautilus_trader.model.events import OrderExpired +from nautilus_trader.model.events import OrderFilled +from nautilus_trader.model.events import OrderInitialized +from nautilus_trader.model.events import OrderModifyRejected +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 + def on_order_initialized(self, event: OrderInitialized) -> None: def on_order_denied(self, event: OrderDenied) -> None: def on_order_emulated(self, event: OrderEmulated) -> None: @@ -128,6 +160,11 @@ Handlers in this category are triggered by events related to positions. 3. `on_event(...)` ```python +from nautilus_trader.model.events import PositionChanged +from nautilus_trader.model.events import PositionClosed +from nautilus_trader.model.events import PositionEvent +from nautilus_trader.model.events import PositionOpened + def on_position_opened(self, event: PositionOpened) -> None: def on_position_changed(self, event: PositionChanged) -> None: def on_position_closed(self, event: PositionClosed) -> None: @@ -140,6 +177,8 @@ This handler will eventually receive all event messages which arrive at the stra which no other specific handler exists. ```python +from nautilus_trader.core.message import Event + def on_event(self, event: Event) -> None: ``` @@ -189,6 +228,8 @@ While there are multiple ways to obtain current timestamps, here are two commonl **UTC Timestamp:** This method returns a timezone-aware (UTC) timestamp: ```python +import pandas as pd + now: pd.Timestamp = self.clock.utc_now() ``` @@ -259,6 +300,14 @@ The following shows a general outline of available methods. #### Account and positional information ```python +import decimal + +from nautilus_trader.model.identifiers import Venue +from nautilus_trader.accounting.accounts.base import Account +from nautilus_trader.model.currency import Currency +from nautilus_trader.model.objects import Money +from nautilus_trader.model.identifiers import InstrumentId + def account(self, venue: Venue) -> Account def balances_locked(self, venue: Venue) -> dict[Currency, Money] @@ -316,6 +365,10 @@ The following examples show method implementations for a `Strategy`. This example submits a `LIMIT` BUY order for emulation (see [OrderEmulator](advanced/emulated_orders.md)): ```python +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import TriggerType +from nautilus_trader.model.orders import LimitOrder + def buy(self) -> None: """ Users simple buy method (example). @@ -337,6 +390,10 @@ It's possible to specify both order emulation, and an execution algorithm. This example submits a `MARKET` BUY order to a TWAP execution algorithm: ```python +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.enums import TimeInForce +from nautilus_trader.model.identifiers import ExecAlgorithmId + def buy(self) -> None: """ Users simple buy method (example). @@ -444,4 +501,3 @@ example the above config would result in a strategy ID of `MyStrategy-001`. ```{tip} See the `StrategyId` [documentation](../api_reference/model/identifiers.md) for further details. ``` - diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md index 6e2d8e3d5952..91f7ae5bea30 100644 --- a/docs/getting_started/installation.md +++ b/docs/getting_started/installation.md @@ -75,3 +75,4 @@ To install a binary wheel from GitHub, first navigate to the [latest release](ht Download the appropriate `.whl` for your operating system and Python version, then run: pip install .whl + \ No newline at end of file diff --git a/docs/integrations/betfair.md b/docs/integrations/betfair.md index e71ba17d4871..6ae6938739ee 100644 --- a/docs/integrations/betfair.md +++ b/docs/integrations/betfair.md @@ -16,6 +16,8 @@ data and execution clients. To achieve this, add a `BETFAIR` section to your cli configuration(s): ```python +from nautilus_trader.config import TradingNodeConfig + config = TradingNodeConfig( ..., # Omitted data_clients={ @@ -41,6 +43,10 @@ config = TradingNodeConfig( Then, create a `TradingNode` and add the client factories: ```python +from nautilus_trader.adapters.betfair.factories import BetfairLiveDataClientFactory +from nautilus_trader.adapters.betfair.factories import BetfairLiveExecClientFactory +from nautilus_trader.live.node import TradingNode + # Instantiate the live trading node with a configuration node = TradingNode(config=config) diff --git a/docs/integrations/binance.md b/docs/integrations/binance.md index 77a7c1152649..d37a745931db 100644 --- a/docs/integrations/binance.md +++ b/docs/integrations/binance.md @@ -70,6 +70,8 @@ data and execution clients. To achieve this, add a `BINANCE` section to your cli configuration(s): ```python +from nautilus_trader.live.node import TradingNode + config = TradingNodeConfig( ..., # Omitted data_clients={ @@ -98,6 +100,10 @@ config = TradingNodeConfig( Then, create a `TradingNode` and add the client factories: ```python +from nautilus_trader.adapters.binance.factories import BinanceLiveDataClientFactory +from nautilus_trader.adapters.binance.factories import BinanceLiveExecClientFactory +from nautilus_trader.live.node import TradingNode + # Instantiate the live trading node with a configuration node = TradingNode(config=config) @@ -197,6 +203,8 @@ configure the provider to not log the warnings, as per the client configuration example below: ```python +from nautilus_trader.config import InstrumentProviderConfig + instrument_provider=InstrumentProviderConfig( load_all=True, log_warnings=False, @@ -218,6 +226,10 @@ You can subscribe to `BinanceFuturesMarkPriceUpdate` (included funding rating in data streams by subscribing in the following way from your actor or strategy: ```python +from nautilus_trader.adapters.binance.futures.types import BinanceFuturesMarkPriceUpdate +from nautilus_trader.model.data import DataType +from nautilus_trader.model.identifiers import ClientId + # In your `on_start` method self.subscribe_data( data_type=DataType(BinanceFuturesMarkPriceUpdate, metadata={"instrument_id": self.instrument.id}), @@ -230,6 +242,8 @@ objects to your `on_data` method. You will need to check the type, as this method acts as a flexible handler for all custom/generic data. ```python +from nautilus_trader.core.data import Data + def on_data(self, data: Data): # First check the type of data if isinstance(data, BinanceFuturesMarkPriceUpdate): diff --git a/docs/integrations/ib.md b/docs/integrations/ib.md index ef2622b6befd..4cc5bd9a2e98 100644 --- a/docs/integrations/ib.md +++ b/docs/integrations/ib.md @@ -26,6 +26,11 @@ queries below for common use cases Example config: ```python +from nautilus_trader.adapters.interactive_brokers.common import IBContract +from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersDataClientConfig +from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersInstrumentProviderConfig +from nautilus_trader.config import TradingNodeConfig + config_node = TradingNodeConfig( data_clients={ "IB": InteractiveBrokersDataClientConfig( @@ -56,6 +61,9 @@ configuration(s) and set the environment variables to your TWS (Traders Workstat ```python import os +from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersExecClientConfig +from nautilus_trader.config import TradingNodeConfig + config = TradingNodeConfig( data_clients={ @@ -65,7 +73,7 @@ config = TradingNodeConfig( ... # Omitted }, exec_clients = { - "IB": InteractiveBrokersExecutionClientConfig( + "IB": InteractiveBrokersExecClientConfig( username=os.getenv("TWS_USERNAME"), password=os.getenv("TWS_PASSWORD"), ... # Omitted @@ -77,6 +85,9 @@ config = TradingNodeConfig( Then, create a `TradingNode` and add the client factories: ```python +from nautilus_trader.adapters.interactive_brokers.factories import InteractiveBrokersLiveDataClientFactory +from nautilus_trader.adapters.interactive_brokers.factories import InteractiveBrokersLiveExecClientFactory + # Instantiate the live trading node with a configuration node = TradingNode(config=config) From 45010e9a41f8f13862defd722658256c76173832 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 24 Oct 2023 19:36:40 +1100 Subject: [PATCH 10/78] Update pyproject.toml extras and release notes --- .pre-commit-config.yaml | 2 +- RELEASES.md | 5 +-- nautilus_core/Cargo.lock | 66 ++++++++++++++++++---------------------- poetry.lock | 48 ++++++++++++++--------------- pyproject.toml | 4 +-- 5 files changed, 59 insertions(+), 66 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4e64b58c0992..e0e88be7f994 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -67,7 +67,7 @@ repos: types: [python] - repo: https://github.com/psf/black - rev: 23.10.0 + rev: 23.10.1 hooks: - id: black types_or: [python, pyi] diff --git a/RELEASES.md b/RELEASES.md index 30c218704633..52f3e675b81f 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -3,13 +3,14 @@ Released on TBC (UTC). ### Enhancements -None +- Added `WebSocketClient` connection headers, thanks @ruthvik125 and @twitu ### Breaking Changes - Dropped support for Python 3.9 ### Fixes -None +- Fixed `ParquetDataCatalog` file writing template, thanks @limx0 +- Interactive Brokers adapter various fixes, thanks @rsmb7z --- # NautilusTrader 1.179.0 Beta diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index eebeaa99cddf..31e1264d1756 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" dependencies = [ "getrandom", "once_cell", @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72832d73be48bac96a5d7944568f305d829ed55b0ce3b483647089dfaf6cf704" +checksum = "cd7d5a2cecb58716e47d67d5703a249964b14c7be1ec3cad3affc295b2d1c35d" dependencies = [ "cfg-if", "const-random", @@ -123,7 +123,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fab9e93ba8ce88a37d5a30dce4b9913b75413dc1ac56cb5d72e5a840543f829" dependencies = [ - "ahash 0.8.4", + "ahash 0.8.5", "arrow-arith", "arrow-array", "arrow-buffer", @@ -161,7 +161,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d02efa7253ede102d45a4e802a129e83bcc3f49884cab795b1ac223918e4318d" dependencies = [ - "ahash 0.8.4", + "ahash 0.8.5", "arrow-buffer", "arrow-data", "arrow-schema", @@ -287,7 +287,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "114a348ab581e7c9b6908fcab23cb39ff9f060eb19e72b13f8fb8eaa37f65d22" dependencies = [ - "ahash 0.8.4", + "ahash 0.8.5", "arrow-array", "arrow-buffer", "arrow-data", @@ -311,7 +311,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5c71e003202e67e9db139e5278c79f5520bb79922261dfe140e4637ee8b6108" dependencies = [ - "ahash 0.8.4", + "ahash 0.8.5", "arrow-array", "arrow-buffer", "arrow-data", @@ -398,9 +398,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "binary-heap-plus" @@ -771,23 +771,21 @@ checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3" [[package]] name = "const-random" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" +checksum = "11df32a13d7892ec42d51d3d175faba5211ffe13ed25d4fb348ac9e9ce835593" dependencies = [ "const-random-macro", - "proc-macro-hack", ] [[package]] name = "const-random-macro" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ "getrandom", "once_cell", - "proc-macro-hack", "tiny-keccak", ] @@ -1007,7 +1005,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7014432223f4d721cb9786cd88bb89e7464e0ba984d4a7f49db7787f5f268674" dependencies = [ - "ahash 0.8.4", + "ahash 0.8.5", "arrow", "arrow-array", "arrow-schema", @@ -1055,7 +1053,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3903ed8f102892f17b48efa437f3542159241d41c564f0d1e78efdc5e663aa" dependencies = [ - "ahash 0.8.4", + "ahash 0.8.5", "arrow", "arrow-array", "arrow-buffer", @@ -1096,7 +1094,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24c382676338d8caba6c027ba0da47260f65ffedab38fda78f6d8043f607557c" dependencies = [ - "ahash 0.8.4", + "ahash 0.8.5", "arrow", "arrow-array", "datafusion-common", @@ -1129,7 +1127,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57b4968e9a998dc0476c4db7a82f280e2026b25f464e4aa0c3bb9807ee63ddfd" dependencies = [ - "ahash 0.8.4", + "ahash 0.8.5", "arrow", "arrow-array", "arrow-buffer", @@ -1163,7 +1161,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd0d1fe54e37a47a2d58a1232c22786f2c28ad35805fdcd08f0253a8b0aaa90" dependencies = [ - "ahash 0.8.4", + "ahash 0.8.5", "arrow", "arrow-array", "arrow-buffer", @@ -1538,7 +1536,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.6", + "ahash 0.7.7", ] [[package]] @@ -1547,7 +1545,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.4", + "ahash 0.8.5", ] [[package]] @@ -1556,7 +1554,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" dependencies = [ - "ahash 0.8.4", + "ahash 0.8.5", "allocator-api2", ] @@ -1973,9 +1971,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi", @@ -2410,7 +2408,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0463cc3b256d5f50408c49a4be3a16674f4c8ceef60941709620a062b1f6bf4d" dependencies = [ - "ahash 0.8.4", + "ahash 0.8.5", "arrow-array", "arrow-buffer", "arrow-cast", @@ -2598,12 +2596,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - [[package]] name = "proc-macro2" version = "1.0.69" @@ -3729,12 +3721,12 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] @@ -3882,7 +3874,7 @@ name = "ustr" version = "0.10.0" source = "git+https://github.com/anderslanglands/ustr#c78ddc25300c4720ffcb5f8ddef6028cef14535f" dependencies = [ - "ahash 0.8.4", + "ahash 0.8.5", "byteorder", "lazy_static", "parking_lot", diff --git a/poetry.lock b/poetry.lock index 267469bde970..824110aed4ba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -210,29 +210,29 @@ msgspec = ">=0.18" [[package]] name = "black" -version = "23.10.0" +version = "23.10.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-23.10.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:f8dc7d50d94063cdfd13c82368afd8588bac4ce360e4224ac399e769d6704e98"}, - {file = "black-23.10.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:f20ff03f3fdd2fd4460b4f631663813e57dc277e37fb216463f3b907aa5a9bdd"}, - {file = "black-23.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3d9129ce05b0829730323bdcb00f928a448a124af5acf90aa94d9aba6969604"}, - {file = "black-23.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:960c21555be135c4b37b7018d63d6248bdae8514e5c55b71e994ad37407f45b8"}, - {file = "black-23.10.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:30b78ac9b54cf87bcb9910ee3d499d2bc893afd52495066c49d9ee6b21eee06e"}, - {file = "black-23.10.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:0e232f24a337fed7a82c1185ae46c56c4a6167fb0fe37411b43e876892c76699"}, - {file = "black-23.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31946ec6f9c54ed7ba431c38bc81d758970dd734b96b8e8c2b17a367d7908171"}, - {file = "black-23.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:c870bee76ad5f7a5ea7bd01dc646028d05568d33b0b09b7ecfc8ec0da3f3f39c"}, - {file = "black-23.10.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:6901631b937acbee93c75537e74f69463adaf34379a04eef32425b88aca88a23"}, - {file = "black-23.10.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:481167c60cd3e6b1cb8ef2aac0f76165843a374346aeeaa9d86765fe0dd0318b"}, - {file = "black-23.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f74892b4b836e5162aa0452393112a574dac85e13902c57dfbaaf388e4eda37c"}, - {file = "black-23.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:47c4510f70ec2e8f9135ba490811c071419c115e46f143e4dce2ac45afdcf4c9"}, - {file = "black-23.10.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:76baba9281e5e5b230c9b7f83a96daf67a95e919c2dfc240d9e6295eab7b9204"}, - {file = "black-23.10.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:a3c2ddb35f71976a4cfeca558848c2f2f89abc86b06e8dd89b5a65c1e6c0f22a"}, - {file = "black-23.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db451a3363b1e765c172c3fd86213a4ce63fb8524c938ebd82919bf2a6e28c6a"}, - {file = "black-23.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:7fb5fc36bb65160df21498d5a3dd330af8b6401be3f25af60c6ebfe23753f747"}, - {file = "black-23.10.0-py3-none-any.whl", hash = "sha256:e223b731a0e025f8ef427dd79d8cd69c167da807f5710add30cdf131f13dd62e"}, - {file = "black-23.10.0.tar.gz", hash = "sha256:31b9f87b277a68d0e99d2905edae08807c007973eaa609da5f0c62def6b7c0bd"}, + {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, + {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, + {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, + {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, + {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, + {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"}, + {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"}, + {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, + {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, + {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, + {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, + {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, ] [package.dependencies] @@ -2728,13 +2728,13 @@ test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)" [[package]] name = "virtualenv" -version = "20.24.5" +version = "20.24.6" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"}, - {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"}, + {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, + {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, ] [package.dependencies] @@ -2881,10 +2881,10 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [extras] betfair = ["betfair_parser"] docker = ["docker"] -ib = ["nautilus_ibapi"] +ib = ["async-timeout", "nautilus_ibapi"] redis = ["hiredis", "redis"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "87d14ae282ab35ab342bedfdbb1a4a76c649ed0b495d7199d5e0805fc9ed8294" +content-hash = "08ed7805e50e6f8aea5a1c66b0251d805a3117bf8c1531343232d06946385be4" diff --git a/pyproject.toml b/pyproject.toml index 8376d56cd747..7044f14e3654 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ uvloop = {version = "^0.19.0", markers = "sys_platform != 'win32'"} hiredis = {version = "^2.2.3", optional = true} redis = {version = "^5.0.1", optional = true} docker = {version = "^6.1.3", optional = true} -async-timeout = "^4.0.3" +async-timeout = {version = "^4.0.3", optional = true} nautilus_ibapi = {version = "==1019.1", optional = true} # Pinned for stability betfair_parser = {version = "==0.7.1", optional = true} # Pinned for stability @@ -82,7 +82,7 @@ redis = ["hiredis", "redis"] optional = true [tool.poetry.group.dev.dependencies] -black = "^23.10.0" +black = "^23.10.1" docformatter = "^1.7.5" mypy = "^1.6.1" pre-commit = "^3.5.0" From 3f472f168f943bd794174d8f6b23f93b7316a096 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Wed, 25 Oct 2023 09:25:48 +0200 Subject: [PATCH 11/78] Add WeightedMovingAverage indicator in rust (#1305) --- nautilus_core/indicators/src/average/ama.rs | 2 +- nautilus_core/indicators/src/average/dema.rs | 77 +----- nautilus_core/indicators/src/average/ema.rs | 83 +----- nautilus_core/indicators/src/average/mod.rs | 1 + nautilus_core/indicators/src/average/sma.rs | 58 +---- nautilus_core/indicators/src/average/wma.rs | 238 ++++++++++++++++++ nautilus_core/indicators/src/lib.rs | 18 +- nautilus_core/indicators/src/momentum/rsi.rs | 67 +---- .../indicators/src/python/average/ama.rs | 104 ++++++++ .../indicators/src/python/average/dema.rs | 99 ++++++++ .../indicators/src/python/average/ema.rs | 105 ++++++++ .../indicators/src/python/average/mod.rs | 20 ++ .../indicators/src/python/average/sma.rs | 99 ++++++++ .../indicators/src/python/average/wma.rs | 97 +++++++ nautilus_core/indicators/src/python/mod.rs | 34 +++ .../indicators/src/python/momentum/mod.rs | 16 ++ .../indicators/src/python/momentum/rsi.rs | 87 +++++++ .../src/python/ratio/efficiency_ratio.rs | 66 +++++ .../indicators/src/python/ratio/mod.rs | 16 ++ .../indicators/src/ratio/efficiency_ratio.rs | 50 +--- nautilus_core/indicators/src/stubs.rs | 9 +- nautilus_core/pyo3/src/lib.rs | 2 +- 22 files changed, 1000 insertions(+), 348 deletions(-) create mode 100644 nautilus_core/indicators/src/average/wma.rs create mode 100644 nautilus_core/indicators/src/python/average/ama.rs create mode 100644 nautilus_core/indicators/src/python/average/dema.rs create mode 100644 nautilus_core/indicators/src/python/average/ema.rs create mode 100644 nautilus_core/indicators/src/python/average/mod.rs create mode 100644 nautilus_core/indicators/src/python/average/sma.rs create mode 100644 nautilus_core/indicators/src/python/average/wma.rs create mode 100644 nautilus_core/indicators/src/python/mod.rs create mode 100644 nautilus_core/indicators/src/python/momentum/mod.rs create mode 100644 nautilus_core/indicators/src/python/momentum/rsi.rs create mode 100644 nautilus_core/indicators/src/python/ratio/efficiency_ratio.rs create mode 100644 nautilus_core/indicators/src/python/ratio/mod.rs diff --git a/nautilus_core/indicators/src/average/ama.rs b/nautilus_core/indicators/src/average/ama.rs index 17997de30223..3f2d6fdf4aa3 100644 --- a/nautilus_core/indicators/src/average/ama.rs +++ b/nautilus_core/indicators/src/average/ama.rs @@ -48,12 +48,12 @@ pub struct AdaptiveMovingAverage { pub value: f64, /// The input count for the indicator. pub count: usize, + pub is_initialized: bool, _efficiency_ratio: EfficiencyRatio, _prior_value: Option, _alpha_fast: f64, _alpha_slow: f64, has_inputs: bool, - is_initialized: bool, } impl Display for AdaptiveMovingAverage { diff --git a/nautilus_core/indicators/src/average/dema.rs b/nautilus_core/indicators/src/average/dema.rs index 40bed375c7aa..c81898716a9a 100644 --- a/nautilus_core/indicators/src/average/dema.rs +++ b/nautilus_core/indicators/src/average/dema.rs @@ -16,7 +16,6 @@ use std::fmt::{Display, Formatter}; use anyhow::Result; -use nautilus_core::python::to_pyvalue_err; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -42,8 +41,8 @@ pub struct DoubleExponentialMovingAverage { pub value: f64, /// The input count for the indicator. pub count: usize, + pub is_initialized: bool, has_inputs: bool, - is_initialized: bool, _ema1: ExponentialMovingAverage, _ema2: ExponentialMovingAverage, } @@ -126,80 +125,6 @@ impl MovingAverage for DoubleExponentialMovingAverage { } } -#[cfg(feature = "python")] -#[pymethods] -impl DoubleExponentialMovingAverage { - #[new] - fn py_new(period: usize, price_type: Option) -> PyResult { - Self::new(period, price_type).map_err(to_pyvalue_err) - } - - #[getter] - #[pyo3(name = "name")] - fn py_name(&self) -> String { - self.name() - } - - #[getter] - #[pyo3(name = "period")] - fn py_period(&self) -> usize { - self.period - } - - #[getter] - #[pyo3(name = "count")] - fn py_count(&self) -> usize { - self.count - } - - #[getter] - #[pyo3(name = "value")] - fn py_value(&self) -> f64 { - self.value - } - - #[getter] - #[pyo3(name = "has_inputs")] - fn py_has_inputs(&self) -> bool { - self.has_inputs() - } - - #[getter] - #[pyo3(name = "initialized")] - fn py_initialized(&self) -> bool { - self.is_initialized - } - - #[pyo3(name = "handle_quote_tick")] - fn py_handle_quote_tick(&mut self, tick: &QuoteTick) { - self.py_update_raw(tick.extract_price(self.price_type).into()); - } - - #[pyo3(name = "handle_trade_tick")] - fn py_handle_trade_tick(&mut self, tick: &TradeTick) { - self.update_raw((&tick.price).into()); - } - - #[pyo3(name = "handle_bar")] - fn py_handle_bar(&mut self, bar: &Bar) { - self.update_raw((&bar.close).into()); - } - - #[pyo3(name = "reset")] - fn py_reset(&mut self) { - self.reset(); - } - - #[pyo3(name = "update_raw")] - fn py_update_raw(&mut self, value: f64) { - self.update_raw(value); - } - - fn __repr__(&self) -> String { - format!("DoubleExponentialMovingAverage({})", self.period) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/indicators/src/average/ema.rs b/nautilus_core/indicators/src/average/ema.rs index 52355400414e..293cc273a913 100644 --- a/nautilus_core/indicators/src/average/ema.rs +++ b/nautilus_core/indicators/src/average/ema.rs @@ -16,7 +16,6 @@ use std::fmt::Display; use anyhow::Result; -use nautilus_core::python::to_pyvalue_err; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -34,8 +33,8 @@ pub struct ExponentialMovingAverage { pub alpha: f64, pub value: f64, pub count: usize, + pub is_initialized: bool, has_inputs: bool, - is_initialized: bool, } impl Display for ExponentialMovingAverage { @@ -117,86 +116,6 @@ impl MovingAverage for ExponentialMovingAverage { } } -#[cfg(feature = "python")] -#[pymethods] -impl ExponentialMovingAverage { - #[new] - fn py_new(period: usize, price_type: Option) -> PyResult { - Self::new(period, price_type).map_err(to_pyvalue_err) - } - - #[getter] - #[pyo3(name = "name")] - fn py_name(&self) -> String { - self.name() - } - - #[getter] - #[pyo3(name = "period")] - fn py_period(&self) -> usize { - self.period - } - - #[getter] - #[pyo3(name = "alpha")] - fn py_alpha(&self) -> f64 { - self.alpha - } - - #[getter] - #[pyo3(name = "count")] - fn py_count(&self) -> usize { - self.count - } - - #[getter] - #[pyo3(name = "value")] - fn py_value(&self) -> f64 { - self.value - } - - #[getter] - #[pyo3(name = "has_inputs")] - fn py_has_inputs(&self) -> bool { - self.has_inputs() - } - - #[getter] - #[pyo3(name = "initialized")] - fn py_initialized(&self) -> bool { - self.is_initialized - } - - #[pyo3(name = "handle_quote_tick")] - fn py_handle_quote_tick(&mut self, tick: &QuoteTick) { - self.py_update_raw(tick.extract_price(self.price_type).into()); - } - - #[pyo3(name = "handle_trade_tick")] - fn py_handle_trade_tick(&mut self, tick: &TradeTick) { - self.update_raw((&tick.price).into()); - } - - #[pyo3(name = "handle_bar")] - fn py_handle_bar(&mut self, bar: &Bar) { - self.update_raw((&bar.close).into()); - } - - #[pyo3(name = "reset")] - fn py_reset(&mut self) { - self.reset(); - } - - #[pyo3(name = "update_raw")] - fn py_update_raw(&mut self, value: f64) { - self.update_raw(value); - } - - fn __repr__(&self) -> String { - format!("ExponentialMovingAverage({})", self.period) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/indicators/src/average/mod.rs b/nautilus_core/indicators/src/average/mod.rs index 3acfda5648d7..8623cc669d84 100644 --- a/nautilus_core/indicators/src/average/mod.rs +++ b/nautilus_core/indicators/src/average/mod.rs @@ -79,3 +79,4 @@ pub mod ama; pub mod dema; pub mod ema; pub mod sma; +pub mod wma; diff --git a/nautilus_core/indicators/src/average/sma.rs b/nautilus_core/indicators/src/average/sma.rs index b77d38fbf91d..406000ed8dad 100644 --- a/nautilus_core/indicators/src/average/sma.rs +++ b/nautilus_core/indicators/src/average/sma.rs @@ -16,7 +16,6 @@ use std::fmt::Display; use anyhow::Result; -use nautilus_core::python::to_pyvalue_err; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -34,7 +33,7 @@ pub struct SimpleMovingAverage { pub value: f64, pub count: usize, pub inputs: Vec, - is_initialized: bool, + pub is_initialized: bool, } impl Display for SimpleMovingAverage { @@ -85,7 +84,7 @@ impl SimpleMovingAverage { price_type: price_type.unwrap_or(PriceType::Last), value: 0.0, count: 0, - inputs: Vec::new(), + inputs: Vec::with_capacity(period), is_initialized: false, }) } @@ -115,59 +114,6 @@ impl MovingAverage for SimpleMovingAverage { } } -#[cfg(feature = "python")] -#[pymethods] -impl SimpleMovingAverage { - #[new] - fn py_new(period: usize, price_type: Option) -> PyResult { - Self::new(period, price_type).map_err(to_pyvalue_err) - } - - #[getter] - #[pyo3(name = "name")] - fn py_name(&self) -> String { - self.name() - } - - #[getter] - #[pyo3(name = "period")] - fn py_period(&self) -> usize { - self.period - } - - #[getter] - #[pyo3(name = "count")] - fn py_count(&self) -> usize { - self.count - } - - #[getter] - #[pyo3(name = "value")] - fn py_value(&self) -> f64 { - self.value - } - - #[getter] - #[pyo3(name = "initialized")] - fn py_initialized(&self) -> bool { - self.is_initialized - } - - #[pyo3(name = "has_inputs")] - fn py_has_inputs(&self) -> bool { - self.has_inputs() - } - - #[pyo3(name = "update_raw")] - fn py_update_raw(&mut self, value: f64) { - self.update_raw(value); - } - - fn __repr__(&self) -> String { - format!("SimpleMovingAverage({})", self.period) - } -} - //////////////////////////////////////////////////////////////////////////////// // Test //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/indicators/src/average/wma.rs b/nautilus_core/indicators/src/average/wma.rs new file mode 100644 index 000000000000..4bff6366cad5 --- /dev/null +++ b/nautilus_core/indicators/src/average/wma.rs @@ -0,0 +1,238 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +use std::fmt::Display; + +use anyhow::Result; +use nautilus_model::{ + data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, + enums::PriceType, +}; +use pyo3::prelude::*; + +use crate::indicator::{Indicator, MovingAverage}; + +/// An indicator which calculates a weighted moving average across a rolling window. +#[repr(C)] +#[derive(Debug)] +#[pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")] +pub struct WeightedMovingAverage { + /// The rolling window period for the indicator (> 0). + pub period: usize, + /// The weights for the moving average calculation + pub weights: Vec, + /// Price type + pub price_type: PriceType, + /// The last indicator value. + pub value: f64, + /// Whether the indicator is initialized. + pub is_initialized: bool, + /// Inputs + pub inputs: Vec, + has_inputs: bool, +} + +impl Display for WeightedMovingAverage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}({},{:?})", self.name(), self.period, self.weights) + } +} + +impl WeightedMovingAverage { + pub fn new(period: usize, weights: Vec, price_type: Option) -> Result { + if weights.len() != period { + return Err(anyhow::anyhow!("Weights length must be equal to period")); + } + Ok(Self { + period, + weights, + price_type: price_type.unwrap_or(PriceType::Last), + value: 0.0, + inputs: Vec::with_capacity(period), + is_initialized: false, + has_inputs: false, + }) + } + + fn weighted_average(&self) -> f64 { + let mut sum = 0.0; + let mut weight_sum = 0.0; + let reverse_weights: Vec = self.weights.iter().cloned().rev().collect(); + for (index, input) in self.inputs.iter().rev().enumerate() { + let weight = reverse_weights.get(index).unwrap(); + sum += input * weight; + weight_sum += weight + } + sum / weight_sum + } +} + +impl Indicator for WeightedMovingAverage { + fn name(&self) -> String { + stringify!(WeightedMovingAverage).to_string() + } + + fn has_inputs(&self) -> bool { + self.has_inputs + } + fn is_initialized(&self) -> bool { + self.is_initialized + } + + fn handle_quote_tick(&mut self, tick: &QuoteTick) { + self.update_raw(tick.extract_price(self.price_type).into()); + } + + fn handle_trade_tick(&mut self, tick: &TradeTick) { + self.update_raw((&tick.price).into()); + } + + fn handle_bar(&mut self, bar: &Bar) { + self.update_raw((&bar.close).into()); + } + + fn reset(&mut self) { + self.value = 0.0; + self.has_inputs = false; + self.is_initialized = false; + self.inputs.clear(); + } +} + +impl MovingAverage for WeightedMovingAverage { + fn value(&self) -> f64 { + self.value + } + + fn count(&self) -> usize { + self.inputs.len() + } + fn update_raw(&mut self, value: f64) { + if !self.has_inputs { + self.has_inputs = true; + self.inputs.push(value); + self.value = value; + return; + } + if self.inputs.len() == self.period { + self.inputs.remove(0); + } + self.inputs.push(value); + self.value = self.weighted_average(); + if !self.is_initialized && self.count() >= self.period { + self.is_initialized = true; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use rstest::rstest; + + use crate::{ + average::wma::WeightedMovingAverage, + indicator::{Indicator, MovingAverage}, + stubs::*, + }; + + #[rstest] + fn test_wma_initialized(indicator_wma_10: WeightedMovingAverage) { + let display_str = format!("{}", indicator_wma_10); + assert_eq!( + display_str, + "WeightedMovingAverage(10,[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])" + ); + assert_eq!(indicator_wma_10.name(), "WeightedMovingAverage"); + assert!(!indicator_wma_10.has_inputs()); + assert!(!indicator_wma_10.is_initialized()); + } + + #[rstest] + fn test_different_weights_len_and_period_error() { + let wma = WeightedMovingAverage::new(10, vec![0.5, 0.5, 0.5], None); + assert!(wma.is_err()); + } + + #[rstest] + fn test_value_with_one_input(mut indicator_wma_10: WeightedMovingAverage) { + indicator_wma_10.update_raw(1.0); + assert_eq!(indicator_wma_10.value, 1.0); + } + + #[rstest] + fn test_value_with_two_inputs_equal_weights() { + let mut wma = WeightedMovingAverage::new(2, vec![0.5, 0.5], None).unwrap(); + wma.update_raw(1.0); + wma.update_raw(2.0); + assert_eq!(wma.value, 1.5); + } + + #[rstest] + fn test_value_with_four_inputs_equal_weights() { + let mut wma = WeightedMovingAverage::new(4, vec![0.25, 0.25, 0.25, 0.25], None).unwrap(); + wma.update_raw(1.0); + wma.update_raw(2.0); + wma.update_raw(3.0); + wma.update_raw(4.0); + assert_eq!(wma.value, 2.5); + } + + #[rstest] + fn test_value_with_two_inputs(mut indicator_wma_10: WeightedMovingAverage) { + indicator_wma_10.update_raw(1.0); + indicator_wma_10.update_raw(2.0); + let result = (2.0 * 1.0 + 1.0 * 0.9) / 1.9; + assert_eq!(indicator_wma_10.value, result); + } + + #[rstest] + fn test_value_with_three_inputs(mut indicator_wma_10: WeightedMovingAverage) { + indicator_wma_10.update_raw(1.0); + indicator_wma_10.update_raw(2.0); + indicator_wma_10.update_raw(3.0); + let result = (3.0 * 1.0 + 2.0 * 0.9 + 1.0 * 0.8) / (1.0 + 0.9 + 0.8); + assert_eq!(indicator_wma_10.value, result); + } + + #[rstest] + fn test_value_expected_with_exact_period(mut indicator_wma_10: WeightedMovingAverage) { + for i in 1..11 { + indicator_wma_10.update_raw(i as f64); + } + assert_eq!(indicator_wma_10.value, 7.0); + } + + #[rstest] + fn test_value_expected_with_more_inputs(mut indicator_wma_10: WeightedMovingAverage) { + for i in 1..=11 { + indicator_wma_10.update_raw(i as f64); + } + assert_eq!(indicator_wma_10.value(), 8.0000000000000018); + } + + #[rstest] + fn test_reset(mut indicator_wma_10: WeightedMovingAverage) { + indicator_wma_10.update_raw(1.0); + indicator_wma_10.update_raw(2.0); + indicator_wma_10.reset(); + assert_eq!(indicator_wma_10.value, 0.0); + assert_eq!(indicator_wma_10.count(), 0); + assert!(!indicator_wma_10.has_inputs); + assert!(!indicator_wma_10.is_initialized); + } +} diff --git a/nautilus_core/indicators/src/lib.rs b/nautilus_core/indicators/src/lib.rs index 3044b4fcb80d..da03add6f2c6 100644 --- a/nautilus_core/indicators/src/lib.rs +++ b/nautilus_core/indicators/src/lib.rs @@ -13,8 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use pyo3::{prelude::*, types::PyModule, Python}; - pub mod average; pub mod indicator; pub mod momentum; @@ -23,17 +21,5 @@ pub mod ratio; #[cfg(test)] mod stubs; -/// Loaded as nautilus_pyo3.indicators -#[pymodule] -pub fn indicators(_: Python<'_>, m: &PyModule) -> PyResult<()> { - // average - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - // ratio - m.add_class::()?; - // momentum - m.add_class::()?; - Ok(()) -} +#[cfg(feature = "python")] +pub mod python; diff --git a/nautilus_core/indicators/src/momentum/rsi.rs b/nautilus_core/indicators/src/momentum/rsi.rs index bcdc541aef1d..48621c0b76df 100644 --- a/nautilus_core/indicators/src/momentum/rsi.rs +++ b/nautilus_core/indicators/src/momentum/rsi.rs @@ -16,7 +16,6 @@ use std::fmt::{Debug, Display}; use anyhow::Result; -use nautilus_core::python::to_pyvalue_err; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -37,13 +36,12 @@ pub struct RelativeStrengthIndex { pub ma_type: MovingAverageType, pub value: f64, pub count: usize, - // pub inputs: Vec, + pub is_initialized: bool, _has_inputs: bool, _last_value: f64, _average_gain: Box, _average_loss: Box, _rsi_max: f64, - is_initialized: bool, } impl Display for RelativeStrengthIndex { @@ -143,69 +141,6 @@ impl RelativeStrengthIndex { } } -#[cfg(feature = "python")] -#[pymethods] -impl RelativeStrengthIndex { - #[new] - pub fn py_new(period: usize, ma_type: Option) -> PyResult { - Self::new(period, ma_type).map_err(to_pyvalue_err) - } - - #[getter] - #[pyo3(name = "name")] - fn py_name(&self) -> String { - self.name() - } - - #[getter] - #[pyo3(name = "period")] - fn py_period(&self) -> usize { - self.period - } - - #[getter] - #[pyo3(name = "count")] - fn py_count(&self) -> usize { - self.count - } - - #[getter] - #[pyo3(name = "value")] - fn py_value(&self) -> f64 { - self.value - } - - #[getter] - #[pyo3(name = "initialized")] - fn py_initialized(&self) -> bool { - self.is_initialized - } - - #[pyo3(name = "update_raw")] - fn py_update_raw(&mut self, value: f64) { - self.update_raw(value); - } - - #[pyo3(name = "handle_quote_tick")] - fn py_handle_quote_tick(&mut self, tick: &QuoteTick) { - self.py_update_raw(tick.extract_price(PriceType::Mid).into()); - } - - #[pyo3(name = "handle_bar")] - fn py_handle_bar(&mut self, bar: &Bar) { - self.update_raw((&bar.close).into()); - } - - #[pyo3(name = "handle_trade_tick")] - fn py_handle_trade_tick(&mut self, tick: &TradeTick) { - self.update_raw((&tick.price).into()); - } - - fn __repr__(&self) -> String { - format!("ExponentialMovingAverage({})", self.period) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/indicators/src/python/average/ama.rs b/nautilus_core/indicators/src/python/average/ama.rs new file mode 100644 index 000000000000..d27568e7b50d --- /dev/null +++ b/nautilus_core/indicators/src/python/average/ama.rs @@ -0,0 +1,104 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +use nautilus_core::python::to_pyvalue_err; +use nautilus_model::{ + data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, + enums::PriceType, +}; +use pyo3::prelude::*; + +use crate::{ + average::ama::AdaptiveMovingAverage, + indicator::{Indicator, MovingAverage}, +}; + +#[pymethods] +impl AdaptiveMovingAverage { + #[new] + pub fn py_new( + period_efficiency_ratio: usize, + period_fast: usize, + period_slow: usize, + price_type: Option, + ) -> PyResult { + Self::new( + period_efficiency_ratio, + period_fast, + period_slow, + price_type, + ) + .map_err(to_pyvalue_err) + } + + #[getter] + #[pyo3(name = "name")] + fn py_name(&self) -> String { + self.name() + } + + #[getter] + #[pyo3(name = "count")] + fn py_count(&self) -> usize { + self.count + } + + #[getter] + #[pyo3(name = "has_inputs")] + fn py_has_inputs(&self) -> bool { + self.has_inputs() + } + + #[getter] + #[pyo3(name = "initialized")] + fn py_initialized(&self) -> bool { + self.is_initialized + } + + #[pyo3(name = "handle_quote_tick")] + fn py_handle_quote_tick(&mut self, tick: &QuoteTick) { + self.py_update_raw(tick.extract_price(self.price_type).into()); + } + + #[pyo3(name = "handle_trade_tick")] + fn py_handle_trade_tick(&mut self, tick: &TradeTick) { + self.update_raw((&tick.price).into()); + } + + #[pyo3(name = "handle_bar")] + fn py_handle_bar(&mut self, bar: &Bar) { + self.update_raw((&bar.close).into()); + } + + #[pyo3(name = "reset")] + fn py_reset(&mut self) { + self.reset(); + } + + #[pyo3(name = "update_raw")] + fn py_update_raw(&mut self, value: f64) { + self.update_raw(value); + } + + fn __repr__(&self) -> String { + format!( + "WeightedMovingAverage({}({},{},{})", + self.name(), + self.period_efficiency_ratio, + self.period_fast, + self.period_slow + ) + } +} diff --git a/nautilus_core/indicators/src/python/average/dema.rs b/nautilus_core/indicators/src/python/average/dema.rs new file mode 100644 index 000000000000..71f0cc6b5784 --- /dev/null +++ b/nautilus_core/indicators/src/python/average/dema.rs @@ -0,0 +1,99 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +use nautilus_core::python::to_pyvalue_err; +use nautilus_model::{ + data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, + enums::PriceType, +}; +use pyo3::prelude::*; + +use crate::{ + average::dema::DoubleExponentialMovingAverage, + indicator::{Indicator, MovingAverage}, +}; + +#[pymethods] +impl DoubleExponentialMovingAverage { + #[new] + fn py_new(period: usize, price_type: Option) -> PyResult { + Self::new(period, price_type).map_err(to_pyvalue_err) + } + + #[getter] + #[pyo3(name = "name")] + fn py_name(&self) -> String { + self.name() + } + + #[getter] + #[pyo3(name = "period")] + fn py_period(&self) -> usize { + self.period + } + + #[getter] + #[pyo3(name = "count")] + fn py_count(&self) -> usize { + self.count + } + + #[getter] + #[pyo3(name = "value")] + fn py_value(&self) -> f64 { + self.value + } + + #[getter] + #[pyo3(name = "has_inputs")] + fn py_has_inputs(&self) -> bool { + self.has_inputs() + } + + #[getter] + #[pyo3(name = "initialized")] + fn py_initialized(&self) -> bool { + self.is_initialized + } + + #[pyo3(name = "handle_quote_tick")] + fn py_handle_quote_tick(&mut self, tick: &QuoteTick) { + self.py_update_raw(tick.extract_price(self.price_type).into()); + } + + #[pyo3(name = "handle_trade_tick")] + fn py_handle_trade_tick(&mut self, tick: &TradeTick) { + self.update_raw((&tick.price).into()); + } + + #[pyo3(name = "handle_bar")] + fn py_handle_bar(&mut self, bar: &Bar) { + self.update_raw((&bar.close).into()); + } + + #[pyo3(name = "reset")] + fn py_reset(&mut self) { + self.reset(); + } + + #[pyo3(name = "update_raw")] + fn py_update_raw(&mut self, value: f64) { + self.update_raw(value); + } + + fn __repr__(&self) -> String { + format!("DoubleExponentialMovingAverage({})", self.period) + } +} diff --git a/nautilus_core/indicators/src/python/average/ema.rs b/nautilus_core/indicators/src/python/average/ema.rs new file mode 100644 index 000000000000..ff0977751d2a --- /dev/null +++ b/nautilus_core/indicators/src/python/average/ema.rs @@ -0,0 +1,105 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +use nautilus_core::python::to_pyvalue_err; +use nautilus_model::{ + data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, + enums::PriceType, +}; +use pyo3::prelude::*; + +use crate::{ + average::ema::ExponentialMovingAverage, + indicator::{Indicator, MovingAverage}, +}; + +#[pymethods] +impl ExponentialMovingAverage { + #[new] + fn py_new(period: usize, price_type: Option) -> PyResult { + Self::new(period, price_type).map_err(to_pyvalue_err) + } + + #[getter] + #[pyo3(name = "name")] + fn py_name(&self) -> String { + self.name() + } + + #[getter] + #[pyo3(name = "period")] + fn py_period(&self) -> usize { + self.period + } + + #[getter] + #[pyo3(name = "alpha")] + fn py_alpha(&self) -> f64 { + self.alpha + } + + #[getter] + #[pyo3(name = "count")] + fn py_count(&self) -> usize { + self.count + } + + #[getter] + #[pyo3(name = "value")] + fn py_value(&self) -> f64 { + self.value + } + + #[getter] + #[pyo3(name = "has_inputs")] + fn py_has_inputs(&self) -> bool { + self.has_inputs() + } + + #[getter] + #[pyo3(name = "initialized")] + fn py_initialized(&self) -> bool { + self.is_initialized + } + + #[pyo3(name = "handle_quote_tick")] + fn py_handle_quote_tick(&mut self, tick: &QuoteTick) { + self.py_update_raw(tick.extract_price(self.price_type).into()); + } + + #[pyo3(name = "handle_trade_tick")] + fn py_handle_trade_tick(&mut self, tick: &TradeTick) { + self.update_raw((&tick.price).into()); + } + + #[pyo3(name = "handle_bar")] + fn py_handle_bar(&mut self, bar: &Bar) { + self.update_raw((&bar.close).into()); + } + + #[pyo3(name = "reset")] + fn py_reset(&mut self) { + self.reset(); + } + + #[pyo3(name = "update_raw")] + fn py_update_raw(&mut self, value: f64) { + self.update_raw(value); + } + + fn __repr__(&self) -> String { + format!("ExponentialMovingAverage({})", self.period) + } +} diff --git a/nautilus_core/indicators/src/python/average/mod.rs b/nautilus_core/indicators/src/python/average/mod.rs new file mode 100644 index 000000000000..521a97e997ec --- /dev/null +++ b/nautilus_core/indicators/src/python/average/mod.rs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +pub mod ama; +pub mod dema; +pub mod ema; +pub mod sma; +pub mod wma; diff --git a/nautilus_core/indicators/src/python/average/sma.rs b/nautilus_core/indicators/src/python/average/sma.rs new file mode 100644 index 000000000000..1b297e95db03 --- /dev/null +++ b/nautilus_core/indicators/src/python/average/sma.rs @@ -0,0 +1,99 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +use nautilus_core::python::to_pyvalue_err; +use nautilus_model::{ + data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, + enums::PriceType, +}; +use pyo3::prelude::*; + +use crate::{ + average::sma::SimpleMovingAverage, + indicator::{Indicator, MovingAverage}, +}; + +#[pymethods] +impl SimpleMovingAverage { + #[new] + fn py_new(period: usize, price_type: Option) -> PyResult { + Self::new(period, price_type).map_err(to_pyvalue_err) + } + + #[getter] + #[pyo3(name = "name")] + fn py_name(&self) -> String { + self.name() + } + + #[getter] + #[pyo3(name = "period")] + fn py_period(&self) -> usize { + self.period + } + + #[getter] + #[pyo3(name = "count")] + fn py_count(&self) -> usize { + self.count + } + + #[getter] + #[pyo3(name = "value")] + fn py_value(&self) -> f64 { + self.value + } + + #[getter] + #[pyo3(name = "has_inputs")] + fn py_has_inputs(&self) -> bool { + self.has_inputs() + } + + #[getter] + #[pyo3(name = "initialized")] + fn py_initialized(&self) -> bool { + self.is_initialized + } + + #[pyo3(name = "handle_quote_tick")] + fn py_handle_quote_tick(&mut self, tick: &QuoteTick) { + self.py_update_raw(tick.extract_price(self.price_type).into()); + } + + #[pyo3(name = "handle_trade_tick")] + fn py_handle_trade_tick(&mut self, tick: &TradeTick) { + self.update_raw((&tick.price).into()); + } + + #[pyo3(name = "handle_bar")] + fn py_handle_bar(&mut self, bar: &Bar) { + self.update_raw((&bar.close).into()); + } + + #[pyo3(name = "reset")] + fn py_reset(&mut self) { + self.reset(); + } + + #[pyo3(name = "update_raw")] + fn py_update_raw(&mut self, value: f64) { + self.update_raw(value); + } + + fn __repr__(&self) -> String { + format!("SimpleMovingAverage({})", self.period) + } +} diff --git a/nautilus_core/indicators/src/python/average/wma.rs b/nautilus_core/indicators/src/python/average/wma.rs new file mode 100644 index 000000000000..84d212d6cc7a --- /dev/null +++ b/nautilus_core/indicators/src/python/average/wma.rs @@ -0,0 +1,97 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +use nautilus_core::python::to_pyvalue_err; +use nautilus_model::{ + data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, + enums::PriceType, +}; +use pyo3::prelude::*; + +use crate::{ + average::wma::WeightedMovingAverage, + indicator::{Indicator, MovingAverage}, +}; + +#[pymethods] +impl WeightedMovingAverage { + #[new] + pub fn py_new( + period: usize, + weights: Vec, + price_type: Option, + ) -> PyResult { + Self::new(period, weights, price_type).map_err(to_pyvalue_err) + } + + #[getter] + #[pyo3(name = "name")] + fn py_name(&self) -> String { + self.name() + } + + #[getter] + #[pyo3(name = "period")] + fn py_period(&self) -> usize { + self.period + } + + #[getter] + #[pyo3(name = "count")] + fn py_count(&self) -> usize { + self.count() + } + + #[getter] + #[pyo3(name = "has_inputs")] + fn py_has_inputs(&self) -> bool { + self.has_inputs() + } + + #[getter] + #[pyo3(name = "initialized")] + fn py_initialized(&self) -> bool { + self.is_initialized + } + + #[pyo3(name = "handle_quote_tick")] + fn py_handle_quote_tick(&mut self, tick: &QuoteTick) { + self.py_update_raw(tick.extract_price(self.price_type).into()); + } + + #[pyo3(name = "handle_trade_tick")] + fn py_handle_trade_tick(&mut self, tick: &TradeTick) { + self.update_raw((&tick.price).into()); + } + + #[pyo3(name = "handle_bar")] + fn py_handle_bar(&mut self, bar: &Bar) { + self.update_raw((&bar.close).into()); + } + + #[pyo3(name = "reset")] + fn py_reset(&mut self) { + self.reset(); + } + + #[pyo3(name = "update_raw")] + fn py_update_raw(&mut self, value: f64) { + self.update_raw(value); + } + + fn __repr__(&self) -> String { + format!("WeightedMovingAverage({},{:?})", self.period, self.weights) + } +} diff --git a/nautilus_core/indicators/src/python/mod.rs b/nautilus_core/indicators/src/python/mod.rs new file mode 100644 index 000000000000..d5e208f9c045 --- /dev/null +++ b/nautilus_core/indicators/src/python/mod.rs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +use pyo3::{prelude::*, pymodule}; + +pub mod average; +pub mod momentum; +pub mod ratio; + +#[pymodule] +pub fn indicators(_: Python<'_>, m: &PyModule) -> PyResult<()> { + // average + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + // ratio + m.add_class::()?; + // momentum + m.add_class::()?; + Ok(()) +} diff --git a/nautilus_core/indicators/src/python/momentum/mod.rs b/nautilus_core/indicators/src/python/momentum/mod.rs new file mode 100644 index 000000000000..fc49c3b4cf36 --- /dev/null +++ b/nautilus_core/indicators/src/python/momentum/mod.rs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +pub mod rsi; diff --git a/nautilus_core/indicators/src/python/momentum/rsi.rs b/nautilus_core/indicators/src/python/momentum/rsi.rs new file mode 100644 index 000000000000..ae87fd3bce52 --- /dev/null +++ b/nautilus_core/indicators/src/python/momentum/rsi.rs @@ -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. +// ------------------------------------------------------------------------------------------------- + +use nautilus_core::python::to_pyvalue_err; +use nautilus_model::{ + data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, + enums::PriceType, +}; +use pyo3::prelude::*; + +use crate::{ + average::MovingAverageType, indicator::Indicator, momentum::rsi::RelativeStrengthIndex, +}; + +#[pymethods] +impl RelativeStrengthIndex { + #[new] + pub fn py_new(period: usize, ma_type: Option) -> PyResult { + Self::new(period, ma_type).map_err(to_pyvalue_err) + } + + #[getter] + #[pyo3(name = "name")] + fn py_name(&self) -> String { + self.name() + } + + #[getter] + #[pyo3(name = "period")] + fn py_period(&self) -> usize { + self.period + } + + #[getter] + #[pyo3(name = "count")] + fn py_count(&self) -> usize { + self.count + } + + #[getter] + #[pyo3(name = "value")] + fn py_value(&self) -> f64 { + self.value + } + + #[getter] + #[pyo3(name = "initialized")] + fn py_initialized(&self) -> bool { + self.is_initialized + } + + #[pyo3(name = "update_raw")] + fn py_update_raw(&mut self, value: f64) { + self.update_raw(value); + } + + #[pyo3(name = "handle_quote_tick")] + fn py_handle_quote_tick(&mut self, tick: &QuoteTick) { + self.py_update_raw(tick.extract_price(PriceType::Mid).into()); + } + + #[pyo3(name = "handle_bar")] + fn py_handle_bar(&mut self, bar: &Bar) { + self.update_raw((&bar.close).into()); + } + + #[pyo3(name = "handle_trade_tick")] + fn py_handle_trade_tick(&mut self, tick: &TradeTick) { + self.update_raw((&tick.price).into()); + } + + fn __repr__(&self) -> String { + format!("ExponentialMovingAverage({})", self.period) + } +} diff --git a/nautilus_core/indicators/src/python/ratio/efficiency_ratio.rs b/nautilus_core/indicators/src/python/ratio/efficiency_ratio.rs new file mode 100644 index 000000000000..43ee463e2721 --- /dev/null +++ b/nautilus_core/indicators/src/python/ratio/efficiency_ratio.rs @@ -0,0 +1,66 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +use nautilus_core::python::to_pyvalue_err; +use nautilus_model::enums::PriceType; +use pyo3::prelude::*; + +use crate::{indicator::Indicator, ratio::efficiency_ratio::EfficiencyRatio}; + +#[pymethods] +impl EfficiencyRatio { + #[new] + fn py_new(period: usize, price_type: Option) -> PyResult { + Self::new(period, price_type).map_err(to_pyvalue_err) + } + + #[getter] + #[pyo3(name = "name")] + fn py_name(&self) -> String { + self.name() + } + + #[getter] + #[pyo3(name = "period")] + fn py_period(&self) -> usize { + self.period + } + + #[getter] + #[pyo3(name = "value")] + fn py_value(&self) -> f64 { + self.value + } + + #[getter] + #[pyo3(name = "initialized")] + fn py_initialized(&self) -> bool { + self.is_initialized + } + + #[pyo3(name = "has_inputs")] + fn py_has_inputs(&self) -> bool { + self.has_inputs() + } + + #[pyo3(name = "update_raw")] + fn py_update_raw(&mut self, value: f64) { + self.update_raw(value); + } + + fn __repr__(&self) -> String { + format!("EfficiencyRatio({})", self.period) + } +} diff --git a/nautilus_core/indicators/src/python/ratio/mod.rs b/nautilus_core/indicators/src/python/ratio/mod.rs new file mode 100644 index 000000000000..1d3e5ababfb9 --- /dev/null +++ b/nautilus_core/indicators/src/python/ratio/mod.rs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +pub mod efficiency_ratio; diff --git a/nautilus_core/indicators/src/ratio/efficiency_ratio.rs b/nautilus_core/indicators/src/ratio/efficiency_ratio.rs index c8345bb88b86..25fb558a60a1 100644 --- a/nautilus_core/indicators/src/ratio/efficiency_ratio.rs +++ b/nautilus_core/indicators/src/ratio/efficiency_ratio.rs @@ -16,7 +16,6 @@ use std::fmt::Display; use anyhow::Result; -use nautilus_core::python::to_pyvalue_err; use nautilus_model::{ data::{bar::Bar, quote::QuoteTick, trade::TradeTick}, enums::PriceType, @@ -37,8 +36,8 @@ pub struct EfficiencyRatio { pub price_type: PriceType, pub value: f64, pub inputs: Vec, + pub is_initialized: bool, _deltas: Vec, - is_initialized: bool, } impl Display for EfficiencyRatio { @@ -111,53 +110,6 @@ impl EfficiencyRatio { } } -#[cfg(feature = "python")] -#[pymethods] -impl EfficiencyRatio { - #[new] - fn py_new(period: usize, price_type: Option) -> PyResult { - Self::new(period, price_type).map_err(to_pyvalue_err) - } - - #[getter] - #[pyo3(name = "name")] - fn py_name(&self) -> String { - self.name() - } - - #[getter] - #[pyo3(name = "period")] - fn py_period(&self) -> usize { - self.period - } - - #[getter] - #[pyo3(name = "value")] - fn py_value(&self) -> f64 { - self.value - } - - #[getter] - #[pyo3(name = "initialized")] - fn py_initialized(&self) -> bool { - self.is_initialized - } - - #[pyo3(name = "has_inputs")] - fn py_has_inputs(&self) -> bool { - self.has_inputs() - } - - #[pyo3(name = "update_raw")] - fn py_update_raw(&mut self, value: f64) { - self.update_raw(value); - } - - fn __repr__(&self) -> String { - format!("EfficiencyRatio({})", self.period) - } -} - //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/indicators/src/stubs.rs b/nautilus_core/indicators/src/stubs.rs index d6ae2566938e..a13295d228e6 100644 --- a/nautilus_core/indicators/src/stubs.rs +++ b/nautilus_core/indicators/src/stubs.rs @@ -27,7 +27,8 @@ use rstest::*; use crate::{ average::{ ama::AdaptiveMovingAverage, dema::DoubleExponentialMovingAverage, - ema::ExponentialMovingAverage, sma::SimpleMovingAverage, MovingAverageType, + ema::ExponentialMovingAverage, sma::SimpleMovingAverage, wma::WeightedMovingAverage, + MovingAverageType, }, momentum::rsi::RelativeStrengthIndex, ratio::efficiency_ratio::EfficiencyRatio, @@ -117,6 +118,12 @@ pub fn indicator_dema_10() -> DoubleExponentialMovingAverage { DoubleExponentialMovingAverage::new(10, Some(PriceType::Mid)).unwrap() } +#[fixture] +pub fn indicator_wma_10() -> WeightedMovingAverage { + let weights = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]; + WeightedMovingAverage::new(10, weights, Some(PriceType::Mid)).unwrap() +} + //////////////////////////////////////////////////////////////////////////////// // Ratios //////////////////////////////////////////////////////////////////////////////// diff --git a/nautilus_core/pyo3/src/lib.rs b/nautilus_core/pyo3/src/lib.rs index e4f639e3f77e..b813d0371c49 100644 --- a/nautilus_core/pyo3/src/lib.rs +++ b/nautilus_core/pyo3/src/lib.rs @@ -126,7 +126,7 @@ fn nautilus_pyo3(py: Python<'_>, m: &PyModule) -> PyResult<()> { // Indicators let n = "indicators"; - let submodule = pyo3::wrap_pymodule!(nautilus_indicators::indicators); + let submodule = pyo3::wrap_pymodule!(nautilus_indicators::python::indicators); m.add_wrapped(submodule)?; sys_modules.set_item(format!("{module_name}.{n}"), m.getattr(n)?)?; re_export_module_attributes(m, n)?; From 07b3af809c3b4ec29faead143e68b6a5f7e106cc Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 25 Oct 2023 18:28:19 +1100 Subject: [PATCH 12/78] Minor formatting --- nautilus_core/indicators/src/indicator.rs | 1 + nautilus_core/indicators/src/stubs.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/nautilus_core/indicators/src/indicator.rs b/nautilus_core/indicators/src/indicator.rs index 1222399ae89b..8d5dab28f6b7 100644 --- a/nautilus_core/indicators/src/indicator.rs +++ b/nautilus_core/indicators/src/indicator.rs @@ -41,6 +41,7 @@ impl Debug for dyn Indicator + Send { write!(f, "Indicator {{ ... }}") } } + impl Debug for dyn MovingAverage + Send { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Implement custom formatting for the Indicator trait object. diff --git a/nautilus_core/indicators/src/stubs.rs b/nautilus_core/indicators/src/stubs.rs index a13295d228e6..fc5f29b4f76d 100644 --- a/nautilus_core/indicators/src/stubs.rs +++ b/nautilus_core/indicators/src/stubs.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------------------------------- + use nautilus_model::{ data::{ bar::{Bar, BarSpecification, BarType}, From 4ac01adf56ee4206376e7f854a8c63ff315f08fa Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 25 Oct 2023 18:46:12 +1100 Subject: [PATCH 13/78] Update dependencies --- .pre-commit-config.yaml | 2 +- nautilus_core/Cargo.lock | 116 ++++++++++++++++++++++++--------------- poetry.lock | 98 ++++++++++++++++----------------- pyproject.toml | 4 +- 4 files changed, 123 insertions(+), 97 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e0e88be7f994..fb9e3d26c61e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,7 +76,7 @@ repos: exclude: "docs/_pygments/monokai.py" - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.1 + rev: v0.1.2 hooks: - id: ruff args: ["--fix"] diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 31e1264d1756..0fcb1a341f9a 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7d5a2cecb58716e47d67d5703a249964b14c7be1ec3cad3affc295b2d1c35d" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", "const-random", @@ -123,7 +123,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fab9e93ba8ce88a37d5a30dce4b9913b75413dc1ac56cb5d72e5a840543f829" dependencies = [ - "ahash 0.8.5", + "ahash 0.8.6", "arrow-arith", "arrow-array", "arrow-buffer", @@ -161,7 +161,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d02efa7253ede102d45a4e802a129e83bcc3f49884cab795b1ac223918e4318d" dependencies = [ - "ahash 0.8.5", + "ahash 0.8.6", "arrow-buffer", "arrow-data", "arrow-schema", @@ -287,7 +287,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "114a348ab581e7c9b6908fcab23cb39ff9f060eb19e72b13f8fb8eaa37f65d22" dependencies = [ - "ahash 0.8.5", + "ahash 0.8.6", "arrow-array", "arrow-buffer", "arrow-data", @@ -311,7 +311,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5c71e003202e67e9db139e5278c79f5520bb79922261dfe140e4637ee8b6108" dependencies = [ - "ahash 0.8.5", + "ahash 0.8.6", "arrow-array", "arrow-buffer", "arrow-data", @@ -562,9 +562,9 @@ dependencies = [ [[package]] name = "bytecount" -version = "0.6.5" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1a12477b7237a01c11a80a51278165f9ba0edd28fa6db00a65ab230320dc58c" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" [[package]] name = "byteorder" @@ -720,21 +720,21 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.6" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.6" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" dependencies = [ "anstyle", - "clap_lex 0.5.1", + "clap_lex 0.6.0", ] [[package]] @@ -748,9 +748,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "comfy-table" @@ -838,7 +838,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.4.6", + "clap 4.4.7", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -1005,7 +1005,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7014432223f4d721cb9786cd88bb89e7464e0ba984d4a7f49db7787f5f268674" dependencies = [ - "ahash 0.8.5", + "ahash 0.8.6", "arrow", "arrow-array", "arrow-schema", @@ -1053,7 +1053,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3903ed8f102892f17b48efa437f3542159241d41c564f0d1e78efdc5e663aa" dependencies = [ - "ahash 0.8.5", + "ahash 0.8.6", "arrow", "arrow-array", "arrow-buffer", @@ -1094,7 +1094,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24c382676338d8caba6c027ba0da47260f65ffedab38fda78f6d8043f607557c" dependencies = [ - "ahash 0.8.5", + "ahash 0.8.6", "arrow", "arrow-array", "datafusion-common", @@ -1127,7 +1127,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57b4968e9a998dc0476c4db7a82f280e2026b25f464e4aa0c3bb9807ee63ddfd" dependencies = [ - "ahash 0.8.5", + "ahash 0.8.6", "arrow", "arrow-array", "arrow-buffer", @@ -1161,7 +1161,7 @@ version = "32.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd0d1fe54e37a47a2d58a1232c22786f2c28ad35805fdcd08f0253a8b0aaa90" dependencies = [ - "ahash 0.8.5", + "ahash 0.8.6", "arrow", "arrow-array", "arrow-buffer", @@ -1545,7 +1545,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.5", + "ahash 0.8.6", ] [[package]] @@ -1554,7 +1554,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" dependencies = [ - "ahash 0.8.5", + "ahash 0.8.6", "allocator-api2", ] @@ -2408,7 +2408,7 @@ version = "47.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0463cc3b256d5f50408c49a4be3a16674f4c8ceef60941709620a062b1f6bf4d" dependencies = [ - "ahash 0.8.5", + "ahash 0.8.6", "arrow-array", "arrow-buffer", "arrow-cast", @@ -2890,12 +2890,26 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", - "untrusted", + "spin 0.5.2", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys", +] + [[package]] name = "rkyv" version = "0.7.42" @@ -3031,13 +3045,13 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.7" +version = "0.21.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" dependencies = [ "log", - "ring", - "rustls-webpki 0.101.6", + "ring 0.17.5", + "rustls-webpki 0.101.7", "sct", ] @@ -3068,18 +3082,18 @@ version = "0.100.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6a5fc258f1c1276dfe3016516945546e2d5383911efc0fc4f1cdc5df3a4ae3" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] name = "rustls-webpki" -version = "0.101.6" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] @@ -3120,12 +3134,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] @@ -3321,6 +3335,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "sqlparser" version = "0.38.0" @@ -3858,6 +3878,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.4.1" @@ -3874,7 +3900,7 @@ name = "ustr" version = "0.10.0" source = "git+https://github.com/anderslanglands/ustr#c78ddc25300c4720ffcb5f8ddef6028cef14535f" dependencies = [ - "ahash 0.8.5", + "ahash 0.8.6", "byteorder", "lazy_static", "parking_lot", @@ -4138,18 +4164,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.11" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c19fae0c8a9efc6a8281f2e623db8af1db9e57852e04cde3e754dd2dc29340f" +checksum = "69c48d63854f77746c68a5fbb4aa17f3997ece1cb301689a257af8cb80610d21" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.11" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc56589e9ddd1f1c28d4b4b5c773ce232910a6bb67a70133d61c9e347585efe9" +checksum = "c258c1040279e4f88763a113de72ce32dde2d50e2a94573f15dd534cea36a16d" dependencies = [ "proc-macro2", "quote", diff --git a/poetry.lock b/poetry.lock index 824110aed4ba..2f31864e23cf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -164,13 +164,13 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte [[package]] name = "babel" -version = "2.13.0" +version = "2.13.1" description = "Internationalization utilities" optional = false python-versions = ">=3.7" files = [ - {file = "Babel-2.13.0-py3-none-any.whl", hash = "sha256:fbfcae1575ff78e26c7449136f1abbefc3c13ce542eeb13d43d50d8b047216ec"}, - {file = "Babel-2.13.0.tar.gz", hash = "sha256:04c3e2d28d2b7681644508f836be388ae49e0cfe91465095340395b60d00f210"}, + {file = "Babel-2.13.1-py3-none-any.whl", hash = "sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"}, + {file = "Babel-2.13.1.tar.gz", hash = "sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900"}, ] [package.extras] @@ -529,34 +529,34 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "41.0.4" +version = "41.0.5" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839"}, - {file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f"}, - {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714"}, - {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb"}, - {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13"}, - {file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143"}, - {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397"}, - {file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860"}, - {file = "cryptography-41.0.4-cp37-abi3-win32.whl", hash = "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd"}, - {file = "cryptography-41.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d"}, - {file = "cryptography-41.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67"}, - {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e"}, - {file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829"}, - {file = "cryptography-41.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca"}, - {file = "cryptography-41.0.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d"}, - {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac"}, - {file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9"}, - {file = "cryptography-41.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"}, - {file = "cryptography-41.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91"}, - {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8"}, - {file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6"}, - {file = "cryptography-41.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311"}, - {file = "cryptography-41.0.4.tar.gz", hash = "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a"}, + {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"}, + {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"}, + {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147"}, + {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696"}, + {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da"}, + {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20"}, + {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548"}, + {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d"}, + {file = "cryptography-41.0.5-cp37-abi3-win32.whl", hash = "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936"}, + {file = "cryptography-41.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81"}, + {file = "cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1"}, + {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72"}, + {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88"}, + {file = "cryptography-41.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf"}, + {file = "cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e"}, + {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8"}, + {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179"}, + {file = "cryptography-41.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d"}, + {file = "cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1"}, + {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86"}, + {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"}, + {file = "cryptography-41.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84"}, + {file = "cryptography-41.0.5.tar.gz", hash = "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7"}, ] [package.dependencies] @@ -1877,13 +1877,13 @@ plugins = ["importlib-metadata"] [[package]] name = "pytest" -version = "7.4.2" +version = "7.4.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"}, - {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"}, + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, ] [package.dependencies] @@ -2166,28 +2166,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.1.1" +version = "0.1.2" description = "An extremely fast Python linter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.1-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:b7cdc893aef23ccc14c54bd79a8109a82a2c527e11d030b62201d86f6c2b81c5"}, - {file = "ruff-0.1.1-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:620d4b34302538dbd8bbbe8fdb8e8f98d72d29bd47e972e2b59ce6c1e8862257"}, - {file = "ruff-0.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a909d3930afdbc2e9fd893b0034479e90e7981791879aab50ce3d9f55205bd6"}, - {file = "ruff-0.1.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3305d1cb4eb8ff6d3e63a48d1659d20aab43b49fe987b3ca4900528342367145"}, - {file = "ruff-0.1.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c34ae501d0ec71acf19ee5d4d889e379863dcc4b796bf8ce2934a9357dc31db7"}, - {file = "ruff-0.1.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6aa7e63c3852cf8fe62698aef31e563e97143a4b801b57f920012d0e07049a8d"}, - {file = "ruff-0.1.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d68367d1379a6b47e61bc9de144a47bcdb1aad7903bbf256e4c3d31f11a87ae"}, - {file = "ruff-0.1.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc11955f6ce3398d2afe81ad7e49d0ebf0a581d8bcb27b8c300281737735e3a3"}, - {file = "ruff-0.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbbd8eead88ea83a250499074e2a8e9d80975f0b324b1e2e679e4594da318c25"}, - {file = "ruff-0.1.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f4780e2bb52f3863a565ec3f699319d3493b83ff95ebbb4993e59c62aaf6e75e"}, - {file = "ruff-0.1.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8f5b24daddf35b6c207619301170cae5d2699955829cda77b6ce1e5fc69340df"}, - {file = "ruff-0.1.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d3f9ac658ba29e07b95c80fa742b059a55aefffa8b1e078bc3c08768bdd4b11a"}, - {file = "ruff-0.1.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3521bf910104bf781e6753282282acc145cbe3eff79a1ce6b920404cd756075a"}, - {file = "ruff-0.1.1-py3-none-win32.whl", hash = "sha256:ba3208543ab91d3e4032db2652dcb6c22a25787b85b8dc3aeff084afdc612e5c"}, - {file = "ruff-0.1.1-py3-none-win_amd64.whl", hash = "sha256:3ff3006c97d9dc396b87fb46bb65818e614ad0181f059322df82bbfe6944e264"}, - {file = "ruff-0.1.1-py3-none-win_arm64.whl", hash = "sha256:e140bd717c49164c8feb4f65c644046fe929c46f42493672853e3213d7bdbce2"}, - {file = "ruff-0.1.1.tar.gz", hash = "sha256:c90461ae4abec261609e5ea436de4a4b5f2822921cf04c16d2cc9327182dbbcc"}, + {file = "ruff-0.1.2-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:0d3ee66b825b713611f89aa35d16de984f76f26c50982a25d52cd0910dff3923"}, + {file = "ruff-0.1.2-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f85f850a320ff532b8f93e8d1da6a36ef03698c446357c8c43b46ef90bb321eb"}, + {file = "ruff-0.1.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:809c6d4e45683696d19ca79e4c6bd3b2e9204fe9546923f2eb3b126ec314b0dc"}, + {file = "ruff-0.1.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46005e4abb268e93cad065244e17e2ea16b6fcb55a5c473f34fbc1fd01ae34cb"}, + {file = "ruff-0.1.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10cdb302f519664d5e2cf954562ac86c9d20ca05855e5b5c2f9d542228f45da4"}, + {file = "ruff-0.1.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f89ebcbe57a1eab7d7b4ceb57ddf0af9ed13eae24e443a7c1dc078000bd8cc6b"}, + {file = "ruff-0.1.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7344eaca057d4c32373c9c3a7afb7274f56040c225b6193dd495fcf69453b436"}, + {file = "ruff-0.1.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dffa25f6e03c4950b6ac6f216bc0f98a4be9719cb0c5260c8e88d1bac36f1683"}, + {file = "ruff-0.1.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42ddaea52cb7ba7c785e8593a7532866c193bc774fe570f0e4b1ccedd95b83c5"}, + {file = "ruff-0.1.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8533efda625bbec0bf27da2886bd641dae0c209104f6c39abc4be5b7b22de2a"}, + {file = "ruff-0.1.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b0b1b82221ba7c50e03b7a86b983157b5d3f4d8d4f16728132bdf02c6d651f77"}, + {file = "ruff-0.1.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c1362eb9288f8cc95535294cb03bd4665c8cef86ec32745476a4e5c6817034c"}, + {file = "ruff-0.1.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ffa7ef5ded0563329a35bd5a1cfdae40f05a75c0cc2dd30f00b1320b1fb461fc"}, + {file = "ruff-0.1.2-py3-none-win32.whl", hash = "sha256:6e8073f85e47072256e2e1909f1ae515cf61ff5a4d24730a63b8b4ac24b6704a"}, + {file = "ruff-0.1.2-py3-none-win_amd64.whl", hash = "sha256:b836ddff662a45385948ee0878b0a04c3a260949905ad861a37b931d6ee1c210"}, + {file = "ruff-0.1.2-py3-none-win_arm64.whl", hash = "sha256:b0c42d00db5639dbd5f7f9923c63648682dd197bf5de1151b595160c96172691"}, + {file = "ruff-0.1.2.tar.gz", hash = "sha256:afd4785ae060ce6edcd52436d0c197628a918d6d09e3107a892a1bad6a4c6608"}, ] [[package]] @@ -2887,4 +2887,4 @@ redis = ["hiredis", "redis"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "08ed7805e50e6f8aea5a1c66b0251d805a3117bf8c1531343232d06946385be4" +content-hash = "e0aeb520bd17e4792242a37451d61ebd5cd325b1c11f3584a75e48a0425931ca" diff --git a/pyproject.toml b/pyproject.toml index 7044f14e3654..7a129837972f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,7 @@ black = "^23.10.1" docformatter = "^1.7.5" mypy = "^1.6.1" pre-commit = "^3.5.0" -ruff = "^0.1.1" +ruff = "^0.1.2" types-pytz = "^2023.3" types-redis = "^4.6" types-requests = "^2.31" @@ -97,7 +97,7 @@ optional = true [tool.poetry.group.test.dependencies] coverage = "^7.3.2" -pytest = "^7.4.2" +pytest = "^7.4.3" pytest-aiohttp = "^1.0.5" pytest-asyncio = "^0.21.1" pytest-benchmark = "^4.0.0" From 63e1b7b74508aded0472cd0255e332bcc1112980 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 25 Oct 2023 20:16:34 +1100 Subject: [PATCH 14/78] Update ParquetDataCatalog.write_data method and docs --- docs/concepts/data.md | 22 +++++++++++-- docs/getting_started/quickstart.md | 5 +++ .../persistence/catalog/parquet.py | 31 +++++++++++++++++-- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/docs/concepts/data.md b/docs/concepts/data.md index 0a70447ff902..8c4c69c9148c 100644 --- a/docs/concepts/data.md +++ b/docs/concepts/data.md @@ -149,9 +149,25 @@ The following example shows the above list of Binance `OrderBookDelta` objects b catalog.write_data(deltas) ``` +### Basename template +Nautilus makes no assumptions about how data may be partitioned between files for a particular +data type and instrument ID. + +The `basename_template` keyword argument is an additional optional naming component for the output files. +The template should include placeholders that will be filled in with actual values at runtime. +These values can be automatically derived from the data or provided as additional keyword arguments. + +For example, using a basename template like `"{date}"` for AUD/USD.SIM quote tick data, +and assuming `"date"` is a provided or derivable field, could result in a filename like +`"2023-01-01.parquet"` under the `"quote_tick/audusd.sim/"` catalog directory. +If not provided, a default naming scheme will be applied. This parameter should be specified as a +keyword argument, like `write_data(data, basename_template="{date}")`. + ```{warning} -Existing data for the same data type, `instrument_id`, and date will be overwritten without prior warning. -Ensure you have appropriate backups or safeguards in place before performing this action. +Any existing data which already exists under a filename will be overwritten. +If a `basename_template` is not provided, then its very likely existing data for the data type and instrument ID will +be overwritten. To prevent data loss, ensure that the `basename_template` (or the default naming scheme) +generates unique filenames for different data sets. ``` Rust Arrow schema implementations and available for the follow data types (enhanced performance): @@ -166,6 +182,7 @@ Any stored data can then we read back into memory: from nautilus_trader.core.datetime import dt_to_unix_nanos import pandas as pd + start = dt_to_unix_nanos(pd.Timestamp("2020-01-03", tz=pytz.utc)) end = dt_to_unix_nanos(pd.Timestamp("2020-01-04", tz=pytz.utc)) @@ -180,6 +197,7 @@ The following example shows how to achieve this by initializing a `BacktestDataC from nautilus_trader.config import BacktestDataConfig from nautilus_trader.model.data import OrderBookDelta + data_config = BacktestDataConfig( catalog_path=str(catalog.path), data_cls=OrderBookDelta, diff --git a/docs/getting_started/quickstart.md b/docs/getting_started/quickstart.md index b76d6e12b450..fc7bd2778c5f 100644 --- a/docs/getting_started/quickstart.md +++ b/docs/getting_started/quickstart.md @@ -47,6 +47,7 @@ If everything worked correctly, you should be able to see a single EUR/USD instr ```python from nautilus_trader.persistence.catalog import ParquetDataCatalog + # You can also use `ParquetDataCatalog.from_env()` which will use the `NAUTILUS_PATH` environment variable # catalog = ParquetDataCatalog.from_env() catalog = ParquetDataCatalog("./catalog") @@ -191,6 +192,7 @@ FX trading is typically done on margin with Non-Deliverable Forward, Swap or CFD ```python from nautilus_trader.config import BacktestVenueConfig + venue = BacktestVenueConfig( name="SIM", oms_type="NETTING", @@ -221,6 +223,7 @@ adding the `QuoteTick`(s) for our EUR/USD instrument: from nautilus_trader.config import BacktestDataConfig from nautilus_trader.model.data import QuoteTick + data = BacktestDataConfig( catalog_path=str(catalog.path), data_cls=QuoteTick, @@ -243,6 +246,7 @@ from nautilus_trader.config import BacktestEngineConfig from nautilus_trader.config import ImportableStrategyConfig from nautilus_trader.config import LoggingConfig + engine = BacktestEngineConfig( strategies=[ ImportableStrategyConfig( @@ -302,6 +306,7 @@ The engine(s) can provide additional reports and information. from nautilus_trader.backtest.engine import BacktestEngine from nautilus_trader.model.identifiers import Venue + engine: BacktestEngine = node.get_engine(config.id) engine.trader.generate_order_fills_report() diff --git a/nautilus_trader/persistence/catalog/parquet.py b/nautilus_trader/persistence/catalog/parquet.py index f082c00b6d5d..f76d32165973 100644 --- a/nautilus_trader/persistence/catalog/parquet.py +++ b/nautilus_trader/persistence/catalog/parquet.py @@ -228,6 +228,7 @@ def write_chunk( data: list[Data], data_cls: type[Data], instrument_id: str | None = None, + basename_template: str = "part-{i}", **kwargs: Any, ) -> None: table = self._objects_to_table(data, data_cls=data_cls) @@ -235,12 +236,18 @@ def write_chunk( kw = dict(**self.dataset_kwargs, **kwargs) if "partitioning" not in kw: - self._fast_write(table=table, path=path, fs=self.fs) + self._fast_write( + table=table, + path=path, + fs=self.fs, + basename_template=basename_template, + ) else: # Write parquet file pds.write_dataset( data=table, base_dir=path, + basename_template=basename_template, format="parquet", filesystem=self.fs, min_rows_per_group=self.min_rows_per_group, @@ -254,7 +261,7 @@ def _fast_write( table: pa.Table, path: str, fs: fsspec.AbstractFileSystem, - basename_template: str = "part-{i}", + basename_template: str, ) -> None: name = basename_template.format(i=0) fs.mkdirs(path, exist_ok=True) @@ -265,7 +272,12 @@ def _fast_write( row_group_size=self.max_rows_per_group, ) - def write_data(self, data: list[Data | Event], **kwargs: Any) -> None: + def write_data( + self, + data: list[Data | Event], + basename_template: str = "part-{i}", + **kwargs: Any, + ) -> None: """ Write the given `data` to the catalog. @@ -277,9 +289,21 @@ def write_data(self, data: list[Data | Event], **kwargs: Any) -> None: ---------- data : list[Data | Event] The data or event objects to be written to the catalog. + basename_template : str, default 'part-{i}' + A template string used to generate basenames of written data files. + The token '{i}' will be replaced with an automatically incremented + integer as files are partitioned. + If not specified, it defaults to 'part-{i}' + the default extension '.parquet'. kwargs : Any Additional keyword arguments to be passed to the `write_chunk` method. + Warnings + -------- + Any existing data which already exists under a filename will be overwritten. + If a `basename_template` is not provided, then its very likely existing data for the data type and instrument ID will + be overwritten. To prevent data loss, ensure that the `basename_template` (or the default naming scheme) + generates unique filenames for different data sets. + Notes ----- - All data of the same type is expected to be monotonically increasing, or non-decreasing @@ -311,6 +335,7 @@ def key(obj: Any) -> tuple[str, str | None]: data=list(single_type), data_cls=name_to_cls[cls_name], instrument_id=instrument_id, + basename_template=basename_template, **kwargs, ) From 958e0a180e7802a123989814243382f964ab9409 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 25 Oct 2023 22:17:37 +1100 Subject: [PATCH 15/78] Fix Strategy cancellation of managed GTD on start --- nautilus_trader/trading/strategy.pyx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nautilus_trader/trading/strategy.pyx b/nautilus_trader/trading/strategy.pyx index d8c81796e7b4..da61278fdd84 100644 --- a/nautilus_trader/trading/strategy.pyx +++ b/nautilus_trader/trading/strategy.pyx @@ -353,8 +353,14 @@ cdef class Strategy(Actor): if self.manage_gtd_expiry: for order in open_orders: - if order.time_in_force == TimeInForce.GTD and not self._has_gtd_expiry_timer(order.client_order_id): - self._set_gtd_expiry(order) + if order.time_in_force == TimeInForce.GTD: + if self._clock.timestamp_ns() >= order.expire_time_ns: + if self._has_gtd_expiry_timer(order.client_order_id): + self.cancel_gtd_expiry(order) + self.cancel_order(order) + continue + if not self._has_gtd_expiry_timer(order.client_order_id): + self._set_gtd_expiry(order) self.on_start() From c2e4bb9066d94026b8347863e458ea11985658b6 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 26 Oct 2023 18:35:26 +1100 Subject: [PATCH 16/78] Update core dependencies --- nautilus_core/Cargo.lock | 24 ++++++++++++------------ nautilus_core/Cargo.toml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 0fcb1a341f9a..6603770b7c61 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2327,9 +2327,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.1.5+3.1.3" +version = "300.1.6+3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491" +checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085" dependencies = [ "cc", ] @@ -3185,18 +3185,18 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.189" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", @@ -3670,9 +3670,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -4164,18 +4164,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69c48d63854f77746c68a5fbb4aa17f3997ece1cb301689a257af8cb80610d21" +checksum = "81ba595b9f2772fbee2312de30eeb80ec773b4cb2f1e8098db024afadda6c06f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c258c1040279e4f88763a113de72ce32dde2d50e2a94573f15dd534cea36a16d" +checksum = "772666c41fb6dceaf520b564b962d738a8e1a83b41bd48945f50837aed78bb1d" dependencies = [ "proc-macro2", "quote", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 0da7730c61e3..cb4721b1464c 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -31,7 +31,7 @@ rand = "0.8.5" rmp-serde = "1.1.2" rust_decimal = "1.32.0" rust_decimal_macros = "1.32.0" -serde = { version = "1.0.189", features = ["derive"] } +serde = { version = "1.0.190", features = ["derive"] } serde_json = "1.0.107" strum = { version = "0.25.0", features = ["derive"] } thiserror = "1.0.50" From 722f46ff1a842f332eb0eed0ae46df1f5430e4f8 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 26 Oct 2023 18:56:26 +1100 Subject: [PATCH 17/78] Fix managed GTD orders timer cancel on order cancel --- RELEASES.md | 4 +++ nautilus_trader/trading/strategy.pyx | 7 +++-- tests/unit_tests/trading/test_strategy.py | 36 +++++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 52f3e675b81f..3d3e95d54d05 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -10,9 +10,13 @@ Released on TBC (UTC). ### Fixes - Fixed `ParquetDataCatalog` file writing template, thanks @limx0 +- Fixed `Binance` all orders requests which would omit order reports when using a `start` param +- Fixed managed GTD orders past expiry cancellation on restart (orders were not being canceled) +- Fixed managed GTD orders cancel timer on order cancel (timers were not being canceled) - Interactive Brokers adapter various fixes, thanks @rsmb7z --- + # NautilusTrader 1.179.0 Beta Released on 22nd October 2023 (UTC). diff --git a/nautilus_trader/trading/strategy.pyx b/nautilus_trader/trading/strategy.pyx index da61278fdd84..c7a0153401a0 100644 --- a/nautilus_trader/trading/strategy.pyx +++ b/nautilus_trader/trading/strategy.pyx @@ -355,8 +355,6 @@ cdef class Strategy(Actor): for order in open_orders: if order.time_in_force == TimeInForce.GTD: if self._clock.timestamp_ns() >= order.expire_time_ns: - if self._has_gtd_expiry_timer(order.client_order_id): - self.cancel_gtd_expiry(order) self.cancel_order(order) continue if not self._has_gtd_expiry_timer(order.client_order_id): @@ -1008,6 +1006,11 @@ cdef class Strategy(Actor): else: self._send_exec_command(command) + # Cancel any GTD expiry timer + if self.manage_gtd_expiry: + if order.time_in_force == TimeInForce.GTD and self._has_gtd_expiry_timer(order.client_order_id): + self.cancel_gtd_expiry(order) + cpdef void cancel_orders(self, list orders, ClientId client_id = None): """ Batch cancel the given list of orders with optional routing instructions. diff --git a/tests/unit_tests/trading/test_strategy.py b/tests/unit_tests/trading/test_strategy.py index 7b7e2a56b04d..36477ed7eb68 100644 --- a/tests/unit_tests/trading/test_strategy.py +++ b/tests/unit_tests/trading/test_strategy.py @@ -32,6 +32,7 @@ from nautilus_trader.common.logging import Logger from nautilus_trader.config import ImportableStrategyConfig from nautilus_trader.config import StrategyConfig +from nautilus_trader.core.datetime import dt_to_unix_nanos from nautilus_trader.core.uuid import UUID4 from nautilus_trader.data.engine import DataEngine from nautilus_trader.execution.engine import ExecutionEngine @@ -855,6 +856,41 @@ def test_start_when_manage_gtd_reactivates_timers(self): "GTD-EXPIRY:O-19700101-0000-000-None-2", ] + def test_start_when_manage_gtd_and_order_past_expiration_then_cancels(self): + # Arrange + config = StrategyConfig(manage_gtd_expiry=True) + strategy = Strategy(config) + strategy.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + + order1 = strategy.order_factory.limit( + USDJPY_SIM.id, + OrderSide.SELL, + Quantity.from_int(100_000), + Price.from_str("100.00"), + time_in_force=TimeInForce.GTD, + expire_time=self.clock.utc_now() + pd.Timedelta(minutes=10), + ) + + strategy.submit_order(order1) + self.exchange.process(0) + + strategy.clock.cancel_timers() # <-- Simulate restart + self.clock.set_time(dt_to_unix_nanos(order1.expire_time + pd.Timedelta(minutes=1))) + + # Act + strategy.start() + + # Assert + assert strategy.clock.timer_count == 0 + assert order1.is_pending_cancel + def test_submit_order_when_duplicate_id_then_denies(self): # Arrange strategy = Strategy() From d18722b8862fedce296e8807de9e5a5db123efa9 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 26 Oct 2023 19:17:17 +1100 Subject: [PATCH 18/78] Fix Binance all order requests during reconciliation --- .../adapters/binance/common/execution.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/nautilus_trader/adapters/binance/common/execution.py b/nautilus_trader/adapters/binance/common/execution.py index 85a85bb0a950..a141a007a6fe 100644 --- a/nautilus_trader/adapters/binance/common/execution.py +++ b/nautilus_trader/adapters/binance/common/execution.py @@ -441,18 +441,23 @@ async def generate_order_status_reports( # Get all orders for those active symbols binance_orders: list[BinanceOrder] = [] for symbol in active_symbols: - response = await self._http_account.query_all_orders( - symbol=symbol, - start_time=secs_to_millis(start.timestamp()) if start is not None else None, - end_time=secs_to_millis(end.timestamp()) if end is not None else None, - ) + # Here we don't pass a `start_time` or `end_time` as order reports appear to go + # randomly missing when these are specified. We filter on the Nautilus side below. + response = await self._http_account.query_all_orders(symbol=symbol) binance_orders.extend(response) except BinanceError as e: self._log.exception(f"Cannot generate OrderStatusReport: {e.message}", e) return [] + start_ms = secs_to_millis(start.timestamp()) if start is not None else None + end_ms = secs_to_millis(end.timestamp()) if end is not None else None + reports: list[OrderStatusReport] = [] for order in binance_orders: + if start_ms is not None and order.time < start_ms: + continue # Filter start on the Nautilus side + if end_ms is not None and order.time > end_ms: + continue # Filter end on the Nautilus side if order.origQty and Decimal(order.origQty) == 0: continue # Cannot parse zero quantity order (filter for Binance) report = order.parse_to_order_status_report( From dc98019fe817366b139fe5d0b7dc0db1aed8038a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 27 Oct 2023 17:54:49 +1100 Subject: [PATCH 19/78] Update dependencies --- .pre-commit-config.yaml | 2 +- nautilus_core/Cargo.lock | 61 ++++++++++------------- nautilus_core/Cargo.toml | 4 +- poetry.lock | 102 +++++++++++++++++++-------------------- pyproject.toml | 4 +- 5 files changed, 82 insertions(+), 91 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb9e3d26c61e..74970264a39c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,7 +76,7 @@ repos: exclude: "docs/_pygments/monokai.py" - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.2 + rev: v0.1.3 hooks: - id: ruff args: ["--fix"] diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 6603770b7c61..ac3bc28353a5 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -813,9 +813,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -1387,9 +1387,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" dependencies = [ "futures-channel", "futures-core", @@ -1402,9 +1402,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -1412,15 +1412,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -1429,15 +1429,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", @@ -1446,15 +1446,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-timer" @@ -1464,9 +1464,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-channel", "futures-core", @@ -2397,7 +2397,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall", "smallvec", "windows-targets", ] @@ -2798,15 +2798,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -3032,9 +3023,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.20" +version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ "bitflags 2.4.1", "errno", @@ -3462,13 +3453,13 @@ checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" [[package]] name = "tempfile" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.3.5", + "redox_syscall", "rustix", "windows-sys", ] diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index cb4721b1464c..0ce35e8e3b80 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -23,7 +23,7 @@ documentation = "https://docs.nautilustrader.io" [workspace.dependencies] anyhow = "1.0.75" chrono = "0.4.31" -futures = "0.3.28" +futures = "0.3.29" once_cell = "1.18.0" pyo3 = { version = "0.19.2", features = ["rust_decimal"] } pyo3-asyncio = { version = "0.19.0", features = ["tokio-runtime", "tokio", "attributes"] } @@ -45,7 +45,7 @@ criterion = "0.5.1" float-cmp = "0.9.0" iai = "0.1" rstest = "0.18.2" -tempfile = "3.8.0" +tempfile = "3.8.1" # build-dependencies cbindgen = "0.26.0" diff --git a/poetry.lock b/poetry.lock index 2f31864e23cf..00006f646ec3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1646,42 +1646,42 @@ files = [ [[package]] name = "pandas" -version = "2.1.1" +version = "2.1.2" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" files = [ - {file = "pandas-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58d997dbee0d4b64f3cb881a24f918b5f25dd64ddf31f467bb9b67ae4c63a1e4"}, - {file = "pandas-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02304e11582c5d090e5a52aec726f31fe3f42895d6bfc1f28738f9b64b6f0614"}, - {file = "pandas-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffa8f0966de2c22de408d0e322db2faed6f6e74265aa0856f3824813cf124363"}, - {file = "pandas-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1f84c144dee086fe4f04a472b5cd51e680f061adf75c1ae4fc3a9275560f8f4"}, - {file = "pandas-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ce97667d06d69396d72be074f0556698c7f662029322027c226fd7a26965cb"}, - {file = "pandas-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:4c3f32fd7c4dccd035f71734df39231ac1a6ff95e8bdab8d891167197b7018d2"}, - {file = "pandas-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e2959720b70e106bb1d8b6eadd8ecd7c8e99ccdbe03ee03260877184bb2877d"}, - {file = "pandas-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25e8474a8eb258e391e30c288eecec565bfed3e026f312b0cbd709a63906b6f8"}, - {file = "pandas-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8bd1685556f3374520466998929bade3076aeae77c3e67ada5ed2b90b4de7f0"}, - {file = "pandas-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc3657869c7902810f32bd072f0740487f9e030c1a3ab03e0af093db35a9d14e"}, - {file = "pandas-2.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:05674536bd477af36aa2effd4ec8f71b92234ce0cc174de34fd21e2ee99adbc2"}, - {file = "pandas-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:b407381258a667df49d58a1b637be33e514b07f9285feb27769cedb3ab3d0b3a"}, - {file = "pandas-2.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c747793c4e9dcece7bb20156179529898abf505fe32cb40c4052107a3c620b49"}, - {file = "pandas-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3bcad1e6fb34b727b016775bea407311f7721db87e5b409e6542f4546a4951ea"}, - {file = "pandas-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5ec7740f9ccb90aec64edd71434711f58ee0ea7f5ed4ac48be11cfa9abf7317"}, - {file = "pandas-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29deb61de5a8a93bdd033df328441a79fcf8dd3c12d5ed0b41a395eef9cd76f0"}, - {file = "pandas-2.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f99bebf19b7e03cf80a4e770a3e65eee9dd4e2679039f542d7c1ace7b7b1daa"}, - {file = "pandas-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:84e7e910096416adec68075dc87b986ff202920fb8704e6d9c8c9897fe7332d6"}, - {file = "pandas-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366da7b0e540d1b908886d4feb3d951f2f1e572e655c1160f5fde28ad4abb750"}, - {file = "pandas-2.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e50e72b667415a816ac27dfcfe686dc5a0b02202e06196b943d54c4f9c7693e"}, - {file = "pandas-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc1ab6a25da197f03ebe6d8fa17273126120874386b4ac11c1d687df288542dd"}, - {file = "pandas-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0dbfea0dd3901ad4ce2306575c54348d98499c95be01b8d885a2737fe4d7a98"}, - {file = "pandas-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0489b0e6aa3d907e909aef92975edae89b1ee1654db5eafb9be633b0124abe97"}, - {file = "pandas-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:4cdb0fab0400c2cb46dafcf1a0fe084c8bb2480a1fa8d81e19d15e12e6d4ded2"}, - {file = "pandas-2.1.1.tar.gz", hash = "sha256:fecb198dc389429be557cde50a2d46da8434a17fe37d7d41ff102e3987fd947b"}, + {file = "pandas-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24057459f19db9ebb02984c6fdd164a970b31a95f38e4a49cf7615b36a1b532c"}, + {file = "pandas-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6cf8fcc8a63d333970b950a7331a30544cf59b1a97baf0a7409e09eafc1ac38"}, + {file = "pandas-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae6ffbd9d614c20d028c7117ee911fc4e266b4dca2065d5c5909e401f8ff683"}, + {file = "pandas-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff794eeb7883c5aefb1ed572e7ff533ae779f6c6277849eab9e77986e352688"}, + {file = "pandas-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:02954e285e8e2f4006b6f22be6f0df1f1c3c97adbb7ed211c6b483426f20d5c8"}, + {file = "pandas-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:5b40c9f494e1f27588c369b9e4a6ca19cd924b3a0e1ef9ef1a8e30a07a438f43"}, + {file = "pandas-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:08d287b68fd28906a94564f15118a7ca8c242e50ae7f8bd91130c362b2108a81"}, + {file = "pandas-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bbd98dcdcd32f408947afdb3f7434fade6edd408c3077bbce7bd840d654d92c6"}, + {file = "pandas-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e90c95abb3285d06f6e4feedafc134306a8eced93cb78e08cf50e224d5ce22e2"}, + {file = "pandas-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52867d69a54e71666cd184b04e839cff7dfc8ed0cd6b936995117fdae8790b69"}, + {file = "pandas-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8d0382645ede2fde352da2a885aac28ec37d38587864c0689b4b2361d17b1d4c"}, + {file = "pandas-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:65177d1c519b55e5b7f094c660ed357bb7d86e799686bb71653b8a4803d8ff0d"}, + {file = "pandas-2.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5aa6b86802e8cf7716bf4b4b5a3c99b12d34e9c6a9d06dad254447a620437931"}, + {file = "pandas-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d594e2ce51b8e0b4074e6644758865dc2bb13fd654450c1eae51201260a539f1"}, + {file = "pandas-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3223f997b6d2ebf9c010260cf3d889848a93f5d22bb4d14cd32638b3d8bba7ad"}, + {file = "pandas-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4944dc004ca6cc701dfa19afb8bdb26ad36b9bed5bcec617d2a11e9cae6902"}, + {file = "pandas-2.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3f76280ce8ec216dde336e55b2b82e883401cf466da0fe3be317c03fb8ee7c7d"}, + {file = "pandas-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:7ad20d24acf3a0042512b7e8d8fdc2e827126ed519d6bd1ed8e6c14ec8a2c813"}, + {file = "pandas-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:021f09c15e1381e202d95d4a21ece8e7f2bf1388b6d7e9cae09dfe27bd2043d1"}, + {file = "pandas-2.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7f12b2de0060b0b858cfec0016e7d980ae5bae455a1746bfcc70929100ee633"}, + {file = "pandas-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c166b9bb27c1715bed94495d9598a7f02950b4749dba9349c1dd2cbf10729d"}, + {file = "pandas-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25c9976c17311388fcd953cb3d0697999b2205333f4e11e669d90ff8d830d429"}, + {file = "pandas-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:851b5afbb0d62f6129ae891b533aa508cc357d5892c240c91933d945fff15731"}, + {file = "pandas-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:e78507adcc730533619de07bfdd1c62b2918a68cd4419ea386e28abf7f6a1e5c"}, + {file = "pandas-2.1.2.tar.gz", hash = "sha256:52897edc2774d2779fbeb6880d2cfb305daa0b1a29c16b91f531a18918a6e0f3"}, ] [package.dependencies] numpy = [ - {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -2166,28 +2166,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.1.2" +version = "0.1.3" description = "An extremely fast Python linter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.2-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:0d3ee66b825b713611f89aa35d16de984f76f26c50982a25d52cd0910dff3923"}, - {file = "ruff-0.1.2-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f85f850a320ff532b8f93e8d1da6a36ef03698c446357c8c43b46ef90bb321eb"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:809c6d4e45683696d19ca79e4c6bd3b2e9204fe9546923f2eb3b126ec314b0dc"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46005e4abb268e93cad065244e17e2ea16b6fcb55a5c473f34fbc1fd01ae34cb"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10cdb302f519664d5e2cf954562ac86c9d20ca05855e5b5c2f9d542228f45da4"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f89ebcbe57a1eab7d7b4ceb57ddf0af9ed13eae24e443a7c1dc078000bd8cc6b"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7344eaca057d4c32373c9c3a7afb7274f56040c225b6193dd495fcf69453b436"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dffa25f6e03c4950b6ac6f216bc0f98a4be9719cb0c5260c8e88d1bac36f1683"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42ddaea52cb7ba7c785e8593a7532866c193bc774fe570f0e4b1ccedd95b83c5"}, - {file = "ruff-0.1.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8533efda625bbec0bf27da2886bd641dae0c209104f6c39abc4be5b7b22de2a"}, - {file = "ruff-0.1.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b0b1b82221ba7c50e03b7a86b983157b5d3f4d8d4f16728132bdf02c6d651f77"}, - {file = "ruff-0.1.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c1362eb9288f8cc95535294cb03bd4665c8cef86ec32745476a4e5c6817034c"}, - {file = "ruff-0.1.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ffa7ef5ded0563329a35bd5a1cfdae40f05a75c0cc2dd30f00b1320b1fb461fc"}, - {file = "ruff-0.1.2-py3-none-win32.whl", hash = "sha256:6e8073f85e47072256e2e1909f1ae515cf61ff5a4d24730a63b8b4ac24b6704a"}, - {file = "ruff-0.1.2-py3-none-win_amd64.whl", hash = "sha256:b836ddff662a45385948ee0878b0a04c3a260949905ad861a37b931d6ee1c210"}, - {file = "ruff-0.1.2-py3-none-win_arm64.whl", hash = "sha256:b0c42d00db5639dbd5f7f9923c63648682dd197bf5de1151b595160c96172691"}, - {file = "ruff-0.1.2.tar.gz", hash = "sha256:afd4785ae060ce6edcd52436d0c197628a918d6d09e3107a892a1bad6a4c6608"}, + {file = "ruff-0.1.3-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:b46d43d51f7061652eeadb426a9e3caa1e0002470229ab2fc19de8a7b0766901"}, + {file = "ruff-0.1.3-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:b8afeb9abd26b4029c72adc9921b8363374f4e7edb78385ffaa80278313a15f9"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca3cf365bf32e9ba7e6db3f48a4d3e2c446cd19ebee04f05338bc3910114528b"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4874c165f96c14a00590dcc727a04dca0cfd110334c24b039458c06cf78a672e"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eec2dd31eed114e48ea42dbffc443e9b7221976554a504767ceaee3dd38edeb8"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dc3ec4edb3b73f21b4aa51337e16674c752f1d76a4a543af56d7d04e97769613"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e3de9ed2e39160800281848ff4670e1698037ca039bda7b9274f849258d26ce"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c595193881922cc0556a90f3af99b1c5681f0c552e7a2a189956141d8666fe8"}, + {file = "ruff-0.1.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f75e670d529aa2288cd00fc0e9b9287603d95e1536d7a7e0cafe00f75e0dd9d"}, + {file = "ruff-0.1.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:76dd49f6cd945d82d9d4a9a6622c54a994689d8d7b22fa1322983389b4892e20"}, + {file = "ruff-0.1.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:918b454bc4f8874a616f0d725590277c42949431ceb303950e87fef7a7d94cb3"}, + {file = "ruff-0.1.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8859605e729cd5e53aa38275568dbbdb4fe882d2ea2714c5453b678dca83784"}, + {file = "ruff-0.1.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0b6c55f5ef8d9dd05b230bb6ab80bc4381ecb60ae56db0330f660ea240cb0d4a"}, + {file = "ruff-0.1.3-py3-none-win32.whl", hash = "sha256:3e7afcbdcfbe3399c34e0f6370c30f6e529193c731b885316c5a09c9e4317eef"}, + {file = "ruff-0.1.3-py3-none-win_amd64.whl", hash = "sha256:7a18df6638cec4a5bd75350639b2bb2a2366e01222825562c7346674bdceb7ea"}, + {file = "ruff-0.1.3-py3-none-win_arm64.whl", hash = "sha256:12fd53696c83a194a2db7f9a46337ce06445fb9aa7d25ea6f293cf75b21aca9f"}, + {file = "ruff-0.1.3.tar.gz", hash = "sha256:3ba6145369a151401d5db79f0a47d50e470384d0d89d0d6f7fab0b589ad07c34"}, ] [[package]] @@ -2570,13 +2570,13 @@ files = [ [[package]] name = "types-redis" -version = "4.6.0.7" +version = "4.6.0.8" description = "Typing stubs for redis" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "types-redis-4.6.0.7.tar.gz", hash = "sha256:28c4153ddb5c9d4f10def44a2454673c361d2d5fc3cd867cf3bb1520f3f59a38"}, - {file = "types_redis-4.6.0.7-py3-none-any.whl", hash = "sha256:05b1bf92879b25df20433fa1af07784a0d7928c616dc2ebf9087618db77ccbd0"}, + {file = "types-redis-4.6.0.8.tar.gz", hash = "sha256:1abb2859bbf9b171a22ef69d1ece0e35ef93e642fba97538497add884ad75b5e"}, + {file = "types_redis-4.6.0.8-py3-none-any.whl", hash = "sha256:4839923b4cce77bbf987290ca83710f8218529eebe1d2c3a0f067416c86847f5"}, ] [package.dependencies] @@ -2887,4 +2887,4 @@ redis = ["hiredis", "redis"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "e0aeb520bd17e4792242a37451d61ebd5cd325b1c11f3584a75e48a0425931ca" +content-hash = "afd2b03a92fafe97c0100246d884b8d0909849fecfe2ab61570cb10fd2b70a5b" diff --git a/pyproject.toml b/pyproject.toml index 7a129837972f..b9b9a191ab96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ frozendict = "^2.3.8" fsspec = "==2023.6.0" # Pinned for stability importlib_metadata = "^6.8.0" msgspec = "^0.18.4" -pandas = "^2.1.1" +pandas = "^2.1.2" psutil = "^5.9.6" pyarrow = ">=12.0.1" # Minimum version set one major version behind the latest for compatibility pytz = "^2023.3.0" @@ -86,7 +86,7 @@ black = "^23.10.1" docformatter = "^1.7.5" mypy = "^1.6.1" pre-commit = "^3.5.0" -ruff = "^0.1.2" +ruff = "^0.1.3" types-pytz = "^2023.3" types-redis = "^4.6" types-requests = "^2.31" From e05356152a0c8614cfb1709d2e5d4c2c7eeef9d2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 27 Oct 2023 18:01:40 +1100 Subject: [PATCH 20/78] Use Final qualifier for some constants --- nautilus_trader/adapters/betfair/constants.py | 16 +- .../adapters/binance/common/constants.py | 4 +- .../adapters/interactive_brokers/common.py | 4 +- nautilus_trader/model/currencies.py | 144 +++++++++--------- 4 files changed, 87 insertions(+), 81 deletions(-) diff --git a/nautilus_trader/adapters/betfair/constants.py b/nautilus_trader/adapters/betfair/constants.py index 4a52aa652bf3..e273583fb736 100644 --- a/nautilus_trader/adapters/betfair/constants.py +++ b/nautilus_trader/adapters/betfair/constants.py @@ -13,6 +13,8 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from typing import Final + from betfair_parser.spec.betting import MarketStatus as BetfairMarketStatus from nautilus_trader.model.enums import BookType @@ -21,15 +23,15 @@ from nautilus_trader.model.objects import Price -BETFAIR_VENUE = Venue("BETFAIR") -BETFAIR_PRICE_PRECISION = 6 -BETFAIR_QUANTITY_PRECISION = 6 -BETFAIR_BOOK_TYPE = BookType.L2_MBP +BETFAIR_VENUE: Final[Venue] = Venue("BETFAIR") +BETFAIR_PRICE_PRECISION: Final[int] = 6 +BETFAIR_QUANTITY_PRECISION: Final[int] = 6 +BETFAIR_BOOK_TYPE: Final[BookType] = BookType.L2_MBP -CLOSE_PRICE_WINNER = Price(1.0, precision=BETFAIR_PRICE_PRECISION) -CLOSE_PRICE_LOSER = Price(0.0, precision=BETFAIR_PRICE_PRECISION) +CLOSE_PRICE_WINNER: Final[Price] = Price(1.0, precision=BETFAIR_PRICE_PRECISION) +CLOSE_PRICE_LOSER: Final[Price] = Price(0.0, precision=BETFAIR_PRICE_PRECISION) -MARKET_STATUS_MAPPING: dict[tuple[MarketStatus, bool], MarketStatus] = { +MARKET_STATUS_MAPPING: Final[dict[tuple[MarketStatus, bool], MarketStatus]] = { (BetfairMarketStatus.INACTIVE, False): MarketStatus.CLOSED, (BetfairMarketStatus.OPEN, False): MarketStatus.PRE_OPEN, (BetfairMarketStatus.OPEN, True): MarketStatus.OPEN, diff --git a/nautilus_trader/adapters/binance/common/constants.py b/nautilus_trader/adapters/binance/common/constants.py index 991b7c4d3b38..742c9564f730 100644 --- a/nautilus_trader/adapters/binance/common/constants.py +++ b/nautilus_trader/adapters/binance/common/constants.py @@ -13,7 +13,9 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from typing import Final + from nautilus_trader.model.identifiers import Venue -BINANCE_VENUE = Venue("BINANCE") +BINANCE_VENUE: Final[Venue] = Venue("BINANCE") diff --git a/nautilus_trader/adapters/interactive_brokers/common.py b/nautilus_trader/adapters/interactive_brokers/common.py index 2e48c2329759..f6b342d1e794 100644 --- a/nautilus_trader/adapters/interactive_brokers/common.py +++ b/nautilus_trader/adapters/interactive_brokers/common.py @@ -14,7 +14,7 @@ # ------------------------------------------------------------------------------------------------- from decimal import Decimal -from typing import Literal +from typing import Final, Literal from ibapi.common import UNSET_DECIMAL @@ -22,7 +22,7 @@ from nautilus_trader.model.identifiers import Venue -IB_VENUE = Venue("InteractiveBrokers") +IB_VENUE: Final[Venue] = Venue("InteractiveBrokers") class ContractId(int): diff --git a/nautilus_trader/model/currencies.py b/nautilus_trader/model/currencies.py index 4d0db8d5d24b..152d988bc682 100644 --- a/nautilus_trader/model/currencies.py +++ b/nautilus_trader/model/currencies.py @@ -13,80 +13,82 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from typing import Final + from nautilus_trader.model.currency import Currency # Fiat currencies -AUD = Currency.from_internal_map("AUD") -BRL = Currency.from_internal_map("BRL") -CAD = Currency.from_internal_map("CAD") -CHF = Currency.from_internal_map("CHF") -CNY = Currency.from_internal_map("CNY") -CNH = Currency.from_internal_map("CNH") -CZK = Currency.from_internal_map("CZK") -DKK = Currency.from_internal_map("DKK") -EUR = Currency.from_internal_map("EUR") -GBP = Currency.from_internal_map("GBP") -HKD = Currency.from_internal_map("HKD") -HUF = Currency.from_internal_map("HUF") -ILS = Currency.from_internal_map("ILS") -INR = Currency.from_internal_map("INR") -JPY = Currency.from_internal_map("JPY") -KRW = Currency.from_internal_map("KRW") -MXN = Currency.from_internal_map("MXN") -NOK = Currency.from_internal_map("NOK") -NZD = Currency.from_internal_map("NZD") -PLN = Currency.from_internal_map("PLN") -RUB = Currency.from_internal_map("RUB") -SAR = Currency.from_internal_map("SAR") -SEK = Currency.from_internal_map("SEK") -SGD = Currency.from_internal_map("SGD") -THB = Currency.from_internal_map("THB") -TRY = Currency.from_internal_map("TRY") -USD = Currency.from_internal_map("USD") -XAG = Currency.from_internal_map("XAG") -XAU = Currency.from_internal_map("XAU") -ZAR = Currency.from_internal_map("ZAR") +AUD: Final[Currency] = Currency.from_internal_map("AUD") +BRL: Final[Currency] = Currency.from_internal_map("BRL") +CAD: Final[Currency] = Currency.from_internal_map("CAD") +CHF: Final[Currency] = Currency.from_internal_map("CHF") +CNY: Final[Currency] = Currency.from_internal_map("CNY") +CNH: Final[Currency] = Currency.from_internal_map("CNH") +CZK: Final[Currency] = Currency.from_internal_map("CZK") +DKK: Final[Currency] = Currency.from_internal_map("DKK") +EUR: Final[Currency] = Currency.from_internal_map("EUR") +GBP: Final[Currency] = Currency.from_internal_map("GBP") +HKD: Final[Currency] = Currency.from_internal_map("HKD") +HUF: Final[Currency] = Currency.from_internal_map("HUF") +ILS: Final[Currency] = Currency.from_internal_map("ILS") +INR: Final[Currency] = Currency.from_internal_map("INR") +JPY: Final[Currency] = Currency.from_internal_map("JPY") +KRW: Final[Currency] = Currency.from_internal_map("KRW") +MXN: Final[Currency] = Currency.from_internal_map("MXN") +NOK: Final[Currency] = Currency.from_internal_map("NOK") +NZD: Final[Currency] = Currency.from_internal_map("NZD") +PLN: Final[Currency] = Currency.from_internal_map("PLN") +RUB: Final[Currency] = Currency.from_internal_map("RUB") +SAR: Final[Currency] = Currency.from_internal_map("SAR") +SEK: Final[Currency] = Currency.from_internal_map("SEK") +SGD: Final[Currency] = Currency.from_internal_map("SGD") +THB: Final[Currency] = Currency.from_internal_map("THB") +TRY: Final[Currency] = Currency.from_internal_map("TRY") +USD: Final[Currency] = Currency.from_internal_map("USD") +XAG: Final[Currency] = Currency.from_internal_map("XAG") +XAU: Final[Currency] = Currency.from_internal_map("XAU") +ZAR: Final[Currency] = Currency.from_internal_map("ZAR") # Crypto currencies -ONEINCH = Currency.from_internal_map("1INCH") -AAVE = Currency.from_internal_map("AAVE") -ACA = Currency.from_internal_map("ACA") -ADA = Currency.from_internal_map("ADA") -AVAX = Currency.from_internal_map("AVAX") -BCH = Currency.from_internal_map("BCH") -BTTC = Currency.from_internal_map("BTTC") -BNB = Currency.from_internal_map("BNB") -BRZ = Currency.from_internal_map("BRZ") -BSV = Currency.from_internal_map("BSV") -BTC = Currency.from_internal_map("BTC") -BUSD = Currency.from_internal_map("BUSD") -XBT = Currency.from_internal_map("XBT") -DASH = Currency.from_internal_map("DASH") -DOGE = Currency.from_internal_map("DOGE") -DOT = Currency.from_internal_map("DOT") -EOS = Currency.from_internal_map("EOS") -ETH = Currency.from_internal_map("ETH") -ETHW = Currency.from_internal_map("ETHW") -EZ = Currency.from_internal_map("EZ") -FTT = Currency.from_internal_map("FTT") -JOE = Currency.from_internal_map("JOE") -LINK = Currency.from_internal_map("LINK") -LTC = Currency.from_internal_map("LTC") -LUNA = Currency.from_internal_map("LUNA") -NBT = Currency.from_internal_map("NBT") -SOL = Currency.from_internal_map("SOL") -TRX = Currency.from_internal_map("TRX") -TRYB = Currency.from_internal_map("TRYB") -TUSD = Currency.from_internal_map("TUSD") -VTC = Currency.from_internal_map("VTC") -XLM = Currency.from_internal_map("XLM") -XMR = Currency.from_internal_map("XMR") -XRP = Currency.from_internal_map("XRP") -XTZ = Currency.from_internal_map("XTZ") -USDC = Currency.from_internal_map("USDC") -USDP = Currency.from_internal_map("USDP") -USDT = Currency.from_internal_map("USDT") -WSB = Currency.from_internal_map("WSB") -XEC = Currency.from_internal_map("XEC") -ZEC = Currency.from_internal_map("ZEC") +ONEINCH: Final[Currency] = Currency.from_internal_map("1INCH") +AAVE: Final[Currency] = Currency.from_internal_map("AAVE") +ACA: Final[Currency] = Currency.from_internal_map("ACA") +ADA: Final[Currency] = Currency.from_internal_map("ADA") +AVAX: Final[Currency] = Currency.from_internal_map("AVAX") +BCH: Final[Currency] = Currency.from_internal_map("BCH") +BTTC: Final[Currency] = Currency.from_internal_map("BTTC") +BNB: Final[Currency] = Currency.from_internal_map("BNB") +BRZ: Final[Currency] = Currency.from_internal_map("BRZ") +BSV: Final[Currency] = Currency.from_internal_map("BSV") +BTC: Final[Currency] = Currency.from_internal_map("BTC") +BUSD: Final[Currency] = Currency.from_internal_map("BUSD") +XBT: Final[Currency] = Currency.from_internal_map("XBT") +DASH: Final[Currency] = Currency.from_internal_map("DASH") +DOGE: Final[Currency] = Currency.from_internal_map("DOGE") +DOT: Final[Currency] = Currency.from_internal_map("DOT") +EOS: Final[Currency] = Currency.from_internal_map("EOS") +ETH: Final[Currency] = Currency.from_internal_map("ETH") +ETHW: Final[Currency] = Currency.from_internal_map("ETHW") +EZ: Final[Currency] = Currency.from_internal_map("EZ") +FTT: Final[Currency] = Currency.from_internal_map("FTT") +JOE: Final[Currency] = Currency.from_internal_map("JOE") +LINK: Final[Currency] = Currency.from_internal_map("LINK") +LTC: Final[Currency] = Currency.from_internal_map("LTC") +LUNA: Final[Currency] = Currency.from_internal_map("LUNA") +NBT: Final[Currency] = Currency.from_internal_map("NBT") +SOL: Final[Currency] = Currency.from_internal_map("SOL") +TRX: Final[Currency] = Currency.from_internal_map("TRX") +TRYB: Final[Currency] = Currency.from_internal_map("TRYB") +TUSD: Final[Currency] = Currency.from_internal_map("TUSD") +VTC: Final[Currency] = Currency.from_internal_map("VTC") +XLM: Final[Currency] = Currency.from_internal_map("XLM") +XMR: Final[Currency] = Currency.from_internal_map("XMR") +XRP: Final[Currency] = Currency.from_internal_map("XRP") +XTZ: Final[Currency] = Currency.from_internal_map("XTZ") +USDC: Final[Currency] = Currency.from_internal_map("USDC") +USDP: Final[Currency] = Currency.from_internal_map("USDP") +USDT: Final[Currency] = Currency.from_internal_map("USDT") +WSB: Final[Currency] = Currency.from_internal_map("WSB") +XEC: Final[Currency] = Currency.from_internal_map("XEC") +ZEC: Final[Currency] = Currency.from_internal_map("ZEC") From 95862bd012d2b81a0615ac3a50e2d953bc6b63f3 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 27 Oct 2023 18:13:47 +1100 Subject: [PATCH 21/78] Improve docstring --- nautilus_trader/execution/manager.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nautilus_trader/execution/manager.pyx b/nautilus_trader/execution/manager.pyx index a1cfd4f6bdeb..b72bb6810bc4 100644 --- a/nautilus_trader/execution/manager.pyx +++ b/nautilus_trader/execution/manager.pyx @@ -81,9 +81,9 @@ cdef class OrderManager: Raises ------ TypeError - If `submit_order_handler` is not of type `Callable`. + If `submit_order_handler` is not ``None`` and not of type `Callable`. TypeError - If `cancel_order_handler` is not of type `Callable`. + If `cancel_order_handler` is not ``None`` and not of type `Callable`. """ def __init__( From ac008964cd8a5b8871edb03b22460d3f95b7dd57 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 27 Oct 2023 18:13:53 +1100 Subject: [PATCH 22/78] Add type annotations --- tests/unit_tests/execution/test_emulator.py | 260 ++++++++++---------- 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/tests/unit_tests/execution/test_emulator.py b/tests/unit_tests/execution/test_emulator.py index a1676f962c03..84bc24f79b9a 100644 --- a/tests/unit_tests/execution/test_emulator.py +++ b/tests/unit_tests/execution/test_emulator.py @@ -235,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_price=5060.0, - ask_price=5070.0, + bid_price=5_060.0, + ask_price=5_070.0, bid_size=10.0, ask_size=10.0, ) @@ -278,7 +278,7 @@ def test_submit_limit_order_with_emulation_trigger_not_supported_then_cancels(se instrument_id=ETHUSDT_PERP_BINANCE.id, order_side=OrderSide.BUY, quantity=Quantity.from_int(10), - price=ETHUSDT_PERP_BINANCE.make_price(2000), + price=ETHUSDT_PERP_BINANCE.make_price(2_000), emulation_trigger=TriggerType.INDEX_PRICE, ) @@ -335,14 +335,14 @@ def test_submit_limit_order_with_instrument_not_found_then_cancels(self) -> None ) def test_submit_limit_order_with_emulation_trigger_default_and_bid_ask_subscribes_to_data( self, - emulation_trigger, - ): + emulation_trigger: TriggerType, + ) -> 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), + price=ETHUSDT_PERP_BINANCE.make_price(2_000), emulation_trigger=emulation_trigger, ) @@ -363,7 +363,7 @@ def test_submit_order_with_emulation_trigger_last_subscribes_to_data(self) -> No instrument_id=ETHUSDT_PERP_BINANCE.id, order_side=OrderSide.BUY, quantity=Quantity.from_int(10), - price=ETHUSDT_PERP_BINANCE.make_price(2000), + price=ETHUSDT_PERP_BINANCE.make_price(2_000), emulation_trigger=TriggerType.LAST_TRADE, ) @@ -384,7 +384,7 @@ def test_emulator_restart_reactivates_emulated_orders(self) -> None: instrument_id=ETHUSDT_PERP_BINANCE.id, order_side=OrderSide.BUY, quantity=Quantity.from_int(10), - price=ETHUSDT_PERP_BINANCE.make_price(2000), + price=ETHUSDT_PERP_BINANCE.make_price(2_000), emulation_trigger=TriggerType.LAST_TRADE, ) @@ -408,7 +408,7 @@ def test_cancel_all_with_emulated_order_cancels_order(self) -> None: instrument_id=ETHUSDT_PERP_BINANCE.id, order_side=OrderSide.BUY, quantity=Quantity.from_int(10), - price=ETHUSDT_PERP_BINANCE.make_price(2000), + price=ETHUSDT_PERP_BINANCE.make_price(2_000), emulation_trigger=TriggerType.LAST_TRADE, ) @@ -426,7 +426,7 @@ def test_cancel_all_buy_orders_with_emulated_orders_cancels_buy_order(self) -> N instrument_id=ETHUSDT_PERP_BINANCE.id, order_side=OrderSide.BUY, quantity=Quantity.from_int(10), - price=ETHUSDT_PERP_BINANCE.make_price(2000), + price=ETHUSDT_PERP_BINANCE.make_price(2_000), emulation_trigger=TriggerType.LAST_TRADE, ) @@ -434,7 +434,7 @@ def test_cancel_all_buy_orders_with_emulated_orders_cancels_buy_order(self) -> N instrument_id=ETHUSDT_PERP_BINANCE.id, order_side=OrderSide.SELL, quantity=Quantity.from_int(10), - price=ETHUSDT_PERP_BINANCE.make_price(2010), + price=ETHUSDT_PERP_BINANCE.make_price(2_010), emulation_trigger=TriggerType.LAST_TRADE, ) @@ -456,7 +456,7 @@ def test_cancel_all_sell_orders_with_emulated_orders_cancels_sell_order(self) -> instrument_id=ETHUSDT_PERP_BINANCE.id, order_side=OrderSide.BUY, quantity=Quantity.from_int(10), - price=ETHUSDT_PERP_BINANCE.make_price(2000), + price=ETHUSDT_PERP_BINANCE.make_price(2_000), emulation_trigger=TriggerType.LAST_TRADE, ) @@ -464,7 +464,7 @@ def test_cancel_all_sell_orders_with_emulated_orders_cancels_sell_order(self) -> instrument_id=ETHUSDT_PERP_BINANCE.id, order_side=OrderSide.SELL, quantity=Quantity.from_int(10), - price=ETHUSDT_PERP_BINANCE.make_price(2010), + price=ETHUSDT_PERP_BINANCE.make_price(2_010), emulation_trigger=TriggerType.LAST_TRADE, ) @@ -483,15 +483,15 @@ def test_cancel_all_sell_orders_with_emulated_orders_cancels_sell_order(self) -> @pytest.mark.parametrize( ("order_side", "trigger_price"), [ - [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5000)], - [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5000)], + [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5_000)], + [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5_000)], ], ) def test_submit_limit_order_last_then_triggered_releases_market_order( self, - order_side, - trigger_price, - ): + order_side: OrderSide, + trigger_price: TriggerType, + ) -> None: # Arrange order = self.strategy.order_factory.limit( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -505,7 +505,7 @@ def test_submit_limit_order_last_then_triggered_releases_market_order( tick = TestDataStubs.trade_tick( instrument=ETHUSDT_PERP_BINANCE, - price=5000.0, + price=5_000.0, ) # Act @@ -525,15 +525,15 @@ def test_submit_limit_order_last_then_triggered_releases_market_order( @pytest.mark.parametrize( ("order_side", "trigger_price"), [ - [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5000)], - [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5000)], + [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5_000)], + [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5_000)], ], ) def test_submit_limit_order_bid_ask_then_triggered_releases_market_order( self, - order_side, - trigger_price, - ): + order_side: OrderSide, + trigger_price: TriggerType, + ) -> None: # Arrange order = self.strategy.order_factory.limit( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -547,8 +547,8 @@ def test_submit_limit_order_bid_ask_then_triggered_releases_market_order( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5000.0, - ask_price=5000.0, + bid_price=5_000.0, + ask_price=5_000.0, ) # Act @@ -569,21 +569,21 @@ def test_submit_limit_order_bid_ask_then_triggered_releases_market_order( @pytest.mark.parametrize( ("order_side", "trigger_price"), [ - [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5000)], - [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5000)], + [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5_000)], + [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5_000)], ], ) def test_submit_limit_if_touched_then_triggered_releases_limit_order( self, - order_side, - trigger_price, - ): + order_side: OrderSide, + trigger_price: TriggerType, + ) -> None: # Arrange order = self.strategy.order_factory.limit_if_touched( instrument_id=ETHUSDT_PERP_BINANCE.id, order_side=order_side, quantity=Quantity.from_int(10), - price=ETHUSDT_PERP_BINANCE.make_price(5000), + price=ETHUSDT_PERP_BINANCE.make_price(5_000), trigger_price=trigger_price, emulation_trigger=TriggerType.DEFAULT, ) @@ -592,8 +592,8 @@ def test_submit_limit_if_touched_then_triggered_releases_limit_order( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5000.0, - ask_price=5000.0, + bid_price=5_000.0, + ask_price=5_000.0, ) # Act @@ -614,15 +614,15 @@ def test_submit_limit_if_touched_then_triggered_releases_limit_order( @pytest.mark.parametrize( ("order_side", "trigger_price"), [ - [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5000)], - [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5000)], + [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5_000)], + [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5_000)], ], ) def test_submit_stop_limit_order_then_triggered_releases_limit_order( self, - order_side, - trigger_price, - ): + order_side: OrderSide, + trigger_price: TriggerType, + ) -> None: # Arrange order = self.strategy.order_factory.stop_limit( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -637,8 +637,8 @@ def test_submit_stop_limit_order_then_triggered_releases_limit_order( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5000.0, - ask_price=5000.0, + bid_price=5_000.0, + ask_price=5_000.0, ) # Act @@ -659,15 +659,15 @@ def test_submit_stop_limit_order_then_triggered_releases_limit_order( @pytest.mark.parametrize( ("order_side", "trigger_price"), [ - [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5070)], - [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5060)], + [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5_070)], + [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5_060)], ], ) def test_submit_market_if_touched_order_then_triggered_releases_market_order( self, - order_side, - trigger_price, - ): + order_side: OrderSide, + trigger_price: TriggerType, + ) -> None: # Arrange order = self.strategy.order_factory.market_if_touched( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -682,8 +682,8 @@ def test_submit_market_if_touched_order_then_triggered_releases_market_order( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5060.0, - ask_price=5070.0, + bid_price=5_060.0, + ask_price=5_070.0, ) # Act @@ -703,15 +703,15 @@ def test_submit_market_if_touched_order_then_triggered_releases_market_order( @pytest.mark.parametrize( ("order_side", "trigger_price"), [ - [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5060)], - [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5070)], + [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5_060)], + [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5_070)], ], ) def test_submit_stop_market_order_then_triggered_releases_market_order( self, - order_side, - trigger_price, - ): + order_side: OrderSide, + trigger_price: TriggerType, + ) -> None: # Arrange order = self.strategy.order_factory.stop_market( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -726,8 +726,8 @@ def test_submit_stop_market_order_then_triggered_releases_market_order( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5060.0, - ask_price=5070.0, + bid_price=5_060.0, + ask_price=5_070.0, ) # Act @@ -748,15 +748,15 @@ def test_submit_stop_market_order_then_triggered_releases_market_order( @pytest.mark.parametrize( ("order_side", "expected_trigger_price"), [ - [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5075)], - [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5055)], + [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5_075)], + [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5_055)], ], ) def test_submit_trailing_stop_market_order_with_no_trigger_price_then_updates( self, - order_side, - expected_trigger_price, - ): + order_side: OrderSide, + expected_trigger_price: Price, + ) -> None: # Arrange order = self.strategy.order_factory.trailing_stop_market( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -770,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_price=5060.0, - ask_price=5070.0, + bid_price=5_060.0, + ask_price=5_070.0, ) self.data_engine.process(tick) @@ -795,22 +795,22 @@ def test_submit_trailing_stop_market_order_with_no_trigger_price_then_updates( [ [ OrderSide.BUY, - ETHUSDT_PERP_BINANCE.make_price(5075.0), - ETHUSDT_PERP_BINANCE.make_price(5070.0), + ETHUSDT_PERP_BINANCE.make_price(5_075.0), + ETHUSDT_PERP_BINANCE.make_price(5_070.0), ], [ OrderSide.SELL, - ETHUSDT_PERP_BINANCE.make_price(5055.0), - ETHUSDT_PERP_BINANCE.make_price(5060.0), + ETHUSDT_PERP_BINANCE.make_price(5_055.0), + ETHUSDT_PERP_BINANCE.make_price(5_060.0), ], ], ) def test_submit_trailing_stop_market_order_with_trigger_price_then_updates( self, - order_side, - trigger_price, - expected_trigger_price, - ): + order_side: OrderSide, + trigger_price: Price, + expected_trigger_price: Price, + ) -> None: # Arrange order = self.strategy.order_factory.trailing_stop_market( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -825,15 +825,15 @@ def test_submit_trailing_stop_market_order_with_trigger_price_then_updates( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5060.0, - ask_price=5070.0, + bid_price=5_060.0, + ask_price=5_070.0, ) self.data_engine.process(tick) tick = TestDataStubs.trade_tick( instrument=ETHUSDT_PERP_BINANCE, - price=5010.0, + price=5_010.0, ) self.data_engine.process(tick) @@ -843,8 +843,8 @@ def test_submit_trailing_stop_market_order_with_trigger_price_then_updates( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5065.0, - ask_price=5065.0, + bid_price=5_065.0, + ask_price=5_065.0, ) self.data_engine.process(tick) @@ -861,15 +861,15 @@ def test_submit_trailing_stop_market_order_with_trigger_price_then_updates( @pytest.mark.parametrize( ("order_side", "trigger_price"), [ - [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5075)], - [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5055)], + [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5_075)], + [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5_055)], ], ) def test_submit_trailing_stop_market_order_with_trigger_price_then_triggers( self, - order_side, - trigger_price, - ): + order_side: OrderSide, + trigger_price: Price, + ) -> None: # Arrange order = self.strategy.order_factory.trailing_stop_market( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -884,8 +884,8 @@ def test_submit_trailing_stop_market_order_with_trigger_price_then_triggers( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5060.0, - ask_price=5070.0, + bid_price=5_060.0, + ask_price=5_070.0, ) self.data_engine.process(tick) @@ -894,8 +894,8 @@ def test_submit_trailing_stop_market_order_with_trigger_price_then_triggers( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5055.0, - ask_price=5075.0, + bid_price=5_055.0, + ask_price=5_075.0, ) self.data_engine.process(tick) @@ -915,22 +915,22 @@ def test_submit_trailing_stop_market_order_with_trigger_price_then_triggers( [ [ OrderSide.BUY, - ETHUSDT_PERP_BINANCE.make_price(5070), - ETHUSDT_PERP_BINANCE.make_price(5075), + ETHUSDT_PERP_BINANCE.make_price(5_070), + ETHUSDT_PERP_BINANCE.make_price(5_075), ], [ OrderSide.SELL, - ETHUSDT_PERP_BINANCE.make_price(5060), - ETHUSDT_PERP_BINANCE.make_price(5055), + ETHUSDT_PERP_BINANCE.make_price(5_060), + ETHUSDT_PERP_BINANCE.make_price(5_055), ], ], ) def test_submit_trailing_stop_limit_order_with_no_trigger_price_then_updates( self, - order_side, - price, - expected_trigger_price, - ): + order_side: OrderSide, + price: Price, + expected_trigger_price: Price, + ) -> None: # Arrange order = self.strategy.order_factory.trailing_stop_limit( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -946,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_price=5060.0, - ask_price=5070.0, + bid_price=5_060.0, + ask_price=5_070.0, ) self.data_engine.process(tick) @@ -969,25 +969,25 @@ def test_submit_trailing_stop_limit_order_with_no_trigger_price_then_updates( [ [ OrderSide.BUY, - ETHUSDT_PERP_BINANCE.make_price(5075.0), - ETHUSDT_PERP_BINANCE.make_price(5070.0), - ETHUSDT_PERP_BINANCE.make_price(5070.0), + ETHUSDT_PERP_BINANCE.make_price(5_075.0), + ETHUSDT_PERP_BINANCE.make_price(5_070.0), + ETHUSDT_PERP_BINANCE.make_price(5_070.0), ], [ OrderSide.SELL, - ETHUSDT_PERP_BINANCE.make_price(5055.0), - ETHUSDT_PERP_BINANCE.make_price(5060.0), - ETHUSDT_PERP_BINANCE.make_price(5060.0), + ETHUSDT_PERP_BINANCE.make_price(5_055.0), + ETHUSDT_PERP_BINANCE.make_price(5_060.0), + ETHUSDT_PERP_BINANCE.make_price(5_060.0), ], ], ) def test_submit_trailing_stop_limit_order_with_trigger_price_then_updates( self, - order_side, - price, - trigger_price, - expected_trigger_price, - ): + order_side: OrderSide, + price: Price, + trigger_price: TriggerType, + expected_trigger_price: Price, + ) -> None: # Arrange order = self.strategy.order_factory.trailing_stop_limit( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -1004,8 +1004,8 @@ def test_submit_trailing_stop_limit_order_with_trigger_price_then_updates( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5060.0, - ask_price=5070.0, + bid_price=5_060.0, + ask_price=5_070.0, ) self.data_engine.process(tick) @@ -1015,8 +1015,8 @@ def test_submit_trailing_stop_limit_order_with_trigger_price_then_updates( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5065.0, - ask_price=5065.0, + bid_price=5_065.0, + ask_price=5_065.0, ) self.data_engine.process(tick) @@ -1033,15 +1033,15 @@ def test_submit_trailing_stop_limit_order_with_trigger_price_then_updates( @pytest.mark.parametrize( ("order_side", "price"), [ - [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5070)], - [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5060)], + [OrderSide.BUY, ETHUSDT_PERP_BINANCE.make_price(5_070)], + [OrderSide.SELL, ETHUSDT_PERP_BINANCE.make_price(5_060)], ], ) def test_submit_trailing_stop_limit_order_with_trigger_price_then_triggers( self, - order_side, - price, - ): + order_side: OrderSide, + price: Price, + ) -> None: # Arrange order = self.strategy.order_factory.trailing_stop_limit( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -1058,8 +1058,8 @@ def test_submit_trailing_stop_limit_order_with_trigger_price_then_triggers( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5060.0, - ask_price=5070.0, + bid_price=5_060.0, + ask_price=5_070.0, ) self.data_engine.process(tick) @@ -1069,8 +1069,8 @@ def test_submit_trailing_stop_limit_order_with_trigger_price_then_triggers( tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5055.0, - ask_price=5075.0, + bid_price=5_055.0, + ask_price=5_075.0, ) self.data_engine.process(tick) @@ -1091,27 +1091,27 @@ def test_submit_trailing_stop_limit_order_with_trigger_price_then_triggers( [ [ OrderSide.BUY, - ETHUSDT_PERP_BINANCE.make_price(5070.0), - ETHUSDT_PERP_BINANCE.make_price(5070.0), + ETHUSDT_PERP_BINANCE.make_price(5_070.0), + ETHUSDT_PERP_BINANCE.make_price(5_070.0), ], [ OrderSide.SELL, - ETHUSDT_PERP_BINANCE.make_price(5060.0), - ETHUSDT_PERP_BINANCE.make_price(5060.0), + ETHUSDT_PERP_BINANCE.make_price(5_060.0), + ETHUSDT_PERP_BINANCE.make_price(5_060.0), ], ], ) def test_submit_limit_if_touched_immediately_triggered_releases_limit_order( self, - order_side, - trigger_price, - price, - ): + order_side: OrderSide, + trigger_price: TriggerType, + price: Price, + ) -> None: # Arrange tick = TestDataStubs.quote_tick( instrument=ETHUSDT_PERP_BINANCE, - bid_price=5060.0, - ask_price=5070.0, + bid_price=5_060.0, + ask_price=5_070.0, ) self.emulator.create_matching_core( @@ -1153,8 +1153,8 @@ def test_submit_limit_if_touched_immediately_triggered_releases_limit_order( ) def test_submit_limit_order_bid_ask_with_synthetic_instrument_trigger( self, - order_side, - ): + order_side: OrderSide, + ) -> None: # Arrange synthetic = TestInstrumentProvider.synthetic_instrument() self.cache.add_synthetic(synthetic) From 859ca86150664204d234f778e5b99d7a4506b213 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 27 Oct 2023 18:17:37 +1100 Subject: [PATCH 23/78] Use Final qualifier for sentinels --- nautilus_trader/live/data_engine.py | 3 ++- nautilus_trader/live/execution_engine.py | 4 ++-- nautilus_trader/live/risk_engine.py | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/nautilus_trader/live/data_engine.py b/nautilus_trader/live/data_engine.py index c95f9c214b32..f6f008d6eab1 100644 --- a/nautilus_trader/live/data_engine.py +++ b/nautilus_trader/live/data_engine.py @@ -15,6 +15,7 @@ import asyncio from asyncio import Queue +from typing import Final from nautilus_trader.cache.cache import Cache from nautilus_trader.common.clock import LiveClock @@ -55,7 +56,7 @@ class LiveDataEngine(DataEngine): """ - _sentinel = None + _sentinel: Final[None] = None def __init__( self, diff --git a/nautilus_trader/live/execution_engine.py b/nautilus_trader/live/execution_engine.py index 5e958b785337..6b9d7b0c3dec 100644 --- a/nautilus_trader/live/execution_engine.py +++ b/nautilus_trader/live/execution_engine.py @@ -17,7 +17,7 @@ import math from asyncio import Queue from decimal import Decimal -from typing import Any +from typing import Any, Final from nautilus_trader.cache.cache import Cache from nautilus_trader.common.clock import LiveClock @@ -93,7 +93,7 @@ class LiveExecutionEngine(ExecutionEngine): """ - _sentinel = None + _sentinel: Final[None] = None def __init__( self, diff --git a/nautilus_trader/live/risk_engine.py b/nautilus_trader/live/risk_engine.py index 889a65d9ff94..c76ac529a41c 100644 --- a/nautilus_trader/live/risk_engine.py +++ b/nautilus_trader/live/risk_engine.py @@ -15,6 +15,7 @@ import asyncio from asyncio import Queue +from typing import Final from nautilus_trader.cache.base import CacheFacade from nautilus_trader.common.clock import LiveClock @@ -56,7 +57,7 @@ class LiveRiskEngine(RiskEngine): """ - _sentinel = None + _sentinel: Final[None] = None def __init__( self, From 26e6fef4ffaadd03a268abae31a822e17af05667 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 28 Oct 2023 08:06:21 +1100 Subject: [PATCH 24/78] Transformed orders retain original ts_init --- RELEASES.md | 1 + nautilus_trader/model/orders/limit.pyx | 3 +++ nautilus_trader/model/orders/market.pyx | 3 +++ tests/unit_tests/model/test_orders.py | 4 ++-- 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 3d3e95d54d05..38bb8d9f9554 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -6,6 +6,7 @@ Released on TBC (UTC). - Added `WebSocketClient` connection headers, thanks @ruthvik125 and @twitu ### Breaking Changes +- Transformed orders will now retain the original `ts_init` timestamp - Dropped support for Python 3.9 ### Fixes diff --git a/nautilus_trader/model/orders/limit.pyx b/nautilus_trader/model/orders/limit.pyx index 0c4f0244d39b..cbbcc64293d7 100644 --- a/nautilus_trader/model/orders/limit.pyx +++ b/nautilus_trader/model/orders/limit.pyx @@ -421,6 +421,9 @@ cdef class LimitOrder(Order): if triggered_price: transformed.set_triggered_price_c(triggered_price) + # Use original order initialization timestamp + transformed.ts_init = order.ts_init + Order._hydrate_initial_events(original=order, transformed=transformed) return transformed diff --git a/nautilus_trader/model/orders/market.pyx b/nautilus_trader/model/orders/market.pyx index 0cc128d2881c..a47239c8ba3c 100644 --- a/nautilus_trader/model/orders/market.pyx +++ b/nautilus_trader/model/orders/market.pyx @@ -323,6 +323,9 @@ cdef class MarketOrder(Order): tags=order.tags, ) + # Use original order initialization timestamp + transformed.ts_init = order.ts_init + Order._hydrate_initial_events(original=order, transformed=transformed) return transformed diff --git a/tests/unit_tests/model/test_orders.py b/tests/unit_tests/model/test_orders.py index b4918fd8144c..015b87eb0e7b 100644 --- a/tests/unit_tests/model/test_orders.py +++ b/tests/unit_tests/model/test_orders.py @@ -2197,7 +2197,7 @@ def test_market_order_transformation_to_limit_order(self) -> None: # Assert assert order.order_type == OrderType.LIMIT assert order.price == price - assert order.ts_init == 1 + assert order.ts_init == 0 # Retains original order `ts_init` def test_limit_order_transformation_to_market_order(self) -> None: # Arrange @@ -2213,4 +2213,4 @@ def test_limit_order_transformation_to_market_order(self) -> None: # Assert assert order.order_type == OrderType.MARKET - assert order.ts_init == 1 + assert order.ts_init == 0 # Retains original order `ts_init` From a78ce23aff0d17772767ea3b3f46ab782e2ede95 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 28 Oct 2023 08:57:50 +1100 Subject: [PATCH 25/78] Increase limit for Binance all orders requests --- nautilus_trader/adapters/binance/common/execution.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nautilus_trader/adapters/binance/common/execution.py b/nautilus_trader/adapters/binance/common/execution.py index a141a007a6fe..4dd9eca12618 100644 --- a/nautilus_trader/adapters/binance/common/execution.py +++ b/nautilus_trader/adapters/binance/common/execution.py @@ -443,7 +443,9 @@ async def generate_order_status_reports( for symbol in active_symbols: # Here we don't pass a `start_time` or `end_time` as order reports appear to go # randomly missing when these are specified. We filter on the Nautilus side below. - response = await self._http_account.query_all_orders(symbol=symbol) + # Explicitly setting limit to the max lookback of 1000, in the future we should + # add pagination. + response = await self._http_account.query_all_orders(symbol=symbol, limit=1_000) binance_orders.extend(response) except BinanceError as e: self._log.exception(f"Cannot generate OrderStatusReport: {e.message}", e) From eb8f5f71a62887e669dbac2ea36a51e05b404a71 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 28 Oct 2023 09:33:24 +1100 Subject: [PATCH 26/78] Fix BacktestEngine logging error on immediate stop --- RELEASES.md | 1 + nautilus_trader/backtest/engine.pyx | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 38bb8d9f9554..3255c3bdf832 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -14,6 +14,7 @@ Released on TBC (UTC). - Fixed `Binance` all orders requests which would omit order reports when using a `start` param - Fixed managed GTD orders past expiry cancellation on restart (orders were not being canceled) - Fixed managed GTD orders cancel timer on order cancel (timers were not being canceled) +- Fixed `BacktestEngine` logging error with immediate stop (caused by certain timestamps being `None`) - Interactive Brokers adapter various fixes, thanks @rsmb7z --- diff --git a/nautilus_trader/backtest/engine.pyx b/nautilus_trader/backtest/engine.pyx index 11527662a3e6..c1ca5ce42137 100644 --- a/nautilus_trader/backtest/engine.pyx +++ b/nautilus_trader/backtest/engine.pyx @@ -1232,6 +1232,16 @@ cdef class BacktestEngine: self._log.info("\033[36m-----------------------------------------------------------------") def _log_post_run(self): + if self._run_finished and self._run_started: + elapsed_time = self._run_finished - self._run_started + else: + elapsed_time = None + + if self._backtest_end and self._backtest_start: + backtest_range = self._backtest_end - self._backtest_start + else: + backtest_range = None + self._log.info("\033[36m=================================================================") self._log.info("\033[36m BACKTEST POST-RUN") self._log.info("\033[36m=================================================================") @@ -1239,10 +1249,10 @@ cdef class BacktestEngine: self._log.info(f"Run ID: {self._run_id}") self._log.info(f"Run started: {self._run_started}") self._log.info(f"Run finished: {self._run_finished}") - self._log.info(f"Elapsed time: {self._run_finished - self._run_started}") + self._log.info(f"Elapsed time: {elapsed_time}") self._log.info(f"Backtest start: {self._backtest_start}") self._log.info(f"Backtest end: {self._backtest_end}") - self._log.info(f"Backtest range: {self._backtest_end - self._backtest_start}") + self._log.info(f"Backtest range: {backtest_range}") self._log.info(f"Iterations: {self._iteration:,}") self._log.info(f"Total events: {self._kernel.exec_engine.event_count:,}") self._log.info(f"Total orders: {self._kernel.cache.orders_total_count():,}") From f79372c51ce378238922ecd630920cec80de8f4a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 28 Oct 2023 10:03:52 +1100 Subject: [PATCH 27/78] Fix BacktestNode error handling for sequential runs --- RELEASES.md | 1 + nautilus_trader/backtest/node.py | 27 ++++++++++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 3255c3bdf832..0b842b7a8cdd 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -15,6 +15,7 @@ Released on TBC (UTC). - Fixed managed GTD orders past expiry cancellation on restart (orders were not being canceled) - Fixed managed GTD orders cancel timer on order cancel (timers were not being canceled) - Fixed `BacktestEngine` logging error with immediate stop (caused by certain timestamps being `None`) +- Fixed `BacktestNode` exceptions during backtest runs preventing next sequential run, thanks for reporting @cavan-black - Interactive Brokers adapter various fixes, thanks @rsmb7z --- diff --git a/nautilus_trader/backtest/node.py b/nautilus_trader/backtest/node.py index 54f506cebb78..52a19cf9cc8b 100644 --- a/nautilus_trader/backtest/node.py +++ b/nautilus_trader/backtest/node.py @@ -112,7 +112,11 @@ def get_engines(self) -> list[BacktestEngine]: def run(self) -> list[BacktestResult]: """ - Execute a group of backtest run configs synchronously. + Run the backtest node which will synchronously execute the list of loaded + backtest run configs. + + Any exceptions raised from a backtest will be printed to stdout and + the next backtest run will commence (if any). Returns ------- @@ -122,14 +126,19 @@ def run(self) -> list[BacktestResult]: """ results: list[BacktestResult] = [] for config in self._configs: - result = self._run( - run_config_id=config.id, - engine_config=config.engine, - venue_configs=config.venues, - data_configs=config.data, - batch_size_bytes=config.batch_size_bytes, - ) - results.append(result) + try: + result = self._run( + run_config_id=config.id, + engine_config=config.engine, + venue_configs=config.venues, + data_configs=config.data, + batch_size_bytes=config.batch_size_bytes, + ) + results.append(result) + except Exception as ex: + # Broad catch all prevents a single backtest run from halting + # the execution of the other backtests (such as a zero balance exception). + print(f"Error running {config}: {ex}") return results From 17ba1187fc5fb3eb6550caf0f2aadf11f103c948 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 28 Oct 2023 10:41:27 +1100 Subject: [PATCH 28/78] Skip nautilus_ibapi package version check --- .../interactive_brokers/client/client.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index 7c00587c7a13..fdbfee544233 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -49,9 +49,6 @@ from ibapi.utils import current_fn_name from ibapi.wrapper import EWrapper -from nautilus_trader import PYPROJECT_PATH -from nautilus_trader import get_package_version_from_toml -from nautilus_trader import get_package_version_installed from nautilus_trader.adapters.interactive_brokers.client.common import AccountOrderRef from nautilus_trader.adapters.interactive_brokers.client.common import IBPosition from nautilus_trader.adapters.interactive_brokers.client.common import Requests @@ -82,15 +79,15 @@ # fmt: on -# Check ibapi package versioning -ibapi_package = "nautilus_ibapi" -ibapi_version_specified = get_package_version_from_toml(PYPROJECT_PATH, ibapi_package, True) -ibapi_version_installed = get_package_version_installed(ibapi_package) - -if ibapi_version_specified != ibapi_version_installed: - raise RuntimeError( - f"Expected `{ibapi_package}` version {ibapi_version_specified}, but found {ibapi_version_installed}", - ) +# Check ibapi package versioning (skipping for now) +# ibapi_package = "nautilus_ibapi" +# ibapi_version_specified = get_package_version_from_toml(PYPROJECT_PATH, ibapi_package, True) +# ibapi_version_installed = get_package_version_installed(ibapi_package) +# +# if ibapi_version_specified != ibapi_version_installed: +# raise RuntimeError( +# f"Expected `{ibapi_package}` version {ibapi_version_specified}, but found {ibapi_version_installed}", +# ) class InteractiveBrokersClient(Component, EWrapper): From db986ba91833aa8e1900685b448f9fd6c8811fbd Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 28 Oct 2023 13:11:16 +1100 Subject: [PATCH 29/78] Apply Betfair execution fixes --- nautilus_trader/adapters/betfair/execution.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nautilus_trader/adapters/betfair/execution.py b/nautilus_trader/adapters/betfair/execution.py index c3a3e04e693d..f944e4c770c6 100644 --- a/nautilus_trader/adapters/betfair/execution.py +++ b/nautilus_trader/adapters/betfair/execution.py @@ -580,8 +580,8 @@ async def _handle_order_stream_update(self, order_change_message: OCM) -> None: continue def check_cache_against_order_image(self, order_change_message: OCM) -> None: - for market in order_change_message.oc: - for selection in market.orc: + for market in order_change_message.oc or []: + for selection in market.orc or []: instrument_id = betfair_instrument_id( market_id=market.id, selection_id=str(selection.id), @@ -589,7 +589,7 @@ def check_cache_against_order_image(self, order_change_message: OCM) -> None: ) orders = self._cache.orders(instrument_id=instrument_id) venue_orders = {o.venue_order_id: o for o in orders} - for unmatched_order in selection.uo: + for unmatched_order in selection.uo or []: # We can match on venue_order_id here order = venue_orders.get(VenueOrderId(str(unmatched_order.id))) if order is not None: From 5175e29d5cf082441a31a6bf5459e33c6c85e230 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 28 Oct 2023 13:17:59 +1100 Subject: [PATCH 30/78] Use sha256 for Betfair TradeId --- nautilus_trader/adapters/betfair/execution.py | 6 +++--- .../adapters/betfair/test_betfair_execution.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nautilus_trader/adapters/betfair/execution.py b/nautilus_trader/adapters/betfair/execution.py index f944e4c770c6..f2e55bc3378e 100644 --- a/nautilus_trader/adapters/betfair/execution.py +++ b/nautilus_trader/adapters/betfair/execution.py @@ -794,7 +794,7 @@ def _handle_stream_execution_complete_order_update( async def wait_for_order( self, venue_order_id: VenueOrderId, - timeout_seconds=10.0, + timeout_seconds: float = 10.0, ) -> ClientOrderId | None: """ We may get an order update from the socket before our submit_order response has @@ -826,7 +826,7 @@ async def wait_for_order( ) return None - def _handle_status_message(self, update: Status): + def _handle_status_message(self, update: Status) -> None: if update.is_error and update.connection_closed: self._log.warning(str(update)) if update.error_code == StatusErrorCode.MAX_CONNECTION_LIMIT_EXCEEDED: @@ -851,4 +851,4 @@ def create_trade_id(uo: UnmatchedOrder) -> TradeId: uo.sm, ), ) - return TradeId(hashlib.sha1(data).hexdigest()) # noqa (S303 insecure SHA1) + return TradeId(hashlib.sha256(data).hexdigest()[:40]) diff --git a/tests/integration_tests/adapters/betfair/test_betfair_execution.py b/tests/integration_tests/adapters/betfair/test_betfair_execution.py index 4864afb81347..e4de588d43fc 100644 --- a/tests/integration_tests/adapters/betfair/test_betfair_execution.py +++ b/tests/integration_tests/adapters/betfair/test_betfair_execution.py @@ -668,9 +668,9 @@ async def test_duplicate_trade_id(exec_client, setup_order_state, fill_events, c assert isinstance(cancel, OrderCanceled) # Second order example, partial fill followed by remainder filled assert isinstance(fill2, OrderFilled) - assert fill2.trade_id.value == "3ca6c34a1420657ca954b4adc7b85d960216a428" + assert fill2.trade_id.value == "87fef5f92a397fdabc3f4112565223e6abc26ed2" assert isinstance(fill3, OrderFilled) - assert fill3.trade_id.value == "1a6688e3e01fdea842bd6e71517bbf4eaf6a1415" + assert fill3.trade_id.value == "bf9b4dd216c963ca7a048cc57a680e11c8f845a7" @pytest.mark.parametrize( From 3b44d30733f597db7a4453fcf04e2acc7ea9d9fe Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 28 Oct 2023 13:21:23 +1100 Subject: [PATCH 31/78] Fix external variable vulnerability --- nautilus_trader/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nautilus_trader/__init__.py b/nautilus_trader/__init__.py index cae6a70296f7..5f75908b251b 100644 --- a/nautilus_trader/__init__.py +++ b/nautilus_trader/__init__.py @@ -17,6 +17,7 @@ """ import os +from importlib import resources import toml from importlib_metadata import version @@ -40,7 +41,6 @@ def clean_version_string(version: str) -> str: def get_package_version_from_toml( - toml_file: str, package_name: str, strip_specifiers: bool = False, ) -> str: @@ -48,8 +48,8 @@ def get_package_version_from_toml( Return the package version specified in the given `toml_file` for the given `package_name`. """ - with open(toml_file) as file: - data = toml.load(file) + with resources.path("your_package_name", "pyproject.toml") as toml_path: + data = toml.load(toml_path) version = data["tool"]["poetry"]["dependencies"][package_name]["version"] if strip_specifiers: version = clean_version_string(version) From b81939e93108219ce7e433536992151c0fe35529 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 28 Oct 2023 13:35:50 +1100 Subject: [PATCH 32/78] Change asserts outside tests --- .../crypto_ema_cross_with_binance_provider.py | 3 +- nautilus_trader/adapters/betfair/client.py | 3 +- nautilus_trader/analysis/analyzer.py | 29 +++++++++---------- nautilus_trader/backtest/__main__.py | 5 +++- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/examples/backtest/crypto_ema_cross_with_binance_provider.py b/examples/backtest/crypto_ema_cross_with_binance_provider.py index b9f41e1c24f1..0b54592f2baa 100644 --- a/examples/backtest/crypto_ema_cross_with_binance_provider.py +++ b/examples/backtest/crypto_ema_cross_with_binance_provider.py @@ -84,7 +84,8 @@ async def create_provider(): instrument_id = InstrumentId(symbol=Symbol("ETHUSDT-PERP"), venue=BINANCE) instrument = provider.find(instrument_id) - assert instrument, f"Unable to find instrument {instrument_id}" + if instrument is None: + raise RuntimeError(f"Unable to find instrument {instrument_id}") engine.add_venue( venue=BINANCE, diff --git a/nautilus_trader/adapters/betfair/client.py b/nautilus_trader/adapters/betfair/client.py index 9579198aaaa0..a4c17f5a1c22 100644 --- a/nautilus_trader/adapters/betfair/client.py +++ b/nautilus_trader/adapters/betfair/client.py @@ -138,7 +138,8 @@ async def connect(self): self._log.info("Connecting (Betfair login)") request = Login.with_params(username=self.username, password=self.password) resp: LoginResponse = await self._post(request) - assert resp.status == LoginStatus.SUCCESS + if resp.status != LoginStatus.SUCCESS: + raise RuntimeError(f"Login not successful: {resp.status.value}") self._log.info("Login success.", color=LogColor.GREEN) self.update_headers(login_resp=resp) diff --git a/nautilus_trader/analysis/analyzer.py b/nautilus_trader/analysis/analyzer.py index 8ee545d892a2..87614a6894e5 100644 --- a/nautilus_trader/analysis/analyzer.py +++ b/nautilus_trader/analysis/analyzer.py @@ -217,9 +217,8 @@ def realized_pnls(self, currency: Currency | None = None) -> pd.Series | None: if not self._realized_pnls: return None if currency is None: - assert ( - len(self._account_balances) == 1 - ), "currency was None for multi-currency portfolio" + if len(self._account_balances) > 1: + raise ValueError("`currency` was `None` for multi-currency portfolio") currency = next(iter(self._account_balances.keys())) return self._realized_pnls.get(currency) @@ -257,14 +256,14 @@ def total_pnl( """ if not self._account_balances: return 0.0 + if currency is None: - assert ( - len(self._account_balances) == 1 - ), "currency was None for multi-currency portfolio" + if len(self._account_balances) > 1: + raise ValueError("`currency` was `None` for multi-currency portfolio") currency = next(iter(self._account_balances.keys())) - assert ( - unrealized_pnl is None or unrealized_pnl.currency == currency - ), f"unrealized PnL curreny is not {currency}" + + if unrealized_pnl is not None and unrealized_pnl.currency != currency: + raise ValueError(f"unrealized PnL currency is not {currency}") account_balance = self._account_balances.get(currency) account_balance_starting = self._account_balances_starting.get(currency, Money(0, currency)) @@ -308,14 +307,14 @@ def total_pnl_percentage( """ if not self._account_balances: return 0.0 + if currency is None: - assert ( - len(self._account_balances) == 1 - ), "currency was None for multi-currency portfolio" + if len(self._account_balances) != 1: + raise ValueError("currency was None for multi-currency portfolio") currency = next(iter(self._account_balances.keys())) - assert ( - unrealized_pnl is None or unrealized_pnl.currency == currency - ), f"unrealized PnL curreny is not {currency}" + + if unrealized_pnl is not None and unrealized_pnl.currency != currency: + raise ValueError(f"unrealized PnL currency is not {currency}") account_balance = self._account_balances.get(currency) account_balance_starting = self._account_balances_starting.get(currency, Money(0, currency)) diff --git a/nautilus_trader/backtest/__main__.py b/nautilus_trader/backtest/__main__.py index adf36c96b92b..31095aa4a51b 100644 --- a/nautilus_trader/backtest/__main__.py +++ b/nautilus_trader/backtest/__main__.py @@ -29,12 +29,15 @@ def main( raw: str | None = None, fsspec_url: str | None = None, ): - assert raw is not None or fsspec_url is not None, "Must pass one of `raw` or `fsspec_url`" + if raw is None and fsspec_url is None: + raise ValueError("Must pass one of `raw` or `fsspec_url`") + if fsspec_url and raw is None: with fsspec.open(fsspec_url, "rb") as f: data = f.read().decode() else: data = raw.encode() + configs = msgspec.json.decode(data, type=list[BacktestRunConfig]) node = BacktestNode(configs=configs) node.run() From ee94864a83b50f41aac29ec65d2b864ca95e5f4d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 28 Oct 2023 13:42:23 +1100 Subject: [PATCH 33/78] Add deepsource --- .deepsource.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 000000000000..2b4ea5027e6b --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,16 @@ +version = 1 + +[[analyzers]] +name = "rust" + + [analyzers.meta] + msrv = "stable" + +[[analyzers]] +name = "shell" + +[[analyzers]] +name = "python" + + [analyzers.meta] + runtime_version = "3.x.x" From 7482ff0d53bd30b8f1fe9bc7ecf4682462cab5a2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 28 Oct 2023 13:50:53 +1100 Subject: [PATCH 34/78] Update docs --- CONTRIBUTING.md | 2 +- README.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97d9368c721f..5a29af101f6b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ To contribute, follow these steps: 6. The CI system will run the full test-suite over your code including all unit and integration tests, so include appropriate tests with the PR. -7. [Codacy](https://www.codacy.com/) will perform an automated code review. +7. [Deepsource](https://deepsource.io) will perform an automated code review. Fix any issues which cause a failed check, and add the commit to your PR. 8. You will also be required to sign a standard Contributor License Agreement (CLA), which is administered automatically through [CLA Assistant](https://cla-assistant.io/). diff --git a/README.md b/README.md index 87e2b6517305..32edd13788b2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # -[![codacy-quality](https://api.codacy.com/project/badge/Grade/a1d3ccf7bccb4483b091975681a5cb23)](https://app.codacy.com/gh/nautechsystems/nautilus_trader?utm_source=github.com&utm_medium=referral&utm_content=nautechsystems/nautilus_trader&utm_campaign=Badge_Grade_Dashboard) [![codecov](https://codecov.io/gh/nautechsystems/nautilus_trader/branch/master/graph/badge.svg?token=DXO9QQI40H)](https://codecov.io/gh/nautechsystems/nautilus_trader) ![pythons](https://img.shields.io/pypi/pyversions/nautilus_trader) ![pypi-version](https://img.shields.io/pypi/v/nautilus_trader) From c9997c052cd8526ffcee3fdf266f6bc9bb795f0e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 28 Oct 2023 14:08:08 +1100 Subject: [PATCH 35/78] Double quote to prevent word splitting --- examples/backtest/crypto_ema_cross_ethusdt_trade_ticks.py | 0 examples/backtest/crypto_ema_cross_ethusdt_trailing_stop.py | 0 examples/backtest/fx_ema_cross_audusd_bars_from_ticks.py | 0 .../backtest/fx_ema_cross_bracket_gbpusd_bars_external.py | 0 .../backtest/fx_ema_cross_bracket_gbpusd_bars_internal.py | 0 examples/backtest/fx_market_maker_gbpusd_bars.py | 0 scripts/test-examples.sh | 4 ++-- 7 files changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 examples/backtest/crypto_ema_cross_ethusdt_trade_ticks.py mode change 100644 => 100755 examples/backtest/crypto_ema_cross_ethusdt_trailing_stop.py mode change 100644 => 100755 examples/backtest/fx_ema_cross_audusd_bars_from_ticks.py mode change 100644 => 100755 examples/backtest/fx_ema_cross_bracket_gbpusd_bars_external.py mode change 100644 => 100755 examples/backtest/fx_ema_cross_bracket_gbpusd_bars_internal.py mode change 100644 => 100755 examples/backtest/fx_market_maker_gbpusd_bars.py diff --git a/examples/backtest/crypto_ema_cross_ethusdt_trade_ticks.py b/examples/backtest/crypto_ema_cross_ethusdt_trade_ticks.py old mode 100644 new mode 100755 diff --git a/examples/backtest/crypto_ema_cross_ethusdt_trailing_stop.py b/examples/backtest/crypto_ema_cross_ethusdt_trailing_stop.py old mode 100644 new mode 100755 diff --git a/examples/backtest/fx_ema_cross_audusd_bars_from_ticks.py b/examples/backtest/fx_ema_cross_audusd_bars_from_ticks.py old mode 100644 new mode 100755 diff --git a/examples/backtest/fx_ema_cross_bracket_gbpusd_bars_external.py b/examples/backtest/fx_ema_cross_bracket_gbpusd_bars_external.py old mode 100644 new mode 100755 diff --git a/examples/backtest/fx_ema_cross_bracket_gbpusd_bars_internal.py b/examples/backtest/fx_ema_cross_bracket_gbpusd_bars_internal.py old mode 100644 new mode 100755 diff --git a/examples/backtest/fx_market_maker_gbpusd_bars.py b/examples/backtest/fx_market_maker_gbpusd_bars.py old mode 100644 new mode 100755 diff --git a/scripts/test-examples.sh b/scripts/test-examples.sh index 91e94c205445..3f5faa4c07cb 100644 --- a/scripts/test-examples.sh +++ b/scripts/test-examples.sh @@ -19,8 +19,8 @@ do start_time=$(date +%s) # Run the backtest script - chmod +x examples/backtest/$script - yes | poetry run examples/backtest/$script + chmod +x "examples/backtest/$script" + yes | poetry run "examples/backtest/$script" # Get the exit status of the last example run exit_status=$? From c7350eaca86237d7a39bded4b8887d9ef25668cb Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 28 Oct 2023 16:22:15 +1100 Subject: [PATCH 36/78] Make OrderManager more generic --- nautilus_trader/config/common.py | 3 + nautilus_trader/execution/emulator.pxd | 1 + nautilus_trader/execution/emulator.pyx | 54 +++++++++++++++- nautilus_trader/execution/manager.pxd | 3 +- nautilus_trader/execution/manager.pyx | 79 ++++++++--------------- nautilus_trader/trading/strategy.pxd | 4 +- nautilus_trader/trading/strategy.pyx | 1 + tests/unit_tests/trading/test_strategy.py | 3 + 8 files changed, 93 insertions(+), 55 deletions(-) diff --git a/nautilus_trader/config/common.py b/nautilus_trader/config/common.py index 667bb82363c3..c2335ab72017 100644 --- a/nautilus_trader/config/common.py +++ b/nautilus_trader/config/common.py @@ -440,6 +440,8 @@ class StrategyConfig(NautilusConfig, kw_only=True, frozen=True): external_order_claims : list[str], optional The external order claim instrument IDs. External orders for matching instrument IDs will be associated with (claimed by) the strategy. + manage_contingencies : bool, default False + If OUO and OCO **open** contingency orders should be managed automatically by the strategy. manage_gtd_expiry : bool, default False If all order GTD time in force expirations should be managed by the strategy. If True then will ensure open orders have their GTD timers re-activated on start. @@ -450,6 +452,7 @@ class StrategyConfig(NautilusConfig, kw_only=True, frozen=True): order_id_tag: str | None = None oms_type: str | None = None external_order_claims: list[str] | None = None + manage_contingencies: bool = False manage_gtd_expiry: bool = False diff --git a/nautilus_trader/execution/emulator.pxd b/nautilus_trader/execution/emulator.pxd index 5bcfd002e42c..a7bed5e25f12 100644 --- a/nautilus_trader/execution/emulator.pxd +++ b/nautilus_trader/execution/emulator.pxd @@ -65,6 +65,7 @@ cdef class OrderEmulator(Actor): cpdef void _check_monitoring(self, StrategyId strategy_id, PositionId position_id) cpdef void _cancel_order(self, Order order) + cpdef void _update_order(self, Order order, Quantity new_quantity) # ------------------------------------------------------------------------------------------------- diff --git a/nautilus_trader/execution/emulator.pyx b/nautilus_trader/execution/emulator.pyx index d350326eba28..31af6284ae06 100644 --- a/nautilus_trader/execution/emulator.pyx +++ b/nautilus_trader/execution/emulator.pyx @@ -74,7 +74,7 @@ from nautilus_trader.model.orders.market cimport MarketOrder from nautilus_trader.msgbus.bus cimport MessageBus -cdef tuple SUPPORTED_TRIGGERS = (TriggerType.DEFAULT, TriggerType.BID_ASK, TriggerType.LAST_TRADE) +cdef set SUPPORTED_TRIGGERS = {TriggerType.DEFAULT, TriggerType.BID_ASK, TriggerType.LAST_TRADE} cdef class OrderEmulator(Actor): @@ -127,6 +127,7 @@ cdef class OrderEmulator(Actor): component_name=type(self).__name__, submit_order_handler=self._handle_submit_order, cancel_order_handler=self._cancel_order, + modify_order_handler=self._update_order, debug=config.debug, ) @@ -598,6 +599,57 @@ cdef class OrderEmulator(Actor): if matching_core is not None: matching_core.delete_order(order) + self.cache.update_order_pending_cancel_local(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._manager.send_exec_event(event) + + cpdef void _update_order(self, Order order, Quantity new_quantity): + if order is None: + self._log.error( + f"Cannot update order: order for {repr(order.client_order_id)} not found.", + ) + return + + if self.debug: + self._log.info( + f"Updating 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._manager.send_risk_event(event) + # ------------------------------------------------------------------------------------------------- cpdef void _trigger_stop_order(self, Order order): diff --git a/nautilus_trader/execution/manager.pxd b/nautilus_trader/execution/manager.pxd index 459c40399818..a4e9731bbb04 100644 --- a/nautilus_trader/execution/manager.pxd +++ b/nautilus_trader/execution/manager.pxd @@ -54,6 +54,7 @@ cdef class OrderManager: cdef dict _submit_order_commands cdef object _submit_order_handler cdef object _cancel_order_handler + cdef object _modify_order_handler cpdef dict get_submit_order_commands(self) cpdef void cache_submit_order_command(self, SubmitOrder command) @@ -63,6 +64,7 @@ cdef class OrderManager: # -- COMMAND HANDLERS ----------------------------------------------------------------------------- cpdef void cancel_order(self, Order order) + cpdef void modify_order_quantity(self, Order order, Quantity new_quantity) cpdef void create_new_submit_order(self, Order order, PositionId position_id=*, ClientId client_id=*) # -- EVENT HANDLERS ------------------------------------------------------------------------------- @@ -75,7 +77,6 @@ cdef class OrderManager: 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 --------------------------------------------------------------------------------------- diff --git a/nautilus_trader/execution/manager.pyx b/nautilus_trader/execution/manager.pyx index b72bb6810bc4..5cd8edee3d52 100644 --- a/nautilus_trader/execution/manager.pyx +++ b/nautilus_trader/execution/manager.pyx @@ -75,6 +75,8 @@ cdef class OrderManager: The handler to call when submitting orders. cancel_order_handler : Callable[[Order], None], optional The handler to call when canceling orders. + modify_order_handler : Callable[[Order], None], optional + The handler to call when modifying orders. debug : bool, default False If debug mode is active (will provide extra debug logging). @@ -95,6 +97,7 @@ cdef class OrderManager: str component_name not None, submit_order_handler: Optional[Callable[[SubmitOrder], None]] = None, cancel_order_handler: Optional[Callable[[Order], None]] = None, + modify_order_handler: Optional[Callable[[Order, Quantity], None]] = None, bint debug = False, ): Condition.valid_string(component_name, "component_name") @@ -107,8 +110,9 @@ cdef class OrderManager: self._cache = cache self.debug = debug - self._submit_order_handler: Callable[[SubmitOrder], None] = submit_order_handler - self._cancel_order_handler: Callable[[Order], None] = cancel_order_handler + self._submit_order_handler = submit_order_handler + self._cancel_order_handler = cancel_order_handler + self._modify_order_handler = modify_order_handler self._submit_order_commands: dict[ClientOrderId, SubmitOrder] = {} @@ -181,8 +185,6 @@ cdef class OrderManager: self._log.warning("Cannot cancel order: already closed.") return - self._cache.update_order_pending_cancel_local(order) - if self.debug: self._log.info(f"Cancelling order {order}.", LogColor.MAGENTA) @@ -191,20 +193,21 @@ cdef class OrderManager: 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 modify_order_quantity(self, Order order, Quantity new_quantity): + """ + Modify the given `order` with the manager. + + Parameters + ---------- + order : Order + The order to modify. + + """ + Condition.not_none(order, "order") + Condition.not_none(new_quantity, "new_quantity") + + if self._modify_order_handler is not None: + self._modify_order_handler(order, new_quantity) cpdef void create_new_submit_order( self, @@ -366,9 +369,9 @@ cdef class OrderManager: 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) + self.modify_order_quantity(child_order, parent_filled_qty) - if child_order.status_c() not in (OrderStatus.INITIALIZED, OrderStatus.EMULATED) or self._submit_order_handler is None: + if not child_order.is_active_local_c() 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: @@ -435,7 +438,7 @@ cdef class OrderManager: 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) + self.modify_order_quantity(contingent_order, filled_qty) elif order.contingency_type == ContingencyType.OCO: if self.debug: self._log.info(f"Processing OCO contingent order {client_order_id}.", LogColor.MAGENTA) @@ -449,7 +452,7 @@ cdef class OrderManager: 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) + self.modify_order_quantity(contingent_order, leaves_qty) cpdef void handle_contingencies_update(self, Order order): Condition.not_none(order, "order") @@ -482,38 +485,10 @@ cdef class OrderManager: if order.contingency_type == ContingencyType.OTO: if quantity._mem.raw != contingent_order.quantity._mem.raw: - self.update_order_quantity(contingent_order, quantity) + self.modify_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) + self.modify_order_quantity(contingent_order, quantity) # -- EGRESS --------------------------------------------------------------------------------------- diff --git a/nautilus_trader/trading/strategy.pxd b/nautilus_trader/trading/strategy.pxd index 9045f8099b7a..f18ad56d5177 100644 --- a/nautilus_trader/trading/strategy.pxd +++ b/nautilus_trader/trading/strategy.pxd @@ -78,8 +78,10 @@ cdef class Strategy(Actor): """The order management system for the strategy.\n\n:returns: `OmsType`""" cdef readonly list external_order_claims """The external order claims instrument IDs for the strategy.\n\n:returns: `list[InstrumentId]`""" + cdef readonly bint manage_contingencies + """If contingency orders should be managed automatically by the strategy.\n\n:returns: `bool`""" cdef readonly bint manage_gtd_expiry - """If all order GTD time in force expirations should be managed by the strategy.\n\n:returns: `bool`""" + """If all order GTD time in force expirations should be managed automatically by the strategy.\n\n:returns: `bool`""" # -- REGISTRATION --------------------------------------------------------------------------------- diff --git a/nautilus_trader/trading/strategy.pyx b/nautilus_trader/trading/strategy.pyx index c7a0153401a0..e8b1ed48e5d0 100644 --- a/nautilus_trader/trading/strategy.pyx +++ b/nautilus_trader/trading/strategy.pyx @@ -151,6 +151,7 @@ cdef class Strategy(Actor): self.config = config self.oms_type = oms_type_from_str(str(config.oms_type).upper()) if config.oms_type else OmsType.UNSPECIFIED self.external_order_claims = self._parse_external_order_claims(config.external_order_claims) + self.manage_contingencies = config.manage_contingencies self.manage_gtd_expiry = config.manage_gtd_expiry # Public components diff --git a/tests/unit_tests/trading/test_strategy.py b/tests/unit_tests/trading/test_strategy.py index 36477ed7eb68..9914ddbd76bb 100644 --- a/tests/unit_tests/trading/test_strategy.py +++ b/tests/unit_tests/trading/test_strategy.py @@ -201,6 +201,7 @@ def test_strategy_to_importable_config_with_no_specific_config(self): "order_id_tag": None, "strategy_id": None, "external_order_claims": None, + "manage_contingencies": False, "manage_gtd_expiry": False, } @@ -210,6 +211,7 @@ def test_strategy_to_importable_config(self): order_id_tag="001", strategy_id="ALPHA-01", external_order_claims=["ETHUSDT-PERP.DYDX"], + manage_contingencies=True, manage_gtd_expiry=True, ) @@ -227,6 +229,7 @@ def test_strategy_to_importable_config(self): "order_id_tag": "001", "strategy_id": "ALPHA-01", "external_order_claims": ["ETHUSDT-PERP.DYDX"], + "manage_contingencies": True, "manage_gtd_expiry": True, } From 179728d7ebc28db628c1cc91fd17d47b5a5e016d Mon Sep 17 00:00:00 2001 From: David Blom Date: Sat, 28 Oct 2023 22:21:49 +0200 Subject: [PATCH 37/78] Validate BacktestDataConfig start before end (#1311) --- nautilus_trader/backtest/node.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nautilus_trader/backtest/node.py b/nautilus_trader/backtest/node.py index 52a19cf9cc8b..47b49c4968f0 100644 --- a/nautilus_trader/backtest/node.py +++ b/nautilus_trader/backtest/node.py @@ -25,6 +25,7 @@ from nautilus_trader.config import BacktestRunConfig from nautilus_trader.config import BacktestVenueConfig from nautilus_trader.core.correctness import PyCondition +from nautilus_trader.core.datetime import dt_to_unix_nanos from nautilus_trader.core.inspect import is_nautilus_class from nautilus_trader.core.nautilus_pyo3 import DataBackendSession from nautilus_trader.model.currency import Currency @@ -151,6 +152,16 @@ def _validate_configs(self, configs: list[BacktestRunConfig]) -> None: for data_config in config.data: if data_config.instrument_id is None: continue # No instrument associated with data + + if data_config.start_time is not None and data_config.end_time is not None: + start = dt_to_unix_nanos(data_config.start_time) + end = dt_to_unix_nanos(data_config.end_time) + + if end < start: + raise ValueError( + f"Invalid data config: end_time ({data_config.end_time}) is before start_time ({data_config.start_time}).", + ) + instrument_id: InstrumentId = InstrumentId.from_str(data_config.instrument_id) if instrument_id.venue not in venue_ids: raise ValueError( From f1b84b8b5f353df313e54706fdd184d83fb2ce7c Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 07:29:36 +1100 Subject: [PATCH 38/78] Update dependencies --- nautilus_core/Cargo.lock | 12 ++++++------ poetry.lock | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index ac3bc28353a5..56eeac084eb8 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -524,9 +524,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -4155,18 +4155,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.15" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ba595b9f2772fbee2312de30eeb80ec773b4cb2f1e8098db024afadda6c06f" +checksum = "ede7d7c7970ca2215b8c1ccf4d4f354c4733201dfaaba72d44ae5b37472e4901" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.15" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772666c41fb6dceaf520b564b962d738a8e1a83b41bd48945f50837aed78bb1d" +checksum = "4b27b1bb92570f989aac0ab7e9cbfbacdd65973f7ee920d9f0e71ebac878fd0b" dependencies = [ "proc-macro2", "quote", diff --git a/poetry.lock b/poetry.lock index 00006f646ec3..dc9f8a2bb0ec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -741,19 +741,19 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "filelock" -version = "3.12.4" +version = "3.13.0" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"}, - {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"}, + {file = "filelock-3.13.0-py3-none-any.whl", hash = "sha256:a552f4fde758f4eab33191e9548f671970f8b06d436d31388c9aa1e5861a710f"}, + {file = "filelock-3.13.0.tar.gz", hash = "sha256:63c6052c82a1a24c873a549fbd39a26982e8f35a3016da231ead11a5be9dad44"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"] -typing = ["typing-extensions (>=4.7.1)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] [[package]] name = "frozendict" @@ -1006,13 +1006,13 @@ files = [ [[package]] name = "identify" -version = "2.5.30" +version = "2.5.31" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.30-py2.py3-none-any.whl", hash = "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54"}, - {file = "identify-2.5.30.tar.gz", hash = "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"}, + {file = "identify-2.5.31-py2.py3-none-any.whl", hash = "sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d"}, + {file = "identify-2.5.31.tar.gz", hash = "sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75"}, ] [package.extras] From 626abfbcf097c449d2086eac350ba7b484d09e06 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 07:35:47 +1100 Subject: [PATCH 39/78] Collapse match to if let --- nautilus_core/network/src/websocket.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/nautilus_core/network/src/websocket.rs b/nautilus_core/network/src/websocket.rs index a25d2a20ba86..1b2098ff2745 100644 --- a/nautilus_core/network/src/websocket.rs +++ b/nautilus_core/network/src/websocket.rs @@ -541,12 +541,8 @@ mod tests { let value = request.headers().get(&self.key); assert!(value.is_some()); - match request.headers().get(&self.key) { - Some(value) => { - assert_eq!(value, self.value); - () - } - _ => (), + if let Some(value) = request.headers().get(&self.key) { + assert_eq!(value, self.value); } Ok(response) From 113ef27fba755c3314c4d6be8b889d937baccecb Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 07:36:38 +1100 Subject: [PATCH 40/78] Refine time conversions --- nautilus_trader/config/backtest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nautilus_trader/config/backtest.py b/nautilus_trader/config/backtest.py index 1557c4947512..eecc7a8af8cf 100644 --- a/nautilus_trader/config/backtest.py +++ b/nautilus_trader/config/backtest.py @@ -30,7 +30,7 @@ from nautilus_trader.config.common import NautilusConfig from nautilus_trader.config.common import NautilusKernelConfig from nautilus_trader.config.common import RiskEngineConfig -from nautilus_trader.core.datetime import maybe_dt_to_unix_nanos +from nautilus_trader.core.datetime import dt_to_unix_nanos from nautilus_trader.model.data import Bar from nautilus_trader.model.identifiers import ClientId from nautilus_trader.persistence.catalog.parquet import ParquetDataCatalog @@ -110,13 +110,13 @@ def query(self) -> dict[str, Any]: def start_time_nanos(self) -> int: if self.start_time is None: return 0 - return maybe_dt_to_unix_nanos(pd.Timestamp(self.start_time)) + return dt_to_unix_nanos(self.start_time) @property def end_time_nanos(self) -> int: if self.end_time is None: return sys.maxsize - return maybe_dt_to_unix_nanos(pd.Timestamp(self.end_time)) + return dt_to_unix_nanos(self.end_time) def catalog(self) -> ParquetDataCatalog: from nautilus_trader.persistence.catalog.parquet import ParquetDataCatalog From fdbfdf38915f6695d67e1711cff9541bcec8322f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 09:50:20 +1100 Subject: [PATCH 41/78] Add OrderManager to Strategy --- RELEASES.md | 19 +- docs/concepts/execution.md | 2 +- docs/concepts/orders.md | 2 +- nautilus_trader/backtest/engine.pyx | 5 + nautilus_trader/backtest/exchange.pxd | 2 + nautilus_trader/backtest/exchange.pyx | 6 + nautilus_trader/backtest/matching_engine.pxd | 1 + nautilus_trader/backtest/matching_engine.pyx | 20 +- nautilus_trader/backtest/node.py | 1 + nautilus_trader/config/backtest.py | 1 + nautilus_trader/config/common.py | 7 +- nautilus_trader/execution/emulator.pyx | 14 +- nautilus_trader/execution/manager.pxd | 4 +- nautilus_trader/execution/manager.pyx | 46 +++- nautilus_trader/trading/strategy.pxd | 7 +- nautilus_trader/trading/strategy.pyx | 47 ++-- tests/unit_tests/backtest/test_config.py | 8 +- tests/unit_tests/trading/test_strategy.py | 265 ++++++++++++++----- 18 files changed, 331 insertions(+), 126 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 0b842b7a8cdd..567fb17eddb6 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,9 +4,12 @@ Released on TBC (UTC). ### Enhancements - Added `WebSocketClient` connection headers, thanks @ruthvik125 and @twitu +- Added `support_contingent_orders` option for venues (to simulate venues which do not support contingent orders) +- Added `StrategyConfig.manage_contingent_orders` option (to automatically manage **open** contingenct orders) ### Breaking Changes - Transformed orders will now retain the original `ts_init` timestamp +- Removed unimplemented `batch_more` option for `Strategy.modify_order` - Dropped support for Python 3.9 ### Fixes @@ -175,7 +178,7 @@ Released on 31st July 2023 (UTC). - Fixed dictionary representation of orders for `venue_order_id` (for three order types) - Fixed `Currency` registration with core global map on creation - Fixed serialization of `OrderInitialized.exec_algorithm_params` to spec (bytes rather than string) -- Fixed assignment of position IDs for contingency orders (when parent filled) +- Fixed assignment of position IDs for contingent orders (when parent filled) - Fixed `PENDING_CANCEL` -> `EXPIRED` as valid state transition (real world possibility) - Fixed fill handling of `reduce_only` orders when partially filled - Fixed Binance reconciliation which was requesting reports for the same symbol multiple times @@ -253,8 +256,8 @@ Released on 19th May 2023 (UTC). - Fixed handling of emulated order contingencies (not based on status of spawned algorithm orders) - Fixed sending execution algorithm commands from strategy - Fixed `OrderEmulator` releasing of already closed orders -- Fixed `MatchingEngine` processing of reduce only for child contingency orders -- Fixed `MatchingEngine` position ID assignment for child contingency orders +- Fixed `MatchingEngine` processing of reduce only for child contingent orders +- Fixed `MatchingEngine` position ID assignment for child contingent orders - Fixed `Actor` handling of historical data from requests (will now call `on_historical_data` regardless of state), thanks for reporting @miller-moore - Fixed pyarrow schema dictionary index keys being too narrow (int8 -> int16), thanks for reporting @rterbush @@ -301,7 +304,7 @@ Released on 30th April 2023 (UTC). - Added `TWAPExecAlgorithm` and `TWAPExecAlgorithmConfig` to examples - Build out `ExecAlgorithm` base class for implementing 'first class' execution algorithms - Rewired execution for improved flow flexibility between emulated orders, execution algorithms and the `RiskEngine` -- Improved handling for `OrderEmulator` updating of contingency orders from execution algorithms +- Improved handling for `OrderEmulator` updating of contingent orders from execution algorithms - Defined public API for instruments, can now import directly from `nautilus_trader.model.instruments` (denest namespace) - Defined public API for orders, can now import directly from `nautilus_trader.model.orders` (denest namespace) - Defined public API for order book, can now import directly from `nautilus_trader.model.orderbook` (denest namespace) @@ -309,7 +312,7 @@ Released on 30th April 2023 (UTC). - Refined build and added additional `debug` Makefile convenience targets ### Fixes -- Fixed processing of contingency orders when in a pending update state +- Fixed processing of contingent orders when in a pending update state - Fixed calculation of PnL for flipped positions (only book realized PnL against open position) - Fixed `WebSocketClient` session disconnect, thanks for reporting @miller-moore - Added missing `BinanceSymbolFilterType.NOTIONAL` @@ -618,7 +621,7 @@ Released on 28th November 2022 (UTC). - Renamed `Instrument.get_cost_currency(...)` to `Instrument.get_settlement_currency(...)` (more accurate terminology) ### Enhancements -- Added emulated contingency orders capability to `OrderEmulator` +- Added emulated contingent orders capability to `OrderEmulator` - Moved `test_kit` module to main package to support downstream project/package testing ### Fixes @@ -650,7 +653,7 @@ Released on 18th November 2022 (UTC). - Fixed bar aggregation start times for bar specs outside typical intervals (60-SECOND rather than 1-MINUTE etc) - Fixed backtest engine main loop ordering of time events with identically timestamped data - Fixed `ModifyOrder` message `str` and `repr` when no quantity -- Fixed OCO contingency orders which were actually implemented as OUO for backtests +- Fixed OCO contingent orders which were actually implemented as OUO for backtests - Fixed various bugs for Interactive Brokers integration, thanks @limx0 and @rsmb7z - Fixed pyarrow version parsing, thanks @ghill2 - Fixed returning venue from InstrumentId, thanks @rsmb7z @@ -1466,7 +1469,7 @@ Released on 12th September 2021. - Added order custom user tags - Added `Actor.register_warning_event` (also applicable to `TradingStrategy`) - Added `Actor.deregister_warning_event` (also applicable to `TradingStrategy`) -- Added `ContingencyType` enum (for contingency orders in an `OrderList`) +- Added `ContingencyType` enum (for contingent orders in an `OrderList`) - All order types can now be `reduce_only` (#437) - Refined backtest configuration options - Improved efficiency of `UUID4` using the Rust `fastuuid` Python bindings diff --git a/docs/concepts/execution.md b/docs/concepts/execution.md index e4462a63e319..25ac8821bc1f 100644 --- a/docs/concepts/execution.md +++ b/docs/concepts/execution.md @@ -215,7 +215,7 @@ e.g. `O-20230404-001-000-E1` (for the first spawned order) ```{note} The "primary" and "secondary" / "spawn" terminology was specifically chosen to avoid conflict -or confusion with the "parent" and "child" contingency orders terminology (an execution algorithm may also deal with contingent orders). +or confusion with the "parent" and "child" contingent orders terminology (an execution algorithm may also deal with contingent orders). ``` ### Managing execution algorithm orders diff --git a/docs/concepts/orders.md b/docs/concepts/orders.md index e1a0672a13f6..bcf7a3bf419d 100644 --- a/docs/concepts/orders.md +++ b/docs/concepts/orders.md @@ -98,7 +98,7 @@ of the stop price based on the offset from the 'market' (bid, ask or last price - `TICKS` - The offset is based on a number of ticks - `PRICE_TIER` - The offset is based on an exchange specific price tier -### Contingency Orders +### Contingent Orders More advanced relationships can be specified between orders such as assigning child order(s) which will only trigger when the parent order is activated or filled, or linking orders together which will cancel or reduce in quantity contingent on each other. More documentation for these options can be found in the [advanced order guide](advanced/advanced_orders.md). diff --git a/nautilus_trader/backtest/engine.pyx b/nautilus_trader/backtest/engine.pyx index c1ca5ce42137..e1a65894fa45 100644 --- a/nautilus_trader/backtest/engine.pyx +++ b/nautilus_trader/backtest/engine.pyx @@ -362,6 +362,7 @@ cdef class BacktestEngine: bar_execution: bool = True, reject_stop_orders: bool = True, support_gtd_orders: bool = True, + support_contingent_orders: bool = True, use_position_ids: bool = True, use_random_ids: bool = False, use_reduce_only: bool = True, @@ -404,6 +405,9 @@ 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. + support_contingent_orders : bool, default True + If contingent orders will be supported/respected by the venue. + If False then its expected the strategy will be managing any contingent orders. use_position_ids : bool, default True If venue position IDs will be generated on order fills. use_random_ids : bool, default False @@ -455,6 +459,7 @@ cdef class BacktestEngine: bar_execution=bar_execution, reject_stop_orders=reject_stop_orders, support_gtd_orders=support_gtd_orders, + support_contingent_orders=support_contingent_orders, use_position_ids=use_position_ids, use_random_ids=use_random_ids, use_reduce_only=use_reduce_only, diff --git a/nautilus_trader/backtest/exchange.pxd b/nautilus_trader/backtest/exchange.pxd index e0190bb2518c..187c07d7c2c5 100644 --- a/nautilus_trader/backtest/exchange.pxd +++ b/nautilus_trader/backtest/exchange.pxd @@ -84,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 support_contingent_orders + """If contingent orders will be supported/respected 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 diff --git a/nautilus_trader/backtest/exchange.pyx b/nautilus_trader/backtest/exchange.pyx index 25dcb46154f5..92a61ed57df4 100644 --- a/nautilus_trader/backtest/exchange.pyx +++ b/nautilus_trader/backtest/exchange.pyx @@ -102,6 +102,9 @@ 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. + support_contingent_orders : bool, default True + If contingent orders will be supported/respected by the venue. + If False then its expected the strategy will be managing any contingent orders. use_position_ids : bool, default True If venue position IDs will be generated on order fills. use_random_ids : bool, default False @@ -147,6 +150,7 @@ cdef class SimulatedExchange: bint bar_execution = True, bint reject_stop_orders = True, bint support_gtd_orders = True, + bint support_contingent_orders = True, bint use_position_ids = True, bint use_random_ids = False, bint use_reduce_only = True, @@ -187,6 +191,7 @@ cdef class SimulatedExchange: self.bar_execution = bar_execution self.reject_stop_orders = reject_stop_orders self.support_gtd_orders = support_gtd_orders + self.support_contingent_orders = support_contingent_orders self.use_position_ids = use_position_ids self.use_random_ids = use_random_ids self.use_reduce_only = use_reduce_only @@ -335,6 +340,7 @@ cdef class SimulatedExchange: bar_execution=self.bar_execution, reject_stop_orders=self.reject_stop_orders, support_gtd_orders=self.support_gtd_orders, + support_contingent_orders=self.support_contingent_orders, use_position_ids=self.use_position_ids, use_random_ids=self.use_random_ids, use_reduce_only=self.use_reduce_only, diff --git a/nautilus_trader/backtest/matching_engine.pxd b/nautilus_trader/backtest/matching_engine.pxd index 83dc061c04f2..ad07fc91716a 100644 --- a/nautilus_trader/backtest/matching_engine.pxd +++ b/nautilus_trader/backtest/matching_engine.pxd @@ -79,6 +79,7 @@ cdef class OrderMatchingEngine: cdef bint _bar_execution cdef bint _reject_stop_orders cdef bint _support_gtd_orders + cdef bint _support_contingent_orders cdef bint _use_position_ids cdef bint _use_random_ids cdef bint _use_reduce_only diff --git a/nautilus_trader/backtest/matching_engine.pyx b/nautilus_trader/backtest/matching_engine.pyx index d0b2982a2071..c813ce2da6f0 100644 --- a/nautilus_trader/backtest/matching_engine.pyx +++ b/nautilus_trader/backtest/matching_engine.pyx @@ -125,6 +125,9 @@ 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. + support_contingent_orders : bool, default True + If contingent orders will be supported/respected by the venue. + If False then its expected the strategy will be managing any contingent orders. use_position_ids : bool, default True If venue position IDs will be generated on order fills. use_random_ids : bool, default False @@ -149,6 +152,7 @@ cdef class OrderMatchingEngine: bint bar_execution = True, bint reject_stop_orders = True, bint support_gtd_orders = True, + bint support_contingent_orders = True, bint use_position_ids = True, bint use_random_ids = False, bint use_reduce_only = True, @@ -172,6 +176,7 @@ cdef class OrderMatchingEngine: self._bar_execution = bar_execution self._reject_stop_orders = reject_stop_orders self._support_gtd_orders = support_gtd_orders + self._support_contingent_orders = support_contingent_orders self._use_position_ids = use_position_ids self._use_random_ids = use_random_ids self._use_reduce_only = use_reduce_only @@ -638,7 +643,7 @@ cdef class OrderMatchingEngine: self._account_ids[order.trader_id] = account_id cdef Order parent - if order.parent_order_id is not None: + if self._support_contingent_orders and order.parent_order_id is not None: parent = self.cache.order(order.parent_order_id) assert parent is not None and parent.contingency_type == ContingencyType.OTO, "OTO parent not found" if parent.status_c() == OrderStatus.REJECTED and order.is_open_c(): @@ -1680,7 +1685,10 @@ cdef class OrderMatchingEngine: # Remove order from market self._core.delete_order(order) - # Check contingency orders + if not self._support_contingent_orders: + return + + # Check contingent orders cdef ClientOrderId client_order_id cdef Order child_order if order.contingency_type == ContingencyType.OTO: @@ -1824,7 +1832,7 @@ cdef class OrderMatchingEngine: self._core.add_order(order) cpdef void expire_order(self, Order order): - if order.contingency_type != ContingencyType.NO_CONTINGENCY: + if self._support_contingent_orders and order.contingency_type != ContingencyType.NO_CONTINGENCY: self._cancel_contingent_orders(order) self._generate_order_expired(order) @@ -1843,7 +1851,7 @@ cdef class OrderMatchingEngine: self._generate_order_canceled(order) - if order.contingency_type != ContingencyType.NO_CONTINGENCY and cancel_contingencies: + if self._support_contingent_orders and order.contingency_type != ContingencyType.NO_CONTINGENCY and cancel_contingencies: self._cancel_contingent_orders(order) cpdef void update_order( @@ -1895,7 +1903,7 @@ cdef class OrderMatchingEngine: raise ValueError( f"invalid `OrderType` was {order.order_type}") # pragma: no cover (design-time error) - if order.contingency_type != ContingencyType.NO_CONTINGENCY and update_contingencies: + if self._support_contingent_orders and order.contingency_type != ContingencyType.NO_CONTINGENCY and update_contingencies: self._update_contingent_orders(order) cpdef void trigger_stop_order(self, Order order): @@ -1959,7 +1967,7 @@ cdef class OrderMatchingEngine: ) cdef void _cancel_contingent_orders(self, Order order): - # Iterate all contingency orders and cancel if active + # Iterate all contingent orders and cancel if active cdef ClientOrderId client_order_id cdef Order contingent_order for client_order_id in order.linked_order_ids: diff --git a/nautilus_trader/backtest/node.py b/nautilus_trader/backtest/node.py index 47b49c4968f0..2cf35f37fd61 100644 --- a/nautilus_trader/backtest/node.py +++ b/nautilus_trader/backtest/node.py @@ -202,6 +202,7 @@ def _create_engine( frozen_account=config.frozen_account, reject_stop_orders=config.reject_stop_orders, support_gtd_orders=config.support_gtd_orders, + support_contingent_orders=config.support_contingent_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/config/backtest.py b/nautilus_trader/config/backtest.py index eecc7a8af8cf..216859ea1e15 100644 --- a/nautilus_trader/config/backtest.py +++ b/nautilus_trader/config/backtest.py @@ -55,6 +55,7 @@ class BacktestVenueConfig(NautilusConfig, frozen=True): bar_execution: bool = True reject_stop_orders: bool = True support_gtd_orders: bool = True + support_contingent_orders: bool = True use_position_ids: bool = True use_random_ids: bool = False use_reduce_only: bool = True diff --git a/nautilus_trader/config/common.py b/nautilus_trader/config/common.py index c2335ab72017..0e4eb2a597b9 100644 --- a/nautilus_trader/config/common.py +++ b/nautilus_trader/config/common.py @@ -440,8 +440,9 @@ class StrategyConfig(NautilusConfig, kw_only=True, frozen=True): external_order_claims : list[str], optional The external order claim instrument IDs. External orders for matching instrument IDs will be associated with (claimed by) the strategy. - manage_contingencies : bool, default False - If OUO and OCO **open** contingency orders should be managed automatically by the strategy. + manage_contingent_orders : bool, default False + If OUO and OCO **open** contingent orders should be managed automatically by the strategy. + Any emulated orders which are active local will be managed by the `OrderEmulator` instead. manage_gtd_expiry : bool, default False If all order GTD time in force expirations should be managed by the strategy. If True then will ensure open orders have their GTD timers re-activated on start. @@ -452,7 +453,7 @@ class StrategyConfig(NautilusConfig, kw_only=True, frozen=True): order_id_tag: str | None = None oms_type: str | None = None external_order_claims: list[str] | None = None - manage_contingencies: bool = False + manage_contingent_orders: bool = False manage_gtd_expiry: bool = False diff --git a/nautilus_trader/execution/emulator.pyx b/nautilus_trader/execution/emulator.pyx index 31af6284ae06..0acc309f2f04 100644 --- a/nautilus_trader/execution/emulator.pyx +++ b/nautilus_trader/execution/emulator.pyx @@ -125,6 +125,7 @@ cdef class OrderEmulator(Actor): msgbus=msgbus, cache=cache, component_name=type(self).__name__, + active_local=True, submit_order_handler=self._handle_submit_order, cancel_order_handler=self._cancel_order, modify_order_handler=self._update_order, @@ -254,18 +255,7 @@ cdef class OrderEmulator(Actor): self._log.info(f"{RECV}{EVT} {event}.", LogColor.MAGENTA) self.event_count += 1 - if isinstance(event, OrderRejected): - self._manager.handle_order_rejected(event) - elif isinstance(event, OrderCanceled): - self._manager.handle_order_canceled(event) - elif isinstance(event, OrderExpired): - self._manager.handle_order_expired(event) - elif isinstance(event, OrderUpdated): - self._manager.handle_order_updated(event) - elif isinstance(event, OrderFilled): - self._manager.handle_order_filled(event) - elif isinstance(event, PositionEvent): - self._manager.handle_position_event(event) + self._manager.handle_event(event) if not isinstance(event, OrderEvent): return diff --git a/nautilus_trader/execution/manager.pxd b/nautilus_trader/execution/manager.pxd index a4e9731bbb04..2b04bfa40afe 100644 --- a/nautilus_trader/execution/manager.pxd +++ b/nautilus_trader/execution/manager.pxd @@ -49,6 +49,7 @@ cdef class OrderManager: cdef MessageBus _msgbus cdef Cache _cache + cdef readonly bint active_local cdef readonly bint debug cdef dict _submit_order_commands @@ -69,7 +70,7 @@ cdef class OrderManager: # -- EVENT HANDLERS ------------------------------------------------------------------------------- - cpdef void handle_position_event(self, PositionEvent event) + cpdef void handle_event(self, Event 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) @@ -77,6 +78,7 @@ cdef class OrderManager: 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 handle_position_event(self, PositionEvent event) # -- EGRESS --------------------------------------------------------------------------------------- diff --git a/nautilus_trader/execution/manager.pyx b/nautilus_trader/execution/manager.pyx index 5cd8edee3d52..fe493c9668f0 100644 --- a/nautilus_trader/execution/manager.pyx +++ b/nautilus_trader/execution/manager.pyx @@ -71,6 +71,8 @@ cdef class OrderManager: The cache for the order manager. component_name : str The component name for the order manager. + active_local : str + If the manager if for active local orders. submit_order_handler : Callable[[SubmitOrder], None], optional The handler to call when submitting orders. cancel_order_handler : Callable[[Order], None], optional @@ -86,6 +88,8 @@ cdef class OrderManager: If `submit_order_handler` is not ``None`` and not of type `Callable`. TypeError If `cancel_order_handler` is not ``None`` and not of type `Callable`. + TypeError + If `modify_order_handler` is not ``None`` and not of type `Callable`. """ def __init__( @@ -95,6 +99,7 @@ cdef class OrderManager: MessageBus msgbus, Cache cache not None, str component_name not None, + bint active_local, submit_order_handler: Optional[Callable[[SubmitOrder], None]] = None, cancel_order_handler: Optional[Callable[[Order], None]] = None, modify_order_handler: Optional[Callable[[Order, Quantity], None]] = None, @@ -103,12 +108,14 @@ cdef class OrderManager: 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") + Condition.callable_or_none(modify_order_handler, "modify_order_handler") self._clock = clock self._log = LoggerAdapter(component_name=component_name, logger=logger) self._msgbus = msgbus self._cache = cache + self.active_local = active_local self.debug = debug self._submit_order_handler = submit_order_handler self._cancel_order_handler = cancel_order_handler @@ -259,9 +266,30 @@ cdef class OrderManager: # -- EVENT HANDLERS ------------------------------------------------------------------------------- - cpdef void handle_position_event(self, PositionEvent event): - Condition.not_none(event, "event") - # TBC + cpdef void handle_event(self, Event event): + """ + Handle the given `event`. + + If a handler for the given event is not implemented then this will simply be a no-op. + + Parameters + ---------- + event : Event + The event to handle + + """ + if isinstance(event, OrderRejected): + self.handle_order_rejected(event) + elif isinstance(event, OrderCanceled): + self.handle_order_canceled(event) + elif isinstance(event, OrderExpired): + self.handle_order_expired(event) + elif isinstance(event, OrderUpdated): + self.handle_order_updated(event) + elif isinstance(event, OrderFilled): + self.handle_order_filled(event) + elif isinstance(event, PositionEvent): + self.handle_position_event(event) cpdef void handle_order_rejected(self, OrderRejected rejected): Condition.not_none(rejected, "rejected") @@ -362,7 +390,7 @@ cdef class OrderManager: self._log.info(f"Processing OTO child order {child_order}.", LogColor.MAGENTA) self._log.info(f"{parent_filled_qty=}.", LogColor.MAGENTA) - if not child_order.is_active_local_c(): + if self.active_local and not child_order.is_active_local_c(): continue if child_order.position_id is None: @@ -371,7 +399,7 @@ cdef class OrderManager: if parent_filled_qty._mem.raw != child_order.leaves_qty._mem.raw: self.modify_order_quantity(child_order, parent_filled_qty) - if not child_order.is_active_local_c() or self._submit_order_handler is None: + if (self.active_local and not child_order.is_active_local_c()) 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: @@ -427,7 +455,7 @@ cdef class OrderManager: 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(): + if contingent_order.is_closed_c() or (self.active_local and not contingent_order.is_active_local_c()): self._submit_order_commands.pop(order.client_order_id, None) continue # Already completed @@ -480,7 +508,7 @@ cdef class OrderManager: 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: + if contingent_order.is_closed_c() or (self.active_local and not contingent_order.is_active_local_c()): continue # Already completed # pragma: no cover if order.contingency_type == ContingencyType.OTO: @@ -490,6 +518,10 @@ cdef class OrderManager: if quantity._mem.raw != contingent_order.quantity._mem.raw: self.modify_order_quantity(contingent_order, quantity) + cpdef void handle_position_event(self, PositionEvent event): + Condition.not_none(event, "event") + # TBC + # -- EGRESS --------------------------------------------------------------------------------------- cpdef void send_emulator_command(self, TradingCommand command): diff --git a/nautilus_trader/trading/strategy.pxd b/nautilus_trader/trading/strategy.pxd index f18ad56d5177..8f9b1912680d 100644 --- a/nautilus_trader/trading/strategy.pxd +++ b/nautilus_trader/trading/strategy.pxd @@ -19,6 +19,7 @@ from nautilus_trader.common.clock cimport Clock from nautilus_trader.common.factories cimport OrderFactory from nautilus_trader.common.logging cimport Logger from nautilus_trader.common.timer cimport TimeEvent +from nautilus_trader.execution.manager cimport OrderManager from nautilus_trader.execution.messages cimport CancelOrder from nautilus_trader.execution.messages cimport ModifyOrder from nautilus_trader.execution.messages cimport TradingCommand @@ -67,6 +68,7 @@ from nautilus_trader.portfolio.base cimport PortfolioFacade cdef class Strategy(Actor): + cdef OrderManager _manager cdef readonly PortfolioFacade portfolio """The read-only portfolio for the strategy.\n\n:returns: `PortfolioFacade`""" @@ -78,8 +80,8 @@ cdef class Strategy(Actor): """The order management system for the strategy.\n\n:returns: `OmsType`""" cdef readonly list external_order_claims """The external order claims instrument IDs for the strategy.\n\n:returns: `list[InstrumentId]`""" - cdef readonly bint manage_contingencies - """If contingency orders should be managed automatically by the strategy.\n\n:returns: `bool`""" + cdef readonly bint manage_contingent_orders + """If contingent orders should be managed automatically by the strategy.\n\n:returns: `bool`""" cdef readonly bint manage_gtd_expiry """If all order GTD time in force expirations should be managed automatically by the strategy.\n\n:returns: `bool`""" @@ -142,7 +144,6 @@ cdef class Strategy(Actor): Price price=*, Price trigger_price=*, ClientId client_id=*, - bint batch_more=*, ) cpdef void cancel_order(self, Order order, ClientId client_id=*) cpdef void cancel_orders(self, list orders, ClientId client_id=*) diff --git a/nautilus_trader/trading/strategy.pyx b/nautilus_trader/trading/strategy.pyx index e8b1ed48e5d0..d64fc50726c1 100644 --- a/nautilus_trader/trading/strategy.pyx +++ b/nautilus_trader/trading/strategy.pyx @@ -151,7 +151,7 @@ cdef class Strategy(Actor): self.config = config self.oms_type = oms_type_from_str(str(config.oms_type).upper()) if config.oms_type else OmsType.UNSPECIFIED self.external_order_claims = self._parse_external_order_claims(config.external_order_claims) - self.manage_contingencies = config.manage_contingencies + self.manage_contingent_orders = config.manage_contingent_orders self.manage_gtd_expiry = config.manage_gtd_expiry # Public components @@ -160,6 +160,9 @@ cdef class Strategy(Actor): self.portfolio = None # Initialized when registered self.order_factory = None # Initialized when registered + # Order management + self._manager = None # Initialized when registered + # Register warning events self.register_warning_event(OrderDenied) self.register_warning_event(OrderRejected) @@ -277,8 +280,21 @@ cdef class Strategy(Actor): self.order_factory = OrderFactory( trader_id=self.trader_id, strategy_id=self.id, - clock=self.clock, - cache=self.cache, + clock=clock, + cache=cache, + ) + + self._manager = OrderManager( + clock=clock, + logger=logger, + msgbus=msgbus, + cache=cache, + component_name=type(self).__name__, + active_local=False, + submit_order_handler=None, + cancel_order_handler=self.cancel_order, + modify_order_handler=self.modify_order, + debug=True, # Set True for debugging ) # Required subscriptions @@ -367,13 +383,14 @@ cdef class Strategy(Actor): if self.order_factory: self.order_factory.reset() - self._pending_requests.clear() - self._indicators.clear() self._indicators_for_quotes.clear() self._indicators_for_trades.clear() self._indicators_for_bars.clear() + if self._manager: + self._manager.reset() + self.on_reset() # -- ABSTRACT METHODS ----------------------------------------------------------------------------- @@ -897,7 +914,6 @@ cdef class Strategy(Actor): Price price = None, Price trigger_price = None, ClientId client_id = None, - bint batch_more = False, ): """ Modify the given order with optional parameters and routing instructions. @@ -925,11 +941,6 @@ cdef class Strategy(Actor): client_id : ClientId, optional The specific client ID for the command. If ``None`` then will be inferred from the venue in the instrument ID. - batch_more : bool, default False - Indicates if this command should be batched (grouped) with subsequent modify order - commands for the venue. When set to `True`, we expect more calls to `modify_order` - which will add to the current batch. Final processing of the batch occurs on a call - with `batch_more=False`. For proper behavior, maintain the correct call sequence. Raises ------ @@ -943,10 +954,6 @@ cdef class Strategy(Actor): If the order is already closed or at `PENDING_CANCEL` status then the command will not be generated, and a warning will be logged. - The `batch_more` flag is an advanced feature which may have unintended consequences if not - called in the correct sequence. If a series of `batch_more=True` calls are not followed by - a `batch_more=False`, then no command will be sent from the strategy. - References ---------- https://www.onixs.biz/fix-dictionary/5.0.SP2/msgType_G_71.html @@ -955,10 +962,6 @@ cdef class Strategy(Actor): Condition.true(self.trader_id is not None, "The strategy has not been registered") Condition.not_none(order, "order") - if batch_more: - self._log.error("The `batch_more` feature is not currently implemented.") - return - cdef ModifyOrder command = self._create_modify_order( order=order, quantity=quantity, @@ -1524,6 +1527,12 @@ cdef class Strategy(Actor): if self._fsm.state != ComponentState.RUNNING: return + if self.manage_contingent_orders and self._manager is not None: + if isinstance(event, OrderEvent): + order = self.cache.order(event.client_order_id) + if order is not None and not order.is_active_local_c(): + self._manager.handle_event(event) + try: # Send to specific event handler if isinstance(event, OrderInitialized): diff --git a/tests/unit_tests/backtest/test_config.py b/tests/unit_tests/backtest/test_config.py index 030cb35e31e6..a3fdad122cb9 100644 --- a/tests/unit_tests/backtest/test_config.py +++ b/tests/unit_tests/backtest/test_config.py @@ -197,7 +197,7 @@ def test_run_config_to_json(self) -> None: ) json = msgspec.json.encode(run_config) result = len(msgspec.json.encode(json)) - assert result == 986 # UNIX + assert result == 1030 # UNIX @pytest.mark.skipif(sys.platform == "win32", reason="redundant to also test Windows") def test_run_config_parse_obj(self) -> None: @@ -218,7 +218,7 @@ def test_run_config_parse_obj(self) -> None: assert isinstance(config, BacktestRunConfig) node = BacktestNode(configs=[config]) assert isinstance(node, BacktestNode) - assert len(raw) == 737 # UNIX + assert len(raw) == 770 # UNIX @pytest.mark.skipif(sys.platform == "win32", reason="redundant to also test Windows") def test_backtest_data_config_to_dict(self) -> None: @@ -239,7 +239,7 @@ def test_backtest_data_config_to_dict(self) -> None: ) json = msgspec.json.encode(run_config) result = len(msgspec.json.encode(json)) - assert result == 1798 + assert result == 1842 @pytest.mark.skipif(sys.platform == "win32", reason="redundant to also test Windows") def test_backtest_run_config_id(self) -> None: @@ -247,7 +247,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 == "d1add7c871b0bdd762b495345e394276431eda714a00d839037df33e8a427fd1" # UNIX + assert token == "1e0c0ddaf6d9a53a1885b1a78102dfcb62d0418374472b091a36899fc25c9004" # UNIX @pytest.mark.skip(reason="fix after merge") @pytest.mark.parametrize( diff --git a/tests/unit_tests/trading/test_strategy.py b/tests/unit_tests/trading/test_strategy.py index 9914ddbd76bb..b88944361b95 100644 --- a/tests/unit_tests/trading/test_strategy.py +++ b/tests/unit_tests/trading/test_strategy.py @@ -40,6 +40,7 @@ from nautilus_trader.model.currencies import USD from nautilus_trader.model.data import Bar from nautilus_trader.model.enums import AccountType +from nautilus_trader.model.enums import ContingencyType from nautilus_trader.model.enums import OmsType from nautilus_trader.model.enums import OrderSide from nautilus_trader.model.enums import OrderStatus @@ -75,7 +76,7 @@ class TestStrategy: - def setup(self): + def setup(self) -> None: # Fixture Setup self.clock = TestClock() self.logger = Logger( @@ -139,6 +140,8 @@ def setup(self): clock=self.clock, logger=self.logger, latency_model=LatencyModel(0), + support_contingent_orders=False, + use_reduce_only=False, ) self.data_client = BacktestMarketDataClient( @@ -183,7 +186,7 @@ def setup(self): self.data_engine.start() self.exec_engine.start() - def test_strategy_to_importable_config_with_no_specific_config(self): + def test_strategy_to_importable_config_with_no_specific_config(self) -> None: # Arrange config = StrategyConfig() @@ -201,17 +204,17 @@ def test_strategy_to_importable_config_with_no_specific_config(self): "order_id_tag": None, "strategy_id": None, "external_order_claims": None, - "manage_contingencies": False, + "manage_contingent_orders": False, "manage_gtd_expiry": False, } - def test_strategy_to_importable_config(self): + def test_strategy_to_importable_config(self) -> None: # Arrange config = StrategyConfig( order_id_tag="001", strategy_id="ALPHA-01", external_order_claims=["ETHUSDT-PERP.DYDX"], - manage_contingencies=True, + manage_contingent_orders=True, manage_gtd_expiry=True, ) @@ -229,11 +232,11 @@ def test_strategy_to_importable_config(self): "order_id_tag": "001", "strategy_id": "ALPHA-01", "external_order_claims": ["ETHUSDT-PERP.DYDX"], - "manage_contingencies": True, + "manage_contingent_orders": True, "manage_gtd_expiry": True, } - def test_strategy_equality(self): + def test_strategy_equality(self) -> None: # Arrange strategy1 = Strategy(config=StrategyConfig(order_id_tag="AUD/USD-001")) strategy2 = Strategy(config=StrategyConfig(order_id_tag="AUD/USD-001")) @@ -244,7 +247,7 @@ def test_strategy_equality(self): assert strategy1 == strategy2 assert strategy2 != strategy3 - def test_str_and_repr(self): + def test_str_and_repr(self) -> None: # Arrange strategy = Strategy(config=StrategyConfig(order_id_tag="GBP/USD-MM")) @@ -252,14 +255,14 @@ def test_str_and_repr(self): assert str(strategy) == "Strategy-GBP/USD-MM" assert repr(strategy) == "Strategy(Strategy-GBP/USD-MM)" - def test_id(self): + def test_id(self) -> None: # Arrange strategy = Strategy() # Act, Assert assert strategy.id == StrategyId("Strategy-None") - def test_initialization(self): + def test_initialization(self) -> None: # Arrange strategy = Strategy(config=StrategyConfig(order_id_tag="001")) strategy.register( @@ -275,7 +278,7 @@ def test_initialization(self): assert strategy.state == ComponentState.READY assert not strategy.indicators_initialized() - def test_on_save_when_not_overridden_does_nothing(self): + def test_on_save_when_not_overridden_does_nothing(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -293,7 +296,7 @@ def test_on_save_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_on_load_when_not_overridden_does_nothing(self): + def test_on_load_when_not_overridden_does_nothing(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -311,7 +314,7 @@ def test_on_load_when_not_overridden_does_nothing(self): # Assert assert True # Exception not raised - def test_save_when_not_registered_logs_error(self): + def test_save_when_not_registered_logs_error(self) -> None: # Arrange config = StrategyConfig() @@ -329,7 +332,7 @@ def test_save_when_not_registered_logs_error(self): # Assert assert True # Exception not raised - def test_save_when_user_code_raises_error_logs_and_reraises(self): + def test_save_when_user_code_raises_error_logs_and_reraises(self) -> None: # Arrange strategy = KaboomStrategy() strategy.register( @@ -345,7 +348,7 @@ def test_save_when_user_code_raises_error_logs_and_reraises(self): with pytest.raises(RuntimeError): strategy.save() - def test_load_when_user_code_raises_error_logs_and_reraises(self): + def test_load_when_user_code_raises_error_logs_and_reraises(self) -> None: # Arrange strategy = KaboomStrategy() strategy.register( @@ -361,7 +364,7 @@ def test_load_when_user_code_raises_error_logs_and_reraises(self): with pytest.raises(RuntimeError): strategy.load({"something": b"123456"}) - def test_load(self): + def test_load(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -373,7 +376,7 @@ def test_load(self): logger=self.logger, ) - state = {} + state: dict[str, bytes] = {} # Act strategy.load(state) @@ -382,7 +385,7 @@ def test_load(self): # TODO: Write a users custom save method assert True - def test_reset(self): + def test_reset(self) -> None: # Arrange bar_type = TestDataStubs.bartype_audusd_1min_bid() strategy = MockStrategy(bar_type) @@ -417,7 +420,7 @@ def test_reset(self): assert strategy.ema1.count == 0 assert strategy.ema2.count == 0 - def test_dispose(self): + def test_dispose(self) -> None: # Arrange bar_type = TestDataStubs.bartype_audusd_1min_bid() strategy = MockStrategy(bar_type) @@ -439,7 +442,7 @@ def test_dispose(self): assert "on_dispose" in strategy.calls assert strategy.is_disposed - def test_save_load(self): + def test_save_load(self) -> None: # Arrange bar_type = TestDataStubs.bartype_audusd_1min_bid() strategy = MockStrategy(bar_type) @@ -461,7 +464,7 @@ def test_save_load(self): assert "on_save" in strategy.calls assert strategy.is_initialized - def test_register_indicator_for_quote_ticks_when_already_registered(self): + def test_register_indicator_for_quote_ticks_when_already_registered(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -485,7 +488,7 @@ def test_register_indicator_for_quote_ticks_when_already_registered(self): assert ema1 in strategy.registered_indicators assert ema2 in strategy.registered_indicators - def test_register_indicator_for_trade_ticks_when_already_registered(self): + def test_register_indicator_for_trade_ticks_when_already_registered(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -509,7 +512,7 @@ def test_register_indicator_for_trade_ticks_when_already_registered(self): assert ema1 in strategy.registered_indicators assert ema2 in strategy.registered_indicators - def test_register_indicator_for_bars_when_already_registered(self): + def test_register_indicator_for_bars_when_already_registered(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -534,7 +537,7 @@ def test_register_indicator_for_bars_when_already_registered(self): assert ema1 in strategy.registered_indicators assert ema2 in strategy.registered_indicators - def test_register_indicator_for_multiple_data_sources(self): + def test_register_indicator_for_multiple_data_sources(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -558,7 +561,7 @@ def test_register_indicator_for_multiple_data_sources(self): assert len(strategy.registered_indicators) == 1 assert ema in strategy.registered_indicators - def test_handle_quote_tick_updates_indicator_registered_for_quote_ticks(self): + def test_handle_quote_tick_updates_indicator_registered_for_quote_ticks(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -582,7 +585,7 @@ def test_handle_quote_tick_updates_indicator_registered_for_quote_ticks(self): # Assert assert ema.count == 2 - def test_handle_quote_ticks_with_no_ticks_logs_and_continues(self): + def test_handle_quote_ticks_with_no_ticks_logs_and_continues(self) -> None: # Arrange strategy = KaboomStrategy() strategy.register( @@ -603,7 +606,7 @@ def test_handle_quote_ticks_with_no_ticks_logs_and_continues(self): # Assert assert ema.count == 0 - def test_handle_quote_ticks_updates_indicator_registered_for_quote_ticks(self): + def test_handle_quote_ticks_updates_indicator_registered_for_quote_ticks(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -626,7 +629,7 @@ def test_handle_quote_ticks_updates_indicator_registered_for_quote_ticks(self): # Assert assert ema.count == 1 - def test_handle_trade_tick_updates_indicator_registered_for_trade_ticks(self): + def test_handle_trade_tick_updates_indicator_registered_for_trade_ticks(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -650,7 +653,7 @@ def test_handle_trade_tick_updates_indicator_registered_for_trade_ticks(self): # Assert assert ema.count == 2 - def test_handle_trade_ticks_updates_indicator_registered_for_trade_ticks(self): + def test_handle_trade_ticks_updates_indicator_registered_for_trade_ticks(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -673,7 +676,7 @@ def test_handle_trade_ticks_updates_indicator_registered_for_trade_ticks(self): # Assert assert ema.count == 1 - def test_handle_trade_ticks_with_no_ticks_logs_and_continues(self): + def test_handle_trade_ticks_with_no_ticks_logs_and_continues(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -694,7 +697,7 @@ def test_handle_trade_ticks_with_no_ticks_logs_and_continues(self): # Assert assert ema.count == 0 - def test_handle_bar_updates_indicator_registered_for_bars(self): + def test_handle_bar_updates_indicator_registered_for_bars(self) -> None: # Arrange bar_type = TestDataStubs.bartype_audusd_1min_bid() strategy = Strategy() @@ -718,7 +721,7 @@ def test_handle_bar_updates_indicator_registered_for_bars(self): # Assert assert ema.count == 2 - def test_handle_bars_updates_indicator_registered_for_bars(self): + def test_handle_bars_updates_indicator_registered_for_bars(self) -> None: # Arrange bar_type = TestDataStubs.bartype_audusd_1min_bid() strategy = Strategy() @@ -741,7 +744,7 @@ def test_handle_bars_updates_indicator_registered_for_bars(self): # Assert assert ema.count == 1 - def test_handle_bars_with_no_bars_logs_and_continues(self): + def test_handle_bars_with_no_bars_logs_and_continues(self) -> None: # Arrange bar_type = TestDataStubs.bartype_gbpusd_1sec_mid() strategy = Strategy() @@ -763,7 +766,7 @@ def test_handle_bars_with_no_bars_logs_and_continues(self): # Assert assert ema.count == 0 - def test_stop_cancels_a_running_time_alert(self): + def test_stop_cancels_a_running_time_alert(self) -> None: # Arrange bar_type = TestDataStubs.bartype_audusd_1min_bid() strategy = MockStrategy(bar_type) @@ -786,7 +789,7 @@ def test_stop_cancels_a_running_time_alert(self): # Assert assert strategy.clock.timer_count == 0 - def test_stop_cancels_a_running_timer(self): + def test_stop_cancels_a_running_timer(self) -> None: # Arrange bar_type = TestDataStubs.bartype_audusd_1min_bid() strategy = MockStrategy(bar_type) @@ -814,7 +817,7 @@ def test_stop_cancels_a_running_timer(self): # Assert assert strategy.clock.timer_count == 0 - def test_start_when_manage_gtd_reactivates_timers(self): + def test_start_when_manage_gtd_reactivates_timers(self) -> None: # Arrange config = StrategyConfig(manage_gtd_expiry=True) strategy = Strategy(config) @@ -859,7 +862,7 @@ def test_start_when_manage_gtd_reactivates_timers(self): "GTD-EXPIRY:O-19700101-0000-000-None-2", ] - def test_start_when_manage_gtd_and_order_past_expiration_then_cancels(self): + def test_start_when_manage_gtd_and_order_past_expiration_then_cancels(self) -> None: # Arrange config = StrategyConfig(manage_gtd_expiry=True) strategy = Strategy(config) @@ -894,7 +897,7 @@ def test_start_when_manage_gtd_and_order_past_expiration_then_cancels(self): assert strategy.clock.timer_count == 0 assert order1.is_pending_cancel - def test_submit_order_when_duplicate_id_then_denies(self): + def test_submit_order_when_duplicate_id_then_denies(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -931,7 +934,7 @@ def test_submit_order_when_duplicate_id_then_denies(self): # Assert assert order2.status == OrderStatus.DENIED - def test_submit_order_with_valid_order_successfully_submits(self): + def test_submit_order_with_valid_order_successfully_submits(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -960,7 +963,7 @@ def test_submit_order_with_valid_order_successfully_submits(self): assert not strategy.cache.is_order_open(order.client_order_id) assert strategy.cache.is_order_closed(order.client_order_id) - def test_submit_order_with_managed_gtd_starts_timer(self): + def test_submit_order_with_managed_gtd_starts_timer(self) -> None: # Arrange config = StrategyConfig(manage_gtd_expiry=True) strategy = Strategy(config) @@ -989,7 +992,7 @@ def test_submit_order_with_managed_gtd_starts_timer(self): assert strategy.clock.timer_count == 1 assert strategy.clock.timer_names == ["GTD-EXPIRY:O-19700101-0000-000-None-1"] - def test_submit_order_with_managed_gtd_when_immediately_filled_cancels_timer(self): + def test_submit_order_with_managed_gtd_when_immediately_filled_cancels_timer(self) -> None: # Arrange config = StrategyConfig(manage_gtd_expiry=True) strategy = Strategy(config) @@ -1019,7 +1022,7 @@ def test_submit_order_with_managed_gtd_when_immediately_filled_cancels_timer(sel assert strategy.clock.timer_count == 0 assert order.status == OrderStatus.FILLED - def test_submit_order_list_with_duplicate_order_list_id_then_denies(self): + def test_submit_order_list_with_duplicate_order_list_id_then_denies(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1083,7 +1086,7 @@ def test_submit_order_list_with_duplicate_order_list_id_then_denies(self): assert stop_loss.status == OrderStatus.DENIED assert take_profit.status == OrderStatus.DENIED - def test_submit_order_list_with_duplicate_order_id_then_denies(self): + def test_submit_order_list_with_duplicate_order_id_then_denies(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1140,7 +1143,7 @@ def test_submit_order_list_with_duplicate_order_id_then_denies(self): assert stop_loss.status == OrderStatus.DENIED assert take_profit.status == OrderStatus.DENIED - def test_submit_order_list_with_valid_order_successfully_submits(self): + def test_submit_order_list_with_valid_order_successfully_submits(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1174,7 +1177,7 @@ def test_submit_order_list_with_valid_order_successfully_submits(self): assert entry.status == OrderStatus.ACCEPTED assert entry in strategy.cache.orders_open() - def test_submit_order_list_with_managed_gtd_starts_timer(self): + def test_submit_order_list_with_managed_gtd_starts_timer(self) -> None: # Arrange config = StrategyConfig(manage_gtd_expiry=True) strategy = Strategy(config) @@ -1207,7 +1210,7 @@ def test_submit_order_list_with_managed_gtd_starts_timer(self): assert strategy.clock.timer_count == 1 assert strategy.clock.timer_names == ["GTD-EXPIRY:O-19700101-0000-000-None-1"] - def test_submit_order_list_with_managed_gtd_when_immediately_filled_cancels_timer(self): + def test_submit_order_list_with_managed_gtd_when_immediately_filled_cancels_timer(self) -> None: # Arrange config = StrategyConfig(manage_gtd_expiry=True) strategy = Strategy(config) @@ -1242,7 +1245,7 @@ def test_submit_order_list_with_managed_gtd_when_immediately_filled_cancels_time assert bracket.orders[1].status == OrderStatus.ACCEPTED assert bracket.orders[2].status == OrderStatus.ACCEPTED - def test_cancel_gtd_expiry(self): + def test_cancel_gtd_expiry(self) -> None: # Arrange config = StrategyConfig(manage_gtd_expiry=True) strategy = Strategy(config) @@ -1272,7 +1275,7 @@ def test_cancel_gtd_expiry(self): # Assert assert strategy.clock.timer_count == 0 - def test_cancel_order(self): + def test_cancel_order(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1307,7 +1310,7 @@ def test_cancel_order(self): assert not strategy.cache.is_order_open(order.client_order_id) assert strategy.cache.is_order_closed(order.client_order_id) - def test_cancel_order_when_pending_cancel_does_not_submit_command(self): + def test_cancel_order_when_pending_cancel_does_not_submit_command(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1341,7 +1344,7 @@ def test_cancel_order_when_pending_cancel_does_not_submit_command(self): assert strategy.cache.is_order_open(order.client_order_id) assert not strategy.cache.is_order_closed(order.client_order_id) - def test_cancel_order_when_closed_does_not_submit_command(self): + def test_cancel_order_when_closed_does_not_submit_command(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1375,7 +1378,7 @@ def test_cancel_order_when_closed_does_not_submit_command(self): assert not strategy.cache.is_order_open(order.client_order_id) assert strategy.cache.is_order_closed(order.client_order_id) - def test_modify_order_when_pending_cancel_does_not_submit_command(self): + def test_modify_order_when_pending_cancel_does_not_submit_command(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1409,7 +1412,7 @@ def test_modify_order_when_pending_cancel_does_not_submit_command(self): # Assert assert self.exec_engine.command_count == 1 - def test_modify_order_when_closed_does_not_submit_command(self): + def test_modify_order_when_closed_does_not_submit_command(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1443,7 +1446,7 @@ def test_modify_order_when_closed_does_not_submit_command(self): # Assert assert self.exec_engine.command_count == 1 - def test_modify_order_when_no_changes_does_not_submit_command(self): + def test_modify_order_when_no_changes_does_not_submit_command(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1474,7 +1477,7 @@ def test_modify_order_when_no_changes_does_not_submit_command(self): # Assert assert self.exec_engine.command_count == 1 - def test_modify_order(self): + def test_modify_order(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1514,7 +1517,7 @@ def test_modify_order(self): assert not strategy.cache.is_order_closed(order.client_order_id) assert strategy.portfolio.is_flat(order.instrument_id) - def test_cancel_orders(self): + def test_cancel_orders(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1552,7 +1555,7 @@ def test_cancel_orders(self): # Assert # TODO: WIP! - def test_cancel_all_orders(self): + def test_cancel_all_orders(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1595,7 +1598,7 @@ def test_cancel_all_orders(self): assert order1 in self.cache.orders_closed() assert order2 in strategy.cache.orders_closed() - def test_close_position_when_position_already_closed_does_nothing(self): + def test_close_position_when_position_already_closed_does_nothing(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1635,7 +1638,7 @@ def test_close_position_when_position_already_closed_does_nothing(self): # Assert assert strategy.portfolio.is_completely_flat() - def test_close_position(self): + def test_close_position(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1670,7 +1673,7 @@ def test_close_position(self): if order.side == OrderSide.SELL: assert order.tags == "EXIT" - def test_close_all_positions(self): + def test_close_all_positions(self) -> None: # Arrange strategy = Strategy() strategy.register( @@ -1681,8 +1684,6 @@ def test_close_all_positions(self): clock=self.clock, logger=self.logger, ) - - # Start strategy and submit orders to open positions strategy.start() order1 = strategy.order_factory.market( @@ -1714,3 +1715,145 @@ def test_close_all_positions(self): for order in orders: if order.side == OrderSide.SELL: assert order.tags == "EXIT" + + @pytest.mark.parametrize( + ("contingency_type"), + [ + ContingencyType.OCO, + ContingencyType.OUO, + ], + ) + def test_managed_contingenies_when_canceled_entry_then_cancels_oto_orders( + self, + contingency_type: ContingencyType, + ) -> None: + # Arrange + config = StrategyConfig( + manage_contingent_orders=True, + manage_gtd_expiry=True, + ) + strategy = Strategy(config=config) + strategy.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + strategy.start() + + bracket = strategy.order_factory.bracket( + USDJPY_SIM.id, + OrderSide.BUY, + Quantity.from_int(100_000), + entry_price=Price.from_str("80.000"), + sl_trigger_price=Price.from_str("90.000"), + tp_price=Price.from_str("90.500"), + entry_order_type=OrderType.LIMIT, + contingency_type=contingency_type, + ) + + strategy.submit_order_list(bracket) + self.exchange.process(0) + + # Act + strategy.cancel_order(bracket.first) + self.exchange.process(0) + + # Assert + assert bracket.orders[0].status == OrderStatus.CANCELED + assert bracket.orders[1].status == OrderStatus.PENDING_CANCEL + assert bracket.orders[2].status == OrderStatus.PENDING_CANCEL + + @pytest.mark.parametrize( + ("contingency_type"), + [ + ContingencyType.OCO, + ContingencyType.OUO, + ], + ) + def test_managed_contingenies_when_canceled_bracket_then_cancels_contingent_order( + self, + contingency_type: ContingencyType, + ) -> None: + # Arrange + config = StrategyConfig( + manage_contingent_orders=True, + manage_gtd_expiry=True, + ) + strategy = Strategy(config=config) + strategy.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + strategy.start() + + bracket = strategy.order_factory.bracket( + USDJPY_SIM.id, + OrderSide.BUY, + Quantity.from_int(100_000), + sl_trigger_price=Price.from_str("90.000"), + tp_price=Price.from_str("90.500"), + entry_order_type=OrderType.MARKET, + contingency_type=contingency_type, + ) + + strategy.submit_order_list(bracket) + self.exchange.process(0) + + # Act + strategy.cancel_order(bracket.orders[1]) + self.exchange.process(0) + + # Assert + assert bracket.orders[0].status == OrderStatus.FILLED + assert bracket.orders[1].status == OrderStatus.CANCELED + assert bracket.orders[2].status == OrderStatus.PENDING_CANCEL + + def test_managed_contingenies_when_modify_bracket_then_modifies_ouo_order( + self, + ) -> None: + # Arrange + config = StrategyConfig( + manage_contingent_orders=True, + manage_gtd_expiry=True, + ) + strategy = Strategy(config=config) + strategy.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + strategy.start() + + bracket = strategy.order_factory.bracket( + USDJPY_SIM.id, + OrderSide.BUY, + Quantity.from_int(100_000), + sl_trigger_price=Price.from_str("90.000"), + tp_price=Price.from_str("90.500"), + entry_order_type=OrderType.MARKET, + contingency_type=ContingencyType.OUO, + ) + + strategy.submit_order_list(bracket) + self.exchange.process(0) + + # Act + new_quantity = Quantity.from_int(50_000) + strategy.modify_order(bracket.orders[1], new_quantity) + self.exchange.process(0) + + # Assert + assert bracket.orders[0].status == OrderStatus.FILLED + assert bracket.orders[1].status == OrderStatus.ACCEPTED + assert bracket.orders[2].status == OrderStatus.PENDING_UPDATE + assert bracket.orders[1].quantity == new_quantity From 15ac1379e65a11314928c39e9e8d2e8f17609369 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 13:16:31 +1100 Subject: [PATCH 42/78] Remove drop when type does not implement --- nautilus_core/common/build.rs | 1 - nautilus_core/core/build.rs | 1 - nautilus_core/model/build.rs | 1 - 3 files changed, 3 deletions(-) diff --git a/nautilus_core/common/build.rs b/nautilus_core/common/build.rs index e166e0d885ea..6ad7f47a6e63 100644 --- a/nautilus_core/common/build.rs +++ b/nautilus_core/common/build.rs @@ -49,7 +49,6 @@ fn main() { let mut data = String::new(); src.read_to_string(&mut data) .expect("invalid UTF-8 in stream"); - drop(src); // Close the file early // Run the replace operation in memory let new_data = data.replace("cdef enum", "cpdef enum"); diff --git a/nautilus_core/core/build.rs b/nautilus_core/core/build.rs index 678614d1059c..e796832f9680 100644 --- a/nautilus_core/core/build.rs +++ b/nautilus_core/core/build.rs @@ -49,7 +49,6 @@ fn main() { let mut data = String::new(); src.read_to_string(&mut data) .expect("invalid UTF-8 in stream"); - drop(src); // Close the file early // Run the replace operation in memory let new_data = data.replace("cdef enum", "cpdef enum"); diff --git a/nautilus_core/model/build.rs b/nautilus_core/model/build.rs index 144e5687f766..35997a9fb56c 100644 --- a/nautilus_core/model/build.rs +++ b/nautilus_core/model/build.rs @@ -49,7 +49,6 @@ fn main() { let mut data = String::new(); src.read_to_string(&mut data) .expect("invalid UTF-8 in stream"); - drop(src); // Close the file early // Run the replace operation in memory let new_data = data.replace("cdef enum", "cpdef enum"); From 75e7dac623d39085ec242d0b7a21110d135f0714 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 13:23:05 +1100 Subject: [PATCH 43/78] Fix clippy lints --- nautilus_core/core/src/python/casing.rs | 1 + nautilus_core/core/src/python/datetime.rs | 7 ++++ nautilus_core/indicators/src/average/mod.rs | 1 + nautilus_core/indicators/src/average/wma.rs | 16 ++++----- nautilus_core/indicators/src/momentum/rsi.rs | 34 +++++++++---------- nautilus_core/network/src/websocket.rs | 4 +-- .../persistence/src/backend/kmerge_batch.rs | 19 +++++------ .../persistence/src/backend/session.rs | 10 +++--- 8 files changed, 50 insertions(+), 42 deletions(-) diff --git a/nautilus_core/core/src/python/casing.rs b/nautilus_core/core/src/python/casing.rs index 62cad36afa19..77c10ab675e7 100644 --- a/nautilus_core/core/src/python/casing.rs +++ b/nautilus_core/core/src/python/casing.rs @@ -16,6 +16,7 @@ use heck::ToSnakeCase; use pyo3::prelude::*; +#[must_use] #[pyfunction(name = "convert_to_snake_case")] pub fn py_convert_to_snake_case(s: String) -> String { s.to_snake_case() diff --git a/nautilus_core/core/src/python/datetime.rs b/nautilus_core/core/src/python/datetime.rs index 843b7282732c..dba1c2077c8c 100644 --- a/nautilus_core/core/src/python/datetime.rs +++ b/nautilus_core/core/src/python/datetime.rs @@ -20,36 +20,43 @@ use crate::datetime::{ secs_to_millis, secs_to_nanos, unix_nanos_to_iso8601, }; +#[must_use] #[pyfunction(name = "secs_to_nanos")] pub fn py_secs_to_nanos(secs: f64) -> u64 { secs_to_nanos(secs) } +#[must_use] #[pyfunction(name = "secs_to_millis")] pub fn py_secs_to_millis(secs: f64) -> u64 { secs_to_millis(secs) } +#[must_use] #[pyfunction(name = "millis_to_nanos")] pub fn py_millis_to_nanos(millis: f64) -> u64 { millis_to_nanos(millis) } +#[must_use] #[pyfunction(name = "micros_to_nanos")] pub fn py_micros_to_nanos(micros: f64) -> u64 { micros_to_nanos(micros) } +#[must_use] #[pyfunction(name = "nanos_to_secs")] pub fn py_nanos_to_secs(nanos: u64) -> f64 { nanos_to_secs(nanos) } +#[must_use] #[pyfunction(name = "nanos_to_millis")] pub fn py_nanos_to_millis(nanos: u64) -> u64 { nanos_to_millis(nanos) } +#[must_use] #[pyfunction(name = "nanos_to_micros")] pub fn py_nanos_to_micros(nanos: u64) -> u64 { nanos_to_micros(nanos) diff --git a/nautilus_core/indicators/src/average/mod.rs b/nautilus_core/indicators/src/average/mod.rs index 8623cc669d84..fc3c3e6a8ef4 100644 --- a/nautilus_core/indicators/src/average/mod.rs +++ b/nautilus_core/indicators/src/average/mod.rs @@ -55,6 +55,7 @@ pub enum MovingAverageType { pub struct MovingAverageFactory; impl MovingAverageFactory { + #[must_use] pub fn create( moving_average_type: MovingAverageType, period: usize, diff --git a/nautilus_core/indicators/src/average/wma.rs b/nautilus_core/indicators/src/average/wma.rs index 4bff6366cad5..03aa1acaf6d9 100644 --- a/nautilus_core/indicators/src/average/wma.rs +++ b/nautilus_core/indicators/src/average/wma.rs @@ -69,11 +69,11 @@ impl WeightedMovingAverage { fn weighted_average(&self) -> f64 { let mut sum = 0.0; let mut weight_sum = 0.0; - let reverse_weights: Vec = self.weights.iter().cloned().rev().collect(); + let reverse_weights: Vec = self.weights.iter().copied().rev().collect(); for (index, input) in self.inputs.iter().rev().enumerate() { let weight = reverse_weights.get(index).unwrap(); sum += input * weight; - weight_sum += weight + weight_sum += weight; } sum / weight_sum } @@ -152,7 +152,7 @@ mod tests { #[rstest] fn test_wma_initialized(indicator_wma_10: WeightedMovingAverage) { - let display_str = format!("{}", indicator_wma_10); + let display_str = format!("{indicator_wma_10}"); assert_eq!( display_str, "WeightedMovingAverage(10,[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])" @@ -196,7 +196,7 @@ mod tests { fn test_value_with_two_inputs(mut indicator_wma_10: WeightedMovingAverage) { indicator_wma_10.update_raw(1.0); indicator_wma_10.update_raw(2.0); - let result = (2.0 * 1.0 + 1.0 * 0.9) / 1.9; + let result = 2.0f64.mul_add(1.0, 1.0 * 0.9) / 1.9; assert_eq!(indicator_wma_10.value, result); } @@ -205,14 +205,14 @@ mod tests { indicator_wma_10.update_raw(1.0); indicator_wma_10.update_raw(2.0); indicator_wma_10.update_raw(3.0); - let result = (3.0 * 1.0 + 2.0 * 0.9 + 1.0 * 0.8) / (1.0 + 0.9 + 0.8); + let result = 1.0f64.mul_add(0.8, 3.0f64.mul_add(1.0, 2.0 * 0.9)) / (1.0 + 0.9 + 0.8); assert_eq!(indicator_wma_10.value, result); } #[rstest] fn test_value_expected_with_exact_period(mut indicator_wma_10: WeightedMovingAverage) { for i in 1..11 { - indicator_wma_10.update_raw(i as f64); + indicator_wma_10.update_raw(f64::from(i)); } assert_eq!(indicator_wma_10.value, 7.0); } @@ -220,9 +220,9 @@ mod tests { #[rstest] fn test_value_expected_with_more_inputs(mut indicator_wma_10: WeightedMovingAverage) { for i in 1..=11 { - indicator_wma_10.update_raw(i as f64); + indicator_wma_10.update_raw(f64::from(i)); } - assert_eq!(indicator_wma_10.value(), 8.0000000000000018); + assert_eq!(indicator_wma_10.value(), 8.000_000_000_000_002); } #[rstest] diff --git a/nautilus_core/indicators/src/momentum/rsi.rs b/nautilus_core/indicators/src/momentum/rsi.rs index 48621c0b76df..0dba6ac89abf 100644 --- a/nautilus_core/indicators/src/momentum/rsi.rs +++ b/nautilus_core/indicators/src/momentum/rsi.rs @@ -104,7 +104,7 @@ impl RelativeStrengthIndex { pub fn update_raw(&mut self, value: f64) { if !self._has_inputs { self._last_value = value; - self._has_inputs = true + self._has_inputs = true; } let gain = value - self._last_value; if gain > 0.0 { @@ -153,40 +153,40 @@ mod tests { #[rstest] fn test_rsi_initialized(rsi_10: RelativeStrengthIndex) { - let display_str = format!("{}", rsi_10); + let display_str = format!("{rsi_10}"); assert_eq!(display_str, "RelativeStrengthIndex(10,EXPONENTIAL)"); assert_eq!(rsi_10.period, 10); - assert_eq!(rsi_10.is_initialized, false) + assert!(!rsi_10.is_initialized); } #[rstest] fn test_initialized_with_required_inputs_returns_true(mut rsi_10: RelativeStrengthIndex) { for i in 0..12 { - rsi_10.update_raw(i as f64); + rsi_10.update_raw(f64::from(i)); } - assert_eq!(rsi_10.is_initialized, true) + assert!(rsi_10.is_initialized); } #[rstest] fn test_value_with_one_input_returns_expected_value(mut rsi_10: RelativeStrengthIndex) { rsi_10.update_raw(1.0); - assert_eq!(rsi_10.value, 1.0) + assert_eq!(rsi_10.value, 1.0); } #[rstest] fn test_value_all_higher_inputs_returns_expected_value(mut rsi_10: RelativeStrengthIndex) { for i in 1..4 { - rsi_10.update_raw(i as f64); + rsi_10.update_raw(f64::from(i)); } - assert_eq!(rsi_10.value, 1.0) + assert_eq!(rsi_10.value, 1.0); } #[rstest] fn test_value_with_all_lower_inputs_returns_expected_value(mut rsi_10: RelativeStrengthIndex) { for i in (1..4).rev() { - rsi_10.update_raw(i as f64); + rsi_10.update_raw(f64::from(i)); } - assert_eq!(rsi_10.value, 0.0) + assert_eq!(rsi_10.value, 0.0); } #[rstest] @@ -198,7 +198,7 @@ mod tests { rsi_10.update_raw(7.0); rsi_10.update_raw(6.0); - assert_eq!(rsi_10.value, 0.6837363325825265) + assert_eq!(rsi_10.value, 0.683_736_332_582_526_5); } #[rstest] @@ -212,7 +212,7 @@ mod tests { rsi_10.update_raw(6.0); rsi_10.update_raw(7.0); - assert_eq!(rsi_10.value, 0.7615344667662725); + assert_eq!(rsi_10.value, 0.761_534_466_766_272_5); } #[rstest] @@ -220,28 +220,28 @@ mod tests { rsi_10.update_raw(1.0); rsi_10.update_raw(2.0); rsi_10.reset(); - assert_eq!(rsi_10.is_initialized(), false); - assert_eq!(rsi_10.count, 0) + assert!(!rsi_10.is_initialized()); + assert_eq!(rsi_10.count, 0); } #[rstest] fn test_handle_quote_tick(mut rsi_10: RelativeStrengthIndex, quote_tick: QuoteTick) { rsi_10.handle_quote_tick("e_tick); assert_eq!(rsi_10.count, 1); - assert_eq!(rsi_10.value, 1.0) + assert_eq!(rsi_10.value, 1.0); } #[rstest] fn test_handle_trade_tick(mut rsi_10: RelativeStrengthIndex, trade_tick: TradeTick) { rsi_10.handle_trade_tick(&trade_tick); assert_eq!(rsi_10.count, 1); - assert_eq!(rsi_10.value, 1.0) + assert_eq!(rsi_10.value, 1.0); } #[rstest] fn test_handle_bar(mut rsi_10: RelativeStrengthIndex, bar_ethusdt_binance_minute_bid: Bar) { rsi_10.handle_bar(&bar_ethusdt_binance_minute_bid); assert_eq!(rsi_10.count, 1); - assert_eq!(rsi_10.value, 1.0) + assert_eq!(rsi_10.value, 1.0); } } diff --git a/nautilus_core/network/src/websocket.rs b/nautilus_core/network/src/websocket.rs index 1b2098ff2745..2c5189320b51 100644 --- a/nautilus_core/network/src/websocket.rs +++ b/nautilus_core/network/src/websocket.rs @@ -121,11 +121,11 @@ impl WebSocketClientInner { let mut request = url.into_client_request()?; let req_headers = request.headers_mut(); - headers.into_iter().for_each(|(key, val)| { + for (key, val) in headers { let header_value = HeaderValue::from_str(&val).unwrap(); let header_name = HeaderName::from_str(&key).unwrap(); req_headers.insert(header_name, header_value); - }); + } connect_async(request).await.map(|resp| resp.0.split()) } diff --git a/nautilus_core/persistence/src/backend/kmerge_batch.rs b/nautilus_core/persistence/src/backend/kmerge_batch.rs index 837c6aa2693a..ff3fe562ff6f 100644 --- a/nautilus_core/persistence/src/backend/kmerge_batch.rs +++ b/nautilus_core/persistence/src/backend/kmerge_batch.rs @@ -46,7 +46,7 @@ impl EagerStream { .await; }); - EagerStream { rx, task, runtime } + Self { rx, task, runtime } } } @@ -85,7 +85,7 @@ where match iter.next() { Some(mut batch) => match batch.next() { Some(item) => { - break Some(ElementBatchIter { item, batch, iter }); + break Some(Self { item, batch, iter }); } None => continue, }, @@ -276,7 +276,7 @@ mod tests { let mut vec: Vec = Arbitrary::arbitrary(g); // Sort the vector - vec.sort(); + vec.sort_unstable(); // Recreate nested Vec structure by splitting the flattened_sorted_vec into sorted chunks let mut nested_sorted_vec = Vec::new(); @@ -292,7 +292,7 @@ mod tests { } // Wrap the sorted nested vector in the SortedNestedVecU64 struct - SortedNestedVec(nested_sorted_vec) + Self(nested_sorted_vec) } // Optionally, implement the `shrink` method if you want to shrink the generated data on test failures @@ -306,18 +306,17 @@ mod tests { let mut kmerge: KMerge<_, u64, _> = KMerge::new(OrdComparator); let copy_data = all_data.clone(); - copy_data.into_iter().for_each(|stream| { - let input = stream.0.into_iter().map(|batch| batch.into_iter()); + for stream in copy_data.into_iter() { + let input = stream.0.into_iter().map(std::iter::IntoIterator::into_iter); kmerge.push_iter(input); - }); + } let merged_data: Vec = kmerge.collect(); let mut sorted_data: Vec = all_data .into_iter() - .map(|stream| stream.0.into_iter().flatten()) - .flatten() + .flat_map(|stream| stream.0.into_iter().flatten()) .collect(); - sorted_data.sort(); + sorted_data.sort_unstable(); merged_data.len() == sorted_data.len() && merged_data.eq(&sorted_data) } diff --git a/nautilus_core/persistence/src/backend/session.rs b/nautilus_core/persistence/src/backend/session.rs index a02542468d1c..214f1d6b00e2 100644 --- a/nautilus_core/persistence/src/backend/session.rs +++ b/nautilus_core/persistence/src/backend/session.rs @@ -95,14 +95,14 @@ impl DataBackendSession { /// Query a file for its records. the caller must specify `T` to indicate /// the kind of data expected from this query. /// - /// table_name: Logical table_name assigned to this file. Queries to this file should address the + /// `table_name`: Logical `table_name` assigned to this file. Queries to this file should address the /// file by its table name. - /// file_path: Path to file - /// sql_query: A custom sql query to retrieve records from file. If no query is provided a default - /// query "SELECT * FROM " is run. + /// `file_path`: Path to file + /// `sql_query`: A custom sql query to retrieve records from file. If no query is provided a default + /// query "SELECT * FROM <`table_name`>" is run. /// /// # Safety - /// The file data must be ordered by the ts_init in ascending order for this + /// The file data must be ordered by the `ts_init` in ascending order for this /// to work correctly. pub fn add_file( &mut self, From a7e785e00c9ce2ad22ea3488a04bc06a83cdf804 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 13:45:56 +1100 Subject: [PATCH 44/78] Standardize exception variables --- nautilus_trader/backtest/node.py | 4 ++-- nautilus_trader/live/data_engine.py | 16 ++++++++-------- nautilus_trader/live/execution_engine.py | 8 ++++---- nautilus_trader/live/risk_engine.py | 8 ++++---- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/nautilus_trader/backtest/node.py b/nautilus_trader/backtest/node.py index 2cf35f37fd61..fcfc08f56869 100644 --- a/nautilus_trader/backtest/node.py +++ b/nautilus_trader/backtest/node.py @@ -136,10 +136,10 @@ def run(self) -> list[BacktestResult]: batch_size_bytes=config.batch_size_bytes, ) results.append(result) - except Exception as ex: + except Exception as e: # Broad catch all prevents a single backtest run from halting # the execution of the other backtests (such as a zero balance exception). - print(f"Error running {config}: {ex}") + print(f"Error running {config}: {e}") return results diff --git a/nautilus_trader/live/data_engine.py b/nautilus_trader/live/data_engine.py index f6f008d6eab1..ed594b907427 100644 --- a/nautilus_trader/live/data_engine.py +++ b/nautilus_trader/live/data_engine.py @@ -384,8 +384,8 @@ async def _run_cmd_queue(self) -> None: self._execute_command(command) except asyncio.CancelledError: self._log.warning("DataCommand message queue canceled.") - except RuntimeError as ex: - self._log.error(f"RuntimeError: {ex}.") + except RuntimeError as e: + self._log.error(f"RuntimeError: {e}.") finally: stopped_msg = "DataCommand message queue stopped" if not self._cmd_queue.empty(): @@ -405,8 +405,8 @@ async def _run_req_queue(self) -> None: self._handle_request(request) except asyncio.CancelledError: self._log.warning("DataRequest message queue canceled.") - except RuntimeError as ex: - self._log.error(f"RuntimeError: {ex}.") + except RuntimeError as e: + self._log.error(f"RuntimeError: {e}.") finally: stopped_msg = "DataRequest message queue stopped" if not self._req_queue.empty(): @@ -426,8 +426,8 @@ async def _run_res_queue(self) -> None: self._handle_response(response) except asyncio.CancelledError: self._log.warning("DataResponse message queue canceled.") - except RuntimeError as ex: - self._log.error(f"RuntimeError: {ex}.") + except RuntimeError as e: + self._log.error(f"RuntimeError: {e}.") finally: stopped_msg = "DataResponse message queue stopped" if not self._res_queue.empty(): @@ -445,8 +445,8 @@ async def _run_data_queue(self) -> None: self._handle_data(data) except asyncio.CancelledError: self._log.warning("Data message queue canceled.") - except RuntimeError as ex: - self._log.error(f"RuntimeError: {ex}.") + except RuntimeError as e: + self._log.error(f"RuntimeError: {e}.") finally: stopped_msg = "Data message queue stopped" if not self._data_queue.empty(): diff --git a/nautilus_trader/live/execution_engine.py b/nautilus_trader/live/execution_engine.py index 6b9d7b0c3dec..3fb572950988 100644 --- a/nautilus_trader/live/execution_engine.py +++ b/nautilus_trader/live/execution_engine.py @@ -363,8 +363,8 @@ async def _run_cmd_queue(self) -> None: self._execute_command(command) except asyncio.CancelledError: self._log.warning("Command message queue canceled.") - except RuntimeError as ex: - self._log.error(f"RuntimeError: {ex}.") + except RuntimeError as e: + self._log.error(f"RuntimeError: {e}.") finally: stopped_msg = "Command message queue stopped" if not self._cmd_queue.empty(): @@ -384,8 +384,8 @@ async def _run_evt_queue(self) -> None: self._handle_event(event) except asyncio.CancelledError: self._log.warning("Event message queue canceled.") - except RuntimeError as ex: - self._log.error(f"RuntimeError: {ex}.") + except RuntimeError as e: + self._log.error(f"RuntimeError: {e}.") finally: stopped_msg = "Event message queue stopped" if not self._evt_queue.empty(): diff --git a/nautilus_trader/live/risk_engine.py b/nautilus_trader/live/risk_engine.py index c76ac529a41c..1f3840390e8c 100644 --- a/nautilus_trader/live/risk_engine.py +++ b/nautilus_trader/live/risk_engine.py @@ -249,8 +249,8 @@ async def _run_cmd_queue(self) -> None: self._execute_command(command) except asyncio.CancelledError: self._log.warning("Command message queue canceled.") - except RuntimeError as ex: - self._log.error(f"RuntimeError: {ex}.") + except RuntimeError as e: + self._log.error(f"RuntimeError: {e}.") finally: stopped_msg = "Command message queue stopped" if not self._cmd_queue.empty(): @@ -270,8 +270,8 @@ async def _run_evt_queue(self) -> None: self._handle_event(event) except asyncio.CancelledError: self._log.warning("Event message queue canceled.") - except RuntimeError as ex: - self._log.error(f"RuntimeError: {ex}.") + except RuntimeError as e: + self._log.error(f"RuntimeError: {e}.") finally: stopped_msg = "Event message queue stopped" if not self._evt_queue.empty(): From 1f59991625715bec7f1b352426a435d0bca55419 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 14:59:03 +1100 Subject: [PATCH 45/78] Refine OrderManager for Strategy and add tests --- nautilus_trader/execution/manager.pxd | 1 + nautilus_trader/execution/manager.pyx | 48 ++- nautilus_trader/trading/strategy.pyx | 5 +- .../execution/test_emulator_list.py | 309 ++++++++++++++++-- 4 files changed, 318 insertions(+), 45 deletions(-) diff --git a/nautilus_trader/execution/manager.pxd b/nautilus_trader/execution/manager.pxd index 2b04bfa40afe..6ec6eaf2bd58 100644 --- a/nautilus_trader/execution/manager.pxd +++ b/nautilus_trader/execution/manager.pxd @@ -67,6 +67,7 @@ cdef class OrderManager: cpdef void cancel_order(self, Order order) cpdef void modify_order_quantity(self, Order order, Quantity new_quantity) cpdef void create_new_submit_order(self, Order order, PositionId position_id=*, ClientId client_id=*) + cpdef bint should_manage_order(self, Order order) # -- EVENT HANDLERS ------------------------------------------------------------------------------- diff --git a/nautilus_trader/execution/manager.pyx b/nautilus_trader/execution/manager.pyx index fe493c9668f0..3896ead82bf9 100644 --- a/nautilus_trader/execution/manager.pyx +++ b/nautilus_trader/execution/manager.pyx @@ -264,6 +264,28 @@ cdef class OrderManager: else: self._submit_order_handler(submit) + cpdef bint should_manage_order(self, Order order): + """ + Check if the given order should be managed. + + Parameters + ---------- + order : Order + The order the check. + + Returns + ------- + bool + True if the order should be managed, else False. + + """ + Condition.not_none(order, "order") + + if self.active_local: + return order.is_active_local_c() + else: + return not order.is_active_local_c() + # -- EVENT HANDLERS ------------------------------------------------------------------------------- cpdef void handle_event(self, Event event): @@ -386,21 +408,21 @@ cdef class OrderManager: if child_order is None: raise RuntimeError(f"Cannot find OTO child order for {repr(client_order_id)}") # pragma: no cover + if not self.should_manage_order(child_order): + continue # Not being managed + if self.debug: self._log.info(f"Processing OTO child order {child_order}.", LogColor.MAGENTA) self._log.info(f"{parent_filled_qty=}.", LogColor.MAGENTA) - if self.active_local and 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.modify_order_quantity(child_order, parent_filled_qty) - if (self.active_local and not child_order.is_active_local_c()) or self._submit_order_handler is None: - return # Order does not need to be released + if self._submit_order_handler is None: + return # No handler to submit if not child_order.client_order_id in self._submit_order_commands: self.create_new_submit_order( @@ -418,8 +440,10 @@ cdef class OrderManager: if self.debug: self._log.info(f"Processing OCO contingent order {contingent_order}.", LogColor.MAGENTA) + if not self.should_manage_order(contingent_order): + continue # Not being managed if contingent_order.is_closed_c(): - continue + continue # Already completed if contingent_order.client_order_id != order.client_order_id: self.cancel_order(contingent_order) elif order.contingency_type == ContingencyType.OUO: @@ -453,9 +477,11 @@ cdef class OrderManager: 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 not self.should_manage_order(contingent_order): + continue # Not being managed if client_order_id == order.client_order_id: continue # Already being handled - if contingent_order.is_closed_c() or (self.active_local and not contingent_order.is_active_local_c()): + if contingent_order.is_closed_c(): self._submit_order_commands.pop(order.client_order_id, None) continue # Already completed @@ -505,10 +531,14 @@ cdef class OrderManager: 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 contingent_order is None: + raise RuntimeError(f"Cannot find OCO contingent order for {repr(client_order_id)}") # pragma: no cover + + if not self.should_manage_order(contingent_order): + continue # Not being managed if client_order_id == order.client_order_id: continue # Already being handled # pragma: no cover - if contingent_order.is_closed_c() or (self.active_local and not contingent_order.is_active_local_c()): + if contingent_order.is_closed_c(): continue # Already completed # pragma: no cover if order.contingency_type == ContingencyType.OTO: diff --git a/nautilus_trader/trading/strategy.pyx b/nautilus_trader/trading/strategy.pyx index d64fc50726c1..b9ff728e2e05 100644 --- a/nautilus_trader/trading/strategy.pyx +++ b/nautilus_trader/trading/strategy.pyx @@ -1528,10 +1528,7 @@ cdef class Strategy(Actor): return if self.manage_contingent_orders and self._manager is not None: - if isinstance(event, OrderEvent): - order = self.cache.order(event.client_order_id) - if order is not None and not order.is_active_local_c(): - self._manager.handle_event(event) + self._manager.handle_event(event) try: # Send to specific event handler diff --git a/tests/unit_tests/execution/test_emulator_list.py b/tests/unit_tests/execution/test_emulator_list.py index 2a70ef98f71e..74f132e54707 100644 --- a/tests/unit_tests/execution/test_emulator_list.py +++ b/tests/unit_tests/execution/test_emulator_list.py @@ -29,6 +29,7 @@ from nautilus_trader.config import ExecEngineConfig from nautilus_trader.config import RiskEngineConfig from nautilus_trader.config.common import OrderEmulatorConfig +from nautilus_trader.config.common import StrategyConfig from nautilus_trader.data.engine import DataEngine from nautilus_trader.execution.emulator import OrderEmulator from nautilus_trader.execution.engine import ExecutionEngine @@ -64,7 +65,7 @@ class TestOrderEmulatorWithOrderLists: - def setup(self): + def setup(self) -> None: # Fixture Setup self.clock = TestClock() self.logger = Logger( @@ -150,6 +151,7 @@ def setup(self): cache=self.cache, clock=self.clock, logger=self.logger, + support_contingent_orders=False, ) self.exec_client = BacktestExecClient( @@ -183,7 +185,7 @@ def setup(self): self.emulator.start() self.strategy.start() - def test_submit_stop_order_bulk_then_emulates(self): + def test_submit_stop_order_bulk_then_emulates(self) -> None: # Arrange stop1 = self.strategy.order_factory.stop_market( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -226,7 +228,7 @@ def test_submit_stop_order_bulk_then_emulates(self): assert stop2 in self.emulator.get_matching_core(ETHUSDT_PERP_BINANCE.id).get_orders() assert stop3 in self.emulator.get_matching_core(ETHUSDT_PERP_BINANCE.id).get_orders() - def test_submit_bracket_order_with_limit_entry_then_emulates_sl_tp(self): + def test_submit_bracket_order_with_limit_entry_then_emulates_sl_tp(self) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -252,7 +254,7 @@ def test_submit_bracket_order_with_limit_entry_then_emulates_sl_tp(self): bracket.first, ] - def test_submit_bracket_order_with_stop_limit_entry_then_emulates_sl_tp(self): + def test_submit_bracket_order_with_stop_limit_entry_then_emulates_sl_tp(self) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -280,7 +282,9 @@ def test_submit_bracket_order_with_stop_limit_entry_then_emulates_sl_tp(self): bracket.first, ] - def test_submit_bracket_order_with_market_entry_immediately_submits_then_emulates_sl_tp(self): + def test_submit_bracket_order_with_market_entry_immediately_submits_then_emulates_sl_tp( + self, + ) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -302,7 +306,7 @@ def test_submit_bracket_order_with_market_entry_immediately_submits_then_emulate assert self.emulator.get_matching_core(ETHUSDT_PERP_BINANCE.id) is None assert self.exec_engine.command_count == 1 - def test_submit_bracket_when_entry_filled_then_emulates_sl_and_tp(self): + def test_submit_bracket_when_entry_filled_then_emulates_sl_and_tp(self) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -346,7 +350,7 @@ def test_submit_bracket_when_entry_filled_then_emulates_sl_and_tp(self): assert bracket.orders[2].position_id == position_id assert self.exec_engine.command_count == 1 - def test_modify_emulated_sl_quantity_also_updates_tp(self): + def test_modify_emulated_sl_quantity_also_updates_tp(self) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -397,7 +401,7 @@ def test_modify_emulated_sl_quantity_also_updates_tp(self): assert bracket.orders[1].position_id == position_id assert bracket.orders[2].position_id == position_id - def test_modify_emulated_tp_price(self): + def test_modify_emulated_tp_price(self) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -447,7 +451,7 @@ def test_modify_emulated_tp_price(self): assert bracket.orders[1].position_id == position_id assert bracket.orders[2].position_id == position_id - def test_submit_bracket_when_stop_limit_entry_filled_then_emulates_sl_and_tp(self): + def test_submit_bracket_when_stop_limit_entry_filled_then_emulates_sl_and_tp(self) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -502,8 +506,8 @@ def test_submit_bracket_when_stop_limit_entry_filled_then_emulates_sl_and_tp(sel ) def test_rejected_oto_entry_cancels_contingencies( self, - contingency_type, - ): + contingency_type: ContingencyType, + ) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -549,8 +553,8 @@ def test_rejected_oto_entry_cancels_contingencies( ) def test_cancel_bracket( self, - contingency_type, - ): + contingency_type: ContingencyType, + ) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -594,8 +598,8 @@ def test_cancel_bracket( ) def test_cancel_oto_entry_cancels_contingencies( self, - contingency_type, - ): + contingency_type: ContingencyType, + ) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -639,8 +643,8 @@ def test_cancel_oto_entry_cancels_contingencies( ) def test_expired_oto_entry_then_cancels_contingencies( self, - contingency_type, - ): + contingency_type: ContingencyType, + ) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -686,8 +690,8 @@ def test_expired_oto_entry_then_cancels_contingencies( ) def test_update_oto_entry_updates_quantity_of_contingencies( self, - contingency_type, - ): + contingency_type: ContingencyType, + ) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -733,8 +737,8 @@ def test_update_oto_entry_updates_quantity_of_contingencies( ) def test_triggered_sl_submits_market_order( self, - contingency_type, - ): + contingency_type: ContingencyType, + ) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -796,8 +800,8 @@ def test_triggered_sl_submits_market_order( ) def test_triggered_stop_limit_tp_submits_limit_order( self, - contingency_type, - ): + contingency_type: ContingencyType, + ) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -868,8 +872,8 @@ def test_triggered_stop_limit_tp_submits_limit_order( ) def test_triggered_then_filled_tp_cancels_sl( self, - contingency_type, - ): + contingency_type: ContingencyType, + ) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -939,7 +943,7 @@ def test_triggered_then_filled_tp_cancels_sl( assert not matching_core.order_exists(sl_order.client_order_id) assert not matching_core.order_exists(tp_order.client_order_id) - def test_triggered_then_partially_filled_oco_sl_cancels_tp(self): + def test_triggered_then_partially_filled_oco_sl_cancels_tp(self) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -1012,7 +1016,7 @@ def test_triggered_then_partially_filled_oco_sl_cancels_tp(self): assert not matching_core.order_exists(sl_order.client_order_id) assert not matching_core.order_exists(tp_order.client_order_id) - def test_triggered_then_partially_filled_ouo_sl_updated_tp(self): + def test_triggered_then_partially_filled_ouo_sl_updated_tp(self) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -1090,7 +1094,9 @@ def test_triggered_then_partially_filled_ouo_sl_updated_tp(self): assert not matching_core.order_exists(sl_order.client_order_id) assert matching_core.order_exists(tp_order.client_order_id) - def test_released_order_with_quote_quantity_sets_contingent_orders_to_base_quantity(self): + def test_released_order_with_quote_quantity_sets_contingent_orders_to_base_quantity( + self, + ) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -1138,7 +1144,7 @@ def test_released_order_with_quote_quantity_sets_contingent_orders_to_base_quant assert sl_order.leaves_qty == ETHUSDT_PERP_BINANCE.make_qty(0.002) assert tp_order.leaves_qty == ETHUSDT_PERP_BINANCE.make_qty(0.002) - def test_restart_emulator_with_emulated_parent(self): + def test_restart_emulator_with_emulated_parent(self) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -1172,7 +1178,7 @@ def test_restart_emulator_with_emulated_parent(self): assert bracket.orders[1].status == OrderStatus.INITIALIZED assert bracket.orders[2].status == OrderStatus.INITIALIZED - def test_restart_emulator_with_partially_filled_parent(self): + def test_restart_emulator_with_partially_filled_parent(self) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -1213,7 +1219,7 @@ def test_restart_emulator_with_partially_filled_parent(self): assert bracket.orders[1].status == OrderStatus.EMULATED assert bracket.orders[2].status == OrderStatus.EMULATED - def test_restart_emulator_then_cancel_bracket(self): + def test_restart_emulator_then_cancel_bracket(self) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -1245,7 +1251,7 @@ def test_restart_emulator_then_cancel_bracket(self): assert bracket.orders[1].status == OrderStatus.CANCELED assert bracket.orders[2].status == OrderStatus.CANCELED - def test_restart_emulator_with_closed_parent_position(self): + def test_restart_emulator_with_closed_parent_position(self) -> None: # Arrange bracket = self.strategy.order_factory.bracket( instrument_id=ETHUSDT_PERP_BINANCE.id, @@ -1296,3 +1302,242 @@ def test_restart_emulator_with_closed_parent_position(self): assert closing_order.status == OrderStatus.FILLED assert bracket.orders[1].status == OrderStatus.CANCELED assert bracket.orders[2].status == OrderStatus.CANCELED + + def test_managed_contingent_orders_with_canceled_bracket(self) -> None: + # Arrange + bracket = self.strategy.order_factory.bracket( + instrument_id=ETHUSDT_PERP_BINANCE.id, + order_side=OrderSide.BUY, + quantity=ETHUSDT_PERP_BINANCE.make_qty(10), + sl_trigger_price=ETHUSDT_PERP_BINANCE.make_price(4900.0), + tp_order_type=OrderType.LIMIT_IF_TOUCHED, + tp_price=ETHUSDT_PERP_BINANCE.make_price(5150.0), + tp_trigger_price=ETHUSDT_PERP_BINANCE.make_price(5100.0), + emulation_trigger=TriggerType.BID_ASK, + contingency_type=ContingencyType.OUO, + ) + + config = StrategyConfig( + manage_contingent_orders=True, + manage_gtd_expiry=True, + ) + strategy = Strategy(config=config) + strategy.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + strategy.start() + + # Prepare market + tick = TestDataStubs.quote_tick( + instrument=ETHUSDT_PERP_BINANCE, + bid_price=5000.0, + ask_price=5000.0, + ) + + self.data_engine.process(tick) + self.exchange.process_quote_tick(tick) + self.exchange.process(0) + + # Submit order + strategy.submit_order_list( + order_list=bracket, + position_id=PositionId("P-001"), + ) + self.exchange.process(0) + + tick = TestDataStubs.quote_tick( + instrument=ETHUSDT_PERP_BINANCE, + bid_price=5100.0, + ask_price=5100.0, + ) + + self.data_engine.process(tick) + self.exchange.process_quote_tick(tick) + self.exchange.process(0) + + # Act + tp_order = self.cache.order(bracket.orders[2].client_order_id) + strategy.cancel_order(tp_order) + self.exchange.process(0) + + # Assert + matching_core = self.emulator.get_matching_core(ETHUSDT_PERP_BINANCE.id) + entry_order = self.cache.order(bracket.orders[0].client_order_id) + sl_order = self.cache.order(bracket.orders[1].client_order_id) + tp_order = self.cache.order(bracket.orders[2].client_order_id) + assert self.exec_engine.command_count == 3 + assert len(self.emulator.get_submit_order_commands()) == 1 + assert self.cache.orders_emulated_count() == 0 + assert entry_order.status == OrderStatus.FILLED + assert sl_order.status == OrderStatus.CANCELED + assert tp_order.status == OrderStatus.CANCELED + assert sl_order.quantity == Quantity.from_int(10) + assert tp_order.quantity == Quantity.from_int(10) + 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) + + def test_managed_contingent_orders_with_modified_open_order(self) -> None: + # Arrange + bracket = self.strategy.order_factory.bracket( + instrument_id=ETHUSDT_PERP_BINANCE.id, + order_side=OrderSide.BUY, + quantity=ETHUSDT_PERP_BINANCE.make_qty(10), + sl_trigger_price=ETHUSDT_PERP_BINANCE.make_price(4900.0), + tp_order_type=OrderType.LIMIT_IF_TOUCHED, + tp_price=ETHUSDT_PERP_BINANCE.make_price(5150.0), + tp_trigger_price=ETHUSDT_PERP_BINANCE.make_price(5100.0), + emulation_trigger=TriggerType.BID_ASK, + contingency_type=ContingencyType.OUO, + ) + + config = StrategyConfig( + manage_contingent_orders=True, + manage_gtd_expiry=True, + ) + strategy = Strategy(config=config) + strategy.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + strategy.start() + + # Prepare market + tick = TestDataStubs.quote_tick( + instrument=ETHUSDT_PERP_BINANCE, + bid_price=5000.0, + ask_price=5000.0, + ) + + self.data_engine.process(tick) + self.exchange.process_quote_tick(tick) + self.exchange.process(0) + + # Submit order + strategy.submit_order_list( + order_list=bracket, + position_id=PositionId("P-001"), + ) + self.exchange.process(0) + + tick = TestDataStubs.quote_tick( + instrument=ETHUSDT_PERP_BINANCE, + bid_price=5100.0, + ask_price=5100.0, + ) + + self.data_engine.process(tick) + self.exchange.process_quote_tick(tick) + self.exchange.process(0) + + # Act + new_quantity = Quantity.from_int(5) + tp_order = self.cache.order(bracket.orders[2].client_order_id) + strategy.modify_order(tp_order, new_quantity) + self.exchange.process(0) + + # Assert + matching_core = self.emulator.get_matching_core(ETHUSDT_PERP_BINANCE.id) + entry_order = self.cache.order(bracket.orders[0].client_order_id) + sl_order = self.cache.order(bracket.orders[1].client_order_id) + tp_order = self.cache.order(bracket.orders[2].client_order_id) + assert self.exec_engine.command_count == 3 + assert len(self.emulator.get_submit_order_commands()) == 2 + assert self.cache.orders_emulated_count() == 1 + assert entry_order.status == OrderStatus.FILLED + assert sl_order.status == OrderStatus.EMULATED + assert tp_order.status == OrderStatus.ACCEPTED + assert sl_order.quantity == new_quantity + assert tp_order.quantity == new_quantity + assert not matching_core.order_exists(entry_order.client_order_id) + assert matching_core.order_exists(sl_order.client_order_id) + assert not matching_core.order_exists(tp_order.client_order_id) + + def test_managed_contingent_orders_with_modified_emulated_order(self) -> None: + # Arrange + bracket = self.strategy.order_factory.bracket( + instrument_id=ETHUSDT_PERP_BINANCE.id, + order_side=OrderSide.BUY, + quantity=ETHUSDT_PERP_BINANCE.make_qty(10), + sl_trigger_price=ETHUSDT_PERP_BINANCE.make_price(4900.0), + tp_order_type=OrderType.LIMIT_IF_TOUCHED, + tp_price=ETHUSDT_PERP_BINANCE.make_price(5150.0), + tp_trigger_price=ETHUSDT_PERP_BINANCE.make_price(5100.0), + emulation_trigger=TriggerType.BID_ASK, + contingency_type=ContingencyType.OUO, + ) + + config = StrategyConfig( + manage_contingent_orders=True, + manage_gtd_expiry=True, + ) + strategy = Strategy(config=config) + strategy.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + strategy.start() + + # Prepare market + tick = TestDataStubs.quote_tick( + instrument=ETHUSDT_PERP_BINANCE, + bid_price=5000.0, + ask_price=5000.0, + ) + + self.data_engine.process(tick) + self.exchange.process_quote_tick(tick) + self.exchange.process(0) + + # Submit order + strategy.submit_order_list( + order_list=bracket, + position_id=PositionId("P-001"), + ) + self.exchange.process(0) + + tick = TestDataStubs.quote_tick( + instrument=ETHUSDT_PERP_BINANCE, + bid_price=5100.0, + ask_price=5100.0, + ) + + self.data_engine.process(tick) + self.exchange.process_quote_tick(tick) + self.exchange.process(0) + + # Act + new_quantity = Quantity.from_int(5) + sl_order = self.cache.order(bracket.orders[1].client_order_id) + strategy.modify_order(sl_order, new_quantity) + self.exchange.process(0) + + # Assert + matching_core = self.emulator.get_matching_core(ETHUSDT_PERP_BINANCE.id) + entry_order = self.cache.order(bracket.orders[0].client_order_id) + sl_order = self.cache.order(bracket.orders[1].client_order_id) + tp_order = self.cache.order(bracket.orders[2].client_order_id) + assert self.exec_engine.command_count == 3 + assert len(self.emulator.get_submit_order_commands()) == 2 + assert self.cache.orders_emulated_count() == 1 + assert entry_order.status == OrderStatus.FILLED + assert sl_order.status == OrderStatus.EMULATED + assert tp_order.status == OrderStatus.ACCEPTED + assert sl_order.quantity == new_quantity + assert tp_order.quantity == new_quantity + assert not matching_core.order_exists(entry_order.client_order_id) + assert matching_core.order_exists(sl_order.client_order_id) + assert not matching_core.order_exists(tp_order.client_order_id) From fe7b8a585824799e82718b158b6ae7c41ffdd2c8 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 16:09:41 +1100 Subject: [PATCH 46/78] Update docs --- docs/concepts/orders.md | 25 ++++ docs/concepts/strategies.md | 171 +++++++++++++++++++------- nautilus_trader/execution/manager.pyx | 2 +- 3 files changed, 153 insertions(+), 45 deletions(-) diff --git a/docs/concepts/orders.md b/docs/concepts/orders.md index bcf7a3bf419d..b5c6d2bbb27e 100644 --- a/docs/concepts/orders.md +++ b/docs/concepts/orders.md @@ -34,6 +34,31 @@ is not available, then the system will not submit the order and an error will be a clear explanatory message. ``` +### Terminology + +- An order is **aggressive** if the type is `MARKET` or if its executing like a `MARKET` order (taking liquidity). +- An order is **passive** if the type is not `MARKET` (providing liquidity). +- An order is **active local** if it is still within the local system boundary in one of the following three (non-terminal) status: + - `INITIALIZED` + - `EMULATED` + - `RELEASED` +- An order is **in-flight** when in one of the following status: + - `SUBMITTED` + - `PENDING_UPDATE` + - `PENDING_CANCEL` +- An order is **open** when in one of the following (non-terminal) status: + - `ACCEPTED` + - `TRIGGERED` + - `PENDING_UPDATE` + - `PENDING_CANCEL` + - `PARTIALLY_FILLED` +- An order is **closed** when in one of the following (terminal) status: + - `DENIED` + - `REJECTED` + - `CANCELED` + - `EXPIRED` + - `FILLED` + ## Execution Instructions Certain exchanges allow a trader to specify conditions and restrictions on diff --git a/docs/concepts/strategies.md b/docs/concepts/strategies.md index 6a8ce60e0e77..8833ab9392bc 100644 --- a/docs/concepts/strategies.md +++ b/docs/concepts/strategies.md @@ -219,21 +219,22 @@ Strategies have access to a comprehensive `Clock` which provides a number of met different timestamps, as well as setting time alerts or timers. ```{note} -See the `Clock` [API reference](../api_reference/common.md#Clock) for a complete list of available methods. +See the `Clock` [API reference](../api_reference/common.md) for a complete list of available methods. ``` #### Current timestamps While there are multiple ways to obtain current timestamps, here are two commonly used methods as examples: -**UTC Timestamp:** This method returns a timezone-aware (UTC) timestamp: +To get the current UTC timestamp as a tz-aware `pd.Timestamp`: ```python import pandas as pd + now: pd.Timestamp = self.clock.utc_now() ``` -**Unix Nanoseconds:** This method provides the current timestamp in nanoseconds since the UNIX epoch: +To get the current UTC timestamp as nanoseconds since the UNIX epoch: ```python unix_nanos: int = self.clock.timestamp_ns() ``` @@ -338,7 +339,7 @@ metrics and statistics. Refer to the `PortfolioAnalyzer` in the [API Reference](../api_reference/analysis.md) for a complete description of all available methods. -```{tip} +```{note} Also see the [Porfolio statistics](../concepts/advanced/portfolio_statistics.md) guide. ``` @@ -349,19 +350,21 @@ tailored for algorithmic trading. These commands are essential for executing str and ensuring seamless interaction with various trading venues. In the following sections, we will delve into the specifics of each command and its use cases. +```{tip} +The [Execution](../concepts/execution.md) guide explains the flow through the system, and can be helpful to read in conjunction with the below. +``` + #### Submitting orders An `OrderFactory` is provided on the base class for every `Strategy` as a convenience, reducing the amount of boilerplate required to create different `Order` objects (although these objects can still be initialized directly with the `Order.__init__(...)` constructor if the trader prefers). -The component an order flows to when submitted for execution depends on the following: - -- If an `emulation_trigger` is specified, the order will _firstly_ be sent to the `OrderEmulator` -- If an `exec_algorithm_id` is specified (with no `emulation_trigger`), the order will _firstly_ be sent to the relevant `ExecAlgorithm` (assuming it exists and has been registered correctly) -- Otherwise, the order will _firstly_ be sent to the `RiskEngine` +The component a `SubmitOrder` or `SubmitOrderList` command will flow to for execution depends on the following: -The following examples show method implementations for a `Strategy`. +- If an `emulation_trigger` is specified, the command will _firstly_ be sent to the `OrderEmulator` +- If an `exec_algorithm_id` is specified (with no `emulation_trigger`), the command will _firstly_ be sent to the relevant `ExecAlgorithm` +- Otherwise, the command will _firstly_ be sent to the `RiskEngine` This example submits a `LIMIT` BUY order for emulation (see [OrderEmulator](advanced/emulated_orders.md)): ```python @@ -369,19 +372,20 @@ from nautilus_trader.model.enums import OrderSide from nautilus_trader.model.enums import TriggerType from nautilus_trader.model.orders import LimitOrder - def buy(self) -> None: - """ - Users simple buy method (example). - """ - order: LimitOrder = self.order_factory.limit( - instrument_id=self.instrument_id, - order_side=OrderSide.BUY, - quantity=self.instrument.make_qty(self.trade_size), - price=self.instrument.make_price(5000.00), - emulation_trigger=TriggerType.LAST_TRADE, - ) - self.submit_order(order) +def buy(self) -> None: + """ + Users simple buy method (example). + """ + order: LimitOrder = self.order_factory.limit( + instrument_id=self.instrument_id, + order_side=OrderSide.BUY, + quantity=self.instrument.make_qty(self.trade_size), + price=self.instrument.make_price(5000.00), + emulation_trigger=TriggerType.LAST_TRADE, + ) + + self.submit_order(order) ``` ```{note} @@ -394,34 +398,100 @@ from nautilus_trader.model.enums import OrderSide from nautilus_trader.model.enums import TimeInForce from nautilus_trader.model.identifiers import ExecAlgorithmId - def buy(self) -> None: - """ - Users simple buy method (example). - """ - order: MarketOrder = self.order_factory.market( - instrument_id=self.instrument_id, - order_side=OrderSide.BUY, - quantity=self.instrument.make_qty(self.trade_size), - time_in_force=TimeInForce.FOK, - exec_algorithm_id=ExecAlgorithmId("TWAP"), - exec_algorithm_params={"horizon_secs": 20, "interval_secs": 2.5}, - ) - self.submit_order(order) +def buy(self) -> None: + """ + Users simple buy method (example). + """ + order: MarketOrder = self.order_factory.market( + instrument_id=self.instrument_id, + order_side=OrderSide.BUY, + quantity=self.instrument.make_qty(self.trade_size), + time_in_force=TimeInForce.FOK, + exec_algorithm_id=ExecAlgorithmId("TWAP"), + exec_algorithm_params={"horizon_secs": 20, "interval_secs": 2.5}, + ) + + self.submit_order(order) ``` -#### Managed GTD expiry +#### Canceling orders -It's possible for the strategy to manage expiry for orders with a time in force of GTD (_Good 'till Date_). -This may be desirable if the exchange/broker does not support this time in force option, or for any -reason you prefer the strategy to manage this. +Orders can be canceled individually, as a batch, or all orders for an instrument (with an optional side filter). -To use this option, pass `manage_gtd_expiry=True` to your `StrategyConfig`. When an order is submitted with -a time in force of GTD, the strategy will automatically start an internal time alert. -Once the internal GTD time alert is reached, the order will be canceled (if not already closed). +If the order is already *closed* or already pending cancel, then a warning will be logged. -Some venues (such as Binance Futures) support the GTD time in force, so to avoid conflicts when using -`managed_gtd_expiry` you should set `use_gtd=False` for your execution client config. +If the order is currently *open* then the status will become `PENDING_CANCEL`. + +The component a `CancelOrder`, `CancelAllOrders` or `BatchCancelOrders` command will flow to for execution depends on the following: + +- If the order is currently emulated, the command will _firstly_ be sent to the `OrderEmulator` +- If an `exec_algorithm_id` is specified (with no `emulation_trigger`), and the order is still active within the local system, the command will _firstly_ be sent to the relevant `ExecAlgorithm` +- Otherwise, the order will _firstly_ be sent to the `ExecutionEngine` + +```{note} +Any managed GTD timer will also be canceled after the command has left the strategy. +``` + +The following shows how to cancel an individual order: +```python + +self.cancel_order(order) + +``` + +The following shows how to cancel a batch of orders: +```python +from nautilus_trader.model import Order + + +my_order_list: list[Order] = [order1, order2, order3] +self.cancel_orders(my_order_list) + +``` + +The following shows how to cancel all orders: + +```python + +self.cancel_all_orders() + +``` + +#### Modifying orders + +Orders can be modified individually when emulated, or *open* on a venue (if supported). + +If the order is already *closed* or already pending cancel, then a warning will be logged. +If the order is currently *open* then the status will become `PENDING_UPDATE`. + +```{warning} +At least one value must differ from the original order for the command to be valid. +``` + +The component a `ModifyOrder` command will flow to for execution depends on the following: + +- If the order is currently emulated, the command will _firstly_ be sent to the `OrderEmulator` +- Otherwise, the order will _firstly_ be sent to the `RiskEngine` + +```{note} +Once an order is under the control of an execution algorithm, it cannot be directly modified by a strategy (only canceled). +``` + +The following shows how to modify the size of `LIMIT` BUY order currently *open* on a venue: +```python +from nautilus_trader.model import Quantity + + +new_quantity: Quantity = Quantity.from_int(5) +self.modify_order(order, new_quantity) + +``` + +```{note} +The price and trigger price can also be modified (when emulated or supported by a venue). + +``` ## Configuration @@ -482,6 +552,19 @@ Even though it often makes sense to define a strategy which will trade a single instrument. The number of instruments a single strategy can work with is only limited by machine resources. ``` +### Managed GTD expiry + +It's possible for the strategy to manage expiry for orders with a time in force of GTD (_Good 'till Date_). +This may be desirable if the exchange/broker does not support this time in force option, or for any +reason you prefer the strategy to manage this. + +To use this option, pass `manage_gtd_expiry=True` to your `StrategyConfig`. When an order is submitted with +a time in force of GTD, the strategy will automatically start an internal time alert. +Once the internal GTD time alert is reached, the order will be canceled (if not already *closed*). + +Some venues (such as Binance Futures) support the GTD time in force, so to avoid conflicts when using +`managed_gtd_expiry` you should set `use_gtd=False` for your execution client config. + ### Multiple strategies If you intend running multiple instances of the same strategy, with different diff --git a/nautilus_trader/execution/manager.pyx b/nautilus_trader/execution/manager.pyx index 3896ead82bf9..da6902e2840c 100644 --- a/nautilus_trader/execution/manager.pyx +++ b/nautilus_trader/execution/manager.pyx @@ -271,7 +271,7 @@ cdef class OrderManager: Parameters ---------- order : Order - The order the check. + The order to check. Returns ------- From 509767b2021dc690f277d1b1fd34d31c4e440fdd Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 20:28:46 +1100 Subject: [PATCH 47/78] Use is to compare types --- nautilus_trader/core/correctness.pxd | 2 +- .../adapters/betfair/test_betfair_factory.py | 4 ++-- tests/unit_tests/model/test_objects_price.py | 4 ++-- tests/unit_tests/model/test_objects_price_pyo3.py | 4 ++-- tests/unit_tests/model/test_objects_quantity.py | 4 ++-- tests/unit_tests/model/test_objects_quantity_pyo3.py | 4 ++-- tests/unit_tests/risk/test_engine.py | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/nautilus_trader/core/correctness.pxd b/nautilus_trader/core/correctness.pxd index 2e41bbbe1536..c4be67031ad4 100644 --- a/nautilus_trader/core/correctness.pxd +++ b/nautilus_trader/core/correctness.pxd @@ -17,7 +17,7 @@ from libc.stdint cimport int64_t cdef inline Exception make_exception(ex_default, ex_type, str msg): - if type(ex_type) == type(Exception): + if type(ex_type) is type(Exception): return ex_type(msg) else: return ex_default(msg) diff --git a/tests/integration_tests/adapters/betfair/test_betfair_factory.py b/tests/integration_tests/adapters/betfair/test_betfair_factory.py index dd35f5c22a5f..9db87343cc6e 100644 --- a/tests/integration_tests/adapters/betfair/test_betfair_factory.py +++ b/tests/integration_tests/adapters/betfair/test_betfair_factory.py @@ -96,5 +96,5 @@ def test_create(self): ) # Assert - assert BetfairDataClient == type(data_client) - assert BetfairExecutionClient == type(exec_client) + assert BetfairDataClient is type(data_client) + assert BetfairExecutionClient is type(exec_client) diff --git a/tests/unit_tests/model/test_objects_price.py b/tests/unit_tests/model/test_objects_price.py index 408ab91c02a0..11556cc213bd 100644 --- a/tests/unit_tests/model/test_objects_price.py +++ b/tests/unit_tests/model/test_objects_price.py @@ -451,7 +451,7 @@ def test_floor_division_with_various_types_returns_expected_result( result = value1 // value2 # Assert - assert type(result) == expected_type + assert type(result) is expected_type assert result == expected_value @pytest.mark.parametrize( @@ -478,7 +478,7 @@ def test_mod_with_various_types_returns_expected_result( result = value1 % value2 # Assert - assert type(result) == expected_type + assert type(result) is expected_type assert result == expected_value @pytest.mark.parametrize( diff --git a/tests/unit_tests/model/test_objects_price_pyo3.py b/tests/unit_tests/model/test_objects_price_pyo3.py index f06421488bbc..a5873c6978fb 100644 --- a/tests/unit_tests/model/test_objects_price_pyo3.py +++ b/tests/unit_tests/model/test_objects_price_pyo3.py @@ -451,7 +451,7 @@ def test_floor_division_with_various_types_returns_expected_result( result = value1 // value2 # Assert - assert type(result) == expected_type + assert type(result) is expected_type assert result == expected_value @pytest.mark.parametrize( @@ -478,7 +478,7 @@ def test_mod_with_various_types_returns_expected_result( result = value1 % value2 # Assert - assert type(result) == expected_type + assert type(result) is expected_type assert result == expected_value @pytest.mark.parametrize( diff --git a/tests/unit_tests/model/test_objects_quantity.py b/tests/unit_tests/model/test_objects_quantity.py index b0b124a61c28..adde72c78164 100644 --- a/tests/unit_tests/model/test_objects_quantity.py +++ b/tests/unit_tests/model/test_objects_quantity.py @@ -410,7 +410,7 @@ def test_floor_division_with_various_types_returns_expected_result( result = value1 // value2 # Assert - assert type(result) == expected_type + assert type(result) is expected_type assert result == expected_value @pytest.mark.parametrize( @@ -436,7 +436,7 @@ def test_mod_with_various_types_returns_expected_result( result = value1 % value2 # Assert - assert type(result) == expected_type + assert type(result) is expected_type assert result == expected_value @pytest.mark.parametrize( diff --git a/tests/unit_tests/model/test_objects_quantity_pyo3.py b/tests/unit_tests/model/test_objects_quantity_pyo3.py index d70428b77520..542fff6b8abd 100644 --- a/tests/unit_tests/model/test_objects_quantity_pyo3.py +++ b/tests/unit_tests/model/test_objects_quantity_pyo3.py @@ -630,7 +630,7 @@ def test_floor_division_with_various_types_returns_expected_result( result = value1 // value2 # Assert - assert type(result) == expected_type + assert type(result) is expected_type assert result == expected_value @pytest.mark.parametrize( @@ -692,7 +692,7 @@ def test_mod_with_various_types_returns_expected_result( result = value1 % value2 # Assert - assert type(result) == expected_type + assert type(result) is expected_type assert result == expected_value @pytest.mark.parametrize( diff --git a/tests/unit_tests/risk/test_engine.py b/tests/unit_tests/risk/test_engine.py index e807fbb33ee3..3a637769c34f 100644 --- a/tests/unit_tests/risk/test_engine.py +++ b/tests/unit_tests/risk/test_engine.py @@ -214,7 +214,7 @@ def test_set_trading_state_changes_value_and_publishes_event(self): self.risk_engine.set_trading_state(TradingState.HALTED) # Assert - assert type(handler[0]) == TradingStateChanged + assert type(handler[0]) is TradingStateChanged assert self.risk_engine.trading_state == TradingState.HALTED def test_max_order_submit_rate_when_no_risk_config_returns_100_per_second(self): From 40905b2f5b08cb2e508ca4c03a04817dae2e2161 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 20:43:44 +1100 Subject: [PATCH 48/78] Avoid assertion on boolean constant --- nautilus_core/persistence/tests/test_catalog.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nautilus_core/persistence/tests/test_catalog.rs b/nautilus_core/persistence/tests/test_catalog.rs index d5235fd9aae2..e6355c1df8a8 100644 --- a/nautilus_core/persistence/tests/test_catalog.rs +++ b/nautilus_core/persistence/tests/test_catalog.rs @@ -82,10 +82,10 @@ fn test_quote_tick_query() { let query_result: QueryResult = catalog.get_query_result(); let ticks: Vec = query_result.collect(); - if let Data::Quote(q) = &ticks[0] { + if let Data::Quote(q) = ticks[0] { assert_eq!("EUR/USD.SIM", q.instrument_id.to_string()); } else { - assert!(false); + panic!("Invalid test"); } assert_eq!(ticks.len(), expected_length); @@ -128,10 +128,10 @@ fn test_trade_tick_query() { let query_result: QueryResult = catalog.get_query_result(); let ticks: Vec = query_result.collect(); - if let Data::Trade(t) = &ticks[0] { + if let Data::Trade(t) = ticks[0] { assert_eq!("EUR/USD.SIM", t.instrument_id.to_string()); } else { - assert!(false); + panic!("Invalid test"); } assert_eq!(ticks.len(), expected_length); @@ -150,7 +150,7 @@ fn test_bar_query() { if let Data::Bar(b) = &ticks[0] { assert_eq!("ADABTC.BINANCE", b.bar_type.instrument_id.to_string()); } else { - assert!(false); + panic!("Invalid test"); } assert_eq!(ticks.len(), expected_length); From a760332ff6d2f6b46080851fa0aca36601fa9428 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 20:57:45 +1100 Subject: [PATCH 49/78] Simplify dictionary iteration --- .../adapters/interactive_brokers/historic/async_actor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_trader/adapters/interactive_brokers/historic/async_actor.py b/nautilus_trader/adapters/interactive_brokers/historic/async_actor.py index 86515d7e91cf..fcc5dfd4217a 100644 --- a/nautilus_trader/adapters/interactive_brokers/historic/async_actor.py +++ b/nautilus_trader/adapters/interactive_brokers/historic/async_actor.py @@ -63,7 +63,7 @@ async def _on_start(self): def _finish_response(self, request_id: UUID4): super()._finish_response(request_id) - if request_id in self._pending_async_requests.keys(): + if request_id in self._pending_async_requests: self._pending_async_requests[request_id].set() async def await_request(self, request_id: UUID4, timeout: int = 30): From 591da92f3e116e80f02e458e49f9d25436cf56eb Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 21:02:51 +1100 Subject: [PATCH 50/78] Simplify if statement --- .../adapters/interactive_brokers/execution.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/nautilus_trader/adapters/interactive_brokers/execution.py b/nautilus_trader/adapters/interactive_brokers/execution.py index bfa5ed5ba905..c4d9cb9f92bb 100644 --- a/nautilus_trader/adapters/interactive_brokers/execution.py +++ b/nautilus_trader/adapters/interactive_brokers/execution.py @@ -773,13 +773,11 @@ def _on_open_order(self, order_ref: str, order: IBOrder, order_state: IBOrderSta trigger_price = ( None if order.auxPrice == UNSET_DOUBLE else instrument.make_price(order.auxPrice) ) - if ( + venue_order_id_modified = bool( nautilus_order.venue_order_id is None - or nautilus_order.venue_order_id != VenueOrderId(str(order.orderId)) - ): - venue_order_id_modified = True - else: - venue_order_id_modified = False + or nautilus_order.venue_order_id != VenueOrderId(str(order.orderId)), + ) + if total_qty != nautilus_order.quantity or price or trigger_price: self.generate_order_updated( strategy_id=nautilus_order.strategy_id, From b9600c8e3f69f4422bc8586226868271c064ac85 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 21:12:39 +1100 Subject: [PATCH 51/78] Refine condition checks --- .../interactive_brokers/parsing/instruments.py | 2 +- nautilus_trader/live/execution_engine.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py index caeb4091787d..6e53402f8009 100644 --- a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py +++ b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py @@ -122,7 +122,7 @@ def parse_instrument( security_type = contract_details.contract.secType if security_type == "STK": return parse_equity_contract(details=contract_details) - elif security_type == "FUT" or security_type == "CONTFUT": + elif security_type in ("FUT", "CONTFUT"): return parse_futures_contract(details=contract_details) elif security_type == "OPT": return parse_options_contract(details=contract_details) diff --git a/nautilus_trader/live/execution_engine.py b/nautilus_trader/live/execution_engine.py index 3fb572950988..870ab3bcea19 100644 --- a/nautilus_trader/live/execution_engine.py +++ b/nautilus_trader/live/execution_engine.py @@ -622,7 +622,7 @@ def _reconcile_order_report( # noqa (too complex) return True # Reconciled # Order must have been accepted from this point - if order.status == OrderStatus.INITIALIZED or order.status == OrderStatus.SUBMITTED: + if order.status in (OrderStatus.INITIALIZED, OrderStatus.SUBMITTED): self._generate_order_accepted(order, report) # Update order quantity and price differences @@ -781,10 +781,10 @@ def _generate_inferred_fill( ) -> OrderFilled: # Infer liquidity side liquidity_side: LiquiditySide = LiquiditySide.NO_LIQUIDITY_SIDE - if ( - order.order_type == OrderType.MARKET - or order.order_type == OrderType.STOP_MARKET - or order.order_type == OrderType.TRAILING_STOP_MARKET + if order.order_type in ( + OrderType.MARKET, + OrderType.STOP_MARKET, + OrderType.TRAILING_STOP_MARKET, ): liquidity_side = LiquiditySide.TAKER elif report.post_only: From cbb4e8239a900d46837765b73ab323df6755234e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 29 Oct 2023 21:22:00 +1100 Subject: [PATCH 52/78] Remove unnecessary lambda --- .../adapters/interactive_brokers/parsing/execution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_trader/adapters/interactive_brokers/parsing/execution.py b/nautilus_trader/adapters/interactive_brokers/parsing/execution.py index ca43441a5dbf..fc1fb547978e 100644 --- a/nautilus_trader/adapters/interactive_brokers/parsing/execution.py +++ b/nautilus_trader/adapters/interactive_brokers/parsing/execution.py @@ -72,7 +72,7 @@ ("client_order_id", "orderRef", lambda x: x.value), ("display_qty", "displaySize", lambda x: x.as_double()), ("expire_time", "goodTillDate", lambda x: x.strftime("%Y%m%d %H:%M:%S %Z")), - ("limit_offset", "lmtPriceOffset", lambda x: float(x)), + ("limit_offset", "lmtPriceOffset", float), ("order_type", "orderType", lambda x: map_order_type[x]), ("price", "lmtPrice", lambda x: x.as_double()), ("quantity", "totalQuantity", lambda x: x.as_decimal()), From 2875c16efdc8fc8d3263c03d31267b43c39bf85d Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Sun, 29 Oct 2023 21:46:35 +0100 Subject: [PATCH 53/78] Add CurrencyPair in rust with pyo3 (#1313) --- .../model/src/instruments/currency_pair.rs | 89 +++++++++--- .../src/python/instruments/currency_pair.rs | 133 ++++++++++++++++++ .../model/src/python/instruments/mod.rs | 1 + nautilus_trader/core/nautilus_pyo3.pyi | 2 +- nautilus_trader/test_kit/rust/instruments.py | 23 +++ .../instruments/test_crypto_future_pyo3.py | 2 +- .../instruments/test_crypto_perpetual_pyo3.py | 2 +- .../instruments/test_currency_pair_pyo3.py | 54 +++++++ 8 files changed, 287 insertions(+), 19 deletions(-) create mode 100644 nautilus_core/model/src/python/instruments/currency_pair.rs create mode 100644 tests/unit_tests/model/instruments/test_currency_pair_pyo3.py diff --git a/nautilus_core/model/src/instruments/currency_pair.rs b/nautilus_core/model/src/instruments/currency_pair.rs index 00f93f0346ee..277d651df41e 100644 --- a/nautilus_core/model/src/instruments/currency_pair.rs +++ b/nautilus_core/model/src/instruments/currency_pair.rs @@ -17,6 +17,7 @@ use std::hash::{Hash, Hasher}; +use anyhow::Result; use pyo3::prelude::*; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -43,19 +44,18 @@ pub struct CurrencyPair { pub size_precision: u8, pub price_increment: Price, pub size_increment: Quantity, + pub margin_init: Decimal, + pub margin_maint: Decimal, + pub maker_fee: Decimal, + pub taker_fee: Decimal, pub lot_size: Option, pub max_quantity: Option, pub min_quantity: Option, pub max_price: Option, pub min_price: Option, - pub margin_init: Decimal, - pub margin_maint: Decimal, - pub maker_fee: Decimal, - pub taker_fee: Decimal, } impl CurrencyPair { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( id: InstrumentId, @@ -66,17 +66,17 @@ impl CurrencyPair { size_precision: u8, price_increment: Price, size_increment: Quantity, + margin_init: Decimal, + margin_maint: Decimal, + maker_fee: Decimal, + taker_fee: Decimal, lot_size: Option, max_quantity: Option, min_quantity: Option, max_price: Option, min_price: Option, - margin_init: Decimal, - margin_maint: Decimal, - maker_fee: Decimal, - taker_fee: Decimal, - ) -> Self { - Self { + ) -> Result { + Ok(Self { id, raw_symbol, quote_currency, @@ -85,16 +85,16 @@ impl CurrencyPair { size_precision, price_increment, size_increment, + margin_init, + margin_maint, + maker_fee, + taker_fee, lot_size, max_quantity, min_quantity, max_price, min_price, - margin_init, - margin_maint, - maker_fee, - taker_fee, - } + }) } } @@ -202,3 +202,60 @@ impl Instrument for CurrencyPair { self.taker_fee } } + +//////////////////////////////////////////////////////////////////////////////// +// Stubs +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +pub mod stubs { + use std::str::FromStr; + + use rstest::fixture; + use rust_decimal::Decimal; + + use crate::{ + identifiers::{instrument_id::InstrumentId, symbol::Symbol}, + instruments::currency_pair::CurrencyPair, + types::{currency::Currency, price::Price, quantity::Quantity}, + }; + + #[fixture] + pub fn currency_pair_btcusdt() -> CurrencyPair { + CurrencyPair::new( + InstrumentId::from("BTCUSDT.BINANCE"), + Symbol::from("BTCUSDT"), + Currency::from("BTC"), + Currency::from("USDT"), + 2, + 6, + Price::from("0.01"), + Quantity::from("0.000001"), + Decimal::from_str("0.0").unwrap(), + Decimal::from_str("0.0").unwrap(), + Decimal::from_str("0.001").unwrap(), + Decimal::from_str("0.001").unwrap(), + None, + Some(Quantity::from("9000")), + Some(Quantity::from("0.000001")), + Some(Price::from("1000000")), + Some(Price::from("0.01")), + ) + .unwrap() + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +/////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use rstest::rstest; + + use crate::instruments::currency_pair::{stubs::currency_pair_btcusdt, CurrencyPair}; + + #[rstest] + fn test_equality(currency_pair_btcusdt: CurrencyPair) { + let cloned = currency_pair_btcusdt.clone(); + assert_eq!(currency_pair_btcusdt, cloned) + } +} diff --git a/nautilus_core/model/src/python/instruments/currency_pair.rs b/nautilus_core/model/src/python/instruments/currency_pair.rs new file mode 100644 index 000000000000..bdb0abf778db --- /dev/null +++ b/nautilus_core/model/src/python/instruments/currency_pair.rs @@ -0,0 +1,133 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use nautilus_core::python::{serialization::from_dict_pyo3, to_pyvalue_err}; +use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; +use rust_decimal::{prelude::ToPrimitive, Decimal}; + +use crate::{ + identifiers::{instrument_id::InstrumentId, symbol::Symbol}, + instruments::currency_pair::CurrencyPair, + types::{currency::Currency, price::Price, quantity::Quantity}, +}; + +#[pymethods] +impl CurrencyPair { + #[allow(clippy::too_many_arguments)] + #[new] + fn py_new( + id: InstrumentId, + raw_symbol: Symbol, + base_currency: Currency, + quote_currency: Currency, + price_precision: u8, + size_precision: u8, + price_increment: Price, + size_increment: Quantity, + margin_init: Decimal, + margin_maint: Decimal, + maker_fee: Decimal, + taker_fee: Decimal, + lot_size: Option, + max_quantity: Option, + min_quantity: Option, + max_price: Option, + min_price: Option, + ) -> PyResult { + Self::new( + id, + raw_symbol, + base_currency, + quote_currency, + price_precision, + size_precision, + price_increment, + size_increment, + margin_init, + margin_maint, + maker_fee, + taker_fee, + lot_size, + max_quantity, + min_quantity, + max_price, + min_price, + ) + .map_err(to_pyvalue_err) + } + + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + _ => panic!("Not implemented"), + } + } + + fn __hash__(&self) -> isize { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() as isize + } + + #[staticmethod] + #[pyo3(name = "from_dict")] + fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { + from_dict_pyo3(py, values) + } + + #[pyo3(name = "to_dict")] + fn py_to_dict(&self, py: Python<'_>) -> PyResult { + let dict = PyDict::new(py); + dict.set_item("type", stringify!(CurrencyPair))?; + dict.set_item("id", self.id.to_string())?; + dict.set_item("raw_symbol", self.raw_symbol.to_string())?; + dict.set_item("base_currency", self.base_currency.code.to_string())?; + dict.set_item("quote_currency", self.quote_currency.code.to_string())?; + dict.set_item("price_precision", self.price_precision)?; + dict.set_item("size_precision", self.size_precision)?; + dict.set_item("price_increment", self.price_increment.to_string())?; + dict.set_item("size_increment", self.size_increment.to_string())?; + dict.set_item("margin_init", self.margin_init.to_f64())?; + dict.set_item("margin_maint", self.margin_maint.to_f64())?; + dict.set_item("maker_fee", self.margin_init.to_f64())?; + dict.set_item("taker_fee", self.margin_init.to_f64())?; + match self.lot_size { + Some(value) => dict.set_item("lot_size", value.to_string())?, + None => dict.set_item("lot_size", py.None())?, + } + match self.max_quantity { + Some(value) => dict.set_item("max_quantity", value.to_string())?, + None => dict.set_item("max_quantity", py.None())?, + } + match self.min_quantity { + Some(value) => dict.set_item("min_quantity", value.to_string())?, + None => dict.set_item("min_quantity", py.None())?, + } + match self.max_price { + Some(value) => dict.set_item("max_price", value.to_string())?, + None => dict.set_item("max_price", py.None())?, + } + match self.min_price { + Some(value) => dict.set_item("min_price", value.to_string())?, + None => dict.set_item("min_price", py.None())?, + } + Ok(dict.into()) + } +} diff --git a/nautilus_core/model/src/python/instruments/mod.rs b/nautilus_core/model/src/python/instruments/mod.rs index 02118ec9c5b5..2b84ce624d5e 100644 --- a/nautilus_core/model/src/python/instruments/mod.rs +++ b/nautilus_core/model/src/python/instruments/mod.rs @@ -15,3 +15,4 @@ pub mod crypto_future; pub mod crypto_perpetual; +pub mod currency_pair; diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 1d6d53327bbe..1e71733bd546 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -590,7 +590,7 @@ class Quantity: class CryptoFuture: ... class CryptoPerpetual: ... -class CurrenyPair: ... +class CurrencyPair: ... class Equity: ... class FuturesContract: ... class OptionsContract: ... diff --git a/nautilus_trader/test_kit/rust/instruments.py b/nautilus_trader/test_kit/rust/instruments.py index 47280fe7c32d..448a75aa025e 100644 --- a/nautilus_trader/test_kit/rust/instruments.py +++ b/nautilus_trader/test_kit/rust/instruments.py @@ -20,6 +20,7 @@ from nautilus_trader.core.nautilus_pyo3 import CryptoFuture from nautilus_trader.core.nautilus_pyo3 import CryptoPerpetual +from nautilus_trader.core.nautilus_pyo3 import CurrencyPair from nautilus_trader.core.nautilus_pyo3 import InstrumentId from nautilus_trader.core.nautilus_pyo3 import Money from nautilus_trader.core.nautilus_pyo3 import Price @@ -83,3 +84,25 @@ def btcusdt_future_binance(expiry: pd.Timestamp | None = None) -> CryptoFuture: Price.from_str("1000000.0"), Price.from_str("0.01"), ) + + @staticmethod + def btcusdt_binance() -> CurrencyPair: + return CurrencyPair( # type: ignore + InstrumentId.from_str("BTCUSDT.BINANCE"), + Symbol("BTCUSDT"), + TestTypesProviderPyo3.currency_btc(), + TestTypesProviderPyo3.currency_usdt(), + 2, + 6, + Price.from_str("0.01"), + Quantity.from_str("0.000001"), + 0.0, + 0.0, + 0.001, + 0.001, + None, + Quantity.from_str("9000"), + Quantity.from_str("0.00001"), + Price.from_str("1000000"), + Price.from_str("0.01"), + ) diff --git a/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py b/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py index 5ef25dccb9ff..25bd6e5605d4 100644 --- a/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py +++ b/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py @@ -20,7 +20,7 @@ crypto_future_btcusdt = TestInstrumentProviderPyo3.btcusdt_future_binance() -class TestCryptoFuture: +class TestCryptoFuturePyo3: def test_equality(self): item_1 = TestInstrumentProviderPyo3.btcusdt_future_binance() item_2 = TestInstrumentProviderPyo3.btcusdt_future_binance() diff --git a/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py b/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py index 47db75f75d9a..ee0186b6f30b 100644 --- a/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py +++ b/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py @@ -20,7 +20,7 @@ crypto_perpetual_ethusdt_perp = TestInstrumentProviderPyo3.ethusdt_perp_binance() -class TestCryptoPerpetual: +class TestCryptoPerpetualPyo3: def test_equality(self): item_1 = TestInstrumentProviderPyo3.ethusdt_perp_binance() item_2 = TestInstrumentProviderPyo3.ethusdt_perp_binance() diff --git a/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py b/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py new file mode 100644 index 000000000000..510863987c56 --- /dev/null +++ b/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py @@ -0,0 +1,54 @@ +# ------------------------------------------------------------------------------------------------- +# 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.core.nautilus_pyo3 import CurrencyPair +from nautilus_trader.test_kit.rust.instruments import TestInstrumentProviderPyo3 + + +btcusdt_binance = TestInstrumentProviderPyo3.btcusdt_binance() + + +class TestCurrencyPairPyo3: + def test_equality(self): + item_1 = TestInstrumentProviderPyo3.btcusdt_binance() + item_2 = TestInstrumentProviderPyo3.btcusdt_binance() + assert item_1 == item_2 + + def test_hash(self): + assert hash(btcusdt_binance) == hash(btcusdt_binance) + + def test_to_dict(self): + dict = btcusdt_binance.to_dict() + assert CurrencyPair.from_dict(dict) == btcusdt_binance + assert dict == { + "type": "CurrencyPair", + "id": "BTCUSDT.BINANCE", + "raw_symbol": "BTCUSDT", + "base_currency": "BTC", + "quote_currency": "USDT", + "price_precision": 2, + "size_precision": 6, + "price_increment": "0.01", + "size_increment": "0.000001", + "margin_maint": 0.0, + "margin_init": 0.0, + "maker_fee": 0.0, + "taker_fee": 0.0, + "lot_size": None, + "max_quantity": "9000", + "min_quantity": "0.00001", + "min_price": "0.01", + "max_price": "1000000", + } From ae94ecb663cd044a80ad982302fb649d61428032 Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Mon, 30 Oct 2023 01:31:37 +0100 Subject: [PATCH 54/78] Add OptionsContract in rust with pyo3 (#1314) --- .../model/src/instruments/options_contract.rs | 88 +++++++++-- .../model/src/python/instruments/mod.rs | 1 + .../python/instruments/options_contract.rs | 143 ++++++++++++++++++ nautilus_trader/test_kit/rust/instruments.py | 26 ++++ nautilus_trader/test_kit/rust/types.py | 4 + .../instruments/test_crypto_future_pyo3.py | 73 ++++----- .../instruments/test_crypto_perpetual_pyo3.py | 71 ++++----- .../instruments/test_currency_pair_pyo3.py | 65 ++++---- .../instruments/test_options_contract_pyo3.py | 57 +++++++ 9 files changed, 413 insertions(+), 115 deletions(-) create mode 100644 nautilus_core/model/src/python/instruments/options_contract.rs create mode 100644 tests/unit_tests/model/instruments/test_options_contract_pyo3.py diff --git a/nautilus_core/model/src/instruments/options_contract.rs b/nautilus_core/model/src/instruments/options_contract.rs index 0769bae68b15..47e98ced969c 100644 --- a/nautilus_core/model/src/instruments/options_contract.rs +++ b/nautilus_core/model/src/instruments/options_contract.rs @@ -17,6 +17,7 @@ use std::hash::{Hash, Hasher}; +use anyhow::Result; use nautilus_core::time::UnixNanos; use pyo3::prelude::*; use rust_decimal::Decimal; @@ -46,19 +47,18 @@ pub struct OptionsContract { pub currency: Currency, pub price_precision: u8, pub price_increment: Price, + pub margin_init: Decimal, + pub margin_maint: Decimal, + pub maker_fee: Decimal, + pub taker_fee: Decimal, pub lot_size: Option, pub max_quantity: Option, pub min_quantity: Option, pub max_price: Option, pub min_price: Option, - pub margin_init: Decimal, - pub margin_maint: Decimal, - pub maker_fee: Decimal, - pub taker_fee: Decimal, } impl OptionsContract { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( id: InstrumentId, @@ -71,17 +71,17 @@ impl OptionsContract { currency: Currency, price_precision: u8, price_increment: Price, + margin_init: Decimal, + margin_maint: Decimal, + maker_fee: Decimal, + taker_fee: Decimal, lot_size: Option, max_quantity: Option, min_quantity: Option, max_price: Option, min_price: Option, - margin_init: Decimal, - margin_maint: Decimal, - maker_fee: Decimal, - taker_fee: Decimal, - ) -> Self { - Self { + ) -> Result { + Ok(Self { id, raw_symbol, asset_class, @@ -101,7 +101,7 @@ impl OptionsContract { margin_maint, maker_fee, taker_fee, - } + }) } } @@ -208,3 +208,67 @@ impl Instrument for OptionsContract { self.taker_fee } } + +//////////////////////////////////////////////////////////////////////////////// +// Stubs +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +pub mod stubs { + use std::str::FromStr; + + use chrono::{TimeZone, Utc}; + use nautilus_core::time::UnixNanos; + use rstest::fixture; + use rust_decimal::Decimal; + + use crate::{ + enums::{AssetClass, OptionKind}, + identifiers::{instrument_id::InstrumentId, symbol::Symbol}, + instruments::options_contract::OptionsContract, + types::{currency::Currency, price::Price, quantity::Quantity}, + }; + + #[fixture] + pub fn options_contract_appl() -> OptionsContract { + let expiration = Utc.with_ymd_and_hms(2021, 12, 17, 0, 0, 0).unwrap(); + OptionsContract::new( + InstrumentId::from("AAPL211217C00150000.OPRA"), + Symbol::from("AAPL211217C00150000"), + AssetClass::Equity, + String::from("AAPL"), + OptionKind::Call, + expiration.timestamp_nanos_opt().unwrap() as UnixNanos, + Price::from("149.0"), + Currency::USD(), + 2, + Price::from("0.01"), + Decimal::from_str("0.0").unwrap(), + Decimal::from_str("0.0").unwrap(), + Decimal::from_str("0.001").unwrap(), + Decimal::from_str("0.001").unwrap(), + Some(Quantity::from("1.0")), + None, + None, + None, + None, + ) + .unwrap() + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::stubs::*; + use crate::instruments::options_contract::OptionsContract; + + #[rstest] + fn test_equality(options_contract_appl: OptionsContract) { + let options_contract_appl2 = options_contract_appl.clone(); + assert_eq!(options_contract_appl, options_contract_appl2); + } +} diff --git a/nautilus_core/model/src/python/instruments/mod.rs b/nautilus_core/model/src/python/instruments/mod.rs index 2b84ce624d5e..ac4762d736f3 100644 --- a/nautilus_core/model/src/python/instruments/mod.rs +++ b/nautilus_core/model/src/python/instruments/mod.rs @@ -16,3 +16,4 @@ pub mod crypto_future; pub mod crypto_perpetual; pub mod currency_pair; +pub mod options_contract; diff --git a/nautilus_core/model/src/python/instruments/options_contract.rs b/nautilus_core/model/src/python/instruments/options_contract.rs new file mode 100644 index 000000000000..856f9c895775 --- /dev/null +++ b/nautilus_core/model/src/python/instruments/options_contract.rs @@ -0,0 +1,143 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use nautilus_core::{ + python::{serialization::from_dict_pyo3, to_pyvalue_err}, + time::UnixNanos, +}; +use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; +use rust_decimal::{prelude::ToPrimitive, Decimal}; + +use crate::{ + enums::{AssetClass, OptionKind}, + identifiers::{instrument_id::InstrumentId, symbol::Symbol}, + instruments::options_contract::OptionsContract, + types::{currency::Currency, price::Price, quantity::Quantity}, +}; + +#[pymethods] +impl OptionsContract { + #[allow(clippy::too_many_arguments)] + #[new] + fn py_new( + id: InstrumentId, + raw_symbol: Symbol, + asset_class: AssetClass, + underlying: String, + option_kind: OptionKind, + expiration: UnixNanos, + strike_price: Price, + currency: Currency, + price_precision: u8, + price_increment: Price, + margin_init: Decimal, + margin_maint: Decimal, + maker_fee: Decimal, + taker_fee: Decimal, + lot_size: Option, + max_quantity: Option, + min_quantity: Option, + max_price: Option, + min_price: Option, + ) -> PyResult { + Self::new( + id, + raw_symbol, + asset_class, + underlying, + option_kind, + expiration, + strike_price, + currency, + price_precision, + price_increment, + margin_init, + margin_maint, + maker_fee, + taker_fee, + lot_size, + max_quantity, + min_quantity, + max_price, + min_price, + ) + .map_err(to_pyvalue_err) + } + + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + _ => panic!("Not implemented"), + } + } + + fn __hash__(&self) -> isize { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() as isize + } + + #[staticmethod] + #[pyo3(name = "from_dict")] + fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { + from_dict_pyo3(py, values) + } + + #[pyo3(name = "to_dict")] + fn py_to_dict(&self, py: Python<'_>) -> PyResult { + let dict = PyDict::new(py); + dict.set_item("type", stringify!(OptionsContract))?; + dict.set_item("id", self.id.to_string())?; + dict.set_item("raw_symbol", self.raw_symbol.to_string())?; + dict.set_item("asset_class", self.asset_class.to_string())?; + dict.set_item("underlying", self.underlying.to_string())?; + dict.set_item("option_kind", self.option_kind.to_string())?; + dict.set_item("expiration", self.expiration.to_i64())?; + dict.set_item("strike_price", self.strike_price.to_string())?; + dict.set_item("currency", self.currency.code.to_string())?; + dict.set_item("price_precision", self.price_precision)?; + dict.set_item("price_increment", self.price_increment.to_string())?; + dict.set_item("margin_init", self.margin_init.to_f64())?; + dict.set_item("margin_maint", self.margin_maint.to_f64())?; + dict.set_item("maker_fee", self.maker_fee.to_f64())?; + dict.set_item("taker_fee", self.taker_fee.to_f64())?; + match self.lot_size { + Some(value) => dict.set_item("lot_size", value.to_string())?, + None => dict.set_item("lot_size", py.None())?, + } + match self.max_quantity { + Some(value) => dict.set_item("max_quantity", value.to_string())?, + None => dict.set_item("max_quantity", py.None())?, + } + match self.min_quantity { + Some(value) => dict.set_item("min_quantity", value.to_string())?, + None => dict.set_item("min_quantity", py.None())?, + } + match self.max_price { + Some(value) => dict.set_item("max_price", value.to_string())?, + None => dict.set_item("max_price", py.None())?, + } + match self.min_price { + Some(value) => dict.set_item("min_price", value.to_string())?, + None => dict.set_item("min_price", py.None())?, + } + Ok(dict.into()) + } +} diff --git a/nautilus_trader/test_kit/rust/instruments.py b/nautilus_trader/test_kit/rust/instruments.py index 448a75aa025e..6da320512df7 100644 --- a/nautilus_trader/test_kit/rust/instruments.py +++ b/nautilus_trader/test_kit/rust/instruments.py @@ -18,11 +18,14 @@ import pandas as pd import pytz +from nautilus_trader.core.nautilus_pyo3 import AssetClass from nautilus_trader.core.nautilus_pyo3 import CryptoFuture from nautilus_trader.core.nautilus_pyo3 import CryptoPerpetual from nautilus_trader.core.nautilus_pyo3 import CurrencyPair from nautilus_trader.core.nautilus_pyo3 import InstrumentId from nautilus_trader.core.nautilus_pyo3 import Money +from nautilus_trader.core.nautilus_pyo3 import OptionKind +from nautilus_trader.core.nautilus_pyo3 import OptionsContract from nautilus_trader.core.nautilus_pyo3 import Price from nautilus_trader.core.nautilus_pyo3 import Quantity from nautilus_trader.core.nautilus_pyo3 import Symbol @@ -106,3 +109,26 @@ def btcusdt_binance() -> CurrencyPair: Price.from_str("1000000"), Price.from_str("0.01"), ) + + @staticmethod + def appl_option(expiry: pd.Timestamp | None = None) -> OptionsContract: + if expiry is None: + expiry = pd.Timestamp(datetime(2021, 12, 17), tz=pytz.UTC) + nanos_expiry = int(expiry.timestamp() * 1e9) + return OptionsContract( # type: ignore + InstrumentId.from_str("AAPL211217C00150000.OPRA"), + Symbol("AAPL211217C00150000"), + AssetClass.EQUITY, + "AAPL", + OptionKind.CALL, + nanos_expiry, + Price.from_str("149.0"), + TestTypesProviderPyo3.currency_usdt(), + 2, + Price.from_str("0.01"), + 0.0, + 0.0, + 0.001, + 0.001, + Quantity.from_str("1.0"), + ) diff --git a/nautilus_trader/test_kit/rust/types.py b/nautilus_trader/test_kit/rust/types.py index d91ccb9d28a3..b06a251a4f34 100644 --- a/nautilus_trader/test_kit/rust/types.py +++ b/nautilus_trader/test_kit/rust/types.py @@ -25,6 +25,10 @@ def currency_btc() -> Currency: def currency_usdt() -> Currency: return Currency.from_str("USDT") + @staticmethod + def currency_usd() -> Currency: + return Currency.from_str("USD") + @staticmethod def currency_aud() -> Currency: return Currency.from_str("AUD") diff --git a/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py b/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py index 25bd6e5605d4..cf45f56fb748 100644 --- a/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py +++ b/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py @@ -20,39 +20,40 @@ crypto_future_btcusdt = TestInstrumentProviderPyo3.btcusdt_future_binance() -class TestCryptoFuturePyo3: - def test_equality(self): - item_1 = TestInstrumentProviderPyo3.btcusdt_future_binance() - item_2 = TestInstrumentProviderPyo3.btcusdt_future_binance() - assert item_1 == item_2 - - def test_hash(self): - assert hash(crypto_future_btcusdt) == hash(crypto_future_btcusdt) - - def test_to_dict(self): - result = crypto_future_btcusdt.to_dict() - assert CryptoFuture.from_dict(result) == crypto_future_btcusdt - assert result == { - "type": "CryptoPerpetual", - "id": "BTCUSDT_220325.BINANCE", - "raw_symbol": "BTCUSDT", - "underlying": "BTC", - "quote_currency": "USDT", - "settlement_currency": "USDT", - "expiration": 1648166400000000000, - "price_precision": 2, - "size_precision": 6, - "price_increment": "0.01", - "size_increment": "0.000001", - "margin_maint": 0.0, - "margin_init": 0.0, - "maker_fee": 0.0, - "taker_fee": 0.0, - "lot_size": None, - "max_notional": None, - "max_price": "1000000.0", - "max_quantity": "9000", - "min_notional": "10.00000000 USDT", - "min_price": "0.01", - "min_quantity": "0.00001", - } +def test_equality(): + item_1 = TestInstrumentProviderPyo3.btcusdt_future_binance() + item_2 = TestInstrumentProviderPyo3.btcusdt_future_binance() + assert item_1 == item_2 + + +def test_hash(): + assert hash(crypto_future_btcusdt) == hash(crypto_future_btcusdt) + + +def test_to_dict(): + result = crypto_future_btcusdt.to_dict() + assert CryptoFuture.from_dict(result) == crypto_future_btcusdt + assert result == { + "type": "CryptoPerpetual", + "id": "BTCUSDT_220325.BINANCE", + "raw_symbol": "BTCUSDT", + "underlying": "BTC", + "quote_currency": "USDT", + "settlement_currency": "USDT", + "expiration": 1648166400000000000, + "price_precision": 2, + "size_precision": 6, + "price_increment": "0.01", + "size_increment": "0.000001", + "margin_maint": 0.0, + "margin_init": 0.0, + "maker_fee": 0.0, + "taker_fee": 0.0, + "lot_size": None, + "max_notional": None, + "max_price": "1000000.0", + "max_quantity": "9000", + "min_notional": "10.00000000 USDT", + "min_price": "0.01", + "min_quantity": "0.00001", + } diff --git a/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py b/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py index ee0186b6f30b..fa6f162e7b7c 100644 --- a/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py +++ b/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py @@ -20,38 +20,39 @@ crypto_perpetual_ethusdt_perp = TestInstrumentProviderPyo3.ethusdt_perp_binance() -class TestCryptoPerpetualPyo3: - def test_equality(self): - item_1 = TestInstrumentProviderPyo3.ethusdt_perp_binance() - item_2 = TestInstrumentProviderPyo3.ethusdt_perp_binance() - assert item_1 == item_2 - - def test_hash(self): - assert hash(crypto_perpetual_ethusdt_perp) == hash(crypto_perpetual_ethusdt_perp) - - def test_to_dict(self): - dict = crypto_perpetual_ethusdt_perp.to_dict() - assert CryptoPerpetual.from_dict(dict) == crypto_perpetual_ethusdt_perp - assert dict == { - "type": "CryptoPerpetual", - "id": "ETHUSDT-PERP.BINANCE", - "raw_symbol": "ETHUSDT", - "base_currency": "ETH", - "quote_currency": "USDT", - "settlement_currency": "USDT", - "price_precision": 2, - "size_precision": 0, - "price_increment": "0.01", - "size_increment": "0.001", - "lot_size": None, - "max_quantity": "10000", - "min_quantity": "0.001", - "max_notional": None, - "min_notional": "10.00000000 USDT", - "max_price": "15000.0", - "min_price": "1.0", - "margin_maint": 0.0, - "margin_init": 0.0, - "maker_fee": 0.0, - "taker_fee": 0.0, - } +def test_equality(): + item_1 = TestInstrumentProviderPyo3.ethusdt_perp_binance() + item_2 = TestInstrumentProviderPyo3.ethusdt_perp_binance() + assert item_1 == item_2 + + +def test_hash(): + assert hash(crypto_perpetual_ethusdt_perp) == hash(crypto_perpetual_ethusdt_perp) + + +def test_to_dict(): + dict = crypto_perpetual_ethusdt_perp.to_dict() + assert CryptoPerpetual.from_dict(dict) == crypto_perpetual_ethusdt_perp + assert dict == { + "type": "CryptoPerpetual", + "id": "ETHUSDT-PERP.BINANCE", + "raw_symbol": "ETHUSDT", + "base_currency": "ETH", + "quote_currency": "USDT", + "settlement_currency": "USDT", + "price_precision": 2, + "size_precision": 0, + "price_increment": "0.01", + "size_increment": "0.001", + "lot_size": None, + "max_quantity": "10000", + "min_quantity": "0.001", + "max_notional": None, + "min_notional": "10.00000000 USDT", + "max_price": "15000.0", + "min_price": "1.0", + "margin_maint": 0.0, + "margin_init": 0.0, + "maker_fee": 0.0, + "taker_fee": 0.0, + } diff --git a/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py b/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py index 510863987c56..14489e41cd9e 100644 --- a/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py +++ b/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py @@ -20,35 +20,36 @@ btcusdt_binance = TestInstrumentProviderPyo3.btcusdt_binance() -class TestCurrencyPairPyo3: - def test_equality(self): - item_1 = TestInstrumentProviderPyo3.btcusdt_binance() - item_2 = TestInstrumentProviderPyo3.btcusdt_binance() - assert item_1 == item_2 - - def test_hash(self): - assert hash(btcusdt_binance) == hash(btcusdt_binance) - - def test_to_dict(self): - dict = btcusdt_binance.to_dict() - assert CurrencyPair.from_dict(dict) == btcusdt_binance - assert dict == { - "type": "CurrencyPair", - "id": "BTCUSDT.BINANCE", - "raw_symbol": "BTCUSDT", - "base_currency": "BTC", - "quote_currency": "USDT", - "price_precision": 2, - "size_precision": 6, - "price_increment": "0.01", - "size_increment": "0.000001", - "margin_maint": 0.0, - "margin_init": 0.0, - "maker_fee": 0.0, - "taker_fee": 0.0, - "lot_size": None, - "max_quantity": "9000", - "min_quantity": "0.00001", - "min_price": "0.01", - "max_price": "1000000", - } +def test_equality(): + item_1 = TestInstrumentProviderPyo3.btcusdt_binance() + item_2 = TestInstrumentProviderPyo3.btcusdt_binance() + assert item_1 == item_2 + + +def test_hash(): + assert hash(btcusdt_binance) == hash(btcusdt_binance) + + +def test_to_dict(): + dict = btcusdt_binance.to_dict() + assert CurrencyPair.from_dict(dict) == btcusdt_binance + assert dict == { + "type": "CurrencyPair", + "id": "BTCUSDT.BINANCE", + "raw_symbol": "BTCUSDT", + "base_currency": "BTC", + "quote_currency": "USDT", + "price_precision": 2, + "size_precision": 6, + "price_increment": "0.01", + "size_increment": "0.000001", + "margin_maint": 0.0, + "margin_init": 0.0, + "maker_fee": 0.0, + "taker_fee": 0.0, + "lot_size": None, + "max_quantity": "9000", + "min_quantity": "0.00001", + "min_price": "0.01", + "max_price": "1000000", + } diff --git a/tests/unit_tests/model/instruments/test_options_contract_pyo3.py b/tests/unit_tests/model/instruments/test_options_contract_pyo3.py new file mode 100644 index 000000000000..971dcad5b158 --- /dev/null +++ b/tests/unit_tests/model/instruments/test_options_contract_pyo3.py @@ -0,0 +1,57 @@ +# ------------------------------------------------------------------------------------------------- +# 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.core.nautilus_pyo3 import OptionsContract +from nautilus_trader.test_kit.rust.instruments import TestInstrumentProviderPyo3 + + +aapl_option = TestInstrumentProviderPyo3.appl_option() + + +def test_equality(): + item_1 = TestInstrumentProviderPyo3.appl_option() + item_2 = TestInstrumentProviderPyo3.appl_option() + assert item_1 == item_2 + + +def test_hash(): + assert hash(aapl_option) == hash(aapl_option) + + +def test_to_dict(): + dict = aapl_option.to_dict() + assert OptionsContract.from_dict(dict) == aapl_option + assert dict == { + "type": "OptionsContract", + "id": "AAPL211217C00150000.OPRA", + "raw_symbol": "AAPL211217C00150000", + "asset_class": "EQUITY", + "underlying": "AAPL", + "option_kind": "CALL", + "expiration": 1639699200000000000, + "strike_price": "149.0", + "currency": "USDT", + "price_precision": 2, + "price_increment": "0.01", + "margin_init": 0.0, + "margin_maint": 0.0, + "maker_fee": 0.001, + "taker_fee": 0.001, + "lot_size": "1.0", + "max_quantity": None, + "min_quantity": None, + "max_price": None, + "min_price": None, + } From a7a877b0e6ad5e16494c0228cb68590a467d99b2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 30 Oct 2023 07:48:57 +1100 Subject: [PATCH 55/78] Reorganize test data --- docs/concepts/data.md | 2 +- docs/tutorials/backtest_low_level.md | 2 +- .../crypto_ema_cross_ethusdt_trade_ticks.py | 2 +- .../crypto_ema_cross_ethusdt_trailing_stop.py | 2 +- tests/acceptance_tests/test_backtest.py | 2 +- .../orderbook/test_orderbook.py | 4 ++-- tests/mem_leak_tests/memray_backtest.py | 2 +- tests/mem_leak_tests/memray_data.py | 2 +- ...c_crypto_ema_cross_ethusdt_trailing_stop.py | 2 +- .../mem_leak_tests/tracemalloc_trade_ticks.py | 2 +- tests/performance_tests/test_perf_wranglers.py | 2 +- .../ADABTC_pipe_separated-1m-2021-11-27.csv | 10 ---------- tests/test_data/bars_eurusd_2019_sim.parquet | Bin 283673 -> 0 bytes .../btcusdt-depth-snap.csv} | 0 .../btcusdt-depth-update.csv} | 0 .../btcusdt-instrument-repr.txt} | 0 .../btcusdt-instrument.txt} | 0 .../btcusdt-quotes.parquet} | Bin .../btcusdt-trades.parquet} | Bin .../ethusdt-trades.csv} | 0 tests/test_data/{ => bitmex}/L2_feed.json | 0 tests/test_data/{ => bitmex}/L3_feed.json | 0 tests/unit_tests/backtest/test_data_loaders.py | 4 ++-- .../unit_tests/backtest/test_data_wranglers.py | 6 +++--- tests/unit_tests/backtest/test_engine.py | 2 +- tests/unit_tests/data/test_aggregation.py | 12 ++++++------ tests/unit_tests/model/test_instrument.py | 2 +- .../unit_tests/persistence/test_transformer.py | 2 +- tests/unit_tests/persistence/test_wranglers.py | 2 +- .../persistence/test_wranglers_v2.py | 2 +- 30 files changed, 28 insertions(+), 38 deletions(-) delete mode 100644 tests/test_data/ADABTC_pipe_separated-1m-2021-11-27.csv delete mode 100644 tests/test_data/bars_eurusd_2019_sim.parquet rename tests/test_data/{binance-btcusdt-depth-snap.csv => binance/btcusdt-depth-snap.csv} (100%) rename tests/test_data/{binance-btcusdt-depth-update.csv => binance/btcusdt-depth-update.csv} (100%) rename tests/test_data/{binance-btcusdt-instrument-repr.txt => binance/btcusdt-instrument-repr.txt} (100%) rename tests/test_data/{binance-btcusdt-instrument.txt => binance/btcusdt-instrument.txt} (100%) rename tests/test_data/{binance-btcusdt-quotes.parquet => binance/btcusdt-quotes.parquet} (100%) rename tests/test_data/{binance-btcusdt-trades.parquet => binance/btcusdt-trades.parquet} (100%) rename tests/test_data/{binance-ethusdt-trades.csv => binance/ethusdt-trades.csv} (100%) rename tests/test_data/{ => bitmex}/L2_feed.json (100%) rename tests/test_data/{ => bitmex}/L3_feed.json (100%) diff --git a/docs/concepts/data.md b/docs/concepts/data.md index 8c4c69c9148c..5a3b8ba2b73a 100644 --- a/docs/concepts/data.md +++ b/docs/concepts/data.md @@ -97,7 +97,7 @@ from nautilus_trader.test_kit.providers import TestInstrumentProvider # Load raw data -data_path = os.path.join(PACKAGE_ROOT, "tests/test_data/binance-btcusdt-depth-snap.csv") +data_path = os.path.join(PACKAGE_ROOT, "tests/test_data/binance/binance/btcusdt-depth-snap.csv") df = BinanceOrderBookDeltaDataLoader.load(data_path) # Setup a wrangler diff --git a/docs/tutorials/backtest_low_level.md b/docs/tutorials/backtest_low_level.md index 8c9e35574a83..109a2be9cf09 100644 --- a/docs/tutorials/backtest_low_level.md +++ b/docs/tutorials/backtest_low_level.md @@ -50,7 +50,7 @@ Next, we need to wrangle this data into a list of Nautilus `TradeTick` objects, ```python # Load stub test data provider = TestDataProvider() -trades_df = provider.read_csv_ticks("binance-ethusdt-trades.csv") +trades_df = provider.read_csv_ticks("binance/ethusdt-trades.csv") # Initialize the instrument which matches the data ETHUSDT_BINANCE = TestInstrumentProvider.ethusdt_binance() diff --git a/examples/backtest/crypto_ema_cross_ethusdt_trade_ticks.py b/examples/backtest/crypto_ema_cross_ethusdt_trade_ticks.py index cbc45db54951..dd364de4902d 100755 --- a/examples/backtest/crypto_ema_cross_ethusdt_trade_ticks.py +++ b/examples/backtest/crypto_ema_cross_ethusdt_trade_ticks.py @@ -59,7 +59,7 @@ # Add data provider = TestDataProvider() wrangler = TradeTickDataWrangler(instrument=ETHUSDT_BINANCE) - ticks = wrangler.process(provider.read_csv_ticks("binance-ethusdt-trades.csv")) + ticks = wrangler.process(provider.read_csv_ticks("binance/ethusdt-trades.csv")) engine.add_data(ticks) # Configure your strategy diff --git a/examples/backtest/crypto_ema_cross_ethusdt_trailing_stop.py b/examples/backtest/crypto_ema_cross_ethusdt_trailing_stop.py index 9b5342cba9e8..a736fcc3c7f7 100755 --- a/examples/backtest/crypto_ema_cross_ethusdt_trailing_stop.py +++ b/examples/backtest/crypto_ema_cross_ethusdt_trailing_stop.py @@ -58,7 +58,7 @@ # Add data provider = TestDataProvider() wrangler = TradeTickDataWrangler(instrument=ETHUSDT_BINANCE) - ticks = wrangler.process(provider.read_csv_ticks("binance-ethusdt-trades.csv")) + ticks = wrangler.process(provider.read_csv_ticks("binance/ethusdt-trades.csv")) engine.add_data(ticks) # Configure your strategy diff --git a/tests/acceptance_tests/test_backtest.py b/tests/acceptance_tests/test_backtest.py index 8edfc4420bff..cfd97caf1d9e 100644 --- a/tests/acceptance_tests/test_backtest.py +++ b/tests/acceptance_tests/test_backtest.py @@ -663,7 +663,7 @@ def setup(self): # Add data provider = TestDataProvider() wrangler = TradeTickDataWrangler(instrument=self.ethusdt) - ticks = wrangler.process(provider.read_csv_ticks("binance-ethusdt-trades.csv")) + ticks = wrangler.process(provider.read_csv_ticks("binance/ethusdt-trades.csv")) self.engine.add_data(ticks) def teardown(self): diff --git a/tests/integration_tests/orderbook/test_orderbook.py b/tests/integration_tests/orderbook/test_orderbook.py index c01f04e464ae..34983a4412b8 100644 --- a/tests/integration_tests/orderbook/test_orderbook.py +++ b/tests/integration_tests/orderbook/test_orderbook.py @@ -38,7 +38,7 @@ def test_l1_orderbook(self): assert i == 1999 def test_l2_feed(self): - filename = TEST_DATA_DIR / "L2_feed.json" + filename = TEST_DATA_DIR / "bitmex" / "L2_feed.json" book = OrderBook( instrument_id=TestIdStubs.audusd_id(), @@ -66,7 +66,7 @@ def test_l2_feed(self): @pytest.mark.skip("segfault on check_integrity") def test_l3_feed(self): - filename = TEST_DATA_DIR / "L3_feed.json" + filename = TEST_DATA_DIR / "bitmex" / "L3_feed.json" book = OrderBook( instrument_id=TestIdStubs.audusd_id(), diff --git a/tests/mem_leak_tests/memray_backtest.py b/tests/mem_leak_tests/memray_backtest.py index fd31cfe66a15..0b82f4aff8a8 100644 --- a/tests/mem_leak_tests/memray_backtest.py +++ b/tests/mem_leak_tests/memray_backtest.py @@ -67,7 +67,7 @@ # Add data provider = TestDataProvider() wrangler = TradeTickDataWrangler(instrument=ETHUSDT_BINANCE) - ticks = wrangler.process(provider.read_csv_ticks("binance-ethusdt-trades.csv")) + ticks = wrangler.process(provider.read_csv_ticks("binance/binance/ethusdt-trades.csv")) engine.add_data(ticks) # Configure your strategy diff --git a/tests/mem_leak_tests/memray_data.py b/tests/mem_leak_tests/memray_data.py index 1a95ffe2819d..e089abc1cad4 100644 --- a/tests/mem_leak_tests/memray_data.py +++ b/tests/mem_leak_tests/memray_data.py @@ -51,7 +51,7 @@ # Process data ticks = quote_tick_wrangler.process(provider.read_csv_ticks("truefx-audusd-ticks.csv")) - ticks = trade_tick_wrangler.process(provider.read_csv_ticks("binance-ethusdt-trades.csv")) + ticks = trade_tick_wrangler.process(provider.read_csv_ticks("binance/ethusdt-trades.csv")) # Add data bid_bars = bid_wrangler.process( 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 fcd5ab872daa..5fd9acba01dc 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 @@ -61,7 +61,7 @@ def run(*args, **kwargs): # Add data provider = TestDataProvider() wrangler = TradeTickDataWrangler(instrument=ETHUSDT_BINANCE) - ticks = wrangler.process(provider.read_csv_ticks("binance-ethusdt-trades.csv")) + ticks = wrangler.process(provider.read_csv_ticks("binance/ethusdt-trades.csv")) engine.add_data(ticks) # Configure your strategy diff --git a/tests/mem_leak_tests/tracemalloc_trade_ticks.py b/tests/mem_leak_tests/tracemalloc_trade_ticks.py index 44b2e4bf92e9..b0938c333ff9 100644 --- a/tests/mem_leak_tests/tracemalloc_trade_ticks.py +++ b/tests/mem_leak_tests/tracemalloc_trade_ticks.py @@ -34,7 +34,7 @@ def run(*args, **kwargs): # provider = TestDataProvider() # wrangler = TradeTickDataWrangler(instrument=ETHUSDT_BINANCE) - # _ = wrangler.process(provider.read_csv_ticks("binance-ethusdt-trades.csv")) + # _ = wrangler.process(provider.read_csv_ticks("binance/binance-ethusdt-trades.csv")) _ = TradeTick( ETHUSDT_BINANCE.id, Price.from_str("1.00000"), diff --git a/tests/performance_tests/test_perf_wranglers.py b/tests/performance_tests/test_perf_wranglers.py index 881a428eb812..031809a51302 100644 --- a/tests/performance_tests/test_perf_wranglers.py +++ b/tests/performance_tests/test_perf_wranglers.py @@ -49,7 +49,7 @@ def test_trade_tick_data_wrangler_process(self): def wrangler_process(): # 69806 ticks in data - wrangler.process(data=provider.read_csv_ticks("binance-ethusdt-trades.csv")) + wrangler.process(data=provider.read_csv_ticks("binance/ethusdt-trades.csv")) PerformanceBench.profile_function( target=wrangler_process, diff --git a/tests/test_data/ADABTC_pipe_separated-1m-2021-11-27.csv b/tests/test_data/ADABTC_pipe_separated-1m-2021-11-27.csv deleted file mode 100644 index dcdbd27e25ff..000000000000 --- a/tests/test_data/ADABTC_pipe_separated-1m-2021-11-27.csv +++ /dev/null @@ -1,10 +0,0 @@ -1637971200000|0.00002853|0.00002854|0.00002851|0.00002854|36304.20000000|1637971259999|1.03547608|79|19523.00000000|0.55694227|0 -1637971260000|0.00002854|0.00002860|0.00002854|0.00002859|89196.60000000|1637971319999|2.54751330|98|59004.20000000|1.68545512|0 -1637971320000|0.00002859|0.00002860|0.00002857|0.00002859|26547.30000000|1637971379999|0.75867802|50|5425.90000000|0.15506127|0 -1637971380000|0.00002858|0.00002862|0.00002849|0.00002857|162305.70000000|1637971439999|4.63429635|158|29169.60000000|0.83389490|0 -1637971440000|0.00002856|0.00002857|0.00002854|0.00002855|4213.90000000|1637971499999|0.12031348|28|2036.50000000|0.05814797|0 -1637971500000|0.00002854|0.00002858|0.00002854|0.00002857|7530.20000000|1637971559999|0.21510805|22|6910.10000000|0.19740082|0 -1637971560000|0.00002858|0.00002859|0.00002857|0.00002858|3764.00000000|1637971619999|0.10756007|17|1536.40000000|0.04391146|0 -1637971620000|0.00002857|0.00002862|0.00002855|0.00002862|42475.40000000|1637971679999|1.21439635|49|36855.50000000|1.05380435|0 -1637971680000|0.00002862|0.00002863|0.00002861|0.00002863|4470.20000000|1637971739999|0.12792384|27|1597.90000000|0.04573202|0 -1637971740000|0.00002863|0.00002865|0.00002863|0.00002865|8160.50000000|1637971799999|0.23366480|38|4978.00000000|0.14254743|0 diff --git a/tests/test_data/bars_eurusd_2019_sim.parquet b/tests/test_data/bars_eurusd_2019_sim.parquet deleted file mode 100644 index 5ba9046cddfb40e1e6e74c7047b1a289c24c1ef9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 283673 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 %M* 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Μ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�!-He7>>h5!mGO3^X&B=jDdhMoi& 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

A|b$F_FRUPR~Tfk zz%OM{b7G9vBZx76W)frUzW|H@g8dU11MesgU<{ll=nsqmf}K{r6LpuJnJ$-Q<3 z5q8VrI^wH7TTkx|X6xzr1h$@*WpX&@?ekN_=k66hik~B*-Pw9tyFXh`A6v@S(-Vtt zeZn!1_w~dE>Y3A!ABgP#{tf5ssjQZ)!mr1?Fi;n%KKp2f0aBr~DltY%T0|O}AypqH zbhbpwcWhl{gVYSZS!It@*41b4=^H`pJ-sxZy{Fr*<8aPD=UTXk+C$%GyNlYfZfCqi zt#d!A-zxEzSiu@k#bmv(0N=^3-xeLA3^wmpu$)r56I?IMh(vS#Uu;6gcc@ZE& z*f5XhaL&)0L4@!*^g4(TD4TAs*jG3Ok_c4}BN1A@hD50SSrQ@1Hxi*EE+j(Z`;Z8^ zEhZ6qa+E|U>kWxe_j)8krZFT!Wiv^HX6+#n3VtYvP)%f-pYP<=cq-csnKBJ2`I}Lk zj8qk!)=?tmNnbk5Luy=_rKcd3k9yu(gp{pcST7B!?sk~q!sHdf#c4-^i{xklE@~nJ zXUxbJ#qV~WQiw zo-R~OU&NJ=lxy+s>JQr#69c|qZHC0PI5!+3!`5*>|I*7ef zozZc@i0j!~TOyW}zYasR`Rfyb`0Y)-<)DIk%a|3^TbxU%w>MdULskgj1K)ogB z8TFRlcAU;>8AZM2{1k$qx%mV^A$JIZJ{fU3=dP9jLD(mcq~20LOXw}qTbBTW0M~vK z1Vwof1W5)G1RY*Z5HwCr5ajxSAn0*Jf}joE2!dkg5d=vO5CoMyB?y{n%juj!?FfS2 zPbLW3wv8Z2aa(|(n#e=$5m{^T+>hwGGas>Jh?tB~@>KL{@Fx)EHg>PK)9vy|XMuNdG0%=#U` z1(>xHzy;vie*hQwru%H_Evf?QEqxvdy~W4M>XgM3lv)m)rPMO|8>Nn{Vh0`(>8)q%jI#DT2eMsY6-ti zsijtjb2$qFD77REqtw!H4W*W3Ne=t2&p(}x`B_#$$kNk=KQc)l4VUNX)cBGpE_pHHiYT7$P;d>Y^zZQXCQ|lxTIe05dh3q4_mGMKcZn

DVR31uPv2ZkTMXQa#6-w#1a^MQ3wD2!*1-?Vz16+Zxw+|w&xVz#XuE;1!k*vqv zcKUQl2dU9*Vr_s_7I*JsjFe4YkYkEedmVaafmFPB?rwvG6*k@;sp{RHxWaN8rIz#C zDYeY45>iW2h)IRBsQnaT?=EV0jT+!3YKLWS@m-I%m|^)V@qgmhA8)a3TIM}jY@4DK z$HNhl74%sSpQ6t)?jwB`x5hU+;@IO@_$>II&V2YR_{Qf!_$>IY`*Zj#_^!Jhhjh-2 zqR$dEl|D;#K7E#Lcj&Vyj5EZx$svS3OT|d~EQ_<~v$VcUpXKLo`YZ>$=(CI(NS~!? zI(?S=YWgf|K6FXLt^C)B13C?2>9eTk(Px={#GAvK2vXqzIHW0$qJK+0TBF0)3eHPtul zkcv!M-8x7~x2_~V#&b!2O81fc%y=TmPf~!*bT?7^u8rDL)aFjo_Z78qd7T0_h*wSJ zO0liDV!-*Fsm(c`Q$Avt*j9YcB=p&LfzZd$iICz{XF`hUvk58u3J59QXb35`SgOCF zu77Jn3fl?)AjRb^E48z5mnk=H>menr^col;RkeY`q)2(e@SWyJO~Tp_R!C*TbHR2< zS=G0hj!5-NS3-)8eF-Uamk?4MKlTq&OnQ6IRTO*HZ|o_GpT~^w5yd&P_WFzBUVFa; zAr%&ykd{cvd5gJWNY$K(b8@7-<#?JbA2-uv*?FBN%TV1%F?byH0%@|`97dC6*&3QG z?atC<`CChqrN|{^5LPDjp~>R9m?q1kqcmC8zoE$zTkqL8EHR0p$#QxoO_t<6G+6>4 z(qwsW-exYA-(3#bGl|maErjvMkc!q|S%{nk+wt(q!4cnkLK0Gc;M6)M{

YvRIpQQs>21 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 None: # Arrange provider = TestDataProvider() wrangler = TradeTickDataWrangler(instrument=ETHUSDT_BINANCE) - ticks = wrangler.process(provider.read_csv_ticks("binance-ethusdt-trades.csv")) + ticks = wrangler.process(provider.read_csv_ticks("binance/ethusdt-trades.csv")) # Act batches_bytes = DataTransformer.pyobjects_to_batches_bytes(ticks) diff --git a/tests/unit_tests/persistence/test_wranglers.py b/tests/unit_tests/persistence/test_wranglers.py index 40172121d26e..6ecf78459ab0 100644 --- a/tests/unit_tests/persistence/test_wranglers.py +++ b/tests/unit_tests/persistence/test_wranglers.py @@ -26,7 +26,7 @@ def test_load_binance_deltas() -> None: # Arrange instrument = TestInstrumentProvider.btcusdt_binance() - data_path = os.path.join(PACKAGE_ROOT, "tests/test_data/binance-btcusdt-depth-snap.csv") + data_path = os.path.join(PACKAGE_ROOT, "tests/test_data/binance/btcusdt-depth-snap.csv") df = BinanceOrderBookDeltaDataLoader.load(data_path) wrangler = OrderBookDeltaDataWrangler(instrument) diff --git a/tests/unit_tests/persistence/test_wranglers_v2.py b/tests/unit_tests/persistence/test_wranglers_v2.py index 9e6699d51428..cb68636c452b 100644 --- a/tests/unit_tests/persistence/test_wranglers_v2.py +++ b/tests/unit_tests/persistence/test_wranglers_v2.py @@ -48,7 +48,7 @@ def test_quote_tick_data_wrangler() -> None: def test_trade_tick_data_wrangler() -> None: # Arrange - path = TEST_DATA_DIR / "binance-ethusdt-trades.csv" + path = TEST_DATA_DIR / "binance" / "ethusdt-trades.csv" df = pd.read_csv(path) # Act From fdb81664b1a17408107e7814677e05a9d61bb55d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 30 Oct 2023 07:50:17 +1100 Subject: [PATCH 56/78] Remove redundant performance tests --- tests/performance_tests/test_perf_decimal.py | 188 ------------------ .../test_perf_identifiers.py | 15 ++ 2 files changed, 15 insertions(+), 188 deletions(-) delete mode 100644 tests/performance_tests/test_perf_decimal.py diff --git a/tests/performance_tests/test_perf_decimal.py b/tests/performance_tests/test_perf_decimal.py deleted file mode 100644 index 750663ef8c25..000000000000 --- a/tests/performance_tests/test_perf_decimal.py +++ /dev/null @@ -1,188 +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. -# ------------------------------------------------------------------------------------------------- - -from decimal import Decimal - -from nautilus_trader.core.inspect import get_size_of -from nautilus_trader.model.objects import Price -from nautilus_trader.model.objects import Quantity -from nautilus_trader.test_kit.performance import PerformanceBench -from nautilus_trader.test_kit.performance import PerformanceHarness - - -_BUILTIN_DECIMAL1 = Decimal("1.00000") -_BUILTIN_DECIMAL2 = Decimal("1.00001") - -_DECIMAL1 = Quantity(1, precision=1) -_DECIMAL2 = Quantity(1.00001, precision=5) - - -class DecimalTesting: - @staticmethod - def float_comparisons(): - 1.0 == 2.0 - - @staticmethod - def float_arithmetic(): - 1.0 * 2.0 - - @staticmethod - def decimal_arithmetic(): - _DECIMAL1 + _DECIMAL1 - _DECIMAL1 - _DECIMAL1 - _DECIMAL1 * _DECIMAL1 - _DECIMAL1 / _DECIMAL1 - - @staticmethod - def decimal_arithmetic_with_floats(): - _DECIMAL1 + 1.0 - _DECIMAL1 - 1.0 - _DECIMAL1 * 1.0 - _DECIMAL1 / 1.0 - - @staticmethod - def builtin_decimal_arithmetic(): - _BUILTIN_DECIMAL1 + _BUILTIN_DECIMAL1 - _BUILTIN_DECIMAL1 - _BUILTIN_DECIMAL1 - _BUILTIN_DECIMAL1 * _BUILTIN_DECIMAL1 - _BUILTIN_DECIMAL1 / _BUILTIN_DECIMAL1 - - @staticmethod - def decimal_comparisons(): - _DECIMAL1 > _DECIMAL2 - _DECIMAL1 >= _DECIMAL2 - _DECIMAL1 == _DECIMAL2 - - @staticmethod - def builtin_decimal_comparisons(): - _BUILTIN_DECIMAL1 > _BUILTIN_DECIMAL2 - _BUILTIN_DECIMAL1 >= _BUILTIN_DECIMAL2 - _BUILTIN_DECIMAL1 == _BUILTIN_DECIMAL2 - - -class TestDecimalPerformance(PerformanceHarness): - def test_builtin_decimal_size(self): - print(get_size_of(Decimal("1.00000"))) - # Object size is 104 bytes. - - def test_decimal_size(self): - print(get_size_of(_DECIMAL1)) - # Object size is 40 bytes. - - def test_make_builtin_decimal(self): - self.benchmark.pedantic( - target=Decimal, - args=("1.23456",), - iterations=1, - rounds=100_000, - ) - # ~0.0ms / ~0.3μs / 253ns minimum of 100,000 runs @ 1 iteration each run. - - def test_make_decimal(self): - self.benchmark.pedantic( - target=Quantity, - args=(1.23456, 5), - iterations=1, - rounds=100_000, - ) - # ~0.0ms / ~0.4μs / 353ns minimum of 100,000 runs @ 1 iteration each run. - - def test_make_price(self): - # self.benchmark.pedantic( - # target=Price, - # args=(1.23456, 5), - # iterations=1, - # rounds=100_000, - # ) - def make_price(): - Price(1.23456, 5) - - PerformanceBench.profile_function( - target=make_price, - runs=100_000, - iterations=1, - ) - # ~0.0ms / ~0.5μs / 526ns minimum of 100,000 runs @ 1 iteration each run. - # ~0.0ms / ~0.2μs / 193ns minimum of 100,000 runs @ 1 iteration each run. - - def test_make_price_from_float(self): - self.benchmark.pedantic( - target=Price, - args=(1.23456, 5), - iterations=1, - rounds=100_000, - ) - - def test_float_comparisons(self): - self.benchmark.pedantic( - target=DecimalTesting.float_comparisons, - iterations=1, - rounds=100_000, - ) - # ~0.0ms / ~0.1μs / 118ns minimum of 100,000 runs @ 1 iteration each run. - - def test_decimal_comparisons(self): - self.benchmark.pedantic( - target=DecimalTesting.decimal_comparisons, - iterations=1, - rounds=100_000, - ) - # ~0.0ms / ~0.4μs / 429ns minimum of 100,000 runs @ 1 iteration each run. - - def test_builtin_decimal_comparisons(self): - self.benchmark.pedantic( - target=DecimalTesting.builtin_decimal_comparisons, - iterations=1, - rounds=100_000, - ) - # ~17.2ms / ~17237.6μs / 17237551ns minimum of 3 runs @ 100,000 iterations each run. - - def test_float_arithmetic(self): - self.benchmark.pedantic( - target=DecimalTesting.float_arithmetic, - iterations=1, - rounds=100_000, - ) - # ~5.0ms / ~5027.3μs / 5027301ns minimum of 3 runs @ 100,000 iterations each run. - - def test_builtin_decimal_arithmetic(self): - self.benchmark.pedantic( - target=DecimalTesting.builtin_decimal_arithmetic, - iterations=1, - rounds=100_000, - ) - - PerformanceBench.profile_function( - target=DecimalTesting.builtin_decimal_arithmetic, - runs=100_000, - iterations=1, - ) - # ~0.0ms / ~0.4μs / 424ns minimum of 100,000 runs @ 1 iteration each run. - - def test_decimal_arithmetic(self): - self.benchmark.pedantic( - target=DecimalTesting.decimal_arithmetic, - iterations=1, - rounds=100_000, - ) - # ~71.0ms / ~70980.9μs / 70980863ns minimum of 3 runs @ 100,000 iterations each run. - - def test_decimal_arithmetic_with_floats(self): - self.benchmark.pedantic( - target=DecimalTesting.decimal_arithmetic_with_floats, - iterations=1, - rounds=100_000, - ) - # ~58.0ms / ~58034.9μs / 58034884ns minimum of 3 runs @ 100,000 iterations each run. diff --git a/tests/performance_tests/test_perf_identifiers.py b/tests/performance_tests/test_perf_identifiers.py index 7db9a06a3366..3f056e2ed1c6 100644 --- a/tests/performance_tests/test_perf_identifiers.py +++ b/tests/performance_tests/test_perf_identifiers.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. +# ------------------------------------------------------------------------------------------------- + from nautilus_trader.model.identifiers import Symbol from nautilus_trader.model.identifiers import Venue from nautilus_trader.test_kit.performance import PerformanceBench From b596bf1be16fa454b0dc5c05aaccbd74c7a9c5c8 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 30 Oct 2023 08:22:42 +1100 Subject: [PATCH 57/78] Reorganize test data --- .../backtest/crypto_ema_cross_with_binance_provider.py | 4 ++-- tests/acceptance_tests/test_backtest.py | 8 ++++---- tests/integration_tests/adapters/tardis/test_loaders.py | 8 ++++---- ...-20220201_1m.csv => btc-perp-20211231-20220201_1m.csv} | 0 tests/test_data/{tardis_quotes.csv => tardis/quotes.csv} | 0 tests/test_data/{tardis_trades.csv => tardis/trades.csv} | 0 6 files changed, 10 insertions(+), 10 deletions(-) rename tests/test_data/{ftx-btc-perp-20211231-20220201_1m.csv => btc-perp-20211231-20220201_1m.csv} (100%) rename tests/test_data/{tardis_quotes.csv => tardis/quotes.csv} (100%) rename tests/test_data/{tardis_trades.csv => tardis/trades.csv} (100%) diff --git a/examples/backtest/crypto_ema_cross_with_binance_provider.py b/examples/backtest/crypto_ema_cross_with_binance_provider.py index 0b54592f2baa..95b4db268f78 100644 --- a/examples/backtest/crypto_ema_cross_with_binance_provider.py +++ b/examples/backtest/crypto_ema_cross_with_binance_provider.py @@ -100,8 +100,8 @@ async def create_provider(): bar_type = f"{instrument_id.value}-1-MINUTE-BID-INTERNAL" wrangler = QuoteTickDataWrangler(instrument=instrument) ticks = wrangler.process_bar_data( - bid_data=TestDataProvider().read_csv_bars("ftx-btc-perp-20211231-20220201_1m.csv"), - ask_data=TestDataProvider().read_csv_bars("ftx-btc-perp-20211231-20220201_1m.csv"), + bid_data=TestDataProvider().read_csv_bars("btc-perp-20211231-20220201_1m.csv"), + ask_data=TestDataProvider().read_csv_bars("btc-perp-20211231-20220201_1m.csv"), ) engine.add_data(ticks) diff --git a/tests/acceptance_tests/test_backtest.py b/tests/acceptance_tests/test_backtest.py index cfd97caf1d9e..829d15a09442 100644 --- a/tests/acceptance_tests/test_backtest.py +++ b/tests/acceptance_tests/test_backtest.py @@ -421,7 +421,7 @@ def test_run_ema_cross_with_minute_trade_bars(self): # Build externally aggregated bars bars = wrangler.process( - data=provider.read_csv_bars("ftx-btc-perp-20211231-20220201_1m.csv")[:10_000], + data=provider.read_csv_bars("btc-perp-20211231-20220201_1m.csv")[:10_000], ) self.engine.add_data(bars) @@ -487,7 +487,7 @@ def test_run_ema_cross_with_minute_trade_bars(self): # Build externally aggregated bars bars = wrangler.process( - data=provider.read_csv_bars("ftx-btc-perp-20211231-20220201_1m.csv")[:10_000], + data=provider.read_csv_bars("btc-perp-20211231-20220201_1m.csv")[:10_000], ) self.engine.add_data(bars) @@ -526,8 +526,8 @@ def test_run_ema_cross_with_trade_ticks_from_bar_data(self): # Build ticks from bar data ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("ftx-btc-perp-20211231-20220201_1m.csv")[:10_000], - ask_data=provider.read_csv_bars("ftx-btc-perp-20211231-20220201_1m.csv")[:10_000], + bid_data=provider.read_csv_bars("btc-perp-20211231-20220201_1m.csv")[:10_000], + ask_data=provider.read_csv_bars("btc-perp-20211231-20220201_1m.csv")[:10_000], ) self.engine.add_data(ticks) diff --git a/tests/integration_tests/adapters/tardis/test_loaders.py b/tests/integration_tests/adapters/tardis/test_loaders.py index a603290dbbd5..6650c268950a 100644 --- a/tests/integration_tests/adapters/tardis/test_loaders.py +++ b/tests/integration_tests/adapters/tardis/test_loaders.py @@ -27,7 +27,7 @@ def test_tardis_quote_data_loader(): # Arrange, Act - path = TEST_DATA_DIR / "tardis_quotes.csv" + path = TEST_DATA_DIR / "tardis/quotes.csv" ticks = TardisQuoteDataLoader.load(path) # Assert @@ -38,7 +38,7 @@ def test_pre_process_with_quote_tick_data(): # Arrange instrument = TestInstrumentProvider.btcusdt_binance() wrangler = QuoteTickDataWrangler(instrument=instrument) - path = TEST_DATA_DIR / "tardis_quotes.csv" + path = TEST_DATA_DIR / "tardis/quotes.csv" data = TardisQuoteDataLoader.load(path) # Act @@ -59,7 +59,7 @@ def test_pre_process_with_quote_tick_data(): def test_tardis_trade_tick_loader(): # Arrange, Act - path = TEST_DATA_DIR / "tardis_trades.csv" + path = TEST_DATA_DIR / "tardis/trades.csv" ticks = TardisTradeDataLoader.load(path) # Assert @@ -70,7 +70,7 @@ def test_pre_process_with_trade_tick_data(): # Arrange instrument = TestInstrumentProvider.btcusdt_binance() wrangler = TradeTickDataWrangler(instrument=instrument) - path = TEST_DATA_DIR / "tardis_trades.csv" + path = TEST_DATA_DIR / "tardis/trades.csv" data = TardisTradeDataLoader.load(path) # Act diff --git a/tests/test_data/ftx-btc-perp-20211231-20220201_1m.csv b/tests/test_data/btc-perp-20211231-20220201_1m.csv similarity index 100% rename from tests/test_data/ftx-btc-perp-20211231-20220201_1m.csv rename to tests/test_data/btc-perp-20211231-20220201_1m.csv diff --git a/tests/test_data/tardis_quotes.csv b/tests/test_data/tardis/quotes.csv similarity index 100% rename from tests/test_data/tardis_quotes.csv rename to tests/test_data/tardis/quotes.csv diff --git a/tests/test_data/tardis_trades.csv b/tests/test_data/tardis/trades.csv similarity index 100% rename from tests/test_data/tardis_trades.csv rename to tests/test_data/tardis/trades.csv From 942494ac33e91c4eb059f655547d496dd33cc98f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 30 Oct 2023 08:36:00 +1100 Subject: [PATCH 58/78] Reorganize test data --- .../fx_ema_cross_audusd_bars_from_ticks.py | 2 +- examples/backtest/fx_ema_cross_audusd_ticks.py | 2 +- ...x_ema_cross_bracket_gbpusd_bars_external.py | 4 ++-- ...x_ema_cross_bracket_gbpusd_bars_internal.py | 4 ++-- .../backtest/fx_market_maker_gbpusd_bars.py | 4 ++-- examples/notebooks/backtest_fx_usdjpy.ipynb | 4 ++-- nautilus_trader/test_kit/mocks/data.py | 2 +- nautilus_trader/test_kit/stubs/data.py | 6 +++--- tests/acceptance_tests/test_backtest.py | 14 +++++++------- tests/conftest.py | 2 +- .../infrastructure/test_cache_database.py | 4 ++-- tests/mem_leak_tests/memray_data.py | 6 +++--- ...alloc_cross_bracket_gbpusd_bars_internal.py | 4 ++-- .../tracemalloc_market_maker_gbpusd_bars.py | 4 ++-- tests/performance_tests/test_perf_backtest.py | 12 ++++++------ tests/performance_tests/test_perf_wranglers.py | 2 +- .../gbpusd-m1-ask-2012.csv} | 0 .../gbpusd-m1-bid-2012.csv} | 0 .../usdjpy-m1-ask-2013.csv} | 0 .../usdjpy-m1-bid-2013.csv} | 0 .../audusd-ticks.csv} | 0 .../usdjpy-ticks.csv} | 0 .../unit_tests/backtest/test_data_wranglers.py | 18 +++++++++--------- tests/unit_tests/backtest/test_engine.py | 14 +++++++------- tests/unit_tests/backtest/test_modules.py | 6 +++--- tests/unit_tests/cache/test_execution.py | 4 ++-- tests/unit_tests/data/test_aggregation.py | 6 +++--- .../unit_tests/persistence/test_transformer.py | 2 +- .../persistence/test_wranglers_v2.py | 2 +- 29 files changed, 64 insertions(+), 64 deletions(-) rename tests/test_data/{fxcm-gbpusd-m1-ask-2012.csv => fxcm/gbpusd-m1-ask-2012.csv} (100%) rename tests/test_data/{fxcm-gbpusd-m1-bid-2012.csv => fxcm/gbpusd-m1-bid-2012.csv} (100%) rename tests/test_data/{fxcm-usdjpy-m1-ask-2013.csv => fxcm/usdjpy-m1-ask-2013.csv} (100%) rename tests/test_data/{fxcm-usdjpy-m1-bid-2013.csv => fxcm/usdjpy-m1-bid-2013.csv} (100%) rename tests/test_data/{truefx-audusd-ticks.csv => truefx/audusd-ticks.csv} (100%) rename tests/test_data/{truefx-usdjpy-ticks.csv => truefx/usdjpy-ticks.csv} (100%) diff --git a/examples/backtest/fx_ema_cross_audusd_bars_from_ticks.py b/examples/backtest/fx_ema_cross_audusd_bars_from_ticks.py index ecf8bc17e8e8..2ea34439d2fd 100755 --- a/examples/backtest/fx_ema_cross_audusd_bars_from_ticks.py +++ b/examples/backtest/fx_ema_cross_audusd_bars_from_ticks.py @@ -68,7 +68,7 @@ # Add data wrangler = QuoteTickDataWrangler(instrument=AUDUSD_SIM) - ticks = wrangler.process(provider.read_csv_ticks("truefx-audusd-ticks.csv")) + ticks = wrangler.process(provider.read_csv_ticks("truefx/audusd-ticks.csv")) engine.add_data(ticks) # Configure your strategy diff --git a/examples/backtest/fx_ema_cross_audusd_ticks.py b/examples/backtest/fx_ema_cross_audusd_ticks.py index 3420c1fecf73..6678331b458d 100644 --- a/examples/backtest/fx_ema_cross_audusd_ticks.py +++ b/examples/backtest/fx_ema_cross_audusd_ticks.py @@ -79,7 +79,7 @@ # Add data wrangler = QuoteTickDataWrangler(instrument=AUDUSD_SIM) - ticks = wrangler.process(provider.read_csv_ticks("truefx-audusd-ticks.csv")) + ticks = wrangler.process(provider.read_csv_ticks("truefx/audusd-ticks.csv")) engine.add_data(ticks) # Configure your strategy diff --git a/examples/backtest/fx_ema_cross_bracket_gbpusd_bars_external.py b/examples/backtest/fx_ema_cross_bracket_gbpusd_bars_external.py index d90865244247..1fa3f811ed28 100755 --- a/examples/backtest/fx_ema_cross_bracket_gbpusd_bars_external.py +++ b/examples/backtest/fx_ema_cross_bracket_gbpusd_bars_external.py @@ -96,10 +96,10 @@ # Add data bid_bars = bid_wrangler.process( - data=provider.read_csv_bars("fxcm-gbpusd-m1-bid-2012.csv")[:10_000], + data=provider.read_csv_bars("fxcm/gbpusd-m1-bid-2012.csv")[:10_000], ) ask_bars = ask_wrangler.process( - data=provider.read_csv_bars("fxcm-gbpusd-m1-ask-2012.csv")[:10_000], + data=provider.read_csv_bars("fxcm/gbpusd-m1-ask-2012.csv")[:10_000], ) engine.add_data(bid_bars) engine.add_data(ask_bars) diff --git a/examples/backtest/fx_ema_cross_bracket_gbpusd_bars_internal.py b/examples/backtest/fx_ema_cross_bracket_gbpusd_bars_internal.py index c8c639c71b4c..9db2f7e84d38 100755 --- a/examples/backtest/fx_ema_cross_bracket_gbpusd_bars_internal.py +++ b/examples/backtest/fx_ema_cross_bracket_gbpusd_bars_internal.py @@ -86,8 +86,8 @@ # Add data wrangler = QuoteTickDataWrangler(instrument=GBPUSD_SIM) ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-gbpusd-m1-bid-2012.csv"), - ask_data=provider.read_csv_bars("fxcm-gbpusd-m1-ask-2012.csv"), + bid_data=provider.read_csv_bars("fxcm/gbpusd-m1-bid-2012.csv"), + ask_data=provider.read_csv_bars("fxcm/gbpusd-m1-ask-2012.csv"), ) engine.add_data(ticks) diff --git a/examples/backtest/fx_market_maker_gbpusd_bars.py b/examples/backtest/fx_market_maker_gbpusd_bars.py index f5eb877c78ae..fa2e12ba511c 100755 --- a/examples/backtest/fx_market_maker_gbpusd_bars.py +++ b/examples/backtest/fx_market_maker_gbpusd_bars.py @@ -81,8 +81,8 @@ # Add data wrangler = QuoteTickDataWrangler(GBPUSD_SIM) ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-gbpusd-m1-bid-2012.csv"), - ask_data=provider.read_csv_bars("fxcm-gbpusd-m1-ask-2012.csv"), + bid_data=provider.read_csv_bars("fxcm/gbpusd-m1-bid-2012.csv"), + ask_data=provider.read_csv_bars("fxcm/gbpusd-m1-ask-2012.csv"), ) engine.add_data(ticks) diff --git a/examples/notebooks/backtest_fx_usdjpy.ipynb b/examples/notebooks/backtest_fx_usdjpy.ipynb index d2a3e78c3a4e..e8e2ac1ce37c 100644 --- a/examples/notebooks/backtest_fx_usdjpy.ipynb +++ b/examples/notebooks/backtest_fx_usdjpy.ipynb @@ -118,8 +118,8 @@ "# Add data\n", "wrangler = QuoteTickDataWrangler(instrument=USDJPY_SIM)\n", "ticks = wrangler.process_bar_data(\n", - " bid_data=provider.read_csv_bars(\"fxcm-usdjpy-m1-bid-2013.csv\"),\n", - " ask_data=provider.read_csv_bars(\"fxcm-usdjpy-m1-ask-2013.csv\"),\n", + " bid_data=provider.read_csv_bars(\"fxcm/usdjpy-m1-bid-2013.csv\"),\n", + " ask_data=provider.read_csv_bars(\"fxcm/usdjpy-m1-ask-2013.csv\"),\n", ")\n", "engine.add_data(ticks)" ] diff --git a/nautilus_trader/test_kit/mocks/data.py b/nautilus_trader/test_kit/mocks/data.py index 69d8b703d91e..7c1379accf1e 100644 --- a/nautilus_trader/test_kit/mocks/data.py +++ b/nautilus_trader/test_kit/mocks/data.py @@ -78,7 +78,7 @@ def aud_usd_data_loader(catalog: ParquetDataCatalog) -> None: instrument_provider.add(instrument) wrangler = QuoteTickDataWrangler(instrument) - ticks = wrangler.process(TestDataProvider().read_csv_ticks("truefx-audusd-ticks.csv")) + ticks = wrangler.process(TestDataProvider().read_csv_ticks("truefx/audusd-ticks.csv")) ticks.sort(key=lambda x: x.ts_init) # CAUTION: data was not originally sorted catalog.write_data([instrument]) catalog.write_data(ticks) diff --git a/nautilus_trader/test_kit/stubs/data.py b/nautilus_trader/test_kit/stubs/data.py index a2f885bd4e79..6a83a3ddcef1 100644 --- a/nautilus_trader/test_kit/stubs/data.py +++ b/nautilus_trader/test_kit/stubs/data.py @@ -119,8 +119,8 @@ def quote_ticks_usdjpy() -> list[QuoteTick]: wrangler = QuoteTickDataWrangler(instrument=usdjpy) provider = TestDataProvider() ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-usdjpy-m1-bid-2013.csv")[:2000], - ask_data=provider.read_csv_bars("fxcm-usdjpy-m1-ask-2013.csv")[:2000], + bid_data=provider.read_csv_bars("fxcm/usdjpy-m1-bid-2013.csv")[:2000], + ask_data=provider.read_csv_bars("fxcm/usdjpy-m1-ask-2013.csv")[:2000], ) return ticks @@ -412,7 +412,7 @@ def instrument_status( def l1_feed(): provider = TestDataProvider() updates = [] - for _, row in provider.read_csv_ticks("truefx-usdjpy-ticks.csv").iterrows(): + for _, row in provider.read_csv_ticks("truefx/usdjpy-ticks.csv").iterrows(): for side, order_side in zip(("bid", "ask"), (OrderSide.BUY, OrderSide.SELL)): updates.append( { diff --git a/tests/acceptance_tests/test_backtest.py b/tests/acceptance_tests/test_backtest.py index 829d15a09442..2470648e6951 100644 --- a/tests/acceptance_tests/test_backtest.py +++ b/tests/acceptance_tests/test_backtest.py @@ -90,8 +90,8 @@ def setup(self): wrangler = QuoteTickDataWrangler(instrument=self.usdjpy) provider = TestDataProvider() ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-usdjpy-m1-bid-2013.csv"), - ask_data=provider.read_csv_bars("fxcm-usdjpy-m1-ask-2013.csv"), + bid_data=provider.read_csv_bars("fxcm/usdjpy-m1-bid-2013.csv"), + ask_data=provider.read_csv_bars("fxcm/usdjpy-m1-ask-2013.csv"), ) self.engine.add_instrument(self.usdjpy) self.engine.add_data(ticks) @@ -214,8 +214,8 @@ def setup(self): wrangler = QuoteTickDataWrangler(self.gbpusd) provider = TestDataProvider() ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-gbpusd-m1-bid-2012.csv"), - ask_data=provider.read_csv_bars("fxcm-gbpusd-m1-ask-2012.csv"), + bid_data=provider.read_csv_bars("fxcm/gbpusd-m1-bid-2012.csv"), + ask_data=provider.read_csv_bars("fxcm/gbpusd-m1-ask-2012.csv"), ) self.engine.add_instrument(self.gbpusd) self.engine.add_data(ticks) @@ -347,10 +347,10 @@ def setup(self): # Build externally aggregated bars bid_bars = bid_wrangler.process( - data=provider.read_csv_bars("fxcm-gbpusd-m1-bid-2012.csv"), + data=provider.read_csv_bars("fxcm/gbpusd-m1-bid-2012.csv"), ) ask_bars = ask_wrangler.process( - data=provider.read_csv_bars("fxcm-gbpusd-m1-ask-2012.csv"), + data=provider.read_csv_bars("fxcm/gbpusd-m1-ask-2012.csv"), ) self.engine.add_instrument(self.gbpusd) @@ -583,7 +583,7 @@ def setup(self): # Setup data self.audusd = TestInstrumentProvider.default_fx_ccy("AUD/USD") wrangler = QuoteTickDataWrangler(self.audusd) - ticks = wrangler.process(provider.read_csv_ticks("truefx-audusd-ticks.csv")) + ticks = wrangler.process(provider.read_csv_ticks("truefx/audusd-ticks.csv")) self.engine.add_instrument(self.audusd) self.engine.add_data(ticks) diff --git a/tests/conftest.py b/tests/conftest.py index b031bb82231b..625a74ded178 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,4 +39,4 @@ def fixture_audusd_quote_ticks( audusd_instrument: CurrencyPair, ) -> list[QuoteTick]: wrangler = QuoteTickDataWrangler(instrument=audusd_instrument) - return wrangler.process(data_provider.read_csv_ticks("truefx-audusd-ticks.csv")) + return wrangler.process(data_provider.read_csv_ticks("truefx/audusd-ticks.csv")) diff --git a/tests/integration_tests/infrastructure/test_cache_database.py b/tests/integration_tests/infrastructure/test_cache_database.py index 600144800852..fa8e76d976f5 100644 --- a/tests/integration_tests/infrastructure/test_cache_database.py +++ b/tests/integration_tests/infrastructure/test_cache_database.py @@ -943,8 +943,8 @@ def setup(self): wrangler = QuoteTickDataWrangler(self.usdjpy) provider = TestDataProvider() ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-usdjpy-m1-bid-2013.csv"), - ask_data=provider.read_csv_bars("fxcm-usdjpy-m1-ask-2013.csv"), + bid_data=provider.read_csv_bars("fxcm/usdjpy-m1-bid-2013.csv"), + ask_data=provider.read_csv_bars("fxcm/usdjpy-m1-ask-2013.csv"), ) self.engine.add_instrument(self.usdjpy) self.engine.add_data(ticks) diff --git a/tests/mem_leak_tests/memray_data.py b/tests/mem_leak_tests/memray_data.py index e089abc1cad4..158910674daa 100644 --- a/tests/mem_leak_tests/memray_data.py +++ b/tests/mem_leak_tests/memray_data.py @@ -50,13 +50,13 @@ print(f"Run: {count}/{total_runs}") # Process data - ticks = quote_tick_wrangler.process(provider.read_csv_ticks("truefx-audusd-ticks.csv")) + ticks = quote_tick_wrangler.process(provider.read_csv_ticks("truefx/audusd-ticks.csv")) ticks = trade_tick_wrangler.process(provider.read_csv_ticks("binance/ethusdt-trades.csv")) # Add data bid_bars = bid_wrangler.process( - data=provider.read_csv_bars("fxcm-gbpusd-m1-bid-2012.csv")[:10_000], + data=provider.read_csv_bars("fxcm/gbpusd-m1-bid-2012.csv")[:10_000], ) ask_bars = ask_wrangler.process( - data=provider.read_csv_bars("fxcm-gbpusd-m1-ask-2012.csv")[:10_000], + data=provider.read_csv_bars("fxcm/gbpusd-m1-ask-2012.csv")[:10_000], ) 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 c345937e643a..44793cd6d8aa 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 @@ -85,8 +85,8 @@ def run(*args, **kwargs): # Add data wrangler = QuoteTickDataWrangler(instrument=GBPUSD_SIM) ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-gbpusd-m1-bid-2012.csv"), - ask_data=provider.read_csv_bars("fxcm-gbpusd-m1-ask-2012.csv"), + bid_data=provider.read_csv_bars("fxcm/gbpusd-m1-bid-2012.csv"), + ask_data=provider.read_csv_bars("fxcm/gbpusd-m1-ask-2012.csv"), ) engine.add_data(ticks) 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 8c188a371e72..dc2c21919895 100644 --- a/tests/mem_leak_tests/tracemalloc_market_maker_gbpusd_bars.py +++ b/tests/mem_leak_tests/tracemalloc_market_maker_gbpusd_bars.py @@ -82,8 +82,8 @@ def run(): # Add data wrangler = QuoteTickDataWrangler(GBPUSD_SIM) ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-gbpusd-m1-bid-2012.csv"), - ask_data=provider.read_csv_bars("fxcm-gbpusd-m1-ask-2012.csv"), + bid_data=provider.read_csv_bars("fxcm/gbpusd-m1-bid-2012.csv"), + ask_data=provider.read_csv_bars("fxcm/fbpusd-m1-ask-2012.csv"), ) engine.add_data(ticks) diff --git a/tests/performance_tests/test_perf_backtest.py b/tests/performance_tests/test_perf_backtest.py index cd4dd6220ada..0c44613ff20a 100644 --- a/tests/performance_tests/test_perf_backtest.py +++ b/tests/performance_tests/test_perf_backtest.py @@ -67,8 +67,8 @@ def setup(): wrangler = QuoteTickDataWrangler(USDJPY_SIM) provider = TestDataProvider() ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-usdjpy-m1-bid-2013.csv"), - ask_data=provider.read_csv_bars("fxcm-usdjpy-m1-ask-2013.csv"), + bid_data=provider.read_csv_bars("fxcm/usdjpy-m1-bid-2013.csv"), + ask_data=provider.read_csv_bars("fxcm/usdjpy-m1-ask-2013.csv"), ) engine.add_data(ticks) @@ -103,8 +103,8 @@ def setup(): wrangler = QuoteTickDataWrangler(USDJPY_SIM) provider = TestDataProvider() ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-usdjpy-m1-bid-2013.csv"), - ask_data=provider.read_csv_bars("fxcm-usdjpy-m1-ask-2013.csv"), + bid_data=provider.read_csv_bars("fxcm/usdjpy-m1-bid-2013.csv"), + ask_data=provider.read_csv_bars("fxcm/usdjpy-m1-ask-2013.csv"), ) engine.add_data(ticks) @@ -153,8 +153,8 @@ def setup(): # Setup data wrangler = QuoteTickDataWrangler(USDJPY_SIM) ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-usdjpy-m1-bid-2013.csv"), - ask_data=provider.read_csv_bars("fxcm-usdjpy-m1-ask-2013.csv"), + bid_data=provider.read_csv_bars("fxcm/usdjpy-m1-bid-2013.csv"), + ask_data=provider.read_csv_bars("fxcm/usdjpy-m1-ask-2013.csv"), ) engine.add_data(ticks) diff --git a/tests/performance_tests/test_perf_wranglers.py b/tests/performance_tests/test_perf_wranglers.py index 031809a51302..1c929cddd77e 100644 --- a/tests/performance_tests/test_perf_wranglers.py +++ b/tests/performance_tests/test_perf_wranglers.py @@ -31,7 +31,7 @@ def test_quote_tick_data_wrangler_process_tick_data(self): def wrangler_process(): # 1000 ticks in data wrangler.process( - data=provider.read_csv_ticks("truefx-usdjpy-ticks.csv"), + data=provider.read_csv_ticks("truefx/usdjpy-ticks.csv"), default_volume=1_000_000, ) diff --git a/tests/test_data/fxcm-gbpusd-m1-ask-2012.csv b/tests/test_data/fxcm/gbpusd-m1-ask-2012.csv similarity index 100% rename from tests/test_data/fxcm-gbpusd-m1-ask-2012.csv rename to tests/test_data/fxcm/gbpusd-m1-ask-2012.csv diff --git a/tests/test_data/fxcm-gbpusd-m1-bid-2012.csv b/tests/test_data/fxcm/gbpusd-m1-bid-2012.csv similarity index 100% rename from tests/test_data/fxcm-gbpusd-m1-bid-2012.csv rename to tests/test_data/fxcm/gbpusd-m1-bid-2012.csv diff --git a/tests/test_data/fxcm-usdjpy-m1-ask-2013.csv b/tests/test_data/fxcm/usdjpy-m1-ask-2013.csv similarity index 100% rename from tests/test_data/fxcm-usdjpy-m1-ask-2013.csv rename to tests/test_data/fxcm/usdjpy-m1-ask-2013.csv diff --git a/tests/test_data/fxcm-usdjpy-m1-bid-2013.csv b/tests/test_data/fxcm/usdjpy-m1-bid-2013.csv similarity index 100% rename from tests/test_data/fxcm-usdjpy-m1-bid-2013.csv rename to tests/test_data/fxcm/usdjpy-m1-bid-2013.csv diff --git a/tests/test_data/truefx-audusd-ticks.csv b/tests/test_data/truefx/audusd-ticks.csv similarity index 100% rename from tests/test_data/truefx-audusd-ticks.csv rename to tests/test_data/truefx/audusd-ticks.csv diff --git a/tests/test_data/truefx-usdjpy-ticks.csv b/tests/test_data/truefx/usdjpy-ticks.csv similarity index 100% rename from tests/test_data/truefx-usdjpy-ticks.csv rename to tests/test_data/truefx/usdjpy-ticks.csv diff --git a/tests/unit_tests/backtest/test_data_wranglers.py b/tests/unit_tests/backtest/test_data_wranglers.py index 17ccaab88c01..ce700164976b 100644 --- a/tests/unit_tests/backtest/test_data_wranglers.py +++ b/tests/unit_tests/backtest/test_data_wranglers.py @@ -41,7 +41,7 @@ def setup(self): def test_tick_data(self): # Arrange, Act provider = TestDataProvider() - ticks = provider.read_csv_ticks("truefx-usdjpy-ticks.csv") + ticks = provider.read_csv_ticks("truefx/usdjpy-ticks.csv") # Assert assert len(ticks) == 1000 @@ -55,7 +55,7 @@ def test_process_tick_data(self): # Act ticks = wrangler.process( - data=provider.read_csv_ticks("truefx-usdjpy-ticks.csv"), + data=provider.read_csv_ticks("truefx/usdjpy-ticks.csv"), default_volume=1000000, ) @@ -78,7 +78,7 @@ def test_process_tick_data_with_delta(self): # Act ticks = wrangler.process( - data=provider.read_csv_ticks("truefx-usdjpy-ticks.csv"), + data=provider.read_csv_ticks("truefx/usdjpy-ticks.csv"), default_volume=1000000, ts_init_delta=1_000_500, ) @@ -116,8 +116,8 @@ def test_pre_process_bar_data_with_delta(self): # Arrange usdjpy = TestInstrumentProvider.default_fx_ccy("USD/JPY") provider = TestDataProvider() - bid_data = provider.read_csv_bars("fxcm-usdjpy-m1-bid-2013.csv")[:100] - ask_data = provider.read_csv_bars("fxcm-usdjpy-m1-ask-2013.csv")[:100] + bid_data = provider.read_csv_bars("fxcm/usdjpy-m1-bid-2013.csv")[:100] + ask_data = provider.read_csv_bars("fxcm/usdjpy-m1-ask-2013.csv")[:100] wrangler = QuoteTickDataWrangler(instrument=usdjpy) @@ -143,8 +143,8 @@ def test_pre_process_bar_data_with_random_seed(self): # Arrange usdjpy = TestInstrumentProvider.default_fx_ccy("USD/JPY") provider = TestDataProvider() - bid_data = provider.read_csv_bars("fxcm-usdjpy-m1-bid-2013.csv")[:100] - ask_data = provider.read_csv_bars("fxcm-usdjpy-m1-ask-2013.csv")[:100] + bid_data = provider.read_csv_bars("fxcm/usdjpy-m1-bid-2013.csv")[:100] + ask_data = provider.read_csv_bars("fxcm/usdjpy-m1-ask-2013.csv")[:100] wrangler = QuoteTickDataWrangler(instrument=usdjpy) @@ -253,7 +253,7 @@ def setup(self): def test_process(self): # Arrange, Act provider = TestDataProvider() - bars = self.wrangler.process(provider.read_csv_bars("fxcm-gbpusd-m1-bid-2012.csv")[:1000]) + bars = self.wrangler.process(provider.read_csv_bars("fxcm/gbpusd-m1-bid-2012.csv")[:1000]) # Assert assert len(bars) == 1000 @@ -269,7 +269,7 @@ def test_process_with_default_volume_and_delta(self): # Arrange, Act provider = TestDataProvider() bars = self.wrangler.process( - data=provider.read_csv_bars("fxcm-gbpusd-m1-bid-2012.csv")[:1000], + data=provider.read_csv_bars("fxcm/gbpusd-m1-bid-2012.csv")[:1000], default_volume=10, ts_init_delta=1_000_500, ) diff --git a/tests/unit_tests/backtest/test_engine.py b/tests/unit_tests/backtest/test_engine.py index 0375fbfc709f..23a1e92ebd08 100644 --- a/tests/unit_tests/backtest/test_engine.py +++ b/tests/unit_tests/backtest/test_engine.py @@ -99,8 +99,8 @@ def create_engine(self, config: BacktestEngineConfig | None = None) -> BacktestE wrangler = QuoteTickDataWrangler(self.usdjpy) provider = TestDataProvider() ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-usdjpy-m1-bid-2013.csv")[:2000], - ask_data=provider.read_csv_bars("fxcm-usdjpy-m1-ask-2013.csv")[:2000], + bid_data=provider.read_csv_bars("fxcm/usdjpy-m1-bid-2013.csv")[:2000], + ask_data=provider.read_csv_bars("fxcm/usdjpy-m1-ask-2013.csv")[:2000], ) engine.add_instrument(USDJPY_SIM) engine.add_data(ticks) @@ -320,7 +320,7 @@ def setup(self): def test_add_pyo3_data_raises_type_error(self) -> None: # Arrange - path = TEST_DATA_DIR / "truefx-audusd-ticks.csv" + path = TEST_DATA_DIR / "truefx" / "audusd-ticks.csv" df = pd.read_csv(path) wrangler = wranglers_v2.QuoteTickDataWrangler.from_instrument(AUDUSD_SIM) @@ -502,7 +502,7 @@ def test_add_quote_ticks_adds_to_engine(self): self.engine.add_instrument(AUDUSD_SIM) wrangler = QuoteTickDataWrangler(AUDUSD_SIM) provider = TestDataProvider() - ticks = wrangler.process(provider.read_csv_ticks("truefx-audusd-ticks.csv")) + ticks = wrangler.process(provider.read_csv_ticks("truefx/audusd-ticks.csv")) # Act self.engine.add_data(ticks) @@ -543,7 +543,7 @@ def test_add_bars_adds_to_engine(self): instrument=USDJPY_SIM, ) provider = TestDataProvider() - bars = wrangler.process(provider.read_csv_bars("fxcm-usdjpy-m1-bid-2013.csv")[:2000]) + bars = wrangler.process(provider.read_csv_bars("fxcm/usdjpy-m1-bid-2013.csv")[:2000]) # Act self.engine.add_instrument(USDJPY_SIM) @@ -621,8 +621,8 @@ def setup(self): ) provider = TestDataProvider() - bid_bars = bid_wrangler.process(provider.read_csv_bars("fxcm-gbpusd-m1-bid-2012.csv")) - ask_bars = ask_wrangler.process(provider.read_csv_bars("fxcm-gbpusd-m1-ask-2012.csv")) + bid_bars = bid_wrangler.process(provider.read_csv_bars("fxcm/gbpusd-m1-bid-2012.csv")) + ask_bars = ask_wrangler.process(provider.read_csv_bars("fxcm/gbpusd-m1-ask-2012.csv")) # Add data self.engine.add_instrument(GBPUSD_SIM) diff --git a/tests/unit_tests/backtest/test_modules.py b/tests/unit_tests/backtest/test_modules.py index e1c3aa3ec85d..cc1d7cb2bd97 100644 --- a/tests/unit_tests/backtest/test_modules.py +++ b/tests/unit_tests/backtest/test_modules.py @@ -38,7 +38,7 @@ class TestSimulationModules: - def create_engine(self, modules: list): + def create_engine(self, modules: list) -> BacktestEngine: engine = BacktestEngine(BacktestEngineConfig(logging=LoggingConfig(bypass_logging=True))) engine.add_venue( venue=Venue("SIM"), @@ -51,8 +51,8 @@ def create_engine(self, modules: list): wrangler = QuoteTickDataWrangler(USDJPY_SIM) provider = TestDataProvider() ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-usdjpy-m1-bid-2013.csv")[:10], - ask_data=provider.read_csv_bars("fxcm-usdjpy-m1-ask-2013.csv")[:10], + bid_data=provider.read_csv_bars("fxcm/usdjpy-m1-bid-2013.csv")[:10], + ask_data=provider.read_csv_bars("fxcm/usdjpy-m1-ask-2013.csv")[:10], ) engine.add_instrument(USDJPY_SIM) engine.add_data(ticks) diff --git a/tests/unit_tests/cache/test_execution.py b/tests/unit_tests/cache/test_execution.py index df94d619f8b0..f9b337c59a43 100644 --- a/tests/unit_tests/cache/test_execution.py +++ b/tests/unit_tests/cache/test_execution.py @@ -1341,8 +1341,8 @@ def setup(self): wrangler = QuoteTickDataWrangler(self.usdjpy) provider = TestDataProvider() ticks = wrangler.process_bar_data( - bid_data=provider.read_csv_bars("fxcm-usdjpy-m1-bid-2013.csv"), - ask_data=provider.read_csv_bars("fxcm-usdjpy-m1-ask-2013.csv"), + bid_data=provider.read_csv_bars("fxcm/usdjpy-m1-bid-2013.csv"), + ask_data=provider.read_csv_bars("fxcm/usdjpy-m1-ask-2013.csv"), ) self.engine.add_instrument(self.usdjpy) self.engine.add_data(ticks) diff --git a/tests/unit_tests/data/test_aggregation.py b/tests/unit_tests/data/test_aggregation.py index 1b5602200694..215575b8702c 100644 --- a/tests/unit_tests/data/test_aggregation.py +++ b/tests/unit_tests/data/test_aggregation.py @@ -439,7 +439,7 @@ def test_run_quote_ticks_through_aggregator_results_in_expected_bars(self): # Setup data wrangler = QuoteTickDataWrangler(instrument) provider = TestDataProvider() - ticks = wrangler.process(provider.read_csv_ticks("truefx-audusd-ticks.csv")[:1000]) + ticks = wrangler.process(provider.read_csv_ticks("truefx/audusd-ticks.csv")[:1000]) # Act for tick in ticks: @@ -805,7 +805,7 @@ def test_run_quote_ticks_through_aggregator_results_in_expected_bars(self): wrangler = QuoteTickDataWrangler(instrument) provider = TestDataProvider() ticks = wrangler.process( - data=provider.read_csv_ticks("truefx-audusd-ticks.csv")[:10000], + data=provider.read_csv_ticks("truefx/audusd-ticks.csv")[:10000], default_volume=1, ) @@ -1050,7 +1050,7 @@ def test_run_quote_ticks_through_aggregator_results_in_expected_bars(self): wrangler = QuoteTickDataWrangler(AUDUSD_SIM) provider = TestDataProvider() ticks = wrangler.process( - data=provider.read_csv_ticks("truefx-audusd-ticks.csv")[:10000], + data=provider.read_csv_ticks("truefx/audusd-ticks.csv")[:10000], default_volume=1, ) diff --git a/tests/unit_tests/persistence/test_transformer.py b/tests/unit_tests/persistence/test_transformer.py index b12d035cb718..fcd8d095e223 100644 --- a/tests/unit_tests/persistence/test_transformer.py +++ b/tests/unit_tests/persistence/test_transformer.py @@ -37,7 +37,7 @@ def test_pyo3_quote_ticks_to_record_batch_reader() -> None: # Arrange - path = TEST_DATA_DIR / "truefx-audusd-ticks.csv" + path = TEST_DATA_DIR / "truefx" / "audusd-ticks.csv" df = pd.read_csv(path) # Act diff --git a/tests/unit_tests/persistence/test_wranglers_v2.py b/tests/unit_tests/persistence/test_wranglers_v2.py index cb68636c452b..b6713242a219 100644 --- a/tests/unit_tests/persistence/test_wranglers_v2.py +++ b/tests/unit_tests/persistence/test_wranglers_v2.py @@ -29,7 +29,7 @@ def test_quote_tick_data_wrangler() -> None: # Arrange - path = TEST_DATA_DIR / "truefx-audusd-ticks.csv" + path = TEST_DATA_DIR / "truefx" / "audusd-ticks.csv" df = pd.read_csv(path) # Act From f599c6738c735931b89176aece6c6adb1bb8b940 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 30 Oct 2023 08:42:48 +1100 Subject: [PATCH 59/78] Add Databento and update dependencies --- README.md | 17 ++++++++------- nautilus_core/Cargo.lock | 10 ++++----- poetry.lock | 47 +++++++++++++++++++++++++++++++++++++++- pyproject.toml | 4 +++- 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 32edd13788b2..6caf598ff42c 100644 --- a/README.md +++ b/README.md @@ -139,14 +139,15 @@ NautilusTrader is designed in a modular way to work with 'adapters' which provid connectivity to data publishers and/or trading venues - converting their raw API into a unified interface. The following integrations are currently supported: -| Name | ID | Type | Status | Docs | -| :-------------------------------------------------------- | :-------- | :---------------------- | :------------------------------------------------------ | :---------------------------------------------------------------- | -| [Betfair](https://betfair.com) | `BETFAIR` | Sports Betting Exchange | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/betfair.html) | -| [Binance](https://binance.com) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | -| [Binance US](https://binance.us) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | -| [Binance Futures](https://www.binance.com/en/futures) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | -| [Bybit](https://www.bybit.com) | `BYBIT` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/building-orange) | | -| [Interactive Brokers](https://www.interactivebrokers.com) | `IB` | Brokerage (multi-venue) | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/ib.html) | +| Name | ID | Type | Status | Docs | +| :-------------------------------------------------------- | :---------- | :---------------------- | :------------------------------------------------------ | :---------------------------------------------------------------- | +| [Betfair](https://betfair.com) | `BETFAIR` | Sports Betting Exchange | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/betfair.html) | +| [Binance](https://binance.com) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | +| [Binance US](https://binance.us) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | +| [Binance Futures](https://www.binance.com/en/futures) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | +| [Bybit](https://www.bybit.com) | `BYBIT` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/building-orange) | | +| [Databento](https://databento.com) | `DATABENTO` | Data provider | ![status](https://img.shields.io/badge/building-orange) | | +| [Interactive Brokers](https://www.interactivebrokers.com) | `IB` | Brokerage (multi-venue) | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/ib.html) | Refer to the [Integrations](https://docs.nautilustrader.io/integrations/index.html) documentation for further details. diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 56eeac084eb8..6ac3afacb7aa 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -3791,7 +3791,7 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" version = "0.20.1" -source = "git+https://github.com/snapview/tungstenite-rs#3d9fd1e5cb8b78e930eead4604e7ba9debec618e" +source = "git+https://github.com/snapview/tungstenite-rs#272d83c4309b68838a139b6288bb94d166ffd7b1" dependencies = [ "byteorder", "bytes", @@ -4155,18 +4155,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7d7c7970ca2215b8c1ccf4d4f354c4733201dfaaba72d44ae5b37472e4901" +checksum = "dd66a62464e3ffd4e37bd09950c2b9dd6c4f8767380fabba0d523f9a775bc85a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b27b1bb92570f989aac0ab7e9cbfbacdd65973f7ee920d9f0e71ebac878fd0b" +checksum = "255c4596d41e6916ced49cfafea18727b24d67878fa180ddfd69b9df34fd1726" dependencies = [ "proc-macro2", "quote", diff --git a/poetry.lock b/poetry.lock index dc9f8a2bb0ec..7b10947b6e71 100644 --- a/poetry.lock +++ b/poetry.lock @@ -650,6 +650,50 @@ files = [ {file = "Cython-3.0.4.tar.gz", hash = "sha256:2e379b491ee985d31e5faaf050f79f4a8f59f482835906efe4477b33b4fbe9ff"}, ] +[[package]] +name = "databento-dbn" +version = "0.13.0" +description = "Python bindings for encoding and decoding Databento Binary Encoding (DBN)" +optional = true +python-versions = ">=3.8" +files = [ + {file = "databento_dbn-0.13.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:e5a0f98d407f78bf77c744b570e52df4a372bf6f4dbeef17cd226a8ab712395a"}, + {file = "databento_dbn-0.13.0-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:36a2878c48004efb6dfccef468880ef507eb88a63264d7c5d3f03d96b8059360"}, + {file = "databento_dbn-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8776feb864b6be0c1a606866d8cca5cd85b0897d8bd3bac3a4b9e2bad5db0742"}, + {file = "databento_dbn-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9490be8be0742f028312054df0136916ee74983d0058a15e8d28cfbd474c6d2f"}, + {file = "databento_dbn-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:53d7c9464f95be5750e00036cb3e421c338adb04faee159839ee8a3429d11f93"}, + {file = "databento_dbn-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0bb0e0914af503db94896a1961b5b56479f1bd7a4285890402f02087ab520090"}, + {file = "databento_dbn-0.13.0-cp310-none-win_amd64.whl", hash = "sha256:f22c61baa7181e6071dcdad59363d8f78e8c8d324d0563f6e56608a45ab14655"}, + {file = "databento_dbn-0.13.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:01865c192cc3963eb9a3e099a0261bb585b647f8a491d994bcf61136678f1706"}, + {file = "databento_dbn-0.13.0-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:88c4652d6ce7c5c0ab7a30f2d314cede5136f8ea96c6bfe99f2b8ec718dc23d4"}, + {file = "databento_dbn-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12792ab9fc7fd407d54c04016fd70789080ad7089f516043da1a70c595435099"}, + {file = "databento_dbn-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0a06c53a8f978ecdd7d33875956e4244e8f626cb798fb00f385d8e3bbd15a72"}, + {file = "databento_dbn-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:28f48b344cba7f51a0f97959b8c0f3063a87d44af5d65ef293c8194709a60674"}, + {file = "databento_dbn-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0f81c5d812dc41011ae898aef6459e24056d4ebab7490b304a6f51facddf0da0"}, + {file = "databento_dbn-0.13.0-cp311-none-win_amd64.whl", hash = "sha256:184e891a2ba101bf18cb7c25fc448fc74f42f73aca7bcdcb2458f474ca44aac6"}, + {file = "databento_dbn-0.13.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:f61bfef8aca879a8a893c3379df155809236733cb66903ffc0b994f62f1a669f"}, + {file = "databento_dbn-0.13.0-cp312-cp312-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:01bf4a68228ce4e29f06fa49258f5e0e5c3e9b174bf69ce9ea5102488291cd0e"}, + {file = "databento_dbn-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b231e43ce16a0902539d0797cfb7a418f43b788f442ac8249d480a05e340bd3"}, + {file = "databento_dbn-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:479527dc31cd01b77f73404425e42409e3cc93f2e8af523d1ef9cbfb06eb4da2"}, + {file = "databento_dbn-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:436a208e584d8e6bca611fc8e2495b6b1e921799a8c160ca1724789c14015d3f"}, + {file = "databento_dbn-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:80dbb3065f173a0128e7f317231dd287767445c57260a9dae0acdf47e21c2245"}, + {file = "databento_dbn-0.13.0-cp312-none-win_amd64.whl", hash = "sha256:bccfac8243adb2cac65ca83cad1493a9e1e4a22b7b62b8f87edcad2c3b359e06"}, + {file = "databento_dbn-0.13.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:2029969086dd39efbe648ebb7bfbbf2106a80aaef7213833987dd903ba2e79eb"}, + {file = "databento_dbn-0.13.0-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:ec6dbb2d88919384bc28e2e42c9041af75dee4014b4dc00009f508ade6e467d2"}, + {file = "databento_dbn-0.13.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a5bfc8cc8eeab9bb1444d1967f8b21b8a0e4410c26b9482ed0e7d1b7ab4b924"}, + {file = "databento_dbn-0.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23de18bd3ce830946724c22e44c6757c67e9095b3e738a4fb734ef3747932a3d"}, + {file = "databento_dbn-0.13.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0883ca5da8b79ea560c59f72010c4f3b87c9953d1d343a4fb77c5ab48a401ffa"}, + {file = "databento_dbn-0.13.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:067516157dd382fe9b838b518a38850286b4b1aeab1b5f2a28b20ac5204d3e2c"}, + {file = "databento_dbn-0.13.0-cp38-none-win_amd64.whl", hash = "sha256:c1e3a474dbff0df1d75a8dca1c3e4d8b1da53e769c0e7a5d8068228933971489"}, + {file = "databento_dbn-0.13.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:90615411aac548b7ec7efee164661d4f5be846e005624ddaed76b65fd986d9b7"}, + {file = "databento_dbn-0.13.0-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e289e59b90d996acf13ab8915cee6ba071b7c662ef6d3592a2a4d97a3e64656b"}, + {file = "databento_dbn-0.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57c36a0eb92f0d502767e0efdebfb38e30e38e794190d49e423f5345d4e2569e"}, + {file = "databento_dbn-0.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8bdf8cdb84cc1d9b29d5aa0c284c9bb1b661ebb9291d7c591ff9cf82e0a8d5a"}, + {file = "databento_dbn-0.13.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7e70dd903f35569515918c69196fb33cfab8f3b28cf412c65e6a7c64ff996e6d"}, + {file = "databento_dbn-0.13.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:390ac47dfc8f269b8d8b923ff60c20911595633d4e30aa991b69c69a704e33b5"}, + {file = "databento_dbn-0.13.0-cp39-none-win_amd64.whl", hash = "sha256:29fc25877adf5f52712bc8b61ca30c2dd918a66dee24ee1cda9429fc6971c56e"}, +] + [[package]] name = "distlib" version = "0.3.7" @@ -2880,6 +2924,7 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [extras] betfair = ["betfair_parser"] +databento = ["databento_dbn"] docker = ["docker"] ib = ["async-timeout", "nautilus_ibapi"] redis = ["hiredis", "redis"] @@ -2887,4 +2932,4 @@ redis = ["hiredis", "redis"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "afd2b03a92fafe97c0100246d884b8d0909849fecfe2ab61570cb10fd2b70a5b" +content-hash = "605d59aa86888014a352d680b8f34e5619bacd8701e492d7fea03e2f6eb09ac1" diff --git a/pyproject.toml b/pyproject.toml index b9b9a191ab96..52f51684b759 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,15 +65,17 @@ pyarrow = ">=12.0.1" # Minimum version set one major version behind the latest pytz = "^2023.3.0" tqdm = "^4.66.1" uvloop = {version = "^0.19.0", markers = "sys_platform != 'win32'"} +databento_dbn = {version = "^0.13.0", optional = true} +docker = {version = "^6.1.3", optional = true} hiredis = {version = "^2.2.3", optional = true} redis = {version = "^5.0.1", optional = true} -docker = {version = "^6.1.3", optional = true} async-timeout = {version = "^4.0.3", optional = true} nautilus_ibapi = {version = "==1019.1", optional = true} # Pinned for stability betfair_parser = {version = "==0.7.1", optional = true} # Pinned for stability [tool.poetry.extras] betfair = ["betfair_parser"] +databento = ["databento_dbn"] docker = ["docker"] ib = ["nautilus_ibapi", "async-timeout"] redis = ["hiredis", "redis"] From 2216cf671c109b8bb7002d44cc87771ec9f06a78 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 30 Oct 2023 08:45:19 +1100 Subject: [PATCH 60/78] Tweak casing in README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6caf598ff42c..3c1e533293b5 100644 --- a/README.md +++ b/README.md @@ -141,11 +141,11 @@ into a unified interface. The following integrations are currently supported: | Name | ID | Type | Status | Docs | | :-------------------------------------------------------- | :---------- | :---------------------- | :------------------------------------------------------ | :---------------------------------------------------------------- | -| [Betfair](https://betfair.com) | `BETFAIR` | Sports Betting Exchange | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/betfair.html) | -| [Binance](https://binance.com) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | -| [Binance US](https://binance.us) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | -| [Binance Futures](https://www.binance.com/en/futures) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | -| [Bybit](https://www.bybit.com) | `BYBIT` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/building-orange) | | +| [Betfair](https://betfair.com) | `BETFAIR` | Sports betting exchange | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/betfair.html) | +| [Binance](https://binance.com) | `BINANCE` | Crypto exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | +| [Binance US](https://binance.us) | `BINANCE` | Crypto exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | +| [Binance Futures](https://www.binance.com/en/futures) | `BINANCE` | Crypto exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](https://docs.nautilustrader.io/integrations/binance.html) | +| [Bybit](https://www.bybit.com) | `BYBIT` | Crypto exchange (CEX) | ![status](https://img.shields.io/badge/building-orange) | | | [Databento](https://databento.com) | `DATABENTO` | Data provider | ![status](https://img.shields.io/badge/building-orange) | | | [Interactive Brokers](https://www.interactivebrokers.com) | `IB` | Brokerage (multi-venue) | ![status](https://img.shields.io/badge/beta-yellow) | [Guide](https://docs.nautilustrader.io/integrations/ib.html) | From 19e63fd8044ef21f92086ab301091591010773e2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 30 Oct 2023 15:47:27 +1100 Subject: [PATCH 61/78] Update databento dependency --- poetry.lock | 93 +++++++++++++++++++++++++++++++++++++++++++++----- pyproject.toml | 4 +-- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7b10947b6e71..124672157d13 100644 --- a/poetry.lock +++ b/poetry.lock @@ -577,7 +577,7 @@ name = "css-html-js-minify" version = "2.5.5" description = "CSS HTML JS Minifier" optional = false -python-versions = ">=3.6" +python-versions = "*" files = [ {file = "css-html-js-minify-2.5.5.zip", hash = "sha256:4a9f11f7e0496f5284d12111f3ba4ff5ff2023d12f15d195c9c48bd97013746c"}, {file = "css_html_js_minify-2.5.5-py2.py3-none-any.whl", hash = "sha256:3da9d35ac0db8ca648c1b543e0e801d7ca0bab9e6bfd8418fee59d5ae001727a"}, @@ -650,6 +650,25 @@ files = [ {file = "Cython-3.0.4.tar.gz", hash = "sha256:2e379b491ee985d31e5faaf050f79f4a8f59f482835906efe4477b33b4fbe9ff"}, ] +[[package]] +name = "databento" +version = "0.23.0" +description = "Official Python client library for Databento" +optional = true +python-versions = ">=3.8,<4.0" +files = [ + {file = "databento-0.23.0-py3-none-any.whl", hash = "sha256:e47458fd46b0b54f5d5293baf07966c419c5a9e009cb1f85cd2da4452e54e226"}, + {file = "databento-0.23.0.tar.gz", hash = "sha256:0e02570349a1b1af1cf2de5ab08b1139bc344b138917de31f204777932f34fcf"}, +] + +[package.dependencies] +aiohttp = ">=3.8.3,<4.0.0" +databento-dbn = "0.13.0" +numpy = ">=1.23.5" +pandas = ">=1.5.3" +requests = ">=2.24.0" +zstandard = ">=0.21.0" + [[package]] name = "databento-dbn" version = "0.13.0" @@ -1899,7 +1918,7 @@ name = "pycparser" version = "2.21" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "*" files = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, @@ -2589,13 +2608,13 @@ telegram = ["requests"] [[package]] name = "types-pyopenssl" -version = "23.2.0.2" +version = "23.3.0.0" description = "Typing stubs for pyOpenSSL" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "types-pyOpenSSL-23.2.0.2.tar.gz", hash = "sha256:6a010dac9ecd42b582d7dd2cc3e9e40486b79b3b64bb2fffba1474ff96af906d"}, - {file = "types_pyOpenSSL-23.2.0.2-py3-none-any.whl", hash = "sha256:19536aa3debfbe25a918cf0d898e9f5fbbe6f3594a429da7914bf331deb1b342"}, + {file = "types-pyOpenSSL-23.3.0.0.tar.gz", hash = "sha256:5ffb077fe70b699c88d5caab999ae80e192fe28bf6cda7989b7e79b1e4e2dcd3"}, + {file = "types_pyOpenSSL-23.3.0.0-py3-none-any.whl", hash = "sha256:00171433653265843b7469ddb9f3c86d698668064cc33ef10537822156130ebf"}, ] [package.dependencies] @@ -2922,9 +2941,67 @@ files = [ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] +[[package]] +name = "zstandard" +version = "0.21.0" +description = "Zstandard bindings for Python" +optional = true +python-versions = ">=3.7" +files = [ + {file = "zstandard-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:649a67643257e3b2cff1c0a73130609679a5673bf389564bc6d4b164d822a7ce"}, + {file = "zstandard-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:144a4fe4be2e747bf9c646deab212666e39048faa4372abb6a250dab0f347a29"}, + {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b72060402524ab91e075881f6b6b3f37ab715663313030d0ce983da44960a86f"}, + {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8257752b97134477fb4e413529edaa04fc0457361d304c1319573de00ba796b1"}, + {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c053b7c4cbf71cc26808ed67ae955836232f7638444d709bfc302d3e499364fa"}, + {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2769730c13638e08b7a983b32cb67775650024632cd0476bf1ba0e6360f5ac7d"}, + {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7d3bc4de588b987f3934ca79140e226785d7b5e47e31756761e48644a45a6766"}, + {file = "zstandard-0.21.0-cp310-cp310-win32.whl", hash = "sha256:67829fdb82e7393ca68e543894cd0581a79243cc4ec74a836c305c70a5943f07"}, + {file = "zstandard-0.21.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6048a287f8d2d6e8bc67f6b42a766c61923641dd4022b7fd3f7439e17ba5a4d"}, + {file = "zstandard-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7f2afab2c727b6a3d466faee6974a7dad0d9991241c498e7317e5ccf53dbc766"}, + {file = "zstandard-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff0852da2abe86326b20abae912d0367878dd0854b8931897d44cfeb18985472"}, + {file = "zstandard-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12fa383e315b62630bd407477d750ec96a0f438447d0e6e496ab67b8b451d39"}, + {file = "zstandard-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1b9703fe2e6b6811886c44052647df7c37478af1b4a1a9078585806f42e5b15"}, + {file = "zstandard-0.21.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df28aa5c241f59a7ab524f8ad8bb75d9a23f7ed9d501b0fed6d40ec3064784e8"}, + {file = "zstandard-0.21.0-cp311-cp311-win32.whl", hash = "sha256:0aad6090ac164a9d237d096c8af241b8dcd015524ac6dbec1330092dba151657"}, + {file = "zstandard-0.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:48b6233b5c4cacb7afb0ee6b4f91820afbb6c0e3ae0fa10abbc20000acdf4f11"}, + {file = "zstandard-0.21.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e7d560ce14fd209db6adacce8908244503a009c6c39eee0c10f138996cd66d3e"}, + {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e6e131a4df2eb6f64961cea6f979cdff22d6e0d5516feb0d09492c8fd36f3bc"}, + {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1e0c62a67ff425927898cf43da2cf6b852289ebcc2054514ea9bf121bec10a5"}, + {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1545fb9cb93e043351d0cb2ee73fa0ab32e61298968667bb924aac166278c3fc"}, + {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe6c821eb6870f81d73bf10e5deed80edcac1e63fbc40610e61f340723fd5f7c"}, + {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ddb086ea3b915e50f6604be93f4f64f168d3fc3cef3585bb9a375d5834392d4f"}, + {file = "zstandard-0.21.0-cp37-cp37m-win32.whl", hash = "sha256:57ac078ad7333c9db7a74804684099c4c77f98971c151cee18d17a12649bc25c"}, + {file = "zstandard-0.21.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1243b01fb7926a5a0417120c57d4c28b25a0200284af0525fddba812d575f605"}, + {file = "zstandard-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ea68b1ba4f9678ac3d3e370d96442a6332d431e5050223626bdce748692226ea"}, + {file = "zstandard-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8070c1cdb4587a8aa038638acda3bd97c43c59e1e31705f2766d5576b329e97c"}, + {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af612c96599b17e4930fe58bffd6514e6c25509d120f4eae6031b7595912f85"}, + {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff891e37b167bc477f35562cda1248acc115dbafbea4f3af54ec70821090965"}, + {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9fec02ce2b38e8b2e86079ff0b912445495e8ab0b137f9c0505f88ad0d61296"}, + {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdbe350691dec3078b187b8304e6a9c4d9db3eb2d50ab5b1d748533e746d099"}, + {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b69cccd06a4a0a1d9fb3ec9a97600055cf03030ed7048d4bcb88c574f7895773"}, + {file = "zstandard-0.21.0-cp38-cp38-win32.whl", hash = "sha256:9980489f066a391c5572bc7dc471e903fb134e0b0001ea9b1d3eff85af0a6f1b"}, + {file = "zstandard-0.21.0-cp38-cp38-win_amd64.whl", hash = "sha256:0e1e94a9d9e35dc04bf90055e914077c80b1e0c15454cc5419e82529d3e70728"}, + {file = "zstandard-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2d61675b2a73edcef5e327e38eb62bdfc89009960f0e3991eae5cc3d54718de"}, + {file = "zstandard-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25fbfef672ad798afab12e8fd204d122fca3bc8e2dcb0a2ba73bf0a0ac0f5f07"}, + {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62957069a7c2626ae80023998757e27bd28d933b165c487ab6f83ad3337f773d"}, + {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e10ed461e4807471075d4b7a2af51f5234c8f1e2a0c1d37d5ca49aaaad49e8"}, + {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cff89a036c639a6a9299bf19e16bfb9ac7def9a7634c52c257166db09d950e7"}, + {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52b2b5e3e7670bd25835e0e0730a236f2b0df87672d99d3bf4bf87248aa659fb"}, + {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b1367da0dde8ae5040ef0413fb57b5baeac39d8931c70536d5f013b11d3fc3a5"}, + {file = "zstandard-0.21.0-cp39-cp39-win32.whl", hash = "sha256:db62cbe7a965e68ad2217a056107cc43d41764c66c895be05cf9c8b19578ce9c"}, + {file = "zstandard-0.21.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8d200617d5c876221304b0e3fe43307adde291b4a897e7b0617a61611dfff6a"}, + {file = "zstandard-0.21.0.tar.gz", hash = "sha256:f08e3a10d01a247877e4cb61a82a319ea746c356a3786558bed2481e6c405546"}, +] + +[package.dependencies] +cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\""} + +[package.extras] +cffi = ["cffi (>=1.11)"] + [extras] betfair = ["betfair_parser"] -databento = ["databento_dbn"] +databento = ["databento"] docker = ["docker"] ib = ["async-timeout", "nautilus_ibapi"] redis = ["hiredis", "redis"] @@ -2932,4 +3009,4 @@ redis = ["hiredis", "redis"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "605d59aa86888014a352d680b8f34e5619bacd8701e492d7fea03e2f6eb09ac1" +content-hash = "a08aadbf4372178c53b429f24527fb57d89d69c3e17d5cd41ebe29c72090d880" diff --git a/pyproject.toml b/pyproject.toml index 52f51684b759..127d30c31336 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ pyarrow = ">=12.0.1" # Minimum version set one major version behind the latest pytz = "^2023.3.0" tqdm = "^4.66.1" uvloop = {version = "^0.19.0", markers = "sys_platform != 'win32'"} -databento_dbn = {version = "^0.13.0", optional = true} +databento = {version = "^0.23.0", optional = true} docker = {version = "^6.1.3", optional = true} hiredis = {version = "^2.2.3", optional = true} redis = {version = "^5.0.1", optional = true} @@ -75,7 +75,7 @@ betfair_parser = {version = "==0.7.1", optional = true} # Pinned for stability [tool.poetry.extras] betfair = ["betfair_parser"] -databento = ["databento_dbn"] +databento = ["databento"] docker = ["docker"] ib = ["nautilus_ibapi", "async-timeout"] redis = ["hiredis", "redis"] From fd701b236598981518cffaa27f328e47081d48c2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 30 Oct 2023 16:01:29 +1100 Subject: [PATCH 62/78] Refine OrderManager and reduce duplication --- nautilus_trader/execution/emulator.pyx | 4 +- nautilus_trader/execution/manager.pxd | 3 +- nautilus_trader/execution/manager.pyx | 11 +++--- nautilus_trader/trading/strategy.pxd | 7 ---- nautilus_trader/trading/strategy.pyx | 52 ++++++++------------------ 5 files changed, 25 insertions(+), 52 deletions(-) diff --git a/nautilus_trader/execution/emulator.pyx b/nautilus_trader/execution/emulator.pyx index 0acc309f2f04..1b7a6703ee3d 100644 --- a/nautilus_trader/execution/emulator.pyx +++ b/nautilus_trader/execution/emulator.pyx @@ -722,7 +722,7 @@ cdef class OrderEmulator(Actor): ) if order.exec_algorithm_id is not None: - self._manager.send_algo_command(command) + self._manager.send_algo_command(command, order.exec_algorithm_id) else: self._manager.send_exec_command(command) @@ -794,7 +794,7 @@ cdef class OrderEmulator(Actor): ) if order.exec_algorithm_id is not None: - self._manager.send_algo_command(command) + self._manager.send_algo_command(command, order.exec_algorithm_id) else: self._manager.send_exec_command(command) diff --git a/nautilus_trader/execution/manager.pxd b/nautilus_trader/execution/manager.pxd index 6ec6eaf2bd58..a9cc920aa4c3 100644 --- a/nautilus_trader/execution/manager.pxd +++ b/nautilus_trader/execution/manager.pxd @@ -34,6 +34,7 @@ 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 from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.identifiers cimport PositionId from nautilus_trader.model.identifiers cimport StrategyId @@ -84,7 +85,7 @@ cdef class OrderManager: # -- EGRESS --------------------------------------------------------------------------------------- cpdef void send_emulator_command(self, TradingCommand command) - cpdef void send_algo_command(self, TradingCommand command) + cpdef void send_algo_command(self, TradingCommand command, ExecAlgorithmId exec_algorithm_id) cpdef void send_risk_command(self, TradingCommand command) cpdef void send_exec_command(self, TradingCommand command) cpdef void send_risk_event(self, OrderEvent event) diff --git a/nautilus_trader/execution/manager.pyx b/nautilus_trader/execution/manager.pyx index da6902e2840c..4f62c9678ac9 100644 --- a/nautilus_trader/execution/manager.pyx +++ b/nautilus_trader/execution/manager.pyx @@ -77,8 +77,8 @@ cdef class OrderManager: The handler to call when submitting orders. cancel_order_handler : Callable[[Order], None], optional The handler to call when canceling orders. - modify_order_handler : Callable[[Order], None], optional - The handler to call when modifying orders. + modify_order_handler : Callable[[Order, Quantity], None], optional + The handler to call when modifying orders (limited to modifying quantity). debug : bool, default False If debug mode is active (will provide extra debug logging). @@ -258,7 +258,7 @@ cdef class OrderManager: self.cache_submit_order_command(submit) if order.exec_algorithm_id is not None: - self.send_algo_command(submit) + self.send_algo_command(submit, order.exec_algorithm_id) else: self.send_risk_command(submit) else: @@ -561,12 +561,13 @@ cdef class OrderManager: 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): + cpdef void send_algo_command(self, TradingCommand command, ExecAlgorithmId exec_algorithm_id): Condition.not_none(command, "command") + Condition.not_none(exec_algorithm_id, "exec_algorithm_id") 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) + self._msgbus.send(endpoint=f"{exec_algorithm_id}.execute", msg=command) cpdef void send_risk_command(self, TradingCommand command): Condition.not_none(command, "command") diff --git a/nautilus_trader/trading/strategy.pxd b/nautilus_trader/trading/strategy.pxd index 8f9b1912680d..a1734d050c6f 100644 --- a/nautilus_trader/trading/strategy.pxd +++ b/nautilus_trader/trading/strategy.pxd @@ -174,10 +174,3 @@ cdef class Strategy(Actor): cdef OrderPendingCancel _generate_order_pending_cancel(self, Order order) cdef void _deny_order(self, Order order, str reason) cdef void _deny_order_list(self, OrderList order_list, str reason) - -# -- EGRESS --------------------------------------------------------------------------------------- - - cdef void _send_emulator_command(self, TradingCommand command) - cdef void _send_algo_command(self, TradingCommand command, ExecAlgorithmId exec_algorithm_id) - cdef void _send_risk_command(self, TradingCommand command) - cdef void _send_exec_command(self, TradingCommand command) diff --git a/nautilus_trader/trading/strategy.pyx b/nautilus_trader/trading/strategy.pyx index b9ff728e2e05..508f47839c08 100644 --- a/nautilus_trader/trading/strategy.pyx +++ b/nautilus_trader/trading/strategy.pyx @@ -804,11 +804,11 @@ cdef class Strategy(Actor): # Route order if order.emulation_trigger != TriggerType.NO_TRIGGER: - self._send_emulator_command(command) + self._manager.send_emulator_command(command) elif order.exec_algorithm_id is not None: - self._send_algo_command(command, order.exec_algorithm_id) + self._manager.send_algo_command(command, order.exec_algorithm_id) else: - self._send_risk_command(command) + self._manager.send_risk_command(command) cpdef void submit_order_list( self, @@ -901,11 +901,11 @@ cdef class Strategy(Actor): # Route order if command.has_emulated_order: - self._send_emulator_command(command) + self._manager.send_emulator_command(command) elif order_list.first.exec_algorithm_id is not None: - self._send_algo_command(command, order_list.first.exec_algorithm_id) + self._manager.send_algo_command(command, order_list.first.exec_algorithm_id) else: - self._send_risk_command(command) + self._manager.send_risk_command(command) cpdef void modify_order( self, @@ -973,9 +973,9 @@ cdef class Strategy(Actor): return if order.is_emulated_c(): - self._send_emulator_command(command) + self._manager.send_emulator_command(command) else: - self._send_risk_command(command) + self._manager.send_risk_command(command) cpdef void cancel_order(self, Order order, ClientId client_id = None): """ @@ -1004,11 +1004,11 @@ cdef class Strategy(Actor): return if order.is_emulated_c() or order.emulation_trigger != TriggerType.NO_TRIGGER: - self._send_emulator_command(command) + self._manager.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) + self._manager.send_algo_command(command, order.exec_algorithm_id) else: - self._send_exec_command(command) + self._manager.send_exec_command(command) # Cancel any GTD expiry timer if self.manage_gtd_expiry: @@ -1087,7 +1087,7 @@ cdef class Strategy(Actor): client_id=client_id, ) - self._send_exec_command(command) + self._manager.send_exec_command(command) cpdef void cancel_all_orders( self, @@ -1185,8 +1185,8 @@ cdef class Strategy(Actor): if order.strategy_id == self.id and not order.is_closed_c(): self.cancel_order(order) - self._send_exec_command(command) - self._send_emulator_command(command) + self._manager.send_exec_command(command) + self._manager.send_emulator_command(command) cpdef void close_position( self, @@ -1317,7 +1317,7 @@ cdef class Strategy(Actor): client_id=client_id, ) - self._send_exec_command(command) + self._manager.send_exec_command(command) cdef ModifyOrder _create_modify_order( self, @@ -1666,25 +1666,3 @@ cdef class Strategy(Actor): for order in order_list.orders: if not order.is_closed_c(): self._deny_order(order=order, reason=reason) - -# -- 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): - if not self.log.is_bypassed: - self.log.info(f"{CMD}{SENT} {command}.") - self._msgbus.send(endpoint=f"{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) From 01304f5966f2de36267068391c1a7c2145ac7d41 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 30 Oct 2023 18:45:19 +1100 Subject: [PATCH 63/78] Remove redundant venue from InstrumentProvider --- RELEASES.md | 1 + nautilus_trader/adapters/betfair/providers.py | 1 - .../adapters/binance/futures/providers.py | 5 ++--- .../adapters/binance/spot/providers.py | 5 ++--- .../adapters/interactive_brokers/providers.py | 2 -- nautilus_trader/adapters/sandbox/execution.py | 2 +- nautilus_trader/common/providers.py | 18 ------------------ nautilus_trader/test_kit/mocks/data.py | 4 +--- .../_template/test_template_providers.py | 2 -- tests/unit_tests/common/test_providers.py | 3 --- tests/unit_tests/live/test_data_client.py | 5 +---- tests/unit_tests/live/test_execution_engine.py | 5 +---- tests/unit_tests/live/test_execution_recon.py | 5 +---- 13 files changed, 10 insertions(+), 48 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 567fb17eddb6..0f1ed74967dc 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -10,6 +10,7 @@ Released on TBC (UTC). ### Breaking Changes - Transformed orders will now retain the original `ts_init` timestamp - Removed unimplemented `batch_more` option for `Strategy.modify_order` +- Removed `InstrumentProvider.venue` property (redundant as a provider may have many venues) - Dropped support for Python 3.9 ### Fixes diff --git a/nautilus_trader/adapters/betfair/providers.py b/nautilus_trader/adapters/betfair/providers.py index 76343a443486..6f718ab22fb0 100644 --- a/nautilus_trader/adapters/betfair/providers.py +++ b/nautilus_trader/adapters/betfair/providers.py @@ -72,7 +72,6 @@ def __init__( ): assert config is not None, "Must pass config to BetfairInstrumentProvider" super().__init__( - venue=BETFAIR_VENUE, logger=logger, config=config, ) diff --git a/nautilus_trader/adapters/binance/futures/providers.py b/nautilus_trader/adapters/binance/futures/providers.py index 4b572ec5b296..3aa499fe5eac 100644 --- a/nautilus_trader/adapters/binance/futures/providers.py +++ b/nautilus_trader/adapters/binance/futures/providers.py @@ -76,7 +76,6 @@ def __init__( config: InstrumentProviderConfig | None = None, ): super().__init__( - venue=BINANCE_VENUE, logger=logger, config=config, ) @@ -153,7 +152,7 @@ async def load_ids_async( # Check all instrument IDs for instrument_id in instrument_ids: - PyCondition.equal(instrument_id.venue, self.venue, "instrument_id.venue", "self.venue") + PyCondition.equal(instrument_id.venue, BINANCE_VENUE, "instrument_id.venue", "BINANCE") filters_str = "..." if not filters else f" with filters {filters}..." self._log.info(f"Loading instruments {instrument_ids}{filters_str}.") @@ -192,7 +191,7 @@ async def load_ids_async( async def load_async(self, instrument_id: InstrumentId, filters: dict | None = None) -> None: PyCondition.not_none(instrument_id, "instrument_id") - PyCondition.equal(instrument_id.venue, self.venue, "instrument_id.venue", "self.venue") + PyCondition.equal(instrument_id.venue, BINANCE_VENUE, "instrument_id.venue", "BINANCE") filters_str = "..." if not filters else f" with filters {filters}..." self._log.debug(f"Loading instrument {instrument_id}{filters_str}.") diff --git a/nautilus_trader/adapters/binance/spot/providers.py b/nautilus_trader/adapters/binance/spot/providers.py index 1d753423eea0..ec4639bfe1e6 100644 --- a/nautilus_trader/adapters/binance/spot/providers.py +++ b/nautilus_trader/adapters/binance/spot/providers.py @@ -77,7 +77,6 @@ def __init__( config: InstrumentProviderConfig | None = None, ): super().__init__( - venue=BINANCE_VENUE, logger=logger, config=config, ) @@ -141,7 +140,7 @@ async def load_ids_async( # Check all instrument IDs for instrument_id in instrument_ids: - PyCondition.equal(instrument_id.venue, self.venue, "instrument_id.venue", "self.venue") + PyCondition.equal(instrument_id.venue, BINANCE_VENUE, "instrument_id.venue", "BINANCE") filters_str = "..." if not filters else f" with filters {filters}..." self._log.info(f"Loading instruments {instrument_ids}{filters_str}.") @@ -183,7 +182,7 @@ async def load_ids_async( async def load_async(self, instrument_id: InstrumentId, filters: dict | None = None) -> None: PyCondition.not_none(instrument_id, "instrument_id") - PyCondition.equal(instrument_id.venue, self.venue, "instrument_id.venue", "self.venue") + PyCondition.equal(instrument_id.venue, BINANCE_VENUE, "instrument_id.venue", "BINANCE") filters_str = "..." if not filters else f" with filters {filters}..." self._log.debug(f"Loading instrument {instrument_id}{filters_str}.") diff --git a/nautilus_trader/adapters/interactive_brokers/providers.py b/nautilus_trader/adapters/interactive_brokers/providers.py index f60b6a8c4cc4..3c684db0e2cb 100644 --- a/nautilus_trader/adapters/interactive_brokers/providers.py +++ b/nautilus_trader/adapters/interactive_brokers/providers.py @@ -20,7 +20,6 @@ # fmt: off from nautilus_trader.adapters.interactive_brokers.client import InteractiveBrokersClient -from nautilus_trader.adapters.interactive_brokers.common import IB_VENUE from nautilus_trader.adapters.interactive_brokers.common import IBContract from nautilus_trader.adapters.interactive_brokers.common import IBContractDetails from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersInstrumentProviderConfig @@ -61,7 +60,6 @@ def __init__( """ super().__init__( - venue=IB_VENUE, logger=logger, config=config, ) diff --git a/nautilus_trader/adapters/sandbox/execution.py b/nautilus_trader/adapters/sandbox/execution.py index 8c539b1a7bfd..3b5a486bf8b9 100644 --- a/nautilus_trader/adapters/sandbox/execution.py +++ b/nautilus_trader/adapters/sandbox/execution.py @@ -99,7 +99,7 @@ def __init__( oms_type=oms_type, account_type=account_type, base_currency=self._currency, - instrument_provider=InstrumentProvider(venue=sandbox_venue, logger=logger), + instrument_provider=InstrumentProvider(logger=logger), msgbus=msgbus, cache=cache, clock=clock, diff --git a/nautilus_trader/common/providers.py b/nautilus_trader/common/providers.py index cee93dad81ad..254083a1a773 100644 --- a/nautilus_trader/common/providers.py +++ b/nautilus_trader/common/providers.py @@ -21,7 +21,6 @@ from nautilus_trader.core.correctness import PyCondition from nautilus_trader.model.currency import Currency from nautilus_trader.model.identifiers import InstrumentId -from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.instruments import Instrument @@ -31,8 +30,6 @@ class InstrumentProvider: Parameters ---------- - venue : Venue - The venue for the provider. logger : Logger The logger for the provider. config :InstrumentProviderConfig, optional @@ -46,18 +43,15 @@ class InstrumentProvider: def __init__( self, - venue: Venue, logger: Logger, config: InstrumentProviderConfig | None = None, ) -> None: - PyCondition.not_none(venue, "venue") PyCondition.not_none(logger, "logger") if config is None: config = InstrumentProviderConfig() self._log = LoggerAdapter(type(self).__name__, logger) - self._venue = venue self._instruments: dict[InstrumentId, Instrument] = {} self._currencies: dict[str, Currency] = {} @@ -70,18 +64,6 @@ def __init__( self._loaded = False self._loading = False - @property - def venue(self) -> Venue: - """ - Return the providers venue. - - Returns - ------- - Venue - - """ - return self._venue - @property def count(self) -> int: """ diff --git a/nautilus_trader/test_kit/mocks/data.py b/nautilus_trader/test_kit/mocks/data.py index 7c1379accf1e..bcbf7919609b 100644 --- a/nautilus_trader/test_kit/mocks/data.py +++ b/nautilus_trader/test_kit/mocks/data.py @@ -65,14 +65,12 @@ def data_catalog_setup( def aud_usd_data_loader(catalog: ParquetDataCatalog) -> None: from nautilus_trader.test_kit.providers import TestInstrumentProvider - venue = Venue("SIM") - instrument = TestInstrumentProvider.default_fx_ccy("AUD/USD", venue=venue) + instrument = TestInstrumentProvider.default_fx_ccy("AUD/USD", venue=Venue("SIM")) clock = TestClock() logger = Logger(clock) instrument_provider = InstrumentProvider( - venue=venue, logger=logger, ) instrument_provider.add(instrument) diff --git a/tests/integration_tests/adapters/_template/test_template_providers.py b/tests/integration_tests/adapters/_template/test_template_providers.py index 2dad50882d85..d5ea86383614 100644 --- a/tests/integration_tests/adapters/_template/test_template_providers.py +++ b/tests/integration_tests/adapters/_template/test_template_providers.py @@ -15,7 +15,6 @@ import pytest -from nautilus_trader.adapters._template.core import TEMPLATE_VENUE from nautilus_trader.adapters._template.providers import TemplateInstrumentProvider from nautilus_trader.common.clock import TestClock from nautilus_trader.common.logging import Logger @@ -28,7 +27,6 @@ def instrument_provider(): clock = TestClock() return TemplateInstrumentProvider( - venue=TEMPLATE_VENUE, logger=Logger(clock), ) diff --git a/tests/unit_tests/common/test_providers.py b/tests/unit_tests/common/test_providers.py index ade4cfbfbe9e..011c87bb602b 100644 --- a/tests/unit_tests/common/test_providers.py +++ b/tests/unit_tests/common/test_providers.py @@ -16,11 +16,9 @@ from nautilus_trader.common.clock import TestClock from nautilus_trader.common.logging import Logger from nautilus_trader.common.providers import InstrumentProvider -from nautilus_trader.model.identifiers import Venue from nautilus_trader.test_kit.stubs.identifiers import TestIdStubs -BITMEX = Venue("BITMEX") AUDUSD = TestIdStubs.audusd_id() @@ -29,7 +27,6 @@ def setup(self): # Fixture Setup clock = TestClock() self.provider = InstrumentProvider( - venue=BITMEX, logger=Logger(clock, bypass=True), ) diff --git a/tests/unit_tests/live/test_data_client.py b/tests/unit_tests/live/test_data_client.py index 226630db16a9..a0bf2ada9802 100644 --- a/tests/unit_tests/live/test_data_client.py +++ b/tests/unit_tests/live/test_data_client.py @@ -120,10 +120,7 @@ def setup(self): loop=self.loop, client_id=ClientId(BINANCE.value), venue=BINANCE, - instrument_provider=InstrumentProvider( - venue=Venue("SIM"), - logger=self.logger, - ), + instrument_provider=InstrumentProvider(logger=self.logger), msgbus=self.msgbus, cache=self.cache, clock=self.clock, diff --git a/tests/unit_tests/live/test_execution_engine.py b/tests/unit_tests/live/test_execution_engine.py index 415623680f56..b8cf4dc80a4a 100644 --- a/tests/unit_tests/live/test_execution_engine.py +++ b/tests/unit_tests/live/test_execution_engine.py @@ -151,10 +151,7 @@ def setup(self): logger=self.logger, ) - self.instrument_provider = InstrumentProvider( - venue=SIM, - logger=self.logger, - ) + self.instrument_provider = InstrumentProvider(logger=self.logger) self.instrument_provider.add(AUDUSD_SIM) self.instrument_provider.add(GBPUSD_SIM) self.cache.add_instrument(AUDUSD_SIM) diff --git a/tests/unit_tests/live/test_execution_recon.py b/tests/unit_tests/live/test_execution_recon.py index ba2a579ce859..84324de21ebc 100644 --- a/tests/unit_tests/live/test_execution_recon.py +++ b/tests/unit_tests/live/test_execution_recon.py @@ -127,10 +127,7 @@ def setup(self): venue=SIM, account_type=AccountType.CASH, base_currency=USD, - instrument_provider=InstrumentProvider( - venue=SIM, - logger=self.logger, - ), + instrument_provider=InstrumentProvider(logger=self.logger), msgbus=self.msgbus, cache=self.cache, clock=self.clock, From 5bd2ef017b2240d12c95be6534e26d80c64b516f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 30 Oct 2023 19:17:28 +1100 Subject: [PATCH 64/78] Relax typing for BinanceSpotSymbolInfo.permissions --- RELEASES.md | 1 + nautilus_trader/adapters/binance/spot/schemas/market.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 0f1ed74967dc..8b3c699d4ec8 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -20,6 +20,7 @@ Released on TBC (UTC). - Fixed managed GTD orders cancel timer on order cancel (timers were not being canceled) - Fixed `BacktestEngine` logging error with immediate stop (caused by certain timestamps being `None`) - Fixed `BacktestNode` exceptions during backtest runs preventing next sequential run, thanks for reporting @cavan-black +- Fixed `BinanceSpotPersmission` value error by relaxing typing for `BinanceSpotSymbolInfo.permissions` - Interactive Brokers adapter various fixes, thanks @rsmb7z --- diff --git a/nautilus_trader/adapters/binance/spot/schemas/market.py b/nautilus_trader/adapters/binance/spot/schemas/market.py index ed89822b2069..5aaab03e34cd 100644 --- a/nautilus_trader/adapters/binance/spot/schemas/market.py +++ b/nautilus_trader/adapters/binance/spot/schemas/market.py @@ -20,7 +20,6 @@ from nautilus_trader.adapters.binance.common.schemas.market import BinanceOrderBookDelta from nautilus_trader.adapters.binance.common.schemas.market import BinanceRateLimit from nautilus_trader.adapters.binance.common.schemas.market import BinanceSymbolFilter -from nautilus_trader.adapters.binance.spot.enums import BinanceSpotPermissions from nautilus_trader.core.datetime import millis_to_nanos from nautilus_trader.model.currency import Currency from nautilus_trader.model.data import BookOrder @@ -62,7 +61,7 @@ class BinanceSpotSymbolInfo(msgspec.Struct, frozen=True): isSpotTradingAllowed: bool isMarginTradingAllowed: bool filters: list[BinanceSymbolFilter] - permissions: list[BinanceSpotPermissions] + permissions: list[str] def parse_to_base_asset(self): return Currency( From 463cc08dc197a3294a37769249c699a73c5fb77a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 30 Oct 2023 21:34:50 +1100 Subject: [PATCH 65/78] Cleanup build --- build.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/build.py b/build.py index 2c0a4f25b881..fadb30ce09a4 100644 --- a/build.py +++ b/build.py @@ -48,9 +48,6 @@ # Use clang as the default compiler os.environ["CC"] = "clang" os.environ["LDSHARED"] = "clang -shared" -# elif platform.system() == "Windows": -# os.environ["CC"] = "cl" -# os.environ["CXX"] = "cl" TARGET_DIR = Path.cwd() / "nautilus_core" / "target" / BUILD_MODE @@ -146,15 +143,8 @@ def _build_extensions() -> list[Extension]: extra_compile_args = [] extra_link_args = RUST_LIBS - if platform.system() == "Darwin": - extra_compile_args.append("-Wno-unreachable-code-fallthrough") - extra_link_args.append("-flat_namespace") - extra_link_args.append("-undefined") - extra_link_args.append("suppress") - if platform.system() != "Windows": # Suppress warnings produced by Cython boilerplate - extra_compile_args.append("-Wno-parentheses-equality") extra_compile_args.append("-Wno-unreachable-code") if BUILD_MODE == "release": extra_compile_args.append("-O2") From ac35686d4a3cf2b68fddeb2922af627e4b1e1e05 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 30 Oct 2023 22:38:13 +1100 Subject: [PATCH 66/78] Use call_soon_threadsafe for live engine queues --- nautilus_trader/live/data_engine.py | 16 ++++++++-------- nautilus_trader/live/execution_engine.py | 8 ++++---- nautilus_trader/live/risk_engine.py | 8 ++++---- tests/unit_tests/live/test_execution_engine.py | 3 +-- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/nautilus_trader/live/data_engine.py b/nautilus_trader/live/data_engine.py index ed594b907427..1a77c839b003 100644 --- a/nautilus_trader/live/data_engine.py +++ b/nautilus_trader/live/data_engine.py @@ -241,7 +241,7 @@ def execute(self, command: DataCommand) -> None: # Do not allow None through (None is a sentinel value which stops the queue) try: - self._cmd_queue.put_nowait(command) + self._loop.call_soon_threadsafe(self._cmd_queue.put_nowait, command) except asyncio.QueueFull: self._log.warning( f"Blocking on `_cmd_queue.put` as queue full at " @@ -272,7 +272,7 @@ def request(self, request: DataRequest) -> None: # Do not allow None through (None is a sentinel value which stops the queue) try: - self._req_queue.put_nowait(request) + self._loop.call_soon_threadsafe(self._req_queue.put_nowait, request) except asyncio.QueueFull: self._log.warning( f"Blocking on `_req_queue.put` as queue full at " @@ -302,7 +302,7 @@ def response(self, response: DataResponse) -> None: PyCondition.not_none(response, "response") try: - self._res_queue.put_nowait(response) + self._loop.call_soon_threadsafe(self._res_queue.put_nowait, response) except asyncio.QueueFull: self._log.warning( f"Blocking on `_res_queue.put` as queue full at " @@ -333,7 +333,7 @@ def process(self, data: Data) -> None: # Do not allow None through (None is a sentinel value which stops the queue) try: - self._data_queue.put_nowait(data) + self._loop.call_soon_threadsafe(self._data_queue.put_nowait, data) except asyncio.QueueFull: self._log.warning( f"Blocking on `_data_queue.put` as queue full at " @@ -345,10 +345,10 @@ def process(self, data: Data) -> None: # -- INTERNAL ------------------------------------------------------------------------------------- def _enqueue_sentinels(self) -> None: - self._cmd_queue.put_nowait(self._sentinel) - self._req_queue.put_nowait(self._sentinel) - self._res_queue.put_nowait(self._sentinel) - self._data_queue.put_nowait(self._sentinel) + self._loop.call_soon_threadsafe(self._cmd_queue.put_nowait, self._sentinel) + self._loop.call_soon_threadsafe(self._req_queue.put_nowait, self._sentinel) + self._loop.call_soon_threadsafe(self._res_queue.put_nowait, self._sentinel) + self._loop.call_soon_threadsafe(self._data_queue.put_nowait, self._sentinel) self._log.debug("Sentinel messages placed on queues.") def _on_start(self) -> None: diff --git a/nautilus_trader/live/execution_engine.py b/nautilus_trader/live/execution_engine.py index 870ab3bcea19..18e965c2a9a4 100644 --- a/nautilus_trader/live/execution_engine.py +++ b/nautilus_trader/live/execution_engine.py @@ -271,7 +271,7 @@ def execute(self, command: TradingCommand) -> None: # Do not allow None through (None is a sentinel value which stops the queue) try: - self._cmd_queue.put_nowait(command) + self._loop.call_soon_threadsafe(self._cmd_queue.put_nowait, command) except asyncio.QueueFull: self._log.warning( f"Blocking on `_cmd_queue.put` as queue full " @@ -301,7 +301,7 @@ def process(self, event: OrderEvent) -> None: PyCondition.not_none(event, "event") try: - self._evt_queue.put_nowait(event) + self._loop.call_soon_threadsafe(self._evt_queue.put_nowait, event) except asyncio.QueueFull: self._log.warning( f"Blocking on `_evt_queue.put` as queue full " @@ -313,8 +313,8 @@ def process(self, event: OrderEvent) -> None: # -- INTERNAL ------------------------------------------------------------------------------------- def _enqueue_sentinel(self) -> None: - self._cmd_queue.put_nowait(self._sentinel) - self._evt_queue.put_nowait(self._sentinel) + self._loop.call_soon_threadsafe(self._cmd_queue.put_nowait, self._sentinel) + self._loop.call_soon_threadsafe(self._evt_queue.put_nowait, self._sentinel) self._log.debug("Sentinel messages placed on queues.") def _on_start(self) -> None: diff --git a/nautilus_trader/live/risk_engine.py b/nautilus_trader/live/risk_engine.py index 1f3840390e8c..a23077fb7dbb 100644 --- a/nautilus_trader/live/risk_engine.py +++ b/nautilus_trader/live/risk_engine.py @@ -174,7 +174,7 @@ def execute(self, command: Command) -> None: # Do not allow None through (None is a sentinel value which stops the queue) try: - self._cmd_queue.put_nowait(command) + self._loop.call_soon_threadsafe(self._cmd_queue.put_nowait, command) except asyncio.QueueFull: self._log.warning( f"Blocking on `_cmd_queue.put` as queue full " @@ -205,7 +205,7 @@ def process(self, event: Event) -> None: # Do not allow None through (None is a sentinel value which stops the queue) try: - self._evt_queue.put_nowait(event) + self._loop.call_soon_threadsafe(self._evt_queue.put_nowait, event) except asyncio.QueueFull: self._log.warning( f"Blocking on `_evt_queue.put` as queue full " @@ -217,8 +217,8 @@ def process(self, event: Event) -> None: # -- INTERNAL ------------------------------------------------------------------------------------- def _enqueue_sentinel(self) -> None: - self._cmd_queue.put_nowait(self._sentinel) - self._evt_queue.put_nowait(self._sentinel) + self._loop.call_soon_threadsafe(self._cmd_queue.put_nowait, self._sentinel) + self._loop.call_soon_threadsafe(self._evt_queue.put_nowait, self._sentinel) self._log.debug("Sentinel messages placed on queues.") def _on_start(self) -> None: diff --git a/tests/unit_tests/live/test_execution_engine.py b/tests/unit_tests/live/test_execution_engine.py index b8cf4dc80a4a..8873ad631dea 100644 --- a/tests/unit_tests/live/test_execution_engine.py +++ b/tests/unit_tests/live/test_execution_engine.py @@ -376,8 +376,7 @@ async def test_kill_when_not_running_with_messages_on_queue(self): self.exec_engine.kill() # Assert - assert self.exec_engine.cmd_qsize() == 0 - assert self.exec_engine.evt_qsize() == 0 + assert self.exec_engine.is_stopped @pytest.mark.asyncio() async def test_execute_command_places_command_on_queue(self): From 301c2d6b53d302cd4dbfc24d068ce302983cce6e Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Tue, 31 Oct 2023 03:12:03 +0100 Subject: [PATCH 67/78] Add Equity in rust with pyo3 (#1315) --- nautilus_core/model/src/instruments/equity.rs | 89 +++++++++--- .../model/src/python/instruments/equity.rs | 130 ++++++++++++++++++ .../model/src/python/instruments/mod.rs | 1 + nautilus_trader/test_kit/rust/instruments.py | 22 +++ .../model/instruments/test_equity_pyo3.py | 53 +++++++ 5 files changed, 279 insertions(+), 16 deletions(-) create mode 100644 nautilus_core/model/src/python/instruments/equity.rs create mode 100644 tests/unit_tests/model/instruments/test_equity_pyo3.py diff --git a/nautilus_core/model/src/instruments/equity.rs b/nautilus_core/model/src/instruments/equity.rs index 8f3dc000d74e..055c3cc8cb24 100644 --- a/nautilus_core/model/src/instruments/equity.rs +++ b/nautilus_core/model/src/instruments/equity.rs @@ -17,6 +17,7 @@ use std::hash::{Hash, Hasher}; +use anyhow::Result; use pyo3::prelude::*; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; @@ -43,19 +44,18 @@ pub struct Equity { pub price_precision: u8, pub price_increment: Price, pub multiplier: Quantity, + pub margin_init: Decimal, + pub margin_maint: Decimal, + pub maker_fee: Decimal, + pub taker_fee: Decimal, pub lot_size: Option, pub max_quantity: Option, pub min_quantity: Option, pub max_price: Option, pub min_price: Option, - pub margin_init: Decimal, - pub margin_maint: Decimal, - pub maker_fee: Decimal, - pub taker_fee: Decimal, } impl Equity { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( id: InstrumentId, @@ -65,17 +65,17 @@ impl Equity { price_precision: u8, price_increment: Price, multiplier: Quantity, + margin_init: Decimal, + margin_maint: Decimal, + maker_fee: Decimal, + taker_fee: Decimal, lot_size: Option, max_quantity: Option, min_quantity: Option, max_price: Option, min_price: Option, - margin_init: Decimal, - margin_maint: Decimal, - maker_fee: Decimal, - taker_fee: Decimal, - ) -> Self { - Self { + ) -> Result { + Ok(Self { id, raw_symbol, isin, @@ -83,16 +83,16 @@ impl Equity { price_precision, price_increment, multiplier, + margin_init, + margin_maint, + maker_fee, + taker_fee, lot_size, max_quantity, min_quantity, max_price, min_price, - margin_init, - margin_maint, - maker_fee, - taker_fee, - } + }) } } @@ -199,3 +199,60 @@ impl Instrument for Equity { self.taker_fee } } + +//////////////////////////////////////////////////////////////////////////////// +// Stubs +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +pub mod stubs { + use std::str::FromStr; + + use rstest::fixture; + use rust_decimal::Decimal; + + use crate::{ + identifiers::{instrument_id::InstrumentId, symbol::Symbol}, + instruments::equity::Equity, + types::{currency::Currency, price::Price, quantity::Quantity}, + }; + + #[fixture] + pub fn equity_aapl() -> Equity { + Equity::new( + InstrumentId::from("AAPL.NASDAQ"), + Symbol::from("AAPL"), + String::from("US0378331005"), + Currency::from("USD"), + 2, + Price::from("0.01"), + Quantity::from(1), + Decimal::from_str("0.0").unwrap(), + Decimal::from_str("0.0").unwrap(), + Decimal::from_str("0.001").unwrap(), + Decimal::from_str("0.001").unwrap(), + Some(Quantity::from(1)), + None, + None, + None, + None, + ) + .unwrap() + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::stubs::*; + use crate::instruments::equity::Equity; + + #[rstest] + fn test_equality(equity_aapl: Equity) { + let cloned = equity_aapl.clone(); + assert_eq!(equity_aapl, cloned) + } +} diff --git a/nautilus_core/model/src/python/instruments/equity.rs b/nautilus_core/model/src/python/instruments/equity.rs new file mode 100644 index 000000000000..cc49504549ee --- /dev/null +++ b/nautilus_core/model/src/python/instruments/equity.rs @@ -0,0 +1,130 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use nautilus_core::python::{serialization::from_dict_pyo3, to_pyvalue_err}; +use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; +use rust_decimal::{prelude::ToPrimitive, Decimal}; + +use crate::{ + identifiers::{instrument_id::InstrumentId, symbol::Symbol}, + instruments::equity::Equity, + types::{currency::Currency, price::Price, quantity::Quantity}, +}; + +#[pymethods] +impl Equity { + #[allow(clippy::too_many_arguments)] + #[new] + fn py_new( + id: InstrumentId, + raw_symbol: Symbol, + isin: String, + currency: Currency, + price_precision: u8, + price_increment: Price, + multiplier: Quantity, + margin_init: Decimal, + margin_maint: Decimal, + maker_fee: Decimal, + taker_fee: Decimal, + lot_size: Option, + max_quantity: Option, + min_quantity: Option, + max_price: Option, + min_price: Option, + ) -> PyResult { + Self::new( + id, + raw_symbol, + isin, + currency, + price_precision, + price_increment, + multiplier, + margin_init, + margin_maint, + maker_fee, + taker_fee, + lot_size, + max_quantity, + min_quantity, + max_price, + min_price, + ) + .map_err(to_pyvalue_err) + } + + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + _ => panic!("Not implemented"), + } + } + + fn __hash__(&self) -> isize { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() as isize + } + + #[staticmethod] + #[pyo3(name = "from_dict")] + fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { + from_dict_pyo3(py, values) + } + + #[pyo3(name = "to_dict")] + fn py_to_dict(&self, py: Python<'_>) -> PyResult { + let dict = PyDict::new(py); + dict.set_item("type", stringify!(Equity))?; + dict.set_item("id", self.id.to_string())?; + dict.set_item("raw_symbol", self.raw_symbol.to_string())?; + dict.set_item("isin", self.isin.to_string())?; + dict.set_item("currency", self.currency.code.to_string())?; + dict.set_item("price_precision", self.price_precision)?; + dict.set_item("price_increment", self.price_increment.to_string())?; + dict.set_item("multiplier", self.multiplier.to_string())?; + dict.set_item("margin_init", self.margin_init.to_f64())?; + dict.set_item("margin_maint", self.margin_maint.to_f64())?; + dict.set_item("maker_fee", self.maker_fee.to_f64())?; + dict.set_item("taker_fee", self.taker_fee.to_f64())?; + match self.lot_size { + Some(value) => dict.set_item("lot_size", value.to_string())?, + None => dict.set_item("lot_size", py.None())?, + } + match self.max_quantity { + Some(value) => dict.set_item("max_quantity", value.to_string())?, + None => dict.set_item("max_quantity", py.None())?, + } + match self.min_quantity { + Some(value) => dict.set_item("min_quantity", value.to_string())?, + None => dict.set_item("min_quantity", py.None())?, + } + match self.max_price { + Some(value) => dict.set_item("max_price", value.to_string())?, + None => dict.set_item("max_price", py.None())?, + } + match self.min_price { + Some(value) => dict.set_item("min_price", value.to_string())?, + None => dict.set_item("min_price", py.None())?, + } + Ok(dict.into()) + } +} diff --git a/nautilus_core/model/src/python/instruments/mod.rs b/nautilus_core/model/src/python/instruments/mod.rs index ac4762d736f3..def0b6325aec 100644 --- a/nautilus_core/model/src/python/instruments/mod.rs +++ b/nautilus_core/model/src/python/instruments/mod.rs @@ -16,4 +16,5 @@ pub mod crypto_future; pub mod crypto_perpetual; pub mod currency_pair; +pub mod equity; pub mod options_contract; diff --git a/nautilus_trader/test_kit/rust/instruments.py b/nautilus_trader/test_kit/rust/instruments.py index 6da320512df7..c873bd4d5452 100644 --- a/nautilus_trader/test_kit/rust/instruments.py +++ b/nautilus_trader/test_kit/rust/instruments.py @@ -22,6 +22,7 @@ from nautilus_trader.core.nautilus_pyo3 import CryptoFuture from nautilus_trader.core.nautilus_pyo3 import CryptoPerpetual from nautilus_trader.core.nautilus_pyo3 import CurrencyPair +from nautilus_trader.core.nautilus_pyo3 import Equity from nautilus_trader.core.nautilus_pyo3 import InstrumentId from nautilus_trader.core.nautilus_pyo3 import Money from nautilus_trader.core.nautilus_pyo3 import OptionKind @@ -132,3 +133,24 @@ def appl_option(expiry: pd.Timestamp | None = None) -> OptionsContract: 0.001, Quantity.from_str("1.0"), ) + + @staticmethod + def appl_equity() -> Equity: + return Equity( # type: ignore + InstrumentId.from_str("AAPL.NASDAQ"), + Symbol("AAPL"), + "US0378331005", + TestTypesProviderPyo3.currency_usd(), + 2, + Price.from_str("0.01"), + Quantity.from_str("1"), + 0.0, + 0.0, + 0.001, + 0.001, + Quantity.from_str("1.0"), + None, + None, + None, + None, + ) diff --git a/tests/unit_tests/model/instruments/test_equity_pyo3.py b/tests/unit_tests/model/instruments/test_equity_pyo3.py new file mode 100644 index 000000000000..2a3fe9b83397 --- /dev/null +++ b/tests/unit_tests/model/instruments/test_equity_pyo3.py @@ -0,0 +1,53 @@ +# ------------------------------------------------------------------------------------------------- +# 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.core.nautilus_pyo3 import Equity +from nautilus_trader.test_kit.rust.instruments import TestInstrumentProviderPyo3 + + +aapl_equity = TestInstrumentProviderPyo3.appl_equity() + + +def test_equality(): + item_1 = TestInstrumentProviderPyo3.appl_equity() + item_2 = TestInstrumentProviderPyo3.appl_equity() + assert item_1 == item_2 + + +def test_hash(): + assert hash(aapl_equity) == hash(aapl_equity) + + +def test_to_dict(): + dict = aapl_equity.to_dict() + assert Equity.from_dict(dict) == aapl_equity + assert dict == { + "type": "Equity", + "id": "AAPL.NASDAQ", + "raw_symbol": "AAPL", + "isin": "US0378331005", + "currency": "USD", + "price_precision": 2, + "price_increment": "0.01", + "multiplier": "1", + "margin_init": 0.0, + "margin_maint": 0.0, + "maker_fee": 0.001, + "taker_fee": 0.001, + "lot_size": "1.0", + "max_quantity": None, + "min_quantity": None, + "max_price": None, + "min_price": None, + } From 4ac53118c4ed08cb66acafa2c3e1e9815734412f Mon Sep 17 00:00:00 2001 From: rsmb7z <105105941+rsmb7z@users.noreply.github.com> Date: Tue, 31 Oct 2023 05:13:36 +0300 Subject: [PATCH 68/78] Improve IB tests and fixtures (#1316) --- .../adapters/interactive_brokers/conftest.py | 6 +++--- .../adapters/interactive_brokers/test_parsing.py | 2 -- .../adapters/interactive_brokers/test_providers.py | 9 +++++++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/integration_tests/adapters/interactive_brokers/conftest.py b/tests/integration_tests/adapters/interactive_brokers/conftest.py index a62361b50861..b20cc6e617e8 100644 --- a/tests/integration_tests/adapters/interactive_brokers/conftest.py +++ b/tests/integration_tests/adapters/interactive_brokers/conftest.py @@ -78,7 +78,7 @@ def exec_client_config(): @pytest.fixture() -def client(data_client_config, loop, msgbus, cache, clock, logger): +def interactive_brokers_client(data_client_config, loop, msgbus, cache, clock, logger): client = InteractiveBrokersClient( loop=loop, msgbus=msgbus, @@ -94,9 +94,9 @@ def client(data_client_config, loop, msgbus, cache, clock, logger): @pytest.fixture() -def instrument_provider(client, logger): +def instrument_provider(interactive_brokers_client, logger): return InteractiveBrokersInstrumentProvider( - client=client, + client=interactive_brokers_client, config=InteractiveBrokersInstrumentProviderConfig(), logger=logger, ) diff --git a/tests/integration_tests/adapters/interactive_brokers/test_parsing.py b/tests/integration_tests/adapters/interactive_brokers/test_parsing.py index 891898ae332d..ce2e2ca3d49f 100644 --- a/tests/integration_tests/adapters/interactive_brokers/test_parsing.py +++ b/tests/integration_tests/adapters/interactive_brokers/test_parsing.py @@ -43,8 +43,6 @@ # fmt: on -pytestmark = pytest.mark.skip(reason="Skip due currently flaky mocks") - @pytest.mark.parametrize( ("contract", "instrument_id"), diff --git a/tests/integration_tests/adapters/interactive_brokers/test_providers.py b/tests/integration_tests/adapters/interactive_brokers/test_providers.py index 889a62d46c71..281af9c38dfe 100644 --- a/tests/integration_tests/adapters/interactive_brokers/test_providers.py +++ b/tests/integration_tests/adapters/interactive_brokers/test_providers.py @@ -58,6 +58,7 @@ async def test_load_equity_contract_instrument(mocker, instrument_provider): IBContract(secType="STK", symbol="AAPL", exchange="NASDAQ"), ) equity = instrument_provider.find(instrument_id) + instrument_provider._client.stop() # Assert assert InstrumentId(symbol=Symbol("AAPL"), venue=Venue("NASDAQ")) == equity.id @@ -81,6 +82,7 @@ async def test_load_futures_contract_instrument(mocker, instrument_provider): # Act await instrument_provider.load_async(IBContract(secType="FUT", symbol="CLZ3", exchange="NYMEX")) future = instrument_provider.find(instrument_id) + instrument_provider._client.stop() # Assert assert future.id == instrument_id @@ -105,6 +107,7 @@ async def test_load_options_contract_instrument(mocker, instrument_provider): IBContract(secType="OPT", symbol="TSLA230120C00100000", exchange="MIAX"), ) option = instrument_provider.find(instrument_id) + instrument_provider._client.stop() # Assert assert option.id == instrument_id @@ -130,6 +133,7 @@ async def test_load_forex_contract_instrument(mocker, instrument_provider): # Act await instrument_provider.load_async(instrument_id) fx = instrument_provider.find(instrument_id) + instrument_provider._client.stop() # Assert assert fx.id == instrument_id @@ -150,6 +154,7 @@ async def test_contract_id_to_instrument_id(mocker, instrument_provider): # Act await instrument_provider.load_async(IBContract(secType="FUT", symbol="CLZ3", exchange="NYMEX")) + instrument_provider._client.stop() # Assert expected = {174230596: InstrumentId.from_str("CLZ23.NYMEX")} @@ -168,6 +173,7 @@ async def test_load_instrument_using_contract_id(mocker, instrument_provider): # Act fx = await instrument_provider.find_with_contract_id(12087792) + instrument_provider._client.stop() # Assert assert fx.id == instrument_id @@ -177,12 +183,14 @@ async def test_load_instrument_using_contract_id(mocker, instrument_provider): assert fx.price_precision == 5 +@pytest.mark.skip(reason="Scope of test not clear!") @pytest.mark.asyncio() async def test_none_filters(instrument_provider): # Act, Arrange, Assert instrument_provider.load_all(None) +@pytest.mark.skip(reason="Scope of test not clear!") @pytest.mark.asyncio() async def test_instrument_filter_callable_none(mocker, instrument_provider): # Arrange @@ -201,6 +209,7 @@ async def test_instrument_filter_callable_none(mocker, instrument_provider): assert len(instrument_provider.get_all()) == 1 +@pytest.mark.skip(reason="Scope of test not clear!") @pytest.mark.asyncio() async def test_instrument_filter_callable_option_filter(mocker, instrument_provider): # Arrange From f259a6b96ea7f204d9c54a3e825c6658f7439367 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 31 Oct 2023 18:13:34 +1100 Subject: [PATCH 69/78] Add kmerge tests ignored --- .../persistence/src/backend/kmerge_batch.rs | 72 +++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/nautilus_core/persistence/src/backend/kmerge_batch.rs b/nautilus_core/persistence/src/backend/kmerge_batch.rs index ff3fe562ff6f..c5161fcc2ca7 100644 --- a/nautilus_core/persistence/src/backend/kmerge_batch.rs +++ b/nautilus_core/persistence/src/backend/kmerge_batch.rs @@ -175,8 +175,10 @@ where //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] mod tests { + use quickcheck::{empty_shrinker, Arbitrary}; use quickcheck_macros::quickcheck; + use rstest::rstest; use super::*; @@ -209,7 +211,7 @@ mod tests { } } - #[test] + #[rstest] fn test1() { let iter_a = vec![vec![1, 2, 3].into_iter(), vec![7, 8, 9].into_iter()].into_iter(); let iter_b = vec![vec![4, 5, 6].into_iter()].into_iter(); @@ -221,7 +223,7 @@ mod tests { assert_eq!(values, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]); } - #[test] + #[rstest] fn test2() { let iter_a = vec![vec![1, 2, 6].into_iter(), vec![7, 8, 9].into_iter()].into_iter(); let iter_b = vec![vec![3, 4, 5, 6].into_iter()].into_iter(); @@ -233,7 +235,7 @@ mod tests { assert_eq!(values, vec![1, 2, 3, 4, 5, 6, 6, 7, 8, 9]); } - #[test] + #[rstest] fn test3() { let iter_a = vec![vec![1, 4, 7].into_iter(), vec![24, 35, 56].into_iter()].into_iter(); let iter_b = vec![vec![2, 4, 8].into_iter()].into_iter(); @@ -250,7 +252,7 @@ mod tests { ); } - #[test] + #[rstest] fn test5() { let iter_a = vec![ vec![1, 3, 5].into_iter(), @@ -267,6 +269,68 @@ mod tests { assert_eq!(values, vec![1, 2, 3, 4, 5, 6, 7, 9, 11]); } + #[ignore] + #[rstest] + fn test6() { + let iter_a = vec![ + vec![15, 22].into_iter(), + vec![1, 5, 9].into_iter(), + vec![2, 6].into_iter(), + ] + .into_iter(); + let iter_b = vec![vec![3, 17, 21].into_iter(), vec![8, 10, 12, 14].into_iter()].into_iter(); + let iter_c = vec![vec![4, 7, 20].into_iter(), vec![11, 13, 18, 19].into_iter()].into_iter(); + let iter_d = vec![vec![16].into_iter()].into_iter(); + + let mut kmerge: KMerge<_, i32, _> = KMerge::new(OrdComparator); + kmerge.push_iter(iter_a); + kmerge.push_iter(iter_b); + kmerge.push_iter(iter_c); + kmerge.push_iter(iter_d); + + let values: Vec = kmerge.collect(); + assert_eq!( + values, + vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22] + ); + } + + #[ignore] + #[rstest] + fn test7() { + let mut kmerge: KMerge<_, i32, _> = KMerge::new(OrdComparator); + + // First batch of iterators + let iter_a = vec![vec![1, 3, 5].into_iter(), vec![2, 4, 6].into_iter()].into_iter(); + kmerge.push_iter(iter_a); + + // Collecting values after the first batch + let mut values: Vec = kmerge.by_ref().collect(); + assert_eq!(values, vec![1, 2, 3, 4, 5, 6]); + + // Second batch of iterators + let iter_b = vec![vec![7, 9, 11].into_iter(), vec![8, 10, 12].into_iter()].into_iter(); + kmerge.push_iter(iter_b); + + // Collecting values after the second batch + values.extend(kmerge.by_ref()); + assert_eq!(values, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + + // Third batch of iterators + let iter_c = vec![vec![13, 15, 17].into_iter(), vec![14, 16, 18].into_iter()].into_iter(); + kmerge.push_iter(iter_c); + + // Collecting values after the third batch + values.extend(kmerge); + assert_eq!( + values, + vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] + ); + + // Validating the entire sequence is in non-decreasing order + assert!(values.windows(2).all(|w| w[0] <= w[1])); + } + #[derive(Debug, Clone)] struct SortedNestedVec(Vec>); From c8d16a717d8bb31a43d8284320e9cec84761ea3f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 31 Oct 2023 18:18:57 +1100 Subject: [PATCH 70/78] Sort BacktestNode streaming data --- nautilus_trader/backtest/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_trader/backtest/node.py b/nautilus_trader/backtest/node.py index fcfc08f56869..9f904b8c14d7 100644 --- a/nautilus_trader/backtest/node.py +++ b/nautilus_trader/backtest/node.py @@ -302,7 +302,7 @@ def _run_streaming( engine.add_data( data=capsule_to_list(chunk), validate=False, # Cannot validate mixed type stream - sort=False, # Already sorted from kmerge + sort=True, # Temporarily sorting # Already sorted from kmerge ) engine.run( run_config_id=run_config_id, From 60f22b67594a27e0564223af7e87c0179d181aca Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 31 Oct 2023 21:50:07 +1100 Subject: [PATCH 71/78] Update dependencies --- nautilus_core/Cargo.lock | 16 ++++++++-------- nautilus_core/Cargo.toml | 2 +- poetry.lock | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 6ac3afacb7aa..8828d086aaa9 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -656,9 +656,9 @@ dependencies = [ [[package]] name = "chrono-tz" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1369bc6b9e9a7dfdae2055f6ec151fe9c554a9d23d357c0237cee2e25eaabb7" +checksum = "e23185c0e21df6ed832a12e2bda87c7d1def6842881fb634a8511ced741b0d76" dependencies = [ "chrono", "chrono-tz-build", @@ -667,9 +667,9 @@ dependencies = [ [[package]] name = "chrono-tz-build" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2f5ebdc942f57ed96d560a6d1a459bae5851102a25d5bf89dc04ae453e31ecf" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" dependencies = [ "parse-zoneinfo", "phf", @@ -771,9 +771,9 @@ checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3" [[package]] name = "const-random" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11df32a13d7892ec42d51d3d175faba5211ffe13ed25d4fb348ac9e9ce835593" +checksum = "5aaf16c9c2c612020bcfd042e170f6e32de9b9d75adb5277cdbbd2e2c8c8299a" dependencies = [ "const-random-macro", ] @@ -3196,9 +3196,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 0ce35e8e3b80..0f909905d18f 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -32,7 +32,7 @@ rmp-serde = "1.1.2" rust_decimal = "1.32.0" rust_decimal_macros = "1.32.0" serde = { version = "1.0.190", features = ["derive"] } -serde_json = "1.0.107" +serde_json = "1.0.108" strum = { version = "0.25.0", features = ["derive"] } thiserror = "1.0.50" tracing = "0.1.40" diff --git a/poetry.lock b/poetry.lock index 124672157d13..fcb957caf3c4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -804,13 +804,13 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "filelock" -version = "3.13.0" +version = "3.13.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.13.0-py3-none-any.whl", hash = "sha256:a552f4fde758f4eab33191e9548f671970f8b06d436d31388c9aa1e5861a710f"}, - {file = "filelock-3.13.0.tar.gz", hash = "sha256:63c6052c82a1a24c873a549fbd39a26982e8f35a3016da231ead11a5be9dad44"}, + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, ] [package.extras] @@ -2827,13 +2827,13 @@ test = ["websockets"] [[package]] name = "wheel" -version = "0.41.2" +version = "0.41.3" description = "A built-package format for Python" optional = false python-versions = ">=3.7" files = [ - {file = "wheel-0.41.2-py3-none-any.whl", hash = "sha256:75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8"}, - {file = "wheel-0.41.2.tar.gz", hash = "sha256:0c5ac5ff2afb79ac23ab82bab027a0be7b5dbcf2e54dc50efe4bf507de1f7985"}, + {file = "wheel-0.41.3-py3-none-any.whl", hash = "sha256:488609bc63a29322326e05560731bf7bfea8e48ad646e1f5e40d366607de0942"}, + {file = "wheel-0.41.3.tar.gz", hash = "sha256:4d4987ce51a49370ea65c0bfd2234e8ce80a12780820d9dc462597a6e60d0841"}, ] [package.extras] From f71bff546c606c855e2e93fc2de3306de2f86bd8 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 31 Oct 2023 22:23:52 +1100 Subject: [PATCH 72/78] Improve RedisCacheDatabase client error handling --- RELEASES.md | 1 + nautilus_trader/infrastructure/cache.pyx | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/RELEASES.md b/RELEASES.md index 8b3c699d4ec8..daed09eb32c9 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -6,6 +6,7 @@ Released on TBC (UTC). - Added `WebSocketClient` connection headers, thanks @ruthvik125 and @twitu - Added `support_contingent_orders` option for venues (to simulate venues which do not support contingent orders) - Added `StrategyConfig.manage_contingent_orders` option (to automatically manage **open** contingenct orders) +- Improved `RedisCacheDatabase` client connection error handling with retries ### Breaking Changes - Transformed orders will now retain the original `ts_init` timestamp diff --git a/nautilus_trader/infrastructure/cache.pyx b/nautilus_trader/infrastructure/cache.pyx index 67d318e1ac56..7f7ec2980148 100644 --- a/nautilus_trader/infrastructure/cache.pyx +++ b/nautilus_trader/infrastructure/cache.pyx @@ -184,6 +184,16 @@ cdef class RedisCacheDatabase(CacheDatabase): username=config.username, password=config.password, ssl=config.ssl, + socket_timeout=10.0, + socket_keepalive=True, + retry_on_timeout=True, + health_check_interval=60, + retry=redis.retry.Retry(redis.backoff.ExponentialBackoff(), retries=12), + retry_on_error=[ + redis.exceptions.BusyLoadingError, + redis.exceptions.TimeoutError, + redis.exceptions.ConnectionError, + ], ) # -- COMMANDS ------------------------------------------------------------------------------------- From 09142978fd3a113f449e5d5c10e265256df24f8a Mon Sep 17 00:00:00 2001 From: Filip Macek Date: Wed, 1 Nov 2023 07:25:26 +0100 Subject: [PATCH 73/78] Add FuturesContract in rust with pyo3 (#1317) --- .../model/src/instruments/futures_contract.rs | 98 ++++++++++-- .../python/instruments/futures_contract.rs | 140 ++++++++++++++++++ .../model/src/python/instruments/mod.rs | 1 + nautilus_trader/test_kit/rust/instruments.py | 23 +++ .../instruments/test_crypto_perpetual_pyo3.py | 6 +- .../instruments/test_currency_pair_pyo3.py | 6 +- .../instruments/test_futures_contract_pyo3.py | 56 +++++++ .../instruments/test_options_contract_pyo3.py | 6 +- 8 files changed, 311 insertions(+), 25 deletions(-) create mode 100644 nautilus_core/model/src/python/instruments/futures_contract.rs create mode 100644 tests/unit_tests/model/instruments/test_futures_contract_pyo3.py diff --git a/nautilus_core/model/src/instruments/futures_contract.rs b/nautilus_core/model/src/instruments/futures_contract.rs index f8e6839f7226..fd2116a3f925 100644 --- a/nautilus_core/model/src/instruments/futures_contract.rs +++ b/nautilus_core/model/src/instruments/futures_contract.rs @@ -17,6 +17,7 @@ use std::hash::{Hash, Hasher}; +use anyhow::Result; use nautilus_core::time::UnixNanos; use pyo3::prelude::*; use rust_decimal::Decimal; @@ -44,19 +45,19 @@ pub struct FuturesContract { pub currency: Currency, pub price_precision: u8, pub price_increment: Price, + pub margin_init: Decimal, + pub margin_maint: Decimal, + pub maker_fee: Decimal, + pub taker_fee: Decimal, + pub multiplier: Quantity, pub lot_size: Option, pub max_quantity: Option, pub min_quantity: Option, pub max_price: Option, pub min_price: Option, - pub margin_init: Decimal, - pub margin_maint: Decimal, - pub maker_fee: Decimal, - pub taker_fee: Decimal, } impl FuturesContract { - #[must_use] #[allow(clippy::too_many_arguments)] pub fn new( id: InstrumentId, @@ -67,17 +68,18 @@ impl FuturesContract { currency: Currency, price_precision: u8, price_increment: Price, + margin_init: Decimal, + margin_maint: Decimal, + maker_fee: Decimal, + taker_fee: Decimal, + multiplier: Quantity, lot_size: Option, max_quantity: Option, min_quantity: Option, max_price: Option, min_price: Option, - margin_init: Decimal, - margin_maint: Decimal, - maker_fee: Decimal, - taker_fee: Decimal, - ) -> Self { - Self { + ) -> Result { + Ok(Self { id, raw_symbol, asset_class, @@ -86,16 +88,17 @@ impl FuturesContract { currency, price_precision, price_increment, + margin_init, + margin_maint, + maker_fee, + taker_fee, + multiplier, lot_size, max_quantity, min_quantity, max_price, min_price, - margin_init, - margin_maint, - maker_fee, - taker_fee, - } + }) } } @@ -202,3 +205,66 @@ impl Instrument for FuturesContract { self.taker_fee } } + +//////////////////////////////////////////////////////////////////////////////// +// Stubs +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +pub mod stubs { + use std::str::FromStr; + + use chrono::{TimeZone, Utc}; + use nautilus_core::time::UnixNanos; + use rstest::fixture; + use rust_decimal::Decimal; + + use crate::{ + enums::AssetClass, + identifiers::{instrument_id::InstrumentId, symbol::Symbol, venue::Venue}, + instruments::futures_contract::FuturesContract, + types::{currency::Currency, price::Price, quantity::Quantity}, + }; + + #[fixture] + pub fn futures_contract_es() -> FuturesContract { + let expiration = Utc.with_ymd_and_hms(2021, 7, 8, 0, 0, 0).unwrap(); + FuturesContract::new( + InstrumentId::new(Symbol::from("ESZ21"), Venue::from("CME")), + Symbol::from("ESZ21"), + AssetClass::Index, + String::from("ES"), + expiration.timestamp_nanos_opt().unwrap() as UnixNanos, + Currency::USD(), + 2, + Price::from("0.01"), + Decimal::from_str("0.0").unwrap(), + Decimal::from_str("0.0").unwrap(), + Decimal::from_str("0.001").unwrap(), + Decimal::from_str("0.001").unwrap(), + Quantity::from("1.0"), + Some(Quantity::from("1.0")), + None, + None, + None, + None, + ) + .unwrap() + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::stubs::*; + use crate::instruments::futures_contract::FuturesContract; + + #[rstest] + fn test_equality(futures_contract_es: FuturesContract) { + let cloned = futures_contract_es.clone(); + assert_eq!(futures_contract_es, cloned); + } +} diff --git a/nautilus_core/model/src/python/instruments/futures_contract.rs b/nautilus_core/model/src/python/instruments/futures_contract.rs new file mode 100644 index 000000000000..4165836fce2e --- /dev/null +++ b/nautilus_core/model/src/python/instruments/futures_contract.rs @@ -0,0 +1,140 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use nautilus_core::{ + python::{serialization::from_dict_pyo3, to_pyvalue_err}, + time::UnixNanos, +}; +use pyo3::{basic::CompareOp, prelude::*, types::PyDict}; +use rust_decimal::{prelude::ToPrimitive, Decimal}; + +use crate::{ + enums::AssetClass, + identifiers::{instrument_id::InstrumentId, symbol::Symbol}, + instruments::futures_contract::FuturesContract, + types::{currency::Currency, price::Price, quantity::Quantity}, +}; + +#[pymethods] +impl FuturesContract { + #[allow(clippy::too_many_arguments)] + #[new] + fn py_new( + id: InstrumentId, + raw_symbol: Symbol, + asset_class: AssetClass, + underlying: String, + expiration: UnixNanos, + currency: Currency, + price_precision: u8, + price_increment: Price, + margin_init: Decimal, + margin_maint: Decimal, + maker_fee: Decimal, + taker_fee: Decimal, + multiplier: Quantity, + lot_size: Option, + max_quantity: Option, + min_quantity: Option, + max_price: Option, + min_price: Option, + ) -> PyResult { + Self::new( + id, + raw_symbol, + asset_class, + underlying, + expiration, + currency, + price_precision, + price_increment, + margin_init, + margin_maint, + maker_fee, + taker_fee, + multiplier, + lot_size, + max_quantity, + min_quantity, + max_price, + min_price, + ) + .map_err(to_pyvalue_err) + } + + fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py { + match op { + CompareOp::Eq => self.eq(other).into_py(py), + _ => panic!("Not implemented"), + } + } + + fn __hash__(&self) -> isize { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() as isize + } + + #[staticmethod] + #[pyo3(name = "from_dict")] + fn py_from_dict(py: Python<'_>, values: Py) -> PyResult { + from_dict_pyo3(py, values) + } + + #[pyo3(name = "to_dict")] + fn py_to_dict(&self, py: Python<'_>) -> PyResult { + let dict = PyDict::new(py); + dict.set_item("type", stringify!(FuturesContract))?; + dict.set_item("id", self.id.to_string())?; + dict.set_item("raw_symbol", self.raw_symbol.to_string())?; + dict.set_item("asset_class", self.asset_class.to_string())?; + dict.set_item("underlying", self.underlying.to_string())?; + dict.set_item("expiration", self.expiration.to_i64())?; + dict.set_item("currency", self.currency.code.to_string())?; + dict.set_item("price_precision", self.price_precision)?; + dict.set_item("price_increment", self.price_increment.to_string())?; + dict.set_item("margin_init", self.margin_init.to_f64())?; + dict.set_item("margin_maint", self.margin_maint.to_f64())?; + dict.set_item("maker_fee", self.maker_fee.to_f64())?; + dict.set_item("taker_fee", self.taker_fee.to_f64())?; + dict.set_item("multiplier", self.multiplier.to_string())?; + match self.lot_size { + Some(value) => dict.set_item("lot_size", value.to_string())?, + None => dict.set_item("lot_size", py.None())?, + } + match self.max_quantity { + Some(value) => dict.set_item("max_quantity", value.to_string())?, + None => dict.set_item("max_quantity", py.None())?, + } + match self.min_quantity { + Some(value) => dict.set_item("min_quantity", value.to_string())?, + None => dict.set_item("min_quantity", py.None())?, + } + match self.max_price { + Some(value) => dict.set_item("max_price", value.to_string())?, + None => dict.set_item("max_price", py.None())?, + } + match self.min_price { + Some(value) => dict.set_item("min_price", value.to_string())?, + None => dict.set_item("min_price", py.None())?, + } + Ok(dict.into()) + } +} diff --git a/nautilus_core/model/src/python/instruments/mod.rs b/nautilus_core/model/src/python/instruments/mod.rs index def0b6325aec..a42fc4315a65 100644 --- a/nautilus_core/model/src/python/instruments/mod.rs +++ b/nautilus_core/model/src/python/instruments/mod.rs @@ -17,4 +17,5 @@ pub mod crypto_future; pub mod crypto_perpetual; pub mod currency_pair; pub mod equity; +pub mod futures_contract; pub mod options_contract; diff --git a/nautilus_trader/test_kit/rust/instruments.py b/nautilus_trader/test_kit/rust/instruments.py index c873bd4d5452..c7a5ba546d64 100644 --- a/nautilus_trader/test_kit/rust/instruments.py +++ b/nautilus_trader/test_kit/rust/instruments.py @@ -23,6 +23,7 @@ from nautilus_trader.core.nautilus_pyo3 import CryptoPerpetual from nautilus_trader.core.nautilus_pyo3 import CurrencyPair from nautilus_trader.core.nautilus_pyo3 import Equity +from nautilus_trader.core.nautilus_pyo3 import FuturesContract from nautilus_trader.core.nautilus_pyo3 import InstrumentId from nautilus_trader.core.nautilus_pyo3 import Money from nautilus_trader.core.nautilus_pyo3 import OptionKind @@ -154,3 +155,25 @@ def appl_equity() -> Equity: None, None, ) + + @staticmethod + def futures_contract_es(expiry: pd.Timestamp | None = None) -> FuturesContract: + if expiry is None: + expiry = pd.Timestamp(datetime(2021, 12, 17), tz=pytz.UTC) + nanos_expiry = int(expiry.timestamp() * 1e9) + return FuturesContract( # type: ignore + InstrumentId.from_str("ESZ21.CME"), + Symbol("ESZ21"), + AssetClass.INDEX, + "ES", + nanos_expiry, + TestTypesProviderPyo3.currency_usd(), + 2, + Price.from_str("0.01"), + 0.0, + 0.0, + 0.001, + 0.001, + Quantity.from_str("1.0"), + Quantity.from_str("1.0"), + ) diff --git a/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py b/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py index fa6f162e7b7c..ddfa8c595306 100644 --- a/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py +++ b/tests/unit_tests/model/instruments/test_crypto_perpetual_pyo3.py @@ -31,9 +31,9 @@ def test_hash(): def test_to_dict(): - dict = crypto_perpetual_ethusdt_perp.to_dict() - assert CryptoPerpetual.from_dict(dict) == crypto_perpetual_ethusdt_perp - assert dict == { + result = crypto_perpetual_ethusdt_perp.to_dict() + assert CryptoPerpetual.from_dict(result) == crypto_perpetual_ethusdt_perp + assert result == { "type": "CryptoPerpetual", "id": "ETHUSDT-PERP.BINANCE", "raw_symbol": "ETHUSDT", diff --git a/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py b/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py index 14489e41cd9e..8bef0e5f6c60 100644 --- a/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py +++ b/tests/unit_tests/model/instruments/test_currency_pair_pyo3.py @@ -31,9 +31,9 @@ def test_hash(): def test_to_dict(): - dict = btcusdt_binance.to_dict() - assert CurrencyPair.from_dict(dict) == btcusdt_binance - assert dict == { + result = btcusdt_binance.to_dict() + assert CurrencyPair.from_dict(result) == btcusdt_binance + assert result == { "type": "CurrencyPair", "id": "BTCUSDT.BINANCE", "raw_symbol": "BTCUSDT", diff --git a/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py b/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py new file mode 100644 index 000000000000..c519e0058c11 --- /dev/null +++ b/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py @@ -0,0 +1,56 @@ +# ------------------------------------------------------------------------------------------------- +# 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.core.nautilus_pyo3 import FuturesContract +from nautilus_trader.test_kit.rust.instruments import TestInstrumentProviderPyo3 + + +futures_contract_es = TestInstrumentProviderPyo3.futures_contract_es() + + +def test_equality(): + item_1 = TestInstrumentProviderPyo3.btcusdt_binance() + item_2 = TestInstrumentProviderPyo3.btcusdt_binance() + assert item_1 == item_2 + + +def test_hash(): + assert hash(futures_contract_es) == hash(futures_contract_es) + + +def test_to_dict(): + result = futures_contract_es.to_dict() + assert FuturesContract.from_dict(result) == futures_contract_es + assert result == { + "type": "FuturesContract", + "id": "ESZ21.CME", + "raw_symbol": "ESZ21", + "asset_class": "INDEX", + "underlying": "ES", + "expiration": 1639699200000000000, + "currency": "USD", + "price_precision": 2, + "price_increment": "0.01", + "maker_fee": 0.001, + "taker_fee": 0.001, + "margin_maint": 0.0, + "margin_init": 0.0, + "lot_size": "1.0", + "multiplier": "1.0", + "max_price": None, + "max_quantity": None, + "min_price": None, + "min_quantity": None, + } diff --git a/tests/unit_tests/model/instruments/test_options_contract_pyo3.py b/tests/unit_tests/model/instruments/test_options_contract_pyo3.py index 971dcad5b158..2ad8c7ca14a4 100644 --- a/tests/unit_tests/model/instruments/test_options_contract_pyo3.py +++ b/tests/unit_tests/model/instruments/test_options_contract_pyo3.py @@ -31,9 +31,9 @@ def test_hash(): def test_to_dict(): - dict = aapl_option.to_dict() - assert OptionsContract.from_dict(dict) == aapl_option - assert dict == { + result = aapl_option.to_dict() + assert OptionsContract.from_dict(result) == aapl_option + assert result == { "type": "OptionsContract", "id": "AAPL211217C00150000.OPRA", "raw_symbol": "AAPL211217C00150000", From 3bf7e1c75ba73c2aa3fdf3aa000cfd329e91b836 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 1 Nov 2023 19:59:13 +1100 Subject: [PATCH 74/78] Refine futures and options contracts expiration --- RELEASES.md | 16 ++++- .../model/src/instruments/crypto_future.rs | 11 ++- .../model/src/instruments/futures_contract.rs | 11 ++- .../model/src/instruments/options_contract.rs | 11 ++- .../src/python/instruments/crypto_future.rs | 9 ++- .../python/instruments/futures_contract.rs | 9 ++- .../python/instruments/options_contract.rs | 9 ++- .../adapters/binance/futures/providers.py | 11 ++- .../parsing/instruments.py | 35 +++++++--- .../model/instruments/crypto_future.pxd | 8 ++- .../model/instruments/crypto_future.pyx | 47 +++++++++++-- .../model/instruments/futures_contract.pxd | 8 ++- .../model/instruments/futures_contract.pyx | 68 ++++++++++++++----- .../model/instruments/options_contract.pxd | 12 +++- .../model/instruments/options_contract.pyx | 66 +++++++++++++----- .../arrow/implementations/instruments.py | 9 ++- nautilus_trader/test_kit/providers.py | 36 ++++++---- nautilus_trader/test_kit/rust/instruments.py | 48 ++++++++----- .../interactive_brokers/test_providers.py | 3 +- .../instruments/test_crypto_future_pyo3.py | 3 +- .../instruments/test_futures_contract_pyo3.py | 3 +- .../instruments/test_options_contract_pyo3.py | 3 +- tests/unit_tests/model/test_instrument.py | 13 ++-- 23 files changed, 326 insertions(+), 123 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index daed09eb32c9..d2b3fb75ca73 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -7,8 +7,20 @@ Released on TBC (UTC). - Added `support_contingent_orders` option for venues (to simulate venues which do not support contingent orders) - Added `StrategyConfig.manage_contingent_orders` option (to automatically manage **open** contingenct orders) - Improved `RedisCacheDatabase` client connection error handling with retries - -### Breaking Changes +- Added `FuturesContract.activation_utc` property which returns a `pd.Timestamp` tz-aware (UTC) +- Added `OptionsContract.activation_utc` property which returns a `pd.Timestamp` tz-aware (UTC) +- Added `CryptoFuture.activation_utc` property which returns a `pd.Timestamp` tz-aware (UTC) +- Added `FuturesContract.expiration_utc` property which returns a `pd.Timestamp` tz-aware (UTC) +- Added `OptionsContract.expiration_utc` property which returns a `pd.Timestamp` tz-aware (UTC) +- Added `CryptoFuture.expiration_utc` property which returns a `pd.Timestamp` tz-aware (UTC) + +### Breaking Changes +- Renamed `FuturesContract.expiry_date` to `expiration_ns` (and associated params) as `uint64_t` UNIX nanoseconds +- Renamed `OptionsContract.expiry_date` to `expiration_ns` (and associated params) as `uint64_t` UNIX nanoseconds +- Renamed `CryptoFuture.expiry_date` to `expiration_ns` (and associated params) as `uint64_t` UNIX nanoseconds +- Changed `FuturesContract` arrow schema +- Changed `OptionsContract` arrow schema +- Changed `CryptoFuture` arrow schema - Transformed orders will now retain the original `ts_init` timestamp - Removed unimplemented `batch_more` option for `Strategy.modify_order` - Removed `InstrumentProvider.venue` property (redundant as a provider may have many venues) diff --git a/nautilus_core/model/src/instruments/crypto_future.rs b/nautilus_core/model/src/instruments/crypto_future.rs index d989e11f38f9..36b9c15c00b3 100644 --- a/nautilus_core/model/src/instruments/crypto_future.rs +++ b/nautilus_core/model/src/instruments/crypto_future.rs @@ -42,7 +42,8 @@ pub struct CryptoFuture { pub underlying: Currency, pub quote_currency: Currency, pub settlement_currency: Currency, - pub expiration: UnixNanos, + pub activation_ns: UnixNanos, + pub expiration_ns: UnixNanos, pub price_precision: u8, pub size_precision: u8, pub price_increment: Price, @@ -68,7 +69,8 @@ impl CryptoFuture { underlying: Currency, quote_currency: Currency, settlement_currency: Currency, - expiration: UnixNanos, + activation_ns: UnixNanos, + expiration_ns: UnixNanos, price_precision: u8, size_precision: u8, price_increment: Price, @@ -91,7 +93,8 @@ impl CryptoFuture { underlying, quote_currency, settlement_currency, - expiration, + activation_ns, + expiration_ns, price_precision, size_precision, price_increment, @@ -236,6 +239,7 @@ pub mod stubs { #[fixture] pub fn crypto_future_btcusdt() -> CryptoFuture { + let activation = Utc.with_ymd_and_hms(2014, 4, 8, 0, 0, 0).unwrap(); let expiration = Utc.with_ymd_and_hms(2014, 7, 8, 0, 0, 0).unwrap(); CryptoFuture::new( InstrumentId::from("ETHUSDT-123.BINANCE"), @@ -243,6 +247,7 @@ pub mod stubs { Currency::from("BTC"), Currency::from("USDT"), Currency::from("USDT"), + activation.timestamp_nanos_opt().unwrap() as UnixNanos, expiration.timestamp_nanos_opt().unwrap() as UnixNanos, 2, 6, diff --git a/nautilus_core/model/src/instruments/futures_contract.rs b/nautilus_core/model/src/instruments/futures_contract.rs index fd2116a3f925..7c1174a837b7 100644 --- a/nautilus_core/model/src/instruments/futures_contract.rs +++ b/nautilus_core/model/src/instruments/futures_contract.rs @@ -41,7 +41,8 @@ pub struct FuturesContract { pub raw_symbol: Symbol, pub asset_class: AssetClass, pub underlying: String, - pub expiration: UnixNanos, + pub activation_ns: UnixNanos, + pub expiration_ns: UnixNanos, pub currency: Currency, pub price_precision: u8, pub price_increment: Price, @@ -64,7 +65,8 @@ impl FuturesContract { raw_symbol: Symbol, asset_class: AssetClass, underlying: String, - expiration: UnixNanos, + activation_ns: UnixNanos, + expiration_ns: UnixNanos, currency: Currency, price_precision: u8, price_increment: Price, @@ -84,7 +86,8 @@ impl FuturesContract { raw_symbol, asset_class, underlying, - expiration, + activation_ns, + expiration_ns, currency, price_precision, price_increment, @@ -227,12 +230,14 @@ pub mod stubs { #[fixture] pub fn futures_contract_es() -> FuturesContract { + let activation = Utc.with_ymd_and_hms(2021, 4, 8, 0, 0, 0).unwrap(); let expiration = Utc.with_ymd_and_hms(2021, 7, 8, 0, 0, 0).unwrap(); FuturesContract::new( InstrumentId::new(Symbol::from("ESZ21"), Venue::from("CME")), Symbol::from("ESZ21"), AssetClass::Index, String::from("ES"), + activation.timestamp_nanos_opt().unwrap() as UnixNanos, expiration.timestamp_nanos_opt().unwrap() as UnixNanos, Currency::USD(), 2, diff --git a/nautilus_core/model/src/instruments/options_contract.rs b/nautilus_core/model/src/instruments/options_contract.rs index 47e98ced969c..4266348f17d4 100644 --- a/nautilus_core/model/src/instruments/options_contract.rs +++ b/nautilus_core/model/src/instruments/options_contract.rs @@ -42,7 +42,8 @@ pub struct OptionsContract { pub asset_class: AssetClass, pub underlying: String, pub option_kind: OptionKind, - pub expiration: UnixNanos, + pub activation_ns: UnixNanos, + pub expiration_ns: UnixNanos, pub strike_price: Price, pub currency: Currency, pub price_precision: u8, @@ -66,7 +67,8 @@ impl OptionsContract { asset_class: AssetClass, underlying: String, option_kind: OptionKind, - expiration: UnixNanos, + activation_ns: UnixNanos, + expiration_ns: UnixNanos, strike_price: Price, currency: Currency, price_precision: u8, @@ -87,7 +89,8 @@ impl OptionsContract { asset_class, underlying, option_kind, - expiration, + activation_ns, + expiration_ns, strike_price, currency, price_precision, @@ -230,6 +233,7 @@ pub mod stubs { #[fixture] pub fn options_contract_appl() -> OptionsContract { + let activation = Utc.with_ymd_and_hms(2021, 9, 17, 0, 0, 0).unwrap(); let expiration = Utc.with_ymd_and_hms(2021, 12, 17, 0, 0, 0).unwrap(); OptionsContract::new( InstrumentId::from("AAPL211217C00150000.OPRA"), @@ -237,6 +241,7 @@ pub mod stubs { AssetClass::Equity, String::from("AAPL"), OptionKind::Call, + activation.timestamp_nanos_opt().unwrap() as UnixNanos, expiration.timestamp_nanos_opt().unwrap() as UnixNanos, Price::from("149.0"), Currency::USD(), diff --git a/nautilus_core/model/src/python/instruments/crypto_future.rs b/nautilus_core/model/src/python/instruments/crypto_future.rs index feec9d6a119a..f6eac08b3a9e 100644 --- a/nautilus_core/model/src/python/instruments/crypto_future.rs +++ b/nautilus_core/model/src/python/instruments/crypto_future.rs @@ -41,7 +41,8 @@ impl CryptoFuture { underlying: Currency, quote_currency: Currency, settlement_currency: Currency, - expiration: UnixNanos, + activation_ns: UnixNanos, + expiration_ns: UnixNanos, price_precision: u8, size_precision: u8, price_increment: Price, @@ -64,7 +65,8 @@ impl CryptoFuture { underlying, quote_currency, settlement_currency, - expiration, + activation_ns, + expiration_ns, price_precision, size_precision, price_increment, @@ -114,7 +116,8 @@ impl CryptoFuture { "settlement_currency", self.settlement_currency.code.to_string(), )?; - dict.set_item("expiration", self.expiration.to_i64())?; + dict.set_item("activation_ns", self.activation_ns.to_u64())?; + dict.set_item("expiration_ns", self.expiration_ns.to_u64())?; dict.set_item("price_precision", self.price_precision)?; dict.set_item("size_precision", self.size_precision)?; dict.set_item("price_increment", self.price_increment.to_string())?; diff --git a/nautilus_core/model/src/python/instruments/futures_contract.rs b/nautilus_core/model/src/python/instruments/futures_contract.rs index 4165836fce2e..0253cb0aa2b1 100644 --- a/nautilus_core/model/src/python/instruments/futures_contract.rs +++ b/nautilus_core/model/src/python/instruments/futures_contract.rs @@ -41,7 +41,8 @@ impl FuturesContract { raw_symbol: Symbol, asset_class: AssetClass, underlying: String, - expiration: UnixNanos, + activation_ns: UnixNanos, + expiration_ns: UnixNanos, currency: Currency, price_precision: u8, price_increment: Price, @@ -61,7 +62,8 @@ impl FuturesContract { raw_symbol, asset_class, underlying, - expiration, + activation_ns, + expiration_ns, currency, price_precision, price_increment, @@ -106,7 +108,8 @@ impl FuturesContract { dict.set_item("raw_symbol", self.raw_symbol.to_string())?; dict.set_item("asset_class", self.asset_class.to_string())?; dict.set_item("underlying", self.underlying.to_string())?; - dict.set_item("expiration", self.expiration.to_i64())?; + dict.set_item("activation_ns", self.activation_ns.to_u64())?; + dict.set_item("expiration_ns", self.expiration_ns.to_u64())?; dict.set_item("currency", self.currency.code.to_string())?; dict.set_item("price_precision", self.price_precision)?; dict.set_item("price_increment", self.price_increment.to_string())?; diff --git a/nautilus_core/model/src/python/instruments/options_contract.rs b/nautilus_core/model/src/python/instruments/options_contract.rs index 856f9c895775..2283c15dbc39 100644 --- a/nautilus_core/model/src/python/instruments/options_contract.rs +++ b/nautilus_core/model/src/python/instruments/options_contract.rs @@ -42,7 +42,8 @@ impl OptionsContract { asset_class: AssetClass, underlying: String, option_kind: OptionKind, - expiration: UnixNanos, + activation_ns: UnixNanos, + expiration_ns: UnixNanos, strike_price: Price, currency: Currency, price_precision: u8, @@ -63,7 +64,8 @@ impl OptionsContract { asset_class, underlying, option_kind, - expiration, + activation_ns, + expiration_ns, strike_price, currency, price_precision, @@ -109,7 +111,8 @@ impl OptionsContract { dict.set_item("asset_class", self.asset_class.to_string())?; dict.set_item("underlying", self.underlying.to_string())?; dict.set_item("option_kind", self.option_kind.to_string())?; - dict.set_item("expiration", self.expiration.to_i64())?; + dict.set_item("activation_ns", self.activation_ns.to_u64())?; + dict.set_item("expiration_ns", self.expiration_ns.to_u64())?; dict.set_item("strike_price", self.strike_price.to_string())?; dict.set_item("currency", self.currency.code.to_string())?; dict.set_item("price_precision", self.price_precision)?; diff --git a/nautilus_trader/adapters/binance/futures/providers.py b/nautilus_trader/adapters/binance/futures/providers.py index 3aa499fe5eac..3e717aaa5c0d 100644 --- a/nautilus_trader/adapters/binance/futures/providers.py +++ b/nautilus_trader/adapters/binance/futures/providers.py @@ -13,10 +13,10 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from datetime import datetime as dt from decimal import Decimal import msgspec +import pandas as pd from nautilus_trader.adapters.binance.common.constants import BINANCE_VENUE from nautilus_trader.adapters.binance.common.enums import BinanceAccountType @@ -328,13 +328,20 @@ def _parse_instrument( BinanceFuturesContractType.NEXT_MONTH, BinanceFuturesContractType.NEXT_QUARTER, ): + expiry_date_part = symbol_info.symbol.partition("_")[2] + expiration = pd.to_datetime(expiry_date_part, format="%y%m%d", utc=True) + expiration += pd.Timedelta(hours=8) + + activation = expiration - pd.Timedelta(days=90) # TODO: Improve accuracy + instrument = CryptoFuture( instrument_id=instrument_id, raw_symbol=raw_symbol, underlying=base_currency, quote_currency=quote_currency, settlement_currency=settlement_currency, - expiry_date=dt.strptime(symbol_info.symbol.partition("_")[2], "%y%m%d").date(), + activation_ns=activation.value, + expiration_ns=expiration.value, price_precision=price_precision, size_precision=size_precision, price_increment=price_increment, diff --git a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py index 6e53402f8009..a7033a61e1d3 100644 --- a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py +++ b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py @@ -19,12 +19,14 @@ from decimal import Decimal import msgspec +import pandas as pd # fmt: off from nautilus_trader.adapters.interactive_brokers.common import IBContract from nautilus_trader.adapters.interactive_brokers.common import IBContractDetails from nautilus_trader.core.correctness import PyCondition from nautilus_trader.model.currency import Currency +from nautilus_trader.model.enums import AssetClass from nautilus_trader.model.enums import OptionKind from nautilus_trader.model.enums import asset_class_from_str from nautilus_trader.model.identifiers import InstrumentId @@ -94,7 +96,7 @@ re_crypto = re.compile(r"^(?P[A-Z]*)\/(?P[A-Z]{3})$") -def _extract_isin(details: IBContractDetails): +def _extract_isin(details: IBContractDetails) -> int: for tag_value in details.secIdList: if tag_value.tag == "ISIN": return tag_value.value @@ -106,7 +108,7 @@ def _tick_size_to_precision(tick_size: float | Decimal) -> int: return len(tick_size_str.partition(".")[2].rstrip("0")) -def sec_type_to_asset_class(sec_type: str): +def sec_type_to_asset_class(sec_type: str) -> AssetClass: mapping = { "STK": "EQUITY", "IND": "INDEX", @@ -145,6 +147,7 @@ def parse_equity_contract(details: IBContractDetails) -> Equity: price_precision: int = _tick_size_to_precision(details.minTick) timestamp = time.time_ns() instrument_id = ib_contract_to_instrument_id(details.contract) + return Equity( instrument_id=instrument_id, raw_symbol=Symbol(details.contract.localSymbol), @@ -166,6 +169,13 @@ def parse_futures_contract( price_precision: int = _tick_size_to_precision(details.minTick) timestamp = time.time_ns() instrument_id = ib_contract_to_instrument_id(details.contract) + expiration = pd.to_datetime( # TODO: Check correctness + details.contract.lastTradeDateOrContractMonth, + format="%Y%m%d", + utc=True, + ) + activation = expiration - pd.Timedelta(days=90) # TODO: Make this more accurate + return FuturesContract( instrument_id=instrument_id, raw_symbol=Symbol(details.contract.localSymbol), @@ -176,10 +186,8 @@ def parse_futures_contract( multiplier=Quantity.from_str(details.contract.multiplier), lot_size=Quantity.from_int(1), underlying=details.underSymbol, - expiry_date=datetime.datetime.strptime( - details.contract.lastTradeDateOrContractMonth, - "%Y%m%d", - ).date(), + activation_ns=activation.value, + expiration_ns=expiration.value, ts_event=timestamp, ts_init=timestamp, info=contract_details_to_dict(details), @@ -197,6 +205,13 @@ def parse_options_contract( "C": OptionKind.CALL, "P": OptionKind.PUT, }[details.contract.right] + expiration = pd.to_datetime( # TODO: Check correctness + details.contract.lastTradeDateOrContractMonth, + format="%Y%m%d", + utc=True, + ) + activation = expiration - pd.Timedelta(days=90) # TODO: Make this more accurate + return OptionsContract( instrument_id=instrument_id, raw_symbol=Symbol(details.contract.localSymbol), @@ -208,10 +223,8 @@ def parse_options_contract( lot_size=Quantity.from_int(1), underlying=details.underSymbol, strike_price=Price(details.contract.strike, price_precision), - expiry_date=datetime.datetime.strptime( - details.contract.lastTradeDateOrContractMonth, - "%Y%m%d", - ).date(), + activation_ns=activation.value, + expiration_ns=expiration.value, kind=kind, ts_event=timestamp, ts_init=timestamp, @@ -226,6 +239,7 @@ def parse_forex_contract( size_precision: int = _tick_size_to_precision(details.minSize) timestamp = time.time_ns() instrument_id = ib_contract_to_instrument_id(details.contract) + return CurrencyPair( instrument_id=instrument_id, raw_symbol=Symbol(details.contract.localSymbol), @@ -259,6 +273,7 @@ def parse_crypto_contract( size_precision: int = _tick_size_to_precision(details.minSize) timestamp = time.time_ns() instrument_id = ib_contract_to_instrument_id(details.contract) + return CryptoPerpetual( instrument_id=instrument_id, raw_symbol=Symbol(details.contract.localSymbol), diff --git a/nautilus_trader/model/instruments/crypto_future.pxd b/nautilus_trader/model/instruments/crypto_future.pxd index 1642437e0e0b..b2e165cbd0dd 100644 --- a/nautilus_trader/model/instruments/crypto_future.pxd +++ b/nautilus_trader/model/instruments/crypto_future.pxd @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from cpython.datetime cimport date +from libc.stdint cimport uint64_t from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.instruments.base cimport Instrument @@ -24,8 +24,10 @@ cdef class CryptoFuture(Instrument): """The underlying asset for the contract.\n\n:returns: `Currency`""" cdef readonly Currency settlement_currency """The settlement currency for the contract.\n\n:returns: `Currency`""" - cdef readonly date expiry_date - """The expiry date for the contract.\n\n:returns: `date`""" + cdef readonly uint64_t activation_ns + """The UNIX timestamp (nanoseconds) for contract activation.\n\n:returns: `unit64_t`""" + cdef readonly uint64_t expiration_ns + """The UNIX timestamp (nanoseconds) for contract expiration.\n\n:returns: `unit64_t`""" @staticmethod cdef CryptoFuture from_dict_c(dict values) diff --git a/nautilus_trader/model/instruments/crypto_future.pyx b/nautilus_trader/model/instruments/crypto_future.pyx index 9ef87729b5e0..8916732b15c6 100644 --- a/nautilus_trader/model/instruments/crypto_future.pyx +++ b/nautilus_trader/model/instruments/crypto_future.pyx @@ -17,8 +17,9 @@ from decimal import Decimal from typing import Optional import msgspec +import pandas as pd +import pytz -from cpython.datetime cimport date from libc.stdint cimport uint64_t from nautilus_trader.core.correctness cimport Condition @@ -48,8 +49,10 @@ cdef class CryptoFuture(Instrument): The underlying asset. quote_currency : Currency The contract quote currency. - expiry_date : date - The contract expiry date. + activation_ns : uint64_t + The UNIX timestamp (nanoseconds) for contract activation. + expiration_ns : uint64_t + The UNIX timestamp (nanoseconds) for contract expiration. price_precision : int The price decimal precision. size_precision : int @@ -122,7 +125,8 @@ cdef class CryptoFuture(Instrument): Currency underlying not None, Currency quote_currency not None, Currency settlement_currency not None, - date expiry_date, + uint64_t activation_ns, + uint64_t expiration_ns, int price_precision, int size_precision, Price price_increment not None, @@ -173,7 +177,8 @@ cdef class CryptoFuture(Instrument): self.underlying = underlying self.settlement_currency = settlement_currency - self.expiry_date = expiry_date + self.activation_ns = activation_ns + self.expiration_ns = expiration_ns cpdef Currency get_base_currency(self): """ @@ -186,6 +191,32 @@ cdef class CryptoFuture(Instrument): """ return self.underlying + @property + def activation_utc(self) -> pd.Timestamp: + """ + Return the contract activation timestamp (UTC). + + Returns + ------- + pd.Timestamp + tz-aware UTC. + + """ + return pd.Timestamp(self.activation_ns, tz=pytz.utc) + + @property + def expiration_utc(self) -> pd.Timestamp: + """ + Return the contract expriation timestamp (UTC). + + Returns + ------- + pd.Timestamp + tz-aware UTC. + + """ + return pd.Timestamp(self.expiration_ns, tz=pytz.utc) + @staticmethod cdef CryptoFuture from_dict_c(dict values): Condition.not_none(values, "values") @@ -202,7 +233,8 @@ cdef class CryptoFuture(Instrument): underlying=Currency.from_str_c(values["underlying"]), quote_currency=Currency.from_str_c(values["quote_currency"]), settlement_currency=Currency.from_str_c(values["settlement_currency"]), - expiry_date=date.fromisoformat(values['expiry_date']), + activation_ns=values["activation_ns"], + expiration_ns=values["expiration_ns"], price_precision=values["price_precision"], size_precision=values["size_precision"], price_increment=Price.from_str_c(values["price_increment"]), @@ -232,7 +264,8 @@ cdef class CryptoFuture(Instrument): "underlying": obj.underlying.code, "quote_currency": obj.quote_currency.code, "settlement_currency": obj.settlement_currency.code, - "expiry_date": obj.expiry_date.isoformat(), + "activation_ns": obj.activation_ns, + "expiration_ns": obj.expiration_ns, "price_precision": obj.price_precision, "price_increment": str(obj.price_increment), "size_precision": obj.size_precision, diff --git a/nautilus_trader/model/instruments/futures_contract.pxd b/nautilus_trader/model/instruments/futures_contract.pxd index bbb430198720..84a661c37094 100644 --- a/nautilus_trader/model/instruments/futures_contract.pxd +++ b/nautilus_trader/model/instruments/futures_contract.pxd @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from cpython.datetime cimport date +from libc.stdint cimport uint64_t from nautilus_trader.model.instruments.base cimport Instrument @@ -21,8 +21,10 @@ from nautilus_trader.model.instruments.base cimport Instrument cdef class FuturesContract(Instrument): cdef readonly str underlying """The underlying asset for the contract.\n\n:returns: `str`""" - cdef readonly date expiry_date - """The expiry date for the contract.\n\n:returns: `date`""" + cdef readonly uint64_t activation_ns + """The UNIX timestamp (nanoseconds) for contract activation.\n\n:returns: `unit64_t`""" + cdef readonly uint64_t expiration_ns + """The UNIX timestamp (nanoseconds) for contract expiration.\n\n:returns: `unit64_t`""" @staticmethod cdef FuturesContract from_dict_c(dict values) diff --git a/nautilus_trader/model/instruments/futures_contract.pyx b/nautilus_trader/model/instruments/futures_contract.pyx index e1c03cc3540b..fbf1937e6126 100644 --- a/nautilus_trader/model/instruments/futures_contract.pyx +++ b/nautilus_trader/model/instruments/futures_contract.pyx @@ -13,11 +13,13 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from cpython.datetime cimport date -from libc.stdint cimport uint64_t - from decimal import Decimal +import pandas as pd +import pytz + +from libc.stdint cimport uint64_t + from nautilus_trader.core.correctness cimport Condition from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.enums_c cimport AssetClass @@ -55,8 +57,10 @@ cdef class FuturesContract(Instrument): The rounded lot unit size (standard/board). underlying : str The underlying asset. - expiry_date : date - The contract expiry date. + activation_ns : uint64_t + The UNIX timestamp (nanoseconds) for contract activation. + expiration_ns : uint64_t + The UNIX timestamp (nanoseconds) for contract expiration. ts_event : uint64_t The UNIX timestamp (nanoseconds) when the data event occurred. ts_init : uint64_t @@ -87,7 +91,8 @@ cdef class FuturesContract(Instrument): Quantity multiplier, Quantity lot_size not None, str underlying, - date expiry_date, + uint64_t activation_ns, + uint64_t expiration_ns, uint64_t ts_event, uint64_t ts_init, dict info = None, @@ -120,7 +125,8 @@ cdef class FuturesContract(Instrument): info=info, ) self.underlying = underlying - self.expiry_date = expiry_date + self.activation_ns = activation_ns + self.expiration_ns = expiration_ns @staticmethod cdef FuturesContract from_dict_c(dict values): @@ -129,15 +135,16 @@ cdef class FuturesContract(Instrument): instrument_id=InstrumentId.from_str_c(values["id"]), raw_symbol=Symbol(values["raw_symbol"]), asset_class=asset_class_from_str(values["asset_class"]), - currency=Currency.from_str_c(values['currency']), - price_precision=values['price_precision'], - price_increment=Price.from_str(values['price_increment']), - multiplier=Quantity.from_str(values['multiplier']), - lot_size=Quantity.from_str(values['lot_size']), - underlying=values['underlying'], - expiry_date=date.fromisoformat(values['expiry_date']), - ts_event=values['ts_event'], - ts_init=values['ts_init'], + currency=Currency.from_str_c(values["currency"]), + price_precision=values["price_precision"], + price_increment=Price.from_str(values["price_increment"]), + multiplier=Quantity.from_str(values["multiplier"]), + lot_size=Quantity.from_str(values["lot_size"]), + underlying=values["underlying"], + activation_ns=values["activation_ns"], + expiration_ns=values["expiration_ns"], + ts_event=values["ts_event"], + ts_init=values["ts_init"], ) @staticmethod @@ -156,13 +163,40 @@ cdef class FuturesContract(Instrument): "multiplier": str(obj.multiplier), "lot_size": str(obj.lot_size), "underlying": obj.underlying, - "expiry_date": obj.expiry_date.isoformat(), + "activation_ns": obj.activation_ns, + "expiration_ns": obj.expiration_ns, "margin_init": str(obj.margin_init), "margin_maint": str(obj.margin_maint), "ts_event": obj.ts_event, "ts_init": obj.ts_init, } + @property + def activation_utc(self) -> pd.Timestamp: + """ + Return the contract activation timestamp (UTC). + + Returns + ------- + pd.Timestamp + tz-aware UTC. + + """ + return pd.Timestamp(self.activation_ns, tz=pytz.utc) + + @property + def expiration_utc(self) -> pd.Timestamp: + """ + Return the contract expriation timestamp (UTC). + + Returns + ------- + pd.Timestamp + tz-aware UTC. + + """ + return pd.Timestamp(self.expiration_ns, tz=pytz.utc) + @staticmethod def from_dict(dict values) -> FuturesContract: """ diff --git a/nautilus_trader/model/instruments/options_contract.pxd b/nautilus_trader/model/instruments/options_contract.pxd index a4a1adc8aa31..4b59050d2fb0 100644 --- a/nautilus_trader/model/instruments/options_contract.pxd +++ b/nautilus_trader/model/instruments/options_contract.pxd @@ -13,7 +13,7 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from cpython.datetime cimport date +from libc.stdint cimport uint64_t from nautilus_trader.model.enums_c cimport OptionKind from nautilus_trader.model.instruments.base cimport Instrument @@ -22,9 +22,15 @@ from nautilus_trader.model.objects cimport Price cdef class OptionsContract(Instrument): cdef readonly str underlying - cdef readonly date expiry_date - cdef readonly Price strike_price + """The underlying asset for the contract.\n\n:returns: `str`""" cdef readonly OptionKind kind + """The options kind (PUT | CALL) for the contract.\n\n:returns: `OptionKind`""" + cdef readonly uint64_t activation_ns + """The UNIX timestamp (nanoseconds) for contract activation.\n\n:returns: `unit64_t`""" + cdef readonly uint64_t expiration_ns + """The UNIX timestamp (nanoseconds) for contract expiration.\n\n:returns: `unit64_t`""" + cdef readonly Price strike_price + """The strike price for the contract.\n\n:returns: `Price`""" @staticmethod cdef OptionsContract from_dict_c(dict values) diff --git a/nautilus_trader/model/instruments/options_contract.pyx b/nautilus_trader/model/instruments/options_contract.pyx index 12350ae91601..1c117c656a0f 100644 --- a/nautilus_trader/model/instruments/options_contract.pyx +++ b/nautilus_trader/model/instruments/options_contract.pyx @@ -13,11 +13,13 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from cpython.datetime cimport date -from libc.stdint cimport uint64_t - from decimal import Decimal +import pandas as pd +import pytz + +from libc.stdint cimport uint64_t + from nautilus_trader.core.correctness cimport Condition from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.enums_c cimport AssetClass @@ -56,12 +58,14 @@ cdef class OptionsContract(Instrument): The option multiplier. lot_size : Quantity The rounded lot unit size (standard/board). - strike_price : Price - The option strike price. underlying : str The underlying asset. - expiry_date : date - The option expiry date. + strike_price : Price + The option strike price. + activation_ns : uint64_t + The UNIX timestamp (nanoseconds) for contract activation. + expiration_ns : uint64_t + The UNIX timestamp (nanoseconds) for contract expiration. ts_event : uint64_t The UNIX timestamp (nanoseconds) when the data event occurred. ts_init : uint64_t @@ -91,10 +95,11 @@ cdef class OptionsContract(Instrument): Price price_increment not None, Quantity multiplier not None, Quantity lot_size not None, - Price strike_price not None, str underlying, - date expiry_date, OptionKind kind, + uint64_t activation_ns, + uint64_t expiration_ns, + Price strike_price not None, uint64_t ts_event, uint64_t ts_init, dict info = None, @@ -128,9 +133,36 @@ cdef class OptionsContract(Instrument): info=info, ) self.underlying = underlying - self.expiry_date = expiry_date - self.strike_price = strike_price self.kind = kind + self.activation_ns = activation_ns + self.expiration_ns = expiration_ns + self.strike_price = strike_price + + @property + def activation_utc(self) -> pd.Timestamp: + """ + Return the contract activation timestamp (UTC). + + Returns + ------- + pd.Timestamp + tz-aware UTC. + + """ + return pd.Timestamp(self.activation_ns, tz=pytz.utc) + + @property + def expiration_utc(self) -> pd.Timestamp: + """ + Return the contract expriation timestamp (UTC). + + Returns + ------- + pd.Timestamp + tz-aware UTC. + + """ + return pd.Timestamp(self.expiration_ns, tz=pytz.utc) @staticmethod cdef OptionsContract from_dict_c(dict values): @@ -144,10 +176,11 @@ cdef class OptionsContract(Instrument): price_increment=Price.from_str(values["price_increment"]), multiplier=Quantity.from_str(values["multiplier"]), lot_size=Quantity.from_str(values["lot_size"]), - underlying=values['underlying'], - expiry_date=date.fromisoformat(values["expiry_date"]), - strike_price=Price.from_str(values["strike_price"]), + underlying=values["underlying"], kind=option_kind_from_str(values["kind"]), + activation_ns=values["activation_ns"], + expiration_ns=values["expiration_ns"], + strike_price=Price.from_str(values["strike_price"]), ts_event=values["ts_event"], ts_init=values["ts_init"], ) @@ -168,11 +201,12 @@ cdef class OptionsContract(Instrument): "multiplier": str(obj.multiplier), "lot_size": str(obj.lot_size), "underlying": str(obj.underlying), - "expiry_date": obj.expiry_date.isoformat(), + "kind": option_kind_to_str(obj.kind), + "activation_ns": obj.activation_ns, + "expiration_ns": obj.expiration_ns, "strike_price": str(obj.strike_price), "margin_init": str(obj.margin_init), "margin_maint": str(obj.margin_maint), - "kind": option_kind_to_str(obj.kind), "ts_event": obj.ts_event, "ts_init": obj.ts_init, } diff --git a/nautilus_trader/serialization/arrow/implementations/instruments.py b/nautilus_trader/serialization/arrow/implementations/instruments.py index 15054caa3820..ca14e800298e 100644 --- a/nautilus_trader/serialization/arrow/implementations/instruments.py +++ b/nautilus_trader/serialization/arrow/implementations/instruments.py @@ -112,7 +112,8 @@ "underlying": pa.dictionary(pa.int16(), pa.string()), "quote_currency": pa.dictionary(pa.int16(), pa.string()), "settlement_currency": pa.dictionary(pa.int16(), pa.string()), - "expiry_date": pa.dictionary(pa.int16(), pa.string()), + "activation_ns": pa.uint64(), + "expiration_ns": pa.uint64(), "price_precision": pa.uint8(), "size_precision": pa.uint8(), "price_increment": pa.dictionary(pa.int16(), pa.string()), @@ -165,7 +166,8 @@ "size_increment": pa.dictionary(pa.int16(), pa.string()), "multiplier": pa.dictionary(pa.int16(), pa.string()), "lot_size": pa.dictionary(pa.int16(), pa.string()), - "expiry_date": pa.dictionary(pa.int16(), pa.string()), + "activation_ns": pa.uint64(), + "expiration_ns": pa.uint64(), "ts_event": pa.uint64(), "ts_init": pa.uint64(), }, @@ -183,7 +185,8 @@ "size_increment": pa.dictionary(pa.int16(), pa.string()), "multiplier": pa.dictionary(pa.int16(), pa.string()), "lot_size": pa.dictionary(pa.int16(), pa.string()), - "expiry_date": pa.dictionary(pa.int64(), pa.string()), + "activation_ns": pa.uint64(), + "expiration_ns": pa.uint64(), "strike_price": pa.dictionary(pa.int64(), pa.string()), "kind": pa.dictionary(pa.int8(), pa.string()), "ts_event": pa.uint64(), diff --git a/nautilus_trader/test_kit/providers.py b/nautilus_trader/test_kit/providers.py index 50287b6a6074..9489e2e3d46c 100644 --- a/nautilus_trader/test_kit/providers.py +++ b/nautilus_trader/test_kit/providers.py @@ -15,13 +15,13 @@ import pathlib import random -from datetime import date from decimal import Decimal from typing import Any import fsspec import numpy as np import pandas as pd +import pytz from fsspec.implementations.local import LocalFileSystem from pandas.io.parsers.readers import TextFileReader @@ -254,32 +254,40 @@ def ethusdt_perp_binance() -> CryptoPerpetual: ) @staticmethod - def btcusdt_future_binance(expiry: date | None = None) -> CryptoFuture: + def btcusdt_future_binance( + activation: pd.Timestamp | None = None, + expiration: pd.Timestamp | None = None, + ) -> CryptoFuture: """ Return the Binance Futures BTCUSDT instrument for backtesting. Parameters ---------- - expiry : date, optional - The expiry date for the contract. + activation : pd.Timestamp, optional + The activation (UTC) for the contract. + expiration : pd.Timestamp, optional + The expiration (UTC) for the contract. Returns ------- CryptoFuture """ - if expiry is None: - expiry = date(2022, 3, 25) + if activation is None: + activation = pd.Timestamp(2021, 12, 25, tz=pytz.utc) + if expiration is None: + expiration = pd.Timestamp(2022, 3, 25, tz=pytz.utc) return CryptoFuture( instrument_id=InstrumentId( - symbol=Symbol(f"BTCUSDT_{expiry.strftime('%y%m%d')}"), + symbol=Symbol(f"BTCUSDT_{expiration.strftime('%y%m%d')}"), venue=Venue("BINANCE"), ), raw_symbol=Symbol("BTCUSDT"), underlying=BTC, quote_currency=USDT, settlement_currency=USDT, - expiry_date=expiry, + activation_ns=activation.value, + expiration_ns=expiration.value, price_precision=2, size_precision=6, price_increment=Price(1e-02, precision=2), @@ -461,7 +469,7 @@ def equity(symbol: str = "AAPL", venue: str = "NASDAQ") -> Equity: def future( symbol: str = "ESZ21", underlying: str = "ES", - venue: str = "CME", + venue: str = "GLBX", ) -> FuturesContract: return FuturesContract( instrument_id=InstrumentId(symbol=Symbol(symbol), venue=Venue(venue)), @@ -473,9 +481,10 @@ def future( multiplier=Quantity.from_int(1), lot_size=Quantity.from_int(1), underlying=underlying, - expiry_date=date(2021, 12, 17), - ts_event=0, - ts_init=0, + activation_ns=1616160600000000000, + expiration_ns=1639751400000000000, + ts_event=1638133151389539971, + ts_init=1638316800000000000, ) @staticmethod @@ -491,8 +500,9 @@ def aapl_option() -> OptionsContract: lot_size=Quantity.from_int(1), underlying="AAPL", kind=OptionKind.CALL, - expiry_date=date(2021, 12, 17), strike_price=Price.from_str("149.00"), + activation_ns=pd.Timestamp(2021, 9, 17, tz=pytz.utc).value, + expiration_ns=pd.Timestamp(2021, 12, 17, tz=pytz.utc).value, ts_event=0, ts_init=0, ) diff --git a/nautilus_trader/test_kit/rust/instruments.py b/nautilus_trader/test_kit/rust/instruments.py index c7a5ba546d64..d73c3d04587a 100644 --- a/nautilus_trader/test_kit/rust/instruments.py +++ b/nautilus_trader/test_kit/rust/instruments.py @@ -61,18 +61,24 @@ def ethusdt_perp_binance() -> CryptoPerpetual: ) @staticmethod - def btcusdt_future_binance(expiry: pd.Timestamp | None = None) -> CryptoFuture: - if expiry is None: - expiry = pd.Timestamp(datetime(2022, 3, 25), tz=pytz.UTC) - nanos_expiry = int(expiry.timestamp() * 1e9) - instrument_id_str = f"BTCUSDT_{expiry.strftime('%y%m%d')}.BINANCE" + def btcusdt_future_binance( + activation: pd.Timestamp | None = None, + expiration: pd.Timestamp | None = None, + ) -> CryptoFuture: + if activation is None: + activation = pd.Timestamp(2021, 12, 25, tz=pytz.utc) + if expiration is None: + expiration = pd.Timestamp(2022, 3, 25, tz=pytz.utc) + + instrument_id_str = f"BTCUSDT_{expiration.strftime('%y%m%d')}.BINANCE" return CryptoFuture( # type: ignore InstrumentId.from_str(instrument_id_str), Symbol("BTCUSDT"), TestTypesProviderPyo3.currency_btc(), TestTypesProviderPyo3.currency_usdt(), TestTypesProviderPyo3.currency_usdt(), - nanos_expiry, + activation.value, + expiration.value, 2, 6, Price.from_str("0.01"), @@ -113,17 +119,22 @@ def btcusdt_binance() -> CurrencyPair: ) @staticmethod - def appl_option(expiry: pd.Timestamp | None = None) -> OptionsContract: - if expiry is None: - expiry = pd.Timestamp(datetime(2021, 12, 17), tz=pytz.UTC) - nanos_expiry = int(expiry.timestamp() * 1e9) + def appl_option( + activation: pd.Timestamp | None = None, + expiration: pd.Timestamp | None = None, + ) -> OptionsContract: + if activation is None: + activation = pd.Timestamp(datetime(2021, 9, 17), tz=pytz.UTC) + if expiration is None: + expiration = pd.Timestamp(datetime(2021, 12, 17), tz=pytz.UTC) return OptionsContract( # type: ignore InstrumentId.from_str("AAPL211217C00150000.OPRA"), Symbol("AAPL211217C00150000"), AssetClass.EQUITY, "AAPL", OptionKind.CALL, - nanos_expiry, + activation.value, + expiration.value, Price.from_str("149.0"), TestTypesProviderPyo3.currency_usdt(), 2, @@ -157,16 +168,21 @@ def appl_equity() -> Equity: ) @staticmethod - def futures_contract_es(expiry: pd.Timestamp | None = None) -> FuturesContract: - if expiry is None: - expiry = pd.Timestamp(datetime(2021, 12, 17), tz=pytz.UTC) - nanos_expiry = int(expiry.timestamp() * 1e9) + def futures_contract_es( + activation: pd.Timestamp | None = None, + expiration: pd.Timestamp | None = None, + ) -> FuturesContract: + if activation is None: + activation = pd.Timestamp(2021, 9, 17, tz=pytz.utc) + if expiration is None: + expiration = pd.Timestamp(2021, 12, 17, tz=pytz.utc) return FuturesContract( # type: ignore InstrumentId.from_str("ESZ21.CME"), Symbol("ESZ21"), AssetClass.INDEX, "ES", - nanos_expiry, + activation.value, + expiration.value, TestTypesProviderPyo3.currency_usd(), 2, Price.from_str("0.01"), diff --git a/tests/integration_tests/adapters/interactive_brokers/test_providers.py b/tests/integration_tests/adapters/interactive_brokers/test_providers.py index 281af9c38dfe..ebda08b0c59a 100644 --- a/tests/integration_tests/adapters/interactive_brokers/test_providers.py +++ b/tests/integration_tests/adapters/interactive_brokers/test_providers.py @@ -13,7 +13,6 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -import datetime from unittest.mock import AsyncMock import msgspec.structs @@ -113,7 +112,7 @@ async def test_load_options_contract_instrument(mocker, instrument_provider): assert option.id == instrument_id assert option.asset_class == AssetClass.EQUITY assert option.multiplier == 100 - assert option.expiry_date == datetime.date(2023, 1, 20) + assert option.expiration_ns == 1674172800000000000 assert option.strike_price == Price.from_str("100.0") assert option.kind == OptionKind.CALL assert option.price_increment == Price.from_str("0.01") diff --git a/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py b/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py index cf45f56fb748..1e7317b28b1f 100644 --- a/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py +++ b/tests/unit_tests/model/instruments/test_crypto_future_pyo3.py @@ -40,7 +40,8 @@ def test_to_dict(): "underlying": "BTC", "quote_currency": "USDT", "settlement_currency": "USDT", - "expiration": 1648166400000000000, + "activation_ns": 1640390400000000000, + "expiration_ns": 1648166400000000000, "price_precision": 2, "size_precision": 6, "price_increment": "0.01", diff --git a/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py b/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py index c519e0058c11..5f03466e75a4 100644 --- a/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py +++ b/tests/unit_tests/model/instruments/test_futures_contract_pyo3.py @@ -39,7 +39,8 @@ def test_to_dict(): "raw_symbol": "ESZ21", "asset_class": "INDEX", "underlying": "ES", - "expiration": 1639699200000000000, + "activation_ns": 1631836800000000000, + "expiration_ns": 1639699200000000000, "currency": "USD", "price_precision": 2, "price_increment": "0.01", diff --git a/tests/unit_tests/model/instruments/test_options_contract_pyo3.py b/tests/unit_tests/model/instruments/test_options_contract_pyo3.py index 2ad8c7ca14a4..e8d409819bcb 100644 --- a/tests/unit_tests/model/instruments/test_options_contract_pyo3.py +++ b/tests/unit_tests/model/instruments/test_options_contract_pyo3.py @@ -40,7 +40,8 @@ def test_to_dict(): "asset_class": "EQUITY", "underlying": "AAPL", "option_kind": "CALL", - "expiration": 1639699200000000000, + "activation_ns": 1631836800000000000, + "expiration_ns": 1639699200000000000, "strike_price": "149.0", "currency": "USDT", "price_precision": 2, diff --git a/tests/unit_tests/model/test_instrument.py b/tests/unit_tests/model/test_instrument.py index f6ba5043dc44..b77ca7378d0a 100644 --- a/tests/unit_tests/model/test_instrument.py +++ b/tests/unit_tests/model/test_instrument.py @@ -198,7 +198,8 @@ def test_crypto_future_instrument_to_dict(self): "underlying": "BTC", "quote_currency": "USDT", "settlement_currency": "USDT", - "expiry_date": "2022-03-25", + "activation_ns": 1640390400000000000, + "expiration_ns": 1648166400000000000, "price_precision": 2, "price_increment": "0.01", "size_precision": 6, @@ -253,7 +254,8 @@ def test_future_instrument_to_dict(self): assert result == { "asset_class": "INDEX", "currency": "USD", - "expiry_date": "2021-12-17", + "activation_ns": 1616160600000000000, + "expiration_ns": 1639751400000000000, "id": "ESZ21.CME", "lot_size": "1", "margin_init": "0", @@ -264,8 +266,8 @@ def test_future_instrument_to_dict(self): "price_precision": 2, "size_increment": "1", "size_precision": 0, - "ts_event": 0, - "ts_init": 0, + "ts_event": 1638133151389539971, + "ts_init": 1638316800000000000, "type": "FuturesContract", "underlying": "ES", } @@ -279,7 +281,8 @@ def test_option_instrument_to_dict(self): assert result == { "asset_class": "EQUITY", "currency": "USD", - "expiry_date": "2021-12-17", + "activation_ns": 1631836800000000000, + "expiration_ns": 1639699200000000000, "id": "AAPL211217C00150000.OPRA", "kind": "CALL", "lot_size": "1", From a448607176d30dd255afc6424f0c0bfa81f862df Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 2 Nov 2023 18:20:25 +1100 Subject: [PATCH 75/78] Update dependencies --- nautilus_core/Cargo.lock | 60 ++--- nautilus_core/model/Cargo.toml | 2 +- poetry.lock | 474 +++++++++++++++++---------------- pyproject.toml | 10 +- 4 files changed, 278 insertions(+), 268 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 8828d086aaa9..22feb21860b2 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -259,7 +259,7 @@ dependencies = [ "arrow-schema", "chrono", "half 2.3.1", - "indexmap 2.0.2", + "indexmap 2.1.0", "lexical-core", "num", "serde", @@ -1027,7 +1027,7 @@ dependencies = [ "glob", "half 2.3.1", "hashbrown 0.14.2", - "indexmap 2.0.2", + "indexmap 2.1.0", "itertools 0.11.0", "log", "num_cpus", @@ -1141,7 +1141,7 @@ dependencies = [ "half 2.3.1", "hashbrown 0.14.2", "hex", - "indexmap 2.0.2", + "indexmap 2.1.0", "itertools 0.11.0", "libc", "log", @@ -1175,7 +1175,7 @@ dependencies = [ "futures", "half 2.3.1", "hashbrown 0.14.2", - "indexmap 2.0.2", + "indexmap 2.1.0", "itertools 0.11.0", "log", "once_cell", @@ -1718,9 +1718,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown 0.14.2", @@ -1784,9 +1784,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" dependencies = [ "wasm-bindgen", ] @@ -2072,7 +2072,7 @@ dependencies = [ "evalexpr", "float-cmp", "iai", - "indexmap 2.0.2", + "indexmap 2.1.0", "nautilus-core", "once_cell", "pyo3", @@ -2295,9 +2295,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "openssl" -version = "0.10.57" +version = "0.10.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +checksum = "a9dfc0783362704e97ef3bd24261995a699468440099ef95d869b4d9732f829a" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -2336,9 +2336,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "2f55da20b29f956fb01f0add8683eb26ee13ebe3ebd935e49898717c6b4b2830" dependencies = [ "cc", "libc", @@ -2464,7 +2464,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.0.2", + "indexmap 2.1.0", ] [[package]] @@ -3958,9 +3958,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3968,9 +3968,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" dependencies = [ "bumpalo", "log", @@ -3983,9 +3983,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3993,9 +3993,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", @@ -4006,15 +4006,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" dependencies = [ "js-sys", "wasm-bindgen", @@ -4155,18 +4155,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.20" +version = "0.7.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd66a62464e3ffd4e37bd09950c2b9dd6c4f8767380fabba0d523f9a775bc85a" +checksum = "686b7e407015242119c33dab17b8f61ba6843534de936d94368856528eae4dcc" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.20" +version = "0.7.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "255c4596d41e6916ced49cfafea18727b24d67878fa180ddfd69b9df34fd1726" +checksum = "020f3dfe25dfc38dfea49ce62d5d45ecdd7f0d8a724fa63eb36b6eba4ec76806" dependencies = [ "proc-macro2", "quote", diff --git a/nautilus_core/model/Cargo.toml b/nautilus_core/model/Cargo.toml index ac655b32c86a..899776896401 100644 --- a/nautilus_core/model/Cargo.toml +++ b/nautilus_core/model/Cargo.toml @@ -26,7 +26,7 @@ ustr = { workspace = true } chrono = { workspace = true } derive_builder = "0.12.0" evalexpr = "11.1.0" -indexmap = "2.0.2" +indexmap = "2.1.0" tabled = "0.12.2" thousands = "0.2.0" diff --git a/poetry.lock b/poetry.lock index fcb957caf3c4..e1c9d1430109 100644 --- a/poetry.lock +++ b/poetry.lock @@ -338,101 +338,101 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.3.1" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.1.tar.gz", hash = "sha256:d9137a876020661972ca6eec0766d81aef8a5627df628b664b234b73396e727e"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8aee051c89e13565c6bd366813c386939f8e928af93c29fda4af86d25b73d8f8"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:352a88c3df0d1fa886562384b86f9a9e27563d4704ee0e9d56ec6fcd270ea690"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:223b4d54561c01048f657fa6ce41461d5ad8ff128b9678cfe8b2ecd951e3f8a2"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f861d94c2a450b974b86093c6c027888627b8082f1299dfd5a4bae8e2292821"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1171ef1fc5ab4693c5d151ae0fdad7f7349920eabbaca6271f95969fa0756c2d"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28f512b9a33235545fbbdac6a330a510b63be278a50071a336afc1b78781b147"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e842112fe3f1a4ffcf64b06dc4c61a88441c2f02f373367f7b4c1aa9be2ad5"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f9bc2ce123637a60ebe819f9fccc614da1bcc05798bbbaf2dd4ec91f3e08846"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f194cce575e59ffe442c10a360182a986535fd90b57f7debfaa5c845c409ecc3"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a74041ba0bfa9bc9b9bb2cd3238a6ab3b7618e759b41bd15b5f6ad958d17605"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b578cbe580e3b41ad17b1c428f382c814b32a6ce90f2d8e39e2e635d49e498d1"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6db3cfb9b4fcecb4390db154e75b49578c87a3b9979b40cdf90d7e4b945656e1"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:debb633f3f7856f95ad957d9b9c781f8e2c6303ef21724ec94bea2ce2fcbd056"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-win32.whl", hash = "sha256:87071618d3d8ec8b186d53cb6e66955ef2a0e4fa63ccd3709c0c90ac5a43520f"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:e372d7dfd154009142631de2d316adad3cc1c36c32a38b16a4751ba78da2a397"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae4070f741f8d809075ef697877fd350ecf0b7c5837ed68738607ee0a2c572cf"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58e875eb7016fd014c0eea46c6fa92b87b62c0cb31b9feae25cbbe62c919f54d"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbd95e300367aa0827496fe75a1766d198d34385a58f97683fe6e07f89ca3e3c"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de0b4caa1c8a21394e8ce971997614a17648f94e1cd0640fbd6b4d14cab13a72"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:985c7965f62f6f32bf432e2681173db41336a9c2611693247069288bcb0c7f8b"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a15c1fe6d26e83fd2e5972425a772cca158eae58b05d4a25a4e474c221053e2d"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae55d592b02c4349525b6ed8f74c692509e5adffa842e582c0f861751701a673"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4d9c2770044a59715eb57c1144dedea7c5d5ae80c68fb9959515037cde2008"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:851cf693fb3aaef71031237cd68699dded198657ec1e76a76eb8be58c03a5d1f"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:31bbaba7218904d2eabecf4feec0d07469284e952a27400f23b6628439439fa7"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:871d045d6ccc181fd863a3cd66ee8e395523ebfbc57f85f91f035f50cee8e3d4"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:501adc5eb6cd5f40a6f77fbd90e5ab915c8fd6e8c614af2db5561e16c600d6f3"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f5fb672c396d826ca16a022ac04c9dce74e00a1c344f6ad1a0fdc1ba1f332213"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-win32.whl", hash = "sha256:bb06098d019766ca16fc915ecaa455c1f1cd594204e7f840cd6258237b5079a8"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:8af5a8917b8af42295e86b64903156b4f110a30dca5f3b5aedea123fbd638bff"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7ae8e5142dcc7a49168f4055255dbcced01dc1714a90a21f87448dc8d90617d1"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5b70bab78accbc672f50e878a5b73ca692f45f5b5e25c8066d748c09405e6a55"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ceca5876032362ae73b83347be8b5dbd2d1faf3358deb38c9c88776779b2e2f"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d95638ff3613849f473afc33f65c401a89f3b9528d0d213c7037c398a51296"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edbe6a5bf8b56a4a84533ba2b2f489d0046e755c29616ef8830f9e7d9cf5728"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6a02a3c7950cafaadcd46a226ad9e12fc9744652cc69f9e5534f98b47f3bbcf"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10b8dd31e10f32410751b3430996f9807fc4d1587ca69772e2aa940a82ab571a"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edc0202099ea1d82844316604e17d2b175044f9bcb6b398aab781eba957224bd"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b891a2f68e09c5ef989007fac11476ed33c5c9994449a4e2c3386529d703dc8b"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:71ef3b9be10070360f289aea4838c784f8b851be3ba58cf796262b57775c2f14"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:55602981b2dbf8184c098bc10287e8c245e351cd4fdcad050bd7199d5a8bf514"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:46fb9970aa5eeca547d7aa0de5d4b124a288b42eaefac677bde805013c95725c"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:520b7a142d2524f999447b3a0cf95115df81c4f33003c51a6ab637cbda9d0bf4"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-win32.whl", hash = "sha256:8ec8ef42c6cd5856a7613dcd1eaf21e5573b2185263d87d27c8edcae33b62a61"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:baec8148d6b8bd5cee1ae138ba658c71f5b03e0d69d5907703e3e1df96db5e41"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63a6f59e2d01310f754c270e4a257426fe5a591dc487f1983b3bbe793cf6bac6"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d6bfc32a68bc0933819cfdfe45f9abc3cae3877e1d90aac7259d57e6e0f85b1"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f3100d86dcd03c03f7e9c3fdb23d92e32abbca07e7c13ebd7ddfbcb06f5991f"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39b70a6f88eebe239fa775190796d55a33cfb6d36b9ffdd37843f7c4c1b5dc67"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e12f8ee80aa35e746230a2af83e81bd6b52daa92a8afaef4fea4a2ce9b9f4fa"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b6cefa579e1237ce198619b76eaa148b71894fb0d6bcf9024460f9bf30fd228"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:61f1e3fb621f5420523abb71f5771a204b33c21d31e7d9d86881b2cffe92c47c"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4f6e2a839f83a6a76854d12dbebde50e4b1afa63e27761549d006fa53e9aa80e"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:1ec937546cad86d0dce5396748bf392bb7b62a9eeb8c66efac60e947697f0e58"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:82ca51ff0fc5b641a2d4e1cc8c5ff108699b7a56d7f3ad6f6da9dbb6f0145b48"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:633968254f8d421e70f91c6ebe71ed0ab140220469cf87a9857e21c16687c034"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-win32.whl", hash = "sha256:c0c72d34e7de5604df0fde3644cc079feee5e55464967d10b24b1de268deceb9"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:63accd11149c0f9a99e3bc095bbdb5a464862d77a7e309ad5938fbc8721235ae"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5a3580a4fdc4ac05f9e53c57f965e3594b2f99796231380adb2baaab96e22761"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2465aa50c9299d615d757c1c888bc6fef384b7c4aec81c05a0172b4400f98557"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb7cd68814308aade9d0c93c5bd2ade9f9441666f8ba5aa9c2d4b389cb5e2a45"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e43805ccafa0a91831f9cd5443aa34528c0c3f2cc48c4cb3d9a7721053874b"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:854cc74367180beb327ab9d00f964f6d91da06450b0855cbbb09187bcdb02de5"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c15070ebf11b8b7fd1bfff7217e9324963c82dbdf6182ff7050519e350e7ad9f"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4c99f98fc3a1835af8179dcc9013f93594d0670e2fa80c83aa36346ee763d2"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb765362688821404ad6cf86772fc54993ec11577cd5a92ac44b4c2ba52155b"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dced27917823df984fe0c80a5c4ad75cf58df0fbfae890bc08004cd3888922a2"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a66bcdf19c1a523e41b8e9d53d0cedbfbac2e93c649a2e9502cb26c014d0980c"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ecd26be9f112c4f96718290c10f4caea6cc798459a3a76636b817a0ed7874e42"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f70fd716855cd3b855316b226a1ac8bdb3caf4f7ea96edcccc6f484217c9597"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:17a866d61259c7de1bdadef418a37755050ddb4b922df8b356503234fff7932c"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-win32.whl", hash = "sha256:548eefad783ed787b38cb6f9a574bd8664468cc76d1538215d510a3cd41406cb"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:45f053a0ece92c734d874861ffe6e3cc92150e32136dd59ab1fb070575189c97"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bc791ec3fd0c4309a753f95bb6c749ef0d8ea3aea91f07ee1cf06b7b02118f2f"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8c61fb505c7dad1d251c284e712d4e0372cef3b067f7ddf82a7fa82e1e9a93"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2c092be3885a1b7899cd85ce24acedc1034199d6fca1483fa2c3a35c86e43041"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2000c54c395d9e5e44c99dc7c20a64dc371f777faf8bae4919ad3e99ce5253e"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cb50a0335382aac15c31b61d8531bc9bb657cfd848b1d7158009472189f3d62"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c30187840d36d0ba2893bc3271a36a517a717f9fd383a98e2697ee890a37c273"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe81b35c33772e56f4b6cf62cf4aedc1762ef7162a31e6ac7fe5e40d0149eb67"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0bf89afcbcf4d1bb2652f6580e5e55a840fdf87384f6063c4a4f0c95e378656"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:06cf46bdff72f58645434d467bf5228080801298fbba19fe268a01b4534467f5"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3c66df3f41abee950d6638adc7eac4730a306b022570f71dd0bd6ba53503ab57"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd805513198304026bd379d1d516afbf6c3c13f4382134a2c526b8b854da1c2e"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:9505dc359edb6a330efcd2be825fdb73ee3e628d9010597aa1aee5aa63442e97"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:31445f38053476a0c4e6d12b047b08ced81e2c7c712e5a1ad97bc913256f91b2"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-win32.whl", hash = "sha256:bd28b31730f0e982ace8663d108e01199098432a30a4c410d06fe08fdb9e93f4"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:555fe186da0068d3354cdf4bbcbc609b0ecae4d04c921cc13e209eece7720727"}, - {file = "charset_normalizer-3.3.1-py3-none-any.whl", hash = "sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] @@ -577,7 +577,7 @@ name = "css-html-js-minify" version = "2.5.5" description = "CSS HTML JS Minifier" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ {file = "css-html-js-minify-2.5.5.zip", hash = "sha256:4a9f11f7e0496f5284d12111f3ba4ff5ff2023d12f15d195c9c48bd97013746c"}, {file = "css_html_js_minify-2.5.5-py2.py3-none-any.whl", hash = "sha256:3da9d35ac0db8ca648c1b543e0e801d7ca0bab9e6bfd8418fee59d5ae001727a"}, @@ -585,69 +585,69 @@ files = [ [[package]] name = "cython" -version = "3.0.4" +version = "3.0.5" description = "The Cython compiler for writing C extensions in the Python language." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ - {file = "Cython-3.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:096cb461bf8d913a4327d93ea38d18bc3dbc577a71d805be04754e4b2cc2c45d"}, - {file = "Cython-3.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf671d712816b48fa2731799017ed68e5e440922d0c7e13dc825c226639ff766"}, - {file = "Cython-3.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beb367fd88fc6ba8c204786f680229106d99da72a60f5906c85fc8d73640b01a"}, - {file = "Cython-3.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6619264ed43d8d8819d4f1cdb8a62ab66f87e92f06f3ff3e2533fd95a9945e59"}, - {file = "Cython-3.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c0fb9e7cf9db38918f19a803fab9bc7b2ed3f33a9e8319c616c464a0a8501b8d"}, - {file = "Cython-3.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c214f6e88ecdc8ff5d13f0914890445fdaad21bddc34a90cd14aeb3ad5e55e2e"}, - {file = "Cython-3.0.4-cp310-cp310-win32.whl", hash = "sha256:c9b1322f0d8ce5445fcc3a625b966f10c4182190026713e217d6f38d29930cb1"}, - {file = "Cython-3.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:146bfaab567157f4aa34114a37e3f98a3d9c4527ee99d4fd730cab56482bd3cf"}, - {file = "Cython-3.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8e0f98d950987b0f9d5e10c41236bef5cb4fba701c6e680af0b9734faa3a85e"}, - {file = "Cython-3.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fe227d6d8e2ea030e82abc8a3e361e31447b66849f8c069caa783999e54a8f2"}, - {file = "Cython-3.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6da74000a672eac0d7cf02adc140b2f9c7d54eae6c196e615a1b5deb694d9203"}, - {file = "Cython-3.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48cda82eb82ad2014d2ad194442ed3c46156366be98e4e02f3e29742cdbf94a0"}, - {file = "Cython-3.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4355a2cb03b257773c0d2bb6af9818c72e836a9b09986e28f52e323d87b1fc67"}, - {file = "Cython-3.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:10b426adc3027d66303f5c7aa8b254d10ed80827ff5cce9e892d550b708dc044"}, - {file = "Cython-3.0.4-cp311-cp311-win32.whl", hash = "sha256:28de18f0d07eb34e2dd7b022ac30beab0fdd277846d07b7a08e69e6294f0762b"}, - {file = "Cython-3.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:9d31d76ed777a8a85be3f8f7f1cfef09b3bc33f6ec4abee1067dcef107f49778"}, - {file = "Cython-3.0.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d5a55749509c7f9f8a33bf9cc02cf76fd6564fcb38f486e43d2858145d735953"}, - {file = "Cython-3.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58cdfdd942cf5ffcee974aabfe9b9e26c0c1538fd31c1b03596d40405f7f4d40"}, - {file = "Cython-3.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b906997e7b98d7d29b84d10a5318993eba1aaff82ff7e1a0ac00254307913d7"}, - {file = "Cython-3.0.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f24114e1777604a28ae1c7a56a2c9964655f1031edecc448ad51e5abb19a279b"}, - {file = "Cython-3.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:07d0e69959f267b79ffd18ece8599711ad2f3d3ed1eddd0d4812d2a97de2b912"}, - {file = "Cython-3.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f7fcd93d15deceb2747b10266a39deccd94f257d610f3bbd52a7e16bc5908eda"}, - {file = "Cython-3.0.4-cp312-cp312-win32.whl", hash = "sha256:0aa2a6bb3ff67794d8d1dafaed22913adcbb327e96eca3ac44e2f3ba4a0ae446"}, - {file = "Cython-3.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:0021350f6d7022a37f598320460b84b2c0daccf6bb65641bbdbc8b990bdf4ad2"}, - {file = "Cython-3.0.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b72c426df1586f967b1c61d2f8236702d75c6bbf34abdc258a59e09155a16414"}, - {file = "Cython-3.0.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a9262408f05eef039981479b38b38252d5b853992e5bc54a2d2dd05a2a0178e"}, - {file = "Cython-3.0.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28af4e7dff1742cb0f0a4823102c89c62a2d94115b68f718145fcfe0763c6e21"}, - {file = "Cython-3.0.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e8c144e2c5814e46868d1f81e2f4265ca1f314a8187d0420cd76e9563294cf8"}, - {file = "Cython-3.0.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:19a64bf2591272348ab08bcd4a5f884259cc3186f008c9038b8ec7d01f847fd5"}, - {file = "Cython-3.0.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fc96efa617184b8581a02663e261b41c13a605da8ef4ba1ed735bf46184f815e"}, - {file = "Cython-3.0.4-cp36-cp36m-win32.whl", hash = "sha256:15d52f7f9d08b264c042aa508bf457f53654b55f533e0262e146002b1c15d1cd"}, - {file = "Cython-3.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:0650460b5fd6f16da4186e0a769b47db5532601e306f3b5d17941283d5e36d24"}, - {file = "Cython-3.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b3ddfc6f05410095ec11491dde05f50973e501567d21cbfcf5832d95f141878a"}, - {file = "Cython-3.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a0b92adfcac68dcf549daddec83c87a86995caa6f87bfb6f72de4797e1a6ad6"}, - {file = "Cython-3.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ada3659608795bb36930d9a206b8dd6b865d85e2999a02ce8b34f3195d88301"}, - {file = "Cython-3.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:061dec1be8d8b601b160582609a78eb08324a4ccf21bee0d04853a3e9dfcbefd"}, - {file = "Cython-3.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:bc42004f181373cd3b39bbacfb71a5b0606ed6e4c199c940cca2212ba0f79525"}, - {file = "Cython-3.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f124ac9ee41e1bfdfb16f53f1db85de296cd2144a4e8fdee8c3560a8fe9b6d5d"}, - {file = "Cython-3.0.4-cp37-cp37m-win32.whl", hash = "sha256:48b35ab009227ee6188991b5666aae1936b82a944f707c042cef267709de12b5"}, - {file = "Cython-3.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:861979428f749faa9883dc4e11e8c3fc2c29bd0031cf49661604459b53ea7c66"}, - {file = "Cython-3.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c7a7dd7c50d07718a5ac2bdea166085565f7217cf1e030cc07c22a8b80a406a7"}, - {file = "Cython-3.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d40d4135f76fb0ed4caa2d422fdb4231616615698709d3c421ecc733f1ac7ca0"}, - {file = "Cython-3.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:207f53893ca22d8c8f5db533f38382eb7ddc2d0b4ab51699bf052423a6febdad"}, - {file = "Cython-3.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0422a40a58dcfbb54c8b4e125461d741031ff046bc678475cc7a6c801d2a7721"}, - {file = "Cython-3.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ef4b144c5b29b4ea0b40c401458b86df8d75382b2e5d03e9f67f607c05b516a9"}, - {file = "Cython-3.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0612439f810cc281e51fead69de0545c4d9772a1e82149c119d1aafc1f6210ba"}, - {file = "Cython-3.0.4-cp38-cp38-win32.whl", hash = "sha256:b86871862bd65806ba0d0aa2b9c77fcdcc6cbd8d36196688f4896a34bb626334"}, - {file = "Cython-3.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:6603a287188dcbc36358a73a7be43e8a2ecf0c6a06835bdfdd1b113943afdd6f"}, - {file = "Cython-3.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0fc9e974419cc0393072b1e9a669f71c3b34209636d2005ff8620687daa82b8c"}, - {file = "Cython-3.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e84988d384dfba678387ea7e4f68786c3703543018d473605d9299c69a07f197"}, - {file = "Cython-3.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36299ffd5663203c25d3a76980f077e23b6d4f574d142f0f43943f57be445639"}, - {file = "Cython-3.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8529cf09919263a6826adc04c5dde9f1406dd7920929b16be19ee9848110e353"}, - {file = "Cython-3.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8692249732d62e049df3884fa601b70fad3358703e137aceeb581e5860e7d9b7"}, - {file = "Cython-3.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f234bc46043856d118ebd94b13ea29df674503bc94ced3d81ca46a1ad5b5b9ae"}, - {file = "Cython-3.0.4-cp39-cp39-win32.whl", hash = "sha256:c2215f436ce3cce49e6e318cb8f7253cfc4d3bea690777c2a5dd52ae93342504"}, - {file = "Cython-3.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:003ccc40e0867770db0018274977d1874e4df64983d5e3e36937f107e0b2fdf6"}, - {file = "Cython-3.0.4-py2.py3-none-any.whl", hash = "sha256:e5e2859f97e9cceb8e70b0934c56157038b8b083245898593008162a70536d7e"}, - {file = "Cython-3.0.4.tar.gz", hash = "sha256:2e379b491ee985d31e5faaf050f79f4a8f59f482835906efe4477b33b4fbe9ff"}, + {file = "Cython-3.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4faf17ea6e8fc3065a862d4b24be84048fd58ed7abe59aa2f9141446a7a72335"}, + {file = "Cython-3.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1cab30c11880f38a27911b569ea38b0bd67fcf32f8a8a8519b613c70562dae2"}, + {file = "Cython-3.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4d4d92182002b2878adb3329de1ccb7f3f7571d3586f92602e790bfeab45d0"}, + {file = "Cython-3.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b94f58e05e69e1a43da551c8f532e9fad057df1641f0f8ae8f103d4ede5a80fe"}, + {file = "Cython-3.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a90f9c7b6635967eacafebe439d518b7dc720aaaf19cb9553f5aad03c13296f4"}, + {file = "Cython-3.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c95bd21d87b08c88fe5296381a5f39cd979a775bf1a1d7379a6ff87c703e510b"}, + {file = "Cython-3.0.5-cp310-cp310-win32.whl", hash = "sha256:ebc901131057c115a8868e14c1df6e56b9190df774b72664c03ebd858296bb81"}, + {file = "Cython-3.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:0759868b4a4d103578375e031e89abd578c26774d957ee4f591764ef8003b363"}, + {file = "Cython-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3679a6693456f5f7ccca9ab2a91408e99ee257e145024fe380da7c78a07e98b6"}, + {file = "Cython-3.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad4eb2608661d63fb57c674dafb9955f5141d748d4579c7722c1a3c6b86a0c2"}, + {file = "Cython-3.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37f4b0d983316242b4b9241ecbbe55220aa92af93ff04626441fe0ea90a54f9"}, + {file = "Cython-3.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34059c3ea6e342ba388cd9774a61761bb4200ca18bd475de65c9cc70ef4e0204"}, + {file = "Cython-3.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4db9eea298e982aee7ba12b3432c66eb2e91bb2f5d026bbd57c35698ea0f557f"}, + {file = "Cython-3.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:452679284c15a7d5a88bff675e1dd660349360f0665aea50be2d98b7650925f8"}, + {file = "Cython-3.0.5-cp311-cp311-win32.whl", hash = "sha256:2d6bb318ddce8b978c81cf78caf8b3836db84f6235d721952685e87871f506e4"}, + {file = "Cython-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:fcfd2255458a5779dbab813550695331d541b24f0ef831ace83f04f9516ddf26"}, + {file = "Cython-3.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0d9fcfc09d67218fce114fe9fd97bba4f9d56add0f775c588d8c626ed47f1aef"}, + {file = "Cython-3.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ac1cf1f2ed01656b33618352f7e42bf75d027425b83cc96cfe13ce4b6cba5de"}, + {file = "Cython-3.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9d17a6ceb301c5dbd3820e62c1b10a4ad3a6eea3e07e7afaf736b5f490c2e32"}, + {file = "Cython-3.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd9cab3b862bec2b110886aedb11765e9deda363c4c7ab5ea205f3d8f143c411"}, + {file = "Cython-3.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:45277bb54c35b11bcdd6cf0f56eb950eb280b67146db0cb57853fb6334c6d11d"}, + {file = "Cython-3.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:77f4d001fb7a03a68b53be20571acd17452d1dda7907d9c325dff0cc704b1ef9"}, + {file = "Cython-3.0.5-cp312-cp312-win32.whl", hash = "sha256:57b44f789794d74c1feddae054dd045b9f601bdffd7288e069b4ca7ed607ec61"}, + {file = "Cython-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:05c4efd36879ff8020af00407e4c14246b894558ea41dc6486f60dd71601fc67"}, + {file = "Cython-3.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:048fe89c2c753c24e1a7a05496907173dab17e238c8dc3c7cad90b3679b0d846"}, + {file = "Cython-3.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c016b3e859b41cf4ce1b8107f364ad5a83c086f75ea4d8d3990b24691f626a1"}, + {file = "Cython-3.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f31d02b831d0fa9bf099b1b714b5a8252eabd8db34b7d33c48e7e808a2dabf9"}, + {file = "Cython-3.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:485f8a3087392e2287e2869adc0385b439f69b9cfbd262fdf39b00417690c988"}, + {file = "Cython-3.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:063220a6dc0909b576ef068c7e2acf5c608d64423a6d231aacb72d06352cd95b"}, + {file = "Cython-3.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:abb2362783521bd9a22fe69b2141abab4db97237665a36a034b161ddee5b3e72"}, + {file = "Cython-3.0.5-cp36-cp36m-win32.whl", hash = "sha256:a993002fe28c840dc29805fde7341c775b7878b311b85f21560fdebf220c247b"}, + {file = "Cython-3.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:13491f1bfcf020fd02751c4a55294aa8957e21b7ecd2542b0521a7aa50c58bb2"}, + {file = "Cython-3.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:45aaceb082ad89365f2f783a40db42359591ad55914fb298841196edf88afdc5"}, + {file = "Cython-3.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3e011fa2ae9e953fe1ab8394329a21bdb54357c7fe509bcfb02b88bc15bffbb"}, + {file = "Cython-3.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f18c13d5ed6fde5efd3b1c039f6a34296d1a0409bb00fbf45bec6f9bcf63ddf5"}, + {file = "Cython-3.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:039877e57dc10abf0d30d2de2c7476f0881d8ecef1f29bdeed1a6a06a8d89141"}, + {file = "Cython-3.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4fbc8f62b8d50f9a2eef99927a9dcb8d0a42e5a801ab14c2e4aeab622c88f54b"}, + {file = "Cython-3.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3cffbba1888f795de2103e6fb1482c8ea8d457e638fa813e090fe747f9e549bb"}, + {file = "Cython-3.0.5-cp37-cp37m-win32.whl", hash = "sha256:c18e125537a96e76c8c34201e5a9aad8625e3d872dd26a63155573462e54e185"}, + {file = "Cython-3.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:93502f45948ae8d7f874ba4c113b50cb6fb4ee664caa82e1ddc398500ee0ffb3"}, + {file = "Cython-3.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0a9206b0720f0cad3e70c018722f6d10e81b32e65477e14ffedd3fbfadfaddca"}, + {file = "Cython-3.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:530a474a79fa6c2127bb7e3ba00857b1f26a563615863f17b7434331aa3fe404"}, + {file = "Cython-3.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:115e76fbe9288119526b66963f614042222d1587f1ba5ddb90088215a3d2a25a"}, + {file = "Cython-3.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:035cb6930a8534f865a3f4616543f78bd27e4de9c3e117b2826e395085ffc4c0"}, + {file = "Cython-3.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:077d9a61e6042174dabf68b8e92c0a80f5aff529439ed314aa6e6a233af80b95"}, + {file = "Cython-3.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ba3f7b433c1721a43674c0889d5fad746bf608416c8f849343859e6d4d3a7113"}, + {file = "Cython-3.0.5-cp38-cp38-win32.whl", hash = "sha256:a95ed0e6f481462a3ff2be4c2e4ffffc5d00fc3884d4ccd1fe5b702d4938ec09"}, + {file = "Cython-3.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:f687539ead9fbc17f499e33ee20c1dc41598f70ad95edb4990c576447cec9d23"}, + {file = "Cython-3.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6fcfef825edb44cf3c6ba2c091ad76a83da62ac9c79553e80e0c2a1f75eda2e"}, + {file = "Cython-3.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0d9431101f600d5a557d55989658cbfd02b7c0dcd1e4675dac8ad7e0da8ea5b"}, + {file = "Cython-3.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db21997270e943aee9cb7694112d24a4702fbe1977fbe53b3cb4db3d02be73d9"}, + {file = "Cython-3.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808f56d4cd0511723b787c341f3cc995fd72893e608102268298c188f4a4f2e7"}, + {file = "Cython-3.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:dee39967168d6326ea2df56ad215a4d5049aa52f44cd5aad45bfb63d5b4fb9e5"}, + {file = "Cython-3.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b77f2b45535bcf3741592fa03183558bd42198b872c1584b896fa0ba5f2ac68d"}, + {file = "Cython-3.0.5-cp39-cp39-win32.whl", hash = "sha256:5742ef31e1e2c9a4824ef6b05af0f4949047a6f73af1d4b238ce12935174aede"}, + {file = "Cython-3.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:ada4852db0e33dbdd1425322db081d22b9725cb9f5eba42885467b4e2c4f2ac0"}, + {file = "Cython-3.0.5-py2.py3-none-any.whl", hash = "sha256:75206369504fc442c10a86ecf57b91592dca744e4592af22a47e9a774d53dd10"}, + {file = "Cython-3.0.5.tar.gz", hash = "sha256:39318348db488a2f24e7c84e08bdc82f2624853c0fea8b475ea0b70b27176492"}, ] [[package]] @@ -1610,13 +1610,13 @@ testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=6,<7)", "pytest-cov", [[package]] name = "nautilus-ibapi" -version = "1019.1" +version = "10.19.1" description = "Python IB API" optional = true -python-versions = ">=3.9,<3.12" +python-versions = ">=3.9" files = [ - {file = "nautilus_ibapi-1019.1-py3-none-any.whl", hash = "sha256:00a521ee8a7870f6a2430d9f5145e42cfc48aae5791d3c54fc0d5d54b099f615"}, - {file = "nautilus_ibapi-1019.1.tar.gz", hash = "sha256:dae539d667d7f06d953ccd5353ff4f5315c9f4d1fe448ef71704fae1daa95585"}, + {file = "nautilus_ibapi-10.19.1-py3-none-any.whl", hash = "sha256:62a8d1c20721549e8e44ce5a185a3345bcd0ad61176cb5a4f66d4501cc415280"}, + {file = "nautilus_ibapi-10.19.1.tar.gz", hash = "sha256:79c7fd1c085929fe30bc3d66285c61a19c028be8d0d6bf838b3193fd4c91b225"}, ] [[package]] @@ -1874,40 +1874,47 @@ files = [ [[package]] name = "pyarrow" -version = "13.0.0" +version = "14.0.0" description = "Python library for Apache Arrow" optional = false python-versions = ">=3.8" files = [ - {file = "pyarrow-13.0.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:1afcc2c33f31f6fb25c92d50a86b7a9f076d38acbcb6f9e74349636109550148"}, - {file = "pyarrow-13.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:70fa38cdc66b2fc1349a082987f2b499d51d072faaa6b600f71931150de2e0e3"}, - {file = "pyarrow-13.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd57b13a6466822498238877892a9b287b0a58c2e81e4bdb0b596dbb151cbb73"}, - {file = "pyarrow-13.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ce69f7bf01de2e2764e14df45b8404fc6f1a5ed9871e8e08a12169f87b7a26"}, - {file = "pyarrow-13.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:588f0d2da6cf1b1680974d63be09a6530fd1bd825dc87f76e162404779a157dc"}, - {file = "pyarrow-13.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6241afd72b628787b4abea39e238e3ff9f34165273fad306c7acf780dd850956"}, - {file = "pyarrow-13.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:fda7857e35993673fcda603c07d43889fca60a5b254052a462653f8656c64f44"}, - {file = "pyarrow-13.0.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:aac0ae0146a9bfa5e12d87dda89d9ef7c57a96210b899459fc2f785303dcbb67"}, - {file = "pyarrow-13.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7759994217c86c161c6a8060509cfdf782b952163569606bb373828afdd82e8"}, - {file = "pyarrow-13.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:868a073fd0ff6468ae7d869b5fc1f54de5c4255b37f44fb890385eb68b68f95d"}, - {file = "pyarrow-13.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51be67e29f3cfcde263a113c28e96aa04362ed8229cb7c6e5f5c719003659d33"}, - {file = "pyarrow-13.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d1b4e7176443d12610874bb84d0060bf080f000ea9ed7c84b2801df851320295"}, - {file = "pyarrow-13.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:69b6f9a089d116a82c3ed819eea8fe67dae6105f0d81eaf0fdd5e60d0c6e0944"}, - {file = "pyarrow-13.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ab1268db81aeb241200e321e220e7cd769762f386f92f61b898352dd27e402ce"}, - {file = "pyarrow-13.0.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:ee7490f0f3f16a6c38f8c680949551053c8194e68de5046e6c288e396dccee80"}, - {file = "pyarrow-13.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3ad79455c197a36eefbd90ad4aa832bece7f830a64396c15c61a0985e337287"}, - {file = "pyarrow-13.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68fcd2dc1b7d9310b29a15949cdd0cb9bc34b6de767aff979ebf546020bf0ba0"}, - {file = "pyarrow-13.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc6fd330fd574c51d10638e63c0d00ab456498fc804c9d01f2a61b9264f2c5b2"}, - {file = "pyarrow-13.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e66442e084979a97bb66939e18f7b8709e4ac5f887e636aba29486ffbf373763"}, - {file = "pyarrow-13.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:0f6eff839a9e40e9c5610d3ff8c5bdd2f10303408312caf4c8003285d0b49565"}, - {file = "pyarrow-13.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b30a27f1cddf5c6efcb67e598d7823a1e253d743d92ac32ec1eb4b6a1417867"}, - {file = "pyarrow-13.0.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:09552dad5cf3de2dc0aba1c7c4b470754c69bd821f5faafc3d774bedc3b04bb7"}, - {file = "pyarrow-13.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3896ae6c205d73ad192d2fc1489cd0edfab9f12867c85b4c277af4d37383c18c"}, - {file = "pyarrow-13.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6647444b21cb5e68b593b970b2a9a07748dd74ea457c7dadaa15fd469c48ada1"}, - {file = "pyarrow-13.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47663efc9c395e31d09c6aacfa860f4473815ad6804311c5433f7085415d62a7"}, - {file = "pyarrow-13.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:b9ba6b6d34bd2563345488cf444510588ea42ad5613df3b3509f48eb80250afd"}, - {file = "pyarrow-13.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:d00d374a5625beeb448a7fa23060df79adb596074beb3ddc1838adb647b6ef09"}, - {file = "pyarrow-13.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:c51afd87c35c8331b56f796eff954b9c7f8d4b7fef5903daf4e05fcf017d23a8"}, - {file = "pyarrow-13.0.0.tar.gz", hash = "sha256:83333726e83ed44b0ac94d8d7a21bbdee4a05029c3b1e8db58a863eec8fd8a33"}, + {file = "pyarrow-14.0.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:4fce1db17efbc453080c5b306f021926de7c636456a128328797e574c151f81a"}, + {file = "pyarrow-14.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28de7c05b4d7a71ec660360639cc9b65ceb1175e0e9d4dfccd879a1545bc38f7"}, + {file = "pyarrow-14.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1541e9209c094e7f4d7b43fdd9de3a8c71d3069cf6fc03b59bf5774042411849"}, + {file = "pyarrow-14.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c05e6c45d303c80e41ab04996430a0251321f70986ed51213903ea7bc0b7efd"}, + {file = "pyarrow-14.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:426ffec63ab9b4dff23dec51be2150e3a4a99eb38e66c10a70e2c48779fe9c9d"}, + {file = "pyarrow-14.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:968844f591902160bd3c9ee240ce8822a3b4e7de731e91daea76ad43fe0ff062"}, + {file = "pyarrow-14.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:dcedbc0b4ea955c530145acfe99e324875c386419a09db150291a24cb01aeb81"}, + {file = "pyarrow-14.0.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:97993a12aacc781efad9c92d4545a877e803c4d106d34237ec4ce987bec825a3"}, + {file = "pyarrow-14.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80225768d94024d59a31320374f5e6abf8899866c958dfb4f4ea8e2d9ec91bde"}, + {file = "pyarrow-14.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b61546977a8bd7e3d0c697ede723341ef4737e761af2239aef6e1db447f97727"}, + {file = "pyarrow-14.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42509e6c93b4a1c8ae8ccd939a43f437097783fe130a1991497a6a1abbba026f"}, + {file = "pyarrow-14.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3eccce331a1392e46573f2ce849a9ee3c074e0d7008e9be0b44566ac149fd6a1"}, + {file = "pyarrow-14.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ecc463c45f2b6b36431f5f2025842245e8c15afe4d42072230575785f3bb00c6"}, + {file = "pyarrow-14.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:4362ed90def81640addcd521811dd16a13015f0a8255bec324a41262c1524b6c"}, + {file = "pyarrow-14.0.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:2fbb7ab62537782c5ab31aa08db0e1f6de92c2c515fdfc0790128384e919adcb"}, + {file = "pyarrow-14.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad7095f8f0fe0bfa3d3fca1909b8fa15c70e630b0cc1ff8d35e143f5e2704064"}, + {file = "pyarrow-14.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6602272fce71c0fb64f266e7cdbe51b93b00c22fc1bb57f2b0cb681c4aeedf4"}, + {file = "pyarrow-14.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b2b8f87951b08a3e72265c8963da3fe4f737bb81290269037e047dd172aa591"}, + {file = "pyarrow-14.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a1c9675966662a042caebbaafa1ae7fc26291287ebc3da06aa63ad74c323ec30"}, + {file = "pyarrow-14.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:771079fddc0b4440c41af541dbdebc711a7062c93d3c4764476a9442606977db"}, + {file = "pyarrow-14.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:c4096136318de1c4937370c0c365f949961c371201c396d8cc94a353f342069d"}, + {file = "pyarrow-14.0.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:6c94056fb5f0ee0bae2206c3f776881e1db2bd0d133d06805755ae7ac5145349"}, + {file = "pyarrow-14.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:687d0df1e08876b2d24d42abae129742fc655367e3fe6700aa4d79fcf2e3215e"}, + {file = "pyarrow-14.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f4054e5ee6c88ca256a67fc8b27f9c59bcd385216346265831d462a6069033f"}, + {file = "pyarrow-14.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:768b962e4c042ab2c96576ca0757935472e220d11af855c7d0be3279d7fced5f"}, + {file = "pyarrow-14.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:77293b1319c7044f68ebfa43db8c929a0a5254ce371f1a0873d343f1460171d0"}, + {file = "pyarrow-14.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:d2bc7c53941d85f0133b1bd5a814bca0af213922f50d8a8dc0eed4d9ed477845"}, + {file = "pyarrow-14.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:378955365dd087c285ef4f34ad939d7e551b7715326710e8cd21cfa2ce511bd7"}, + {file = "pyarrow-14.0.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:f05e81b4c621e6ad4bcd8f785e3aa1d6c49a935818b809ea6e7bf206a5b1a4e8"}, + {file = "pyarrow-14.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6867f6a8057eaef5a7ac6d27fe5518133f67973c5d4295d79a943458350e7c61"}, + {file = "pyarrow-14.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca54b87c46abdfe027f18f959ca388102bd7326c344838f72244807462d091b2"}, + {file = "pyarrow-14.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35abf61bd0cc9daca3afc715f6ba74ea83d792fa040025352624204bec66bf6a"}, + {file = "pyarrow-14.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:65c377523b369f7ef1ba02be814e832443bb3b15065010838f02dae5bdc0f53c"}, + {file = "pyarrow-14.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8a1e470e4b5f7bda7bede0410291daec55ab69f346d77795d34fd6a45b41579"}, + {file = "pyarrow-14.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:466c1a5a7a4b279cfa363ac34dedd0c3c6af388cec9e6a468ffc095a6627849a"}, + {file = "pyarrow-14.0.0.tar.gz", hash = "sha256:45d3324e1c9871a07de6b4d514ebd73225490963a6dd46c64c465c4b6079fe1e"}, ] [package.dependencies] @@ -1918,7 +1925,7 @@ name = "pycparser" version = "2.21" description = "C parser in Python" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, @@ -2943,54 +2950,57 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [[package]] name = "zstandard" -version = "0.21.0" +version = "0.22.0" description = "Zstandard bindings for Python" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "zstandard-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:649a67643257e3b2cff1c0a73130609679a5673bf389564bc6d4b164d822a7ce"}, - {file = "zstandard-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:144a4fe4be2e747bf9c646deab212666e39048faa4372abb6a250dab0f347a29"}, - {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b72060402524ab91e075881f6b6b3f37ab715663313030d0ce983da44960a86f"}, - {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8257752b97134477fb4e413529edaa04fc0457361d304c1319573de00ba796b1"}, - {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c053b7c4cbf71cc26808ed67ae955836232f7638444d709bfc302d3e499364fa"}, - {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2769730c13638e08b7a983b32cb67775650024632cd0476bf1ba0e6360f5ac7d"}, - {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7d3bc4de588b987f3934ca79140e226785d7b5e47e31756761e48644a45a6766"}, - {file = "zstandard-0.21.0-cp310-cp310-win32.whl", hash = "sha256:67829fdb82e7393ca68e543894cd0581a79243cc4ec74a836c305c70a5943f07"}, - {file = "zstandard-0.21.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6048a287f8d2d6e8bc67f6b42a766c61923641dd4022b7fd3f7439e17ba5a4d"}, - {file = "zstandard-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7f2afab2c727b6a3d466faee6974a7dad0d9991241c498e7317e5ccf53dbc766"}, - {file = "zstandard-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff0852da2abe86326b20abae912d0367878dd0854b8931897d44cfeb18985472"}, - {file = "zstandard-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12fa383e315b62630bd407477d750ec96a0f438447d0e6e496ab67b8b451d39"}, - {file = "zstandard-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1b9703fe2e6b6811886c44052647df7c37478af1b4a1a9078585806f42e5b15"}, - {file = "zstandard-0.21.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df28aa5c241f59a7ab524f8ad8bb75d9a23f7ed9d501b0fed6d40ec3064784e8"}, - {file = "zstandard-0.21.0-cp311-cp311-win32.whl", hash = "sha256:0aad6090ac164a9d237d096c8af241b8dcd015524ac6dbec1330092dba151657"}, - {file = "zstandard-0.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:48b6233b5c4cacb7afb0ee6b4f91820afbb6c0e3ae0fa10abbc20000acdf4f11"}, - {file = "zstandard-0.21.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e7d560ce14fd209db6adacce8908244503a009c6c39eee0c10f138996cd66d3e"}, - {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e6e131a4df2eb6f64961cea6f979cdff22d6e0d5516feb0d09492c8fd36f3bc"}, - {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1e0c62a67ff425927898cf43da2cf6b852289ebcc2054514ea9bf121bec10a5"}, - {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1545fb9cb93e043351d0cb2ee73fa0ab32e61298968667bb924aac166278c3fc"}, - {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe6c821eb6870f81d73bf10e5deed80edcac1e63fbc40610e61f340723fd5f7c"}, - {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ddb086ea3b915e50f6604be93f4f64f168d3fc3cef3585bb9a375d5834392d4f"}, - {file = "zstandard-0.21.0-cp37-cp37m-win32.whl", hash = "sha256:57ac078ad7333c9db7a74804684099c4c77f98971c151cee18d17a12649bc25c"}, - {file = "zstandard-0.21.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1243b01fb7926a5a0417120c57d4c28b25a0200284af0525fddba812d575f605"}, - {file = "zstandard-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ea68b1ba4f9678ac3d3e370d96442a6332d431e5050223626bdce748692226ea"}, - {file = "zstandard-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8070c1cdb4587a8aa038638acda3bd97c43c59e1e31705f2766d5576b329e97c"}, - {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af612c96599b17e4930fe58bffd6514e6c25509d120f4eae6031b7595912f85"}, - {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff891e37b167bc477f35562cda1248acc115dbafbea4f3af54ec70821090965"}, - {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9fec02ce2b38e8b2e86079ff0b912445495e8ab0b137f9c0505f88ad0d61296"}, - {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdbe350691dec3078b187b8304e6a9c4d9db3eb2d50ab5b1d748533e746d099"}, - {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b69cccd06a4a0a1d9fb3ec9a97600055cf03030ed7048d4bcb88c574f7895773"}, - {file = "zstandard-0.21.0-cp38-cp38-win32.whl", hash = "sha256:9980489f066a391c5572bc7dc471e903fb134e0b0001ea9b1d3eff85af0a6f1b"}, - {file = "zstandard-0.21.0-cp38-cp38-win_amd64.whl", hash = "sha256:0e1e94a9d9e35dc04bf90055e914077c80b1e0c15454cc5419e82529d3e70728"}, - {file = "zstandard-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2d61675b2a73edcef5e327e38eb62bdfc89009960f0e3991eae5cc3d54718de"}, - {file = "zstandard-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25fbfef672ad798afab12e8fd204d122fca3bc8e2dcb0a2ba73bf0a0ac0f5f07"}, - {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62957069a7c2626ae80023998757e27bd28d933b165c487ab6f83ad3337f773d"}, - {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e10ed461e4807471075d4b7a2af51f5234c8f1e2a0c1d37d5ca49aaaad49e8"}, - {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cff89a036c639a6a9299bf19e16bfb9ac7def9a7634c52c257166db09d950e7"}, - {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52b2b5e3e7670bd25835e0e0730a236f2b0df87672d99d3bf4bf87248aa659fb"}, - {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b1367da0dde8ae5040ef0413fb57b5baeac39d8931c70536d5f013b11d3fc3a5"}, - {file = "zstandard-0.21.0-cp39-cp39-win32.whl", hash = "sha256:db62cbe7a965e68ad2217a056107cc43d41764c66c895be05cf9c8b19578ce9c"}, - {file = "zstandard-0.21.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8d200617d5c876221304b0e3fe43307adde291b4a897e7b0617a61611dfff6a"}, - {file = "zstandard-0.21.0.tar.gz", hash = "sha256:f08e3a10d01a247877e4cb61a82a319ea746c356a3786558bed2481e6c405546"}, + {file = "zstandard-0.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:275df437ab03f8c033b8a2c181e51716c32d831082d93ce48002a5227ec93019"}, + {file = "zstandard-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ac9957bc6d2403c4772c890916bf181b2653640da98f32e04b96e4d6fb3252a"}, + {file = "zstandard-0.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe3390c538f12437b859d815040763abc728955a52ca6ff9c5d4ac707c4ad98e"}, + {file = "zstandard-0.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1958100b8a1cc3f27fa21071a55cb2ed32e9e5df4c3c6e661c193437f171cba2"}, + {file = "zstandard-0.22.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93e1856c8313bc688d5df069e106a4bc962eef3d13372020cc6e3ebf5e045202"}, + {file = "zstandard-0.22.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1a90ba9a4c9c884bb876a14be2b1d216609385efb180393df40e5172e7ecf356"}, + {file = "zstandard-0.22.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3db41c5e49ef73641d5111554e1d1d3af106410a6c1fb52cf68912ba7a343a0d"}, + {file = "zstandard-0.22.0-cp310-cp310-win32.whl", hash = "sha256:d8593f8464fb64d58e8cb0b905b272d40184eac9a18d83cf8c10749c3eafcd7e"}, + {file = "zstandard-0.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1a4b358947a65b94e2501ce3e078bbc929b039ede4679ddb0460829b12f7375"}, + {file = "zstandard-0.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:589402548251056878d2e7c8859286eb91bd841af117dbe4ab000e6450987e08"}, + {file = "zstandard-0.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a97079b955b00b732c6f280d5023e0eefe359045e8b83b08cf0333af9ec78f26"}, + {file = "zstandard-0.22.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:445b47bc32de69d990ad0f34da0e20f535914623d1e506e74d6bc5c9dc40bb09"}, + {file = "zstandard-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33591d59f4956c9812f8063eff2e2c0065bc02050837f152574069f5f9f17775"}, + {file = "zstandard-0.22.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:888196c9c8893a1e8ff5e89b8f894e7f4f0e64a5af4d8f3c410f0319128bb2f8"}, + {file = "zstandard-0.22.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:53866a9d8ab363271c9e80c7c2e9441814961d47f88c9bc3b248142c32141d94"}, + {file = "zstandard-0.22.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4ac59d5d6910b220141c1737b79d4a5aa9e57466e7469a012ed42ce2d3995e88"}, + {file = "zstandard-0.22.0-cp311-cp311-win32.whl", hash = "sha256:2b11ea433db22e720758cba584c9d661077121fcf60ab43351950ded20283440"}, + {file = "zstandard-0.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:11f0d1aab9516a497137b41e3d3ed4bbf7b2ee2abc79e5c8b010ad286d7464bd"}, + {file = "zstandard-0.22.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6c25b8eb733d4e741246151d895dd0308137532737f337411160ff69ca24f93a"}, + {file = "zstandard-0.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f9b2cde1cd1b2a10246dbc143ba49d942d14fb3d2b4bccf4618d475c65464912"}, + {file = "zstandard-0.22.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88b7df61a292603e7cd662d92565d915796b094ffb3d206579aaebac6b85d5f"}, + {file = "zstandard-0.22.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466e6ad8caefb589ed281c076deb6f0cd330e8bc13c5035854ffb9c2014b118c"}, + {file = "zstandard-0.22.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1d67d0d53d2a138f9e29d8acdabe11310c185e36f0a848efa104d4e40b808e4"}, + {file = "zstandard-0.22.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:39b2853efc9403927f9065cc48c9980649462acbdf81cd4f0cb773af2fd734bc"}, + {file = "zstandard-0.22.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8a1b2effa96a5f019e72874969394edd393e2fbd6414a8208fea363a22803b45"}, + {file = "zstandard-0.22.0-cp312-cp312-win32.whl", hash = "sha256:88c5b4b47a8a138338a07fc94e2ba3b1535f69247670abfe422de4e0b344aae2"}, + {file = "zstandard-0.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:de20a212ef3d00d609d0b22eb7cc798d5a69035e81839f549b538eff4105d01c"}, + {file = "zstandard-0.22.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d75f693bb4e92c335e0645e8845e553cd09dc91616412d1d4650da835b5449df"}, + {file = "zstandard-0.22.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:36a47636c3de227cd765e25a21dc5dace00539b82ddd99ee36abae38178eff9e"}, + {file = "zstandard-0.22.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68953dc84b244b053c0d5f137a21ae8287ecf51b20872eccf8eaac0302d3e3b0"}, + {file = "zstandard-0.22.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2612e9bb4977381184bb2463150336d0f7e014d6bb5d4a370f9a372d21916f69"}, + {file = "zstandard-0.22.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23d2b3c2b8e7e5a6cb7922f7c27d73a9a615f0a5ab5d0e03dd533c477de23004"}, + {file = "zstandard-0.22.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d43501f5f31e22baf822720d82b5547f8a08f5386a883b32584a185675c8fbf"}, + {file = "zstandard-0.22.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a493d470183ee620a3df1e6e55b3e4de8143c0ba1b16f3ded83208ea8ddfd91d"}, + {file = "zstandard-0.22.0-cp38-cp38-win32.whl", hash = "sha256:7034d381789f45576ec3f1fa0e15d741828146439228dc3f7c59856c5bcd3292"}, + {file = "zstandard-0.22.0-cp38-cp38-win_amd64.whl", hash = "sha256:d8fff0f0c1d8bc5d866762ae95bd99d53282337af1be9dc0d88506b340e74b73"}, + {file = "zstandard-0.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2fdd53b806786bd6112d97c1f1e7841e5e4daa06810ab4b284026a1a0e484c0b"}, + {file = "zstandard-0.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:73a1d6bd01961e9fd447162e137ed949c01bdb830dfca487c4a14e9742dccc93"}, + {file = "zstandard-0.22.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9501f36fac6b875c124243a379267d879262480bf85b1dbda61f5ad4d01b75a3"}, + {file = "zstandard-0.22.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48f260e4c7294ef275744210a4010f116048e0c95857befb7462e033f09442fe"}, + {file = "zstandard-0.22.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959665072bd60f45c5b6b5d711f15bdefc9849dd5da9fb6c873e35f5d34d8cfb"}, + {file = "zstandard-0.22.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d22fdef58976457c65e2796e6730a3ea4a254f3ba83777ecfc8592ff8d77d303"}, + {file = "zstandard-0.22.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a7ccf5825fd71d4542c8ab28d4d482aace885f5ebe4b40faaa290eed8e095a4c"}, + {file = "zstandard-0.22.0-cp39-cp39-win32.whl", hash = "sha256:f058a77ef0ece4e210bb0450e68408d4223f728b109764676e1a13537d056bb0"}, + {file = "zstandard-0.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:e9e9d4e2e336c529d4c435baad846a181e39a982f823f7e4495ec0b0ec8538d2"}, + {file = "zstandard-0.22.0.tar.gz", hash = "sha256:8226a33c542bcb54cd6bd0a366067b610b41713b64c9abec1bc4533d69f51e70"}, ] [package.dependencies] @@ -3009,4 +3019,4 @@ redis = ["hiredis", "redis"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "a08aadbf4372178c53b429f24527fb57d89d69c3e17d5cd41ebe29c72090d880" +content-hash = "a9495238ed0c78e1ffb72675020308050d3a34dcbf9f6bda51c0b64e5c1d84e5" diff --git a/pyproject.toml b/pyproject.toml index 127d30c31336..2cf0a743ecc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,9 +38,9 @@ include = [ [build-system] requires = [ "setuptools", - "poetry-core>=1.7.0", + "poetry-core>=1.8.1", "numpy>=1.26.1", - "Cython==3.0.4", + "Cython==3.0.5", "toml>=0.10.2", ] build-backend = "poetry.core.masonry.api" @@ -51,7 +51,7 @@ generate-setup-file = false [tool.poetry.dependencies] python = ">=3.10,<3.12" -cython = "==3.0.4" # Build dependency (pinned for stability) +cython = "==3.0.5" # Build dependency (pinned for stability) numpy = "^1.26.1" # Build dependency toml = "^0.10.2" # Build dependency click = "^8.1.7" @@ -61,7 +61,7 @@ importlib_metadata = "^6.8.0" msgspec = "^0.18.4" pandas = "^2.1.2" psutil = "^5.9.6" -pyarrow = ">=12.0.1" # Minimum version set one major version behind the latest for compatibility +pyarrow = ">=14.0.0" # Minimum version set one major version behind the latest for compatibility pytz = "^2023.3.0" tqdm = "^4.66.1" uvloop = {version = "^0.19.0", markers = "sys_platform != 'win32'"} @@ -70,7 +70,7 @@ docker = {version = "^6.1.3", optional = true} hiredis = {version = "^2.2.3", optional = true} redis = {version = "^5.0.1", optional = true} async-timeout = {version = "^4.0.3", optional = true} -nautilus_ibapi = {version = "==1019.1", optional = true} # Pinned for stability +nautilus_ibapi = {version = "==10.19.1", optional = true} # Pinned for stability betfair_parser = {version = "==0.7.1", optional = true} # Pinned for stability [tool.poetry.extras] From 0b2745e3e729277e2f21a0ff53c5da678745ae86 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 2 Nov 2023 18:56:18 +1100 Subject: [PATCH 76/78] Add gitlint --- .github/workflows/build.yml | 4 +++- .gitlint | 8 ++++++++ .pre-commit-config.yaml | 6 ++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 .gitlint diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3d9e29cb610..0e6f81741874 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -89,7 +89,9 @@ jobs: key: ${{ runner.os }}-${{ matrix.python-version }}-poetry-${{ hashFiles('**/poetry.lock') }} - name: Run pre-commit - run: pre-commit run --all-files + run: | + pre-commit run --hook-stage manual gitlint-ci + pre-commit run --all-files - name: Install Redis (macOS) if: runner.os == 'macOS' diff --git a/.gitlint b/.gitlint new file mode 100644 index 000000000000..967e03a134de --- /dev/null +++ b/.gitlint @@ -0,0 +1,8 @@ +[general] +ignore=body-is-missing,body-min-length + +[title-max-length] +line-length=80 + +[title-min-length] +min-length=5 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74970264a39c..38e3d3056888 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,6 +24,12 @@ repos: - id: check-xml - id: check-yaml + - repo: https://github.com/jorisroovers/gitlint + rev: v0.19.1 + hooks: + - id: gitlint + - id: gitlint-ci + - repo: https://github.com/codespell-project/codespell rev: v2.2.6 hooks: From b0c8dc962893258b70d8ab9fabf6f8cf83975042 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 2 Nov 2023 19:16:50 +1100 Subject: [PATCH 77/78] Disable gitlint-ci for now --- .github/workflows/build.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0e6f81741874..5ff6c33084cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -90,7 +90,7 @@ jobs: - name: Run pre-commit run: | - pre-commit run --hook-stage manual gitlint-ci + # pre-commit run --hook-stage manual gitlint-ci pre-commit run --all-files - name: Install Redis (macOS) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 38e3d3056888..2160509eec45 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,7 +28,7 @@ repos: rev: v0.19.1 hooks: - id: gitlint - - id: gitlint-ci + # - id: gitlint-ci - repo: https://github.com/codespell-project/codespell rev: v2.2.6 From 7db5619c93e50b093fe2a544bde8c6209de5fcc4 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 3 Nov 2023 17:00:38 +1100 Subject: [PATCH 78/78] Update release notes --- RELEASES.md | 5 +++-- nautilus_core/Cargo.lock | 8 ++++---- poetry.lock | 10 +++++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index d2b3fb75ca73..f72ce5dce4f3 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,12 +1,13 @@ # NautilusTrader 1.180.0 Beta -Released on TBC (UTC). +Released on 3rd November 2023 (UTC). ### Enhancements +- Improved internal latency for live engines by using `loop.call_soon_threadsafe(...)` +- Improved `RedisCacheDatabase` client connection error handling with retries - Added `WebSocketClient` connection headers, thanks @ruthvik125 and @twitu - Added `support_contingent_orders` option for venues (to simulate venues which do not support contingent orders) - Added `StrategyConfig.manage_contingent_orders` option (to automatically manage **open** contingenct orders) -- Improved `RedisCacheDatabase` client connection error handling with retries - Added `FuturesContract.activation_utc` property which returns a `pd.Timestamp` tz-aware (UTC) - Added `OptionsContract.activation_utc` property which returns a `pd.Timestamp` tz-aware (UTC) - Added `CryptoFuture.activation_utc` property which returns a `pd.Timestamp` tz-aware (UTC) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 22feb21860b2..229db2591ea3 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -4155,18 +4155,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.21" +version = "0.7.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686b7e407015242119c33dab17b8f61ba6843534de936d94368856528eae4dcc" +checksum = "e50cbb27c30666a6108abd6bc7577556265b44f243e2be89a8bc4e07a528c107" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.21" +version = "0.7.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020f3dfe25dfc38dfea49ce62d5d45ecdd7f0d8a724fa63eb36b6eba4ec76806" +checksum = "a25f293fe55f0a48e7010d65552bb63704f6ceb55a1a385da10d41d8f78e4a3d" dependencies = [ "proc-macro2", "quote", diff --git a/poetry.lock b/poetry.lock index e1c9d1430109..b8c0ecfed09a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -577,7 +577,7 @@ name = "css-html-js-minify" version = "2.5.5" description = "CSS HTML JS Minifier" optional = false -python-versions = ">=3.6" +python-versions = "*" files = [ {file = "css-html-js-minify-2.5.5.zip", hash = "sha256:4a9f11f7e0496f5284d12111f3ba4ff5ff2023d12f15d195c9c48bd97013746c"}, {file = "css_html_js_minify-2.5.5-py2.py3-none-any.whl", hash = "sha256:3da9d35ac0db8ca648c1b543e0e801d7ca0bab9e6bfd8418fee59d5ae001727a"}, @@ -1925,7 +1925,7 @@ name = "pycparser" version = "2.21" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "*" files = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, @@ -2640,13 +2640,13 @@ files = [ [[package]] name = "types-redis" -version = "4.6.0.8" +version = "4.6.0.9" description = "Typing stubs for redis" optional = false python-versions = ">=3.7" files = [ - {file = "types-redis-4.6.0.8.tar.gz", hash = "sha256:1abb2859bbf9b171a22ef69d1ece0e35ef93e642fba97538497add884ad75b5e"}, - {file = "types_redis-4.6.0.8-py3-none-any.whl", hash = "sha256:4839923b4cce77bbf987290ca83710f8218529eebe1d2c3a0f067416c86847f5"}, + {file = "types-redis-4.6.0.9.tar.gz", hash = "sha256:06ac31ed7b23aae2d230a62e4bf7d0037aee10ab9f68eee261ac8be8402daf92"}, + {file = "types_redis-4.6.0.9-py3-none-any.whl", hash = "sha256:12fb29ff019b62998b17bb086cff260e625477db1a17bfca6bae0f43ab3447a5"}, ] [package.dependencies]