diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd49bdcb5bc6..f062cf8efcf3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -167,7 +167,6 @@ jobs: make test-examples build-windows: - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/nightly' strategy: fail-fast: false matrix: @@ -187,6 +186,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Free disk space (Windows) + run: | + rm -rf "/c/Program Files/dotnet" + rm -rf "/c/Program Files (x86)/Microsoft Visual Studio/2019" + + - name: Install nasm dependency + run: | + choco install nasm + choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' + - name: Get Rust version from rust-toolchain.toml id: rust-version run: | @@ -268,7 +277,6 @@ jobs: PARALLEL_BUILD: false build-macos: - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/nightly' strategy: fail-fast: false matrix: @@ -288,7 +296,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Free up disk space (macOS) + - name: Free disk space (macOS) run: | sudo rm -rf ~/Library/Caches/* sudo rm -rf ~/Library/Developer/Xcode/DerivedData/* diff --git a/Makefile b/Makefile index 1143d8894474..b9ea7518c68d 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,7 @@ docs-python: install-docs .PHONY: docs-rust docs-rust: - (cd nautilus_core && RUSTDOCFLAGS="--enable-index-page -Zunstable-options" cargo +nightly doc --all-features --no-deps --workspace --exclude tokio-tungstenite) + (cd nautilus_core && RUSTDOCFLAGS="--enable-index-page -Zunstable-options" cargo +nightly doc --all-features --no-deps --workspace) .PHONY: clippy clippy: @@ -98,7 +98,7 @@ cargo-test: echo "cargo-nextest is not installed. You can install it using 'cargo install cargo-nextest'"; \ exit 1; \ fi - RUST_BACKTRACE=1 && (cd nautilus_core && cargo nextest run --workspace --exclude tokio-tungstenite) + RUST_BACKTRACE=1 && (cd nautilus_core && cargo nextest run --workspace) .PHONY: cargo-test-coverage cargo-test-coverage: @@ -110,7 +110,7 @@ cargo-test-coverage: echo "cargo-llvm-cov is not installed. You can install it using 'cargo install cargo-llvm-cov'"; \ exit 1; \ fi - RUST_BACKTRACE=1 && (cd nautilus_core && cargo llvm-cov nextest run --workspace --exclude tokio-tungstenite) + RUST_BACKTRACE=1 && (cd nautilus_core && cargo llvm-cov nextest run --workspace) .PHONY: cargo-bench cargo-bench: diff --git a/RELEASES.md b/RELEASES.md index ad0f8ff48265..73183e14e5b7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,9 +1,28 @@ +# NautilusTrader 1.201.0 Beta + +Released on 9th September 2024 (UTC). + +### Enhancements +- Added order book deltas triggering support for `OrderEmulator` +- Added `OrderCancelRejected` event generation for dYdX adapter (#1916), thanks @davidsblom +- Refined handling of Binance private key types (RSA, Ed25519) and integrated into configs +- Implemented cryptographic signing in Rust (replacing `pycryptodome` for Binance) +- Removed the vendored `tokio-tungstenite` crate (#1902), thanks @VioletSakura-7 + +### Breaking Changes +None + +### Fixes +- Fixed `BinanceFuturesEventType` by adding new `TRADE_LITE` member, reflecting the Binance update on 2024-09-03 (UTC) + +--- + # NautilusTrader 1.200.0 Beta Released on 7th September 2024 (UTC). ### Enhancements -- Added dYdX integration (#1861, #1868, #1873, #1874, #1875, #1877, #1879, #1880, #1882, #1886, #1887, #1890, #1891, #1896, #1901, #1903, #1907, #1910, #1911, #1913), thanks @davidsblom +- Added dYdX integration (#1861, #1868, #1873, #1874, #1875, #1877, #1879, #1880, #1882, #1886, #1887, #1890, #1891, #1896, #1901, #1903, #1907, #1910, #1911, #1913, #1915), thanks @davidsblom - Added composite bar types, bars aggregated from other bar types (#1859, #1885, #1888, #1894, #1905), thanks @faysou - Added `OrderBookDeltas.batch` for batching groups of deltas based on record flags (batch until `F_LAST`) - Added `OrderBookDeltas` batching support for `ParquetDataCatalog` (use `data_cls` of `OrderBookDeltas` to batch with the same flags method as live adapters) diff --git a/build.py b/build.py index ff7d182f7c93..e40d026efd1a 100644 --- a/build.py +++ b/build.py @@ -361,9 +361,9 @@ def build() -> None: print(f"Rust: {_get_rustc_version()}") print(f"Python: {platform.python_version()} ({sys.executable})") print(f"Cython: {cython_compiler_version}") - print(f"NumPy: {np.__version__}\n") + print(f"NumPy: {np.__version__}") - print(f"RUST_TOOLCHAIN={RUST_TOOLCHAIN}") + print(f"\nRUST_TOOLCHAIN={RUST_TOOLCHAIN}") print(f"BUILD_MODE={BUILD_MODE}") print(f"BUILD_DIR={BUILD_DIR}") print(f"PROFILE_MODE={PROFILE_MODE}") @@ -371,16 +371,10 @@ def build() -> None: print(f"PARALLEL_BUILD={PARALLEL_BUILD}") print(f"COPY_TO_SOURCE={COPY_TO_SOURCE}") print(f"PYO3_ONLY={PYO3_ONLY}") + print(f"CFLAGS={os.environ['CFLAGS']}") if "CFLAGS" in os.environ else None + print(f"LDFLAGS={os.environ['LDFLAGS']}") if "LDFLAGS" in os.environ else None - CFLAGS = os.environ.get("CFLAGS") - if CFLAGS: - print(f"CFLAGS={CFLAGS}") - - LDFLAGS = os.environ.get("LDFLAGS") - if LDFLAGS: - print(f"LDFLAGS={LDFLAGS}\n") - - print("Starting build...") + print("\nStarting build...") ts_start = datetime.datetime.now(datetime.timezone.utc) build() print(f"Build time: {datetime.datetime.now(datetime.timezone.utc) - ts_start}") diff --git a/docs/getting_started/installation.md b/docs/getting_started/installation.md index 4c5ffa3f590d..6934546e17ec 100644 --- a/docs/getting_started/installation.md +++ b/docs/getting_started/installation.md @@ -23,7 +23,6 @@ To install the latest binary wheel (or sdist package) from PyPI using Pythons _p Install optional dependencies as 'extras' for specific integrations: - `betfair`: Betfair adapter (integration) dependencies -- `binance`: Binance adapter (integration) dependencies - `docker`: Needed for Docker when using the IB gateway (with the Interactive Brokers adapter) - `dydx`: dYdX adapter (integration) dependencies - `ib`: Interactive Brokers adapter (integration) dependencies diff --git a/docs/integrations/binance.md b/docs/integrations/binance.md index 9db845a4d1e7..3401b4c44379 100644 --- a/docs/integrations/binance.md +++ b/docs/integrations/binance.md @@ -5,10 +5,6 @@ of daily trading volume, and open interest of crypto assets and crypto derivative products. This integration supports live market data ingest and order execution with Binance. -## Installation - -The Binance integration requires the `pycryptodome` package to be installed for signing RSA and Ed25519 API key types. - ## Overview The following documentation assumes a trader is setting up for both live market diff --git a/examples/live/bybit/bybit_ema_cross.py b/examples/live/bybit/bybit_ema_cross.py index 12862a2542a8..ece439534b88 100644 --- a/examples/live/bybit/bybit_ema_cross.py +++ b/examples/live/bybit/bybit_ema_cross.py @@ -37,7 +37,7 @@ # *** IT IS NOT INTENDED TO BE USED TO TRADE LIVE WITH REAL MONEY. *** # SPOT/LINEAR -product_type = BybitProductType.SPOT +product_type = BybitProductType.LINEAR symbol = f"ETHUSDT-{product_type.value.upper()}" trade_size = Decimal("0.010") @@ -71,7 +71,7 @@ api_secret=None, # 'BYBIT_API_SECRET' env var base_url_http=None, # Override with custom endpoint instrument_provider=InstrumentProviderConfig(load_all=True), - # product_types=[product_type], # Will load all instruments + product_types=[product_type], # Will load all instruments testnet=False, # If client uses the testnet ), }, diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index b64f9b79bc92..40486b97fdf9 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -4,19 +4,13 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.0" @@ -470,6 +464,33 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "aws-lc-rs" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5055edc4a9a1b2a917a818258cdfb86a535947feebd9981adc99667a062c6f85" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "axum" version = "0.7.5" @@ -527,17 +548,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -561,6 +582,29 @@ dependencies = [ "compare", ] +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.77", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -739,15 +783,24 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.16" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" dependencies = [ "jobserver", "libc", "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -823,6 +876,17 @@ dependencies = [ "half", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "3.2.25" @@ -887,6 +951,15 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.2" @@ -1630,6 +1703,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.13.0" @@ -1737,7 +1816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide", ] [[package]] @@ -1790,6 +1869,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -1925,9 +2010,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "glob" @@ -2142,10 +2227,10 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls 0.23.12", + "rustls", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls", "tower-service", ] @@ -2277,9 +2362,9 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" [[package]] name = "is-terminal" @@ -2368,6 +2453,12 @@ dependencies = [ "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lexical-core" version = "0.8.5" @@ -2438,6 +2529,16 @@ version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.8" @@ -2553,15 +2654,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - [[package]] name = "miniz_oxide" version = "0.8.0" @@ -2583,6 +2675,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + [[package]] name = "native-tls" version = "0.2.12" @@ -2602,21 +2700,25 @@ dependencies = [ [[package]] name = "nautilus-adapters" -version = "0.30.0" +version = "0.31.0" dependencies = [ "anyhow", + "base64", "chrono", "criterion", "databento", "fallible-streaming-iterator", + "hex", "indexmap 2.5.0", "itoa", "nautilus-common", "nautilus-core", "nautilus-model", + "pem", "pyo3", "pyo3-asyncio-0-21", "rand", + "ring", "rstest", "rust_decimal", "rust_decimal_macros", @@ -2632,7 +2734,7 @@ dependencies = [ [[package]] name = "nautilus-analysis" -version = "0.30.0" +version = "0.31.0" dependencies = [ "anyhow", "chrono", @@ -2652,7 +2754,7 @@ dependencies = [ [[package]] name = "nautilus-backtest" -version = "0.30.0" +version = "0.31.0" dependencies = [ "anyhow", "cbindgen", @@ -2674,7 +2776,7 @@ dependencies = [ [[package]] name = "nautilus-cli" -version = "0.30.0" +version = "0.31.0" dependencies = [ "anyhow", "clap 4.5.17", @@ -2691,7 +2793,7 @@ dependencies = [ [[package]] name = "nautilus-common" -version = "0.30.0" +version = "0.31.0" dependencies = [ "anyhow", "bytes", @@ -2721,7 +2823,7 @@ dependencies = [ [[package]] name = "nautilus-core" -version = "0.30.0" +version = "0.31.0" dependencies = [ "anyhow", "bytes", @@ -2743,7 +2845,7 @@ dependencies = [ [[package]] name = "nautilus-data" -version = "0.30.0" +version = "0.31.0" dependencies = [ "anyhow", "chrono", @@ -2769,7 +2871,7 @@ dependencies = [ [[package]] name = "nautilus-execution" -version = "0.30.0" +version = "0.31.0" dependencies = [ "anyhow", "criterion", @@ -2793,7 +2895,7 @@ dependencies = [ [[package]] name = "nautilus-indicators" -version = "0.30.0" +version = "0.31.0" dependencies = [ "anyhow", "log", @@ -2806,7 +2908,7 @@ dependencies = [ [[package]] name = "nautilus-infrastructure" -version = "0.30.0" +version = "0.31.0" dependencies = [ "anyhow", "async-stream", @@ -2835,7 +2937,7 @@ dependencies = [ [[package]] name = "nautilus-model" -version = "0.30.0" +version = "0.31.0" dependencies = [ "anyhow", "cbindgen", @@ -2863,7 +2965,7 @@ dependencies = [ [[package]] name = "nautilus-network" -version = "0.30.0" +version = "0.31.0" dependencies = [ "anyhow", "axum", @@ -2880,17 +2982,20 @@ dependencies = [ "pyo3-asyncio-0-21", "reqwest", "rstest", + "rustls", "serde_json", "thiserror", "tokio", + "tokio-rustls", "tokio-tungstenite", "tracing", "tracing-test", + "tungstenite 0.24.0", ] [[package]] name = "nautilus-persistence" -version = "0.30.0" +version = "0.31.0" dependencies = [ "anyhow", "binary-heap-plus", @@ -2914,7 +3019,7 @@ dependencies = [ [[package]] name = "nautilus-pyo3" -version = "0.30.0" +version = "0.31.0" dependencies = [ "nautilus-adapters", "nautilus-common", @@ -3173,15 +3278,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" -[[package]] -name = "openssl-src" -version = "300.3.2+3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" -dependencies = [ - "cc", -] - [[package]] name = "openssl-sys" version = "0.9.103" @@ -3190,7 +3286,6 @@ checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", - "openssl-src", "pkg-config", "vcpkg", ] @@ -3229,9 +3324,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" @@ -3307,6 +3402,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3431,9 +3536,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plotters" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", @@ -3444,15 +3549,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] @@ -3488,6 +3593,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn 2.0.77", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -3769,7 +3884,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rand", - "rustls 0.23.12", + "rustls", "rustls-native-certs", "rustls-pemfile", "rustls-pki-types", @@ -3778,7 +3893,7 @@ dependencies = [ "socket2", "tokio", "tokio-retry", - "tokio-rustls 0.26.0", + "tokio-rustls", "tokio-util", "url", "webpki-roots", @@ -4045,6 +4160,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.1" @@ -4067,26 +4188,14 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - [[package]] name = "rustls" version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ + "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -4130,6 +4239,7 @@ version = "0.102.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4158,11 +4268,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5020,24 +5130,13 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.12", + "rustls", "rustls-pki-types", "tokio", ] @@ -5056,18 +5155,17 @@ dependencies = [ [[package]] name = "tokio-tungstenite" version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" dependencies = [ "futures-util", "log", - "native-tls", - "rustls 0.22.4", + "rustls", "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-native-tls", - "tokio-rustls 0.25.0", - "tungstenite", - "webpki-roots", + "tokio-rustls", + "tungstenite 0.23.0", ] [[package]] @@ -5226,6 +5324,26 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror", + "utf-8", +] + [[package]] name = "tungstenite" version = "0.24.0" @@ -5238,10 +5356,7 @@ dependencies = [ "http", "httparse", "log", - "native-tls", "rand", - "rustls 0.23.12", - "rustls-pki-types", "sha1", "thiserror", "utf-8", @@ -5539,6 +5654,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "whoami" version = "1.5.2" diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index b9850a819a2c..34b40ff4b90c 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -12,7 +12,6 @@ members = [ "infrastructure", "model", "network", - "network/tokio-tungstenite", "persistence", "pyo3", "cli" @@ -20,7 +19,7 @@ members = [ [workspace.package] rust-version = "1.81.0" -version = "0.30.0" +version = "0.31.0" edition = "2021" authors = ["Nautech Systems "] description = "A high-performance algorithmic trading platform and event-driven backtester" diff --git a/nautilus_core/adapters/Cargo.toml b/nautilus_core/adapters/Cargo.toml index a186f40a3b53..e712118acf1a 100644 --- a/nautilus_core/adapters/Cargo.toml +++ b/nautilus_core/adapters/Cargo.toml @@ -36,6 +36,10 @@ thiserror = { workspace = true } ustr = { workspace = true } databento = { version = "0.12.1", optional = true } fallible-streaming-iterator = "0.1.9" +base64 = "0.22.1" +hex = "0.4.3" +pem = "3.0.4" +ring = "0.17.8" time = "0.3.36" [dev-dependencies] diff --git a/nautilus_core/adapters/src/crypto/mod.rs b/nautilus_core/adapters/src/crypto/mod.rs new file mode 100644 index 000000000000..ab52d8d184cd --- /dev/null +++ b/nautilus_core/adapters/src/crypto/mod.rs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +pub mod signing; + +#[cfg(feature = "python")] +pub mod python; diff --git a/nautilus_core/adapters/src/crypto/python/mod.rs b/nautilus_core/adapters/src/crypto/python/mod.rs new file mode 100644 index 000000000000..ebbb7baa8609 --- /dev/null +++ b/nautilus_core/adapters/src/crypto/python/mod.rs @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Python bindings from `pyo3`. + +#![allow(warnings)] // non-local `impl` definition, temporary allow until pyo3 upgrade +use pyo3::prelude::*; + +use crate::crypto::python; + +pub mod signing; + +/// Loaded as nautilus_pyo3.crypto +#[pymodule] +pub fn crypto(py: Python, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(python::signing::py_hmac_sign, m)?)?; + m.add_function(wrap_pyfunction!(python::signing::py_rsa_signature, m)?)?; + m.add_function(wrap_pyfunction!(python::signing::py_ed25519_signature, m)?)?; + Ok(()) +} diff --git a/nautilus_core/adapters/src/crypto/python/signing.rs b/nautilus_core/adapters/src/crypto/python/signing.rs new file mode 100644 index 000000000000..352368631db9 --- /dev/null +++ b/nautilus_core/adapters/src/crypto/python/signing.rs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +use nautilus_core::python::to_pyvalue_err; +use pyo3::prelude::*; + +use crate::crypto::signing::{ed25519_signature, hmac_sign, rsa_signature}; + +#[pyfunction(name = "hmac_sign")] +pub fn py_hmac_sign(secret: &str, data: &str) -> PyResult { + Ok(hmac_sign(secret, data)) +} + +#[pyfunction(name = "rsa_signature")] +pub fn py_rsa_signature(private_key_pem: &str, data: &str) -> PyResult { + rsa_signature(private_key_pem, data).map_err(to_pyvalue_err) +} + +#[pyfunction(name = "ed25519_signature")] +pub fn py_ed25519_signature(private_key: &[u8], data: &str) -> PyResult { + ed25519_signature(private_key, data).map_err(to_pyvalue_err) +} diff --git a/nautilus_core/adapters/src/crypto/signing.rs b/nautilus_core/adapters/src/crypto/signing.rs new file mode 100644 index 000000000000..e7f28c3b4c48 --- /dev/null +++ b/nautilus_core/adapters/src/crypto/signing.rs @@ -0,0 +1,149 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +use base64::{engine::general_purpose, Engine}; +use hex; +use ring::{ + hmac, + rand::SystemRandom, + signature::{Ed25519KeyPair, RsaKeyPair, Signature, RSA_PKCS1_SHA256}, +}; + +pub fn hmac_sign(secret: &str, data: &str) -> String { + let key = hmac::Key::new(hmac::HMAC_SHA256, secret.as_bytes()); + let signature = hmac::sign(&key, data.as_bytes()); + hex::encode(signature.as_ref()) +} + +pub fn rsa_signature(private_key_pem: &str, data: &str) -> anyhow::Result { + if data.is_empty() { + return Err(anyhow::anyhow!("Query string cannot be empty")); + } + + let pem = pem::parse(private_key_pem)?; + let private_key = + RsaKeyPair::from_pkcs8(pem.contents()).map_err(|e| anyhow::anyhow!("{e:?}"))?; + let mut signature = vec![0; private_key.public().modulus_len()]; + let rng = SystemRandom::new(); + + private_key + .sign(&RSA_PKCS1_SHA256, &rng, data.as_bytes(), &mut signature) + .map_err(|e| anyhow::anyhow!("{e:?}"))?; + + Ok(general_purpose::STANDARD.encode(&signature)) +} + +pub fn ed25519_signature(private_key: &[u8], data: &str) -> anyhow::Result { + let key_pair = + Ed25519KeyPair::from_seed_unchecked(private_key).map_err(|e| anyhow::anyhow!("{e:?}"))?; + let signature: Signature = key_pair.sign(data.as_bytes()); + Ok(hex::encode(signature.as_ref())) +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[rstest] + #[case( + "mysecretkey", + "data-to-sign", + "19ed21a8b2a6b847d7d7aea059ab3134cd58f13c860cfbe89338c718685fe077" + )] + #[case( + "anothersecretkey", + "somedata", + "fb44dab41435775b44a96aa008af58cbf1fa1cea32f4605562c586b98f7326c5" + )] + #[case( + "", + "data-without-secret", + "740c92f9c332fbb22d80aa6a3c9c10197a3e9dc61ca7e3c298c21597e4672133" + )] + #[case( + "mysecretkey", + "", + "bb4e89236de3b03c17e36d48ca059fa277b88165cb14813a49f082ed8974b9f4" + )] + #[case( + "", + "", + "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad" + )] + fn test_hmac_sign(#[case] secret: &str, #[case] data: &str, #[case] expected_signature: &str) { + let result = hmac_sign(secret, data); + assert_eq!( + result, expected_signature, + "Expected signature did not match" + ); + } + + #[rstest] + #[case( + r#"-----BEGIN TEST KEY----- +MIIBVwIBADANBgkqhkiG9w0BAQEFAASCATswggE3AgEAAkEAu/... +-----END PRIVATE KEY-----"#, + "" + )] + fn test_rsa_signature_empty_query(#[case] private_key_pem: &str, #[case] query_string: &str) { + let result = rsa_signature(private_key_pem, query_string); + assert!( + result.is_err(), + "Expected an error with empty query string, but got Ok" + ); + } + + #[rstest] + #[case( + r#"-----BEGIN INVALID KEY----- +INVALID_KEY_DATA +-----END INVALID KEY-----"#, + "This is a test query" + )] + fn test_rsa_signature_invalid_key(#[case] private_key_pem: &str, #[case] query_string: &str) { + let result = rsa_signature(private_key_pem, query_string); + assert!( + result.is_err(), + "Expected an error due to invalid key, but got Ok" + ); + } + + fn valid_ed25519_private_key() -> [u8; 32] { + [ + 0x0c, 0x74, 0x18, 0x92, 0x6b, 0x5d, 0xe9, 0x8f, 0xe2, 0xb6, 0x47, 0x8a, 0x51, 0xf9, + 0x97, 0x31, 0x9a, 0xcd, 0x2d, 0xbc, 0xf9, 0x94, 0xea, 0x8f, 0xc3, 0x1b, 0x65, 0x24, + 0x1f, 0x91, 0xd8, 0x6f, + ] + } + + #[rstest] + #[case(valid_ed25519_private_key(), "This is a test query")] + #[case(valid_ed25519_private_key(), "")] + fn test_ed25519_signature(#[case] private_key_bytes: [u8; 32], #[case] query_string: &str) { + let result = ed25519_signature(&private_key_bytes, query_string); + assert!( + result.is_ok(), + "Expected valid signature but got an error: {:?}", + result + ); + assert!(!result.unwrap().is_empty(), "Signature should not be empty"); + } +} diff --git a/nautilus_core/adapters/src/lib.rs b/nautilus_core/adapters/src/lib.rs index 49ea7ea63d94..04cc192fc9e0 100644 --- a/nautilus_core/adapters/src/lib.rs +++ b/nautilus_core/adapters/src/lib.rs @@ -34,3 +34,5 @@ #[cfg(feature = "databento")] pub mod databento; + +pub mod crypto; diff --git a/nautilus_core/common/src/logging/logger.rs b/nautilus_core/common/src/logging/logger.rs index 4882675041d7..9e77970156f6 100644 --- a/nautilus_core/common/src/logging/logger.rs +++ b/nautilus_core/common/src/logging/logger.rs @@ -52,9 +52,9 @@ const LOGGING: &str = "logging"; pub struct LoggerConfig { /// Maximum log level to write to stdout. pub stdout_level: LevelFilter, - /// Maximum log level to write to file. + /// Maximum log level to write to file (disabled is `Off`). pub fileout_level: LevelFilter, - /// Maximum log level to write for a given component. + /// Per-component log levels, allowing finer-grained control. component_level: HashMap, /// If logger is using ANSI color codes. pub is_colored: bool, @@ -141,21 +141,16 @@ impl LoggerConfig { } } -/// Initialize tracing. -/// -/// Tracing is meant to be used to trace/debug async Rust code. It can be -/// configured to filter modules and write up to a specific level only using -/// by passing a configuration using the `RUST_LOG` environment variable. - /// A high-performance logger utilizing a MPSC channel under the hood. /// -/// A separate thread is spawned at initialization which receives [`LogEvent`] structs over the -/// channel. +/// A logger is initialized with a [`LoggerConfig`] to set up different logging levels for +/// stdout, file, and components. The logger spawns a thread that listens for [`LogEvent`]s +/// sent via an MPSC channel. #[derive(Debug)] pub struct Logger { - /// Configure maximum levels for components and IO. + /// Configuration for logging levels and behavior. pub config: LoggerConfig, - /// Send log events to a separate thread. + /// Transmitter for sending log events to the 'logging' thread. tx: std::sync::mpsc::Sender, } @@ -186,11 +181,22 @@ impl Display for LogLine { } } +/// A wrapper around a log line that provides formatted and cached representations. +/// +/// This struct contains a log line and provides various formatted versions +/// of it, such as plain string, colored string, and JSON. It also caches the +/// results for repeated calls, optimizing performance when the same message +/// needs to be logged multiple times in different formats. pub struct LogLineWrapper { + /// The underlying log line that contains the log data. line: LogLine, + /// Cached plain string representation of the log line. cache: Option, + /// Cached colored string representation of the log line. colored: Option, + /// The timestamp of when the log event occurred. timestamp: String, + /// The ID of the trader associated with this log event. trader_id: Ustr, } @@ -207,6 +213,10 @@ impl LogLineWrapper { } } + /// Returns the plain log message string, caching the result. + /// + /// This method constructs the log line format and caches it for repeated calls. Useful when the + /// same log message needs to be printed multiple times. pub fn get_string(&mut self) -> &str { self.cache.get_or_insert_with(|| { format!( @@ -220,6 +230,11 @@ impl LogLineWrapper { }) } + /// Returns the colored log message string, caching the result. + /// + /// This method constructs the colored log line format and caches the result + /// for repeated calls, providing the message with ANSI color codes if the + /// logger is configured to use colors. pub fn get_colored(&mut self) -> &str { self.colored.get_or_insert_with(|| { format!( @@ -234,6 +249,11 @@ impl LogLineWrapper { }) } + /// Returns the log message as a JSON string. + /// + /// This method serializes the log line and its associated metadata + /// (timestamp, trader ID, etc.) into a JSON string format. This is useful + /// for structured logging or when logs need to be stored in a JSON format. #[must_use] pub fn get_json(&self) -> String { let json_string = @@ -310,6 +330,15 @@ impl Logger { Self::init_with_config(trader_id, instance_id, config, file_config) } + /// Initializes the logger with the given configuration. + /// + /// # Examples + /// + /// ```rust + /// let config = LoggerConfig::from_spec("stdout=Info;fileout=Debug;RiskEngine=Error"); + /// let file_config = FileWriterConfig::default(); + /// let log_guard = Logger::init_with_config(trader_id, instance_id, config, file_config); + /// ``` #[must_use] pub fn init_with_config( trader_id: TraderId, diff --git a/nautilus_core/common/src/logging/mod.rs b/nautilus_core/common/src/logging/mod.rs index c3b348d48dc0..469f0c1e118f 100644 --- a/nautilus_core/common/src/logging/mod.rs +++ b/nautilus_core/common/src/logging/mod.rs @@ -95,6 +95,11 @@ pub extern "C" fn logging_clock_set_static_time(time_ns: u64) { clock.set_time(time_ns.into()); } +/// Initialize tracing. +/// +/// Tracing is meant to be used to trace/debug async Rust code. It can be +/// configured to filter modules and write up to a specific level by passing +/// a configuration using the `RUST_LOG` environment variable. /// /// # Safety /// diff --git a/nautilus_core/network/Cargo.toml b/nautilus_core/network/Cargo.toml index 673f6bd576b5..c134fddb6c68 100644 --- a/nautilus_core/network/Cargo.toml +++ b/nautilus_core/network/Cargo.toml @@ -26,14 +26,17 @@ http = "1.1.0" hyper = "1.4.1" nonzero_ext = "0.3.0" reqwest = "0.12.7" -tokio-tungstenite = { path = "./tokio-tungstenite", features = ["rustls-tls-native-roots"] } +rustls = "0.23.12" +tokio-rustls = "0.26.0" +tokio-tungstenite = { version = "0.23.1", features = ["rustls-tls-native-roots"] } +tungstenite = "0.24.0" [dev-dependencies] criterion = { workspace = true } serde_json = { workspace = true } rstest = { workspace = true } axum = "0.7.5" -tracing-test = "0.2.4" +tracing-test = "0.2.5" [features] default = ["python"] diff --git a/nautilus_core/network/src/lib.rs b/nautilus_core/network/src/lib.rs index a92b15797e0a..2715be4616fc 100644 --- a/nautilus_core/network/src/lib.rs +++ b/nautilus_core/network/src/lib.rs @@ -32,6 +32,7 @@ pub mod http; #[allow(dead_code)] mod ratelimiter; pub mod socket; +mod tls; pub mod websocket; #[cfg(feature = "python")] diff --git a/nautilus_core/network/src/socket.rs b/nautilus_core/network/src/socket.rs index c5bb3181d613..eedca1a47996 100644 --- a/nautilus_core/network/src/socket.rs +++ b/nautilus_core/network/src/socket.rs @@ -25,6 +25,7 @@ use std::{ use nautilus_core::python::to_pyruntime_err; use pyo3::prelude::*; +use rustls::crypto::{aws_lc_rs, CryptoProvider}; use tokio::{ io::{split, AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}, net::TcpStream, @@ -33,11 +34,12 @@ use tokio::{ time::sleep, }; use tokio_tungstenite::{ - tls::tcp_tls, tungstenite::{client::IntoClientRequest, stream::Mode, Error}, MaybeTlsStream, }; +use crate::tls::tcp_tls; + type TcpWriter = WriteHalf>; type SharedTcpWriter = Arc>>>; type TcpReader = ReadHalf>; @@ -89,6 +91,12 @@ struct SocketClientInner { impl SocketClientInner { pub async fn connect_url(config: SocketConfig) -> Result { + if CryptoProvider::get_default().is_none() { + aws_lc_rs::default_provider() + .install_default() + .expect("Error installing crypto provider"); + } + let SocketConfig { url, mode, diff --git a/nautilus_core/network/src/tls.rs b/nautilus_core/network/src/tls.rs new file mode 100644 index 000000000000..05ed2d7a58f4 --- /dev/null +++ b/nautilus_core/network/src/tls.rs @@ -0,0 +1,149 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Module for wrapping raw socket streams with TLS encryption. + +use rustls; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_tungstenite::{ + tungstenite::{ + client::IntoClientRequest, + handshake::client::{Request, Response}, + stream::Mode, + Error, + }, + MaybeTlsStream, +}; +use tungstenite; + +/// A connector that can be used when establishing connections, allowing to control whether +/// `native-tls` or `rustls` is used to create a TLS connection. Or TLS can be disabled with the +/// `Plain` variant. +#[non_exhaustive] +#[derive(Clone)] +pub enum Connector { + /// No TLS connection. + Plain, + /// TLS connection using `rustls`. + Rustls(std::sync::Arc), +} + +mod encryption { + + pub mod rustls { + use std::{convert::TryFrom, sync::Arc}; + + pub use rustls::ClientConfig; + use rustls::{pki_types::ServerName, RootCertStore}; + use tokio::io::{AsyncRead, AsyncWrite}; + use tokio_rustls::TlsConnector as TokioTlsConnector; + use tokio_tungstenite::{ + tungstenite::{error::TlsError, stream::Mode, Error}, + MaybeTlsStream, + }; + + pub async fn wrap_stream( + socket: S, + domain: String, + mode: Mode, + tls_connector: Option>, + ) -> Result, Error> + where + S: 'static + AsyncRead + AsyncWrite + Send + Unpin, + { + match mode { + Mode::Plain => Ok(MaybeTlsStream::Plain(socket)), + Mode::Tls => { + let config = match tls_connector { + Some(config) => config, + None => { + #[allow(unused_mut)] + let mut root_store = RootCertStore::empty(); + + Arc::new( + ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(), + ) + } + }; + let domain = ServerName::try_from(domain.as_str()) + .map_err(|_| TlsError::InvalidDnsName)? + .to_owned(); + let stream = TokioTlsConnector::from(config); + let connected = stream.connect(domain, socket).await; + + match connected { + Err(e) => Err(Error::Io(e)), + Ok(s) => Ok(MaybeTlsStream::Rustls(s)), + } + } + } + } + } + + pub mod plain { + use tokio::io::{AsyncRead, AsyncWrite}; + use tokio_tungstenite::{ + tungstenite::{ + error::{Error, UrlError}, + stream::Mode, + }, + MaybeTlsStream, + }; + + pub async fn wrap_stream(socket: S, mode: Mode) -> Result, Error> + where + S: 'static + AsyncRead + AsyncWrite + Send + Unpin, + { + match mode { + Mode::Plain => Ok(MaybeTlsStream::Plain(socket)), + Mode::Tls => Err(Error::Url(UrlError::TlsFeatureNotEnabled)), + } + } + } +} + +pub async fn tcp_tls( + request: &Request, + mode: Mode, + stream: S, + connector: Option, +) -> Result, Error> +where + S: 'static + AsyncRead + AsyncWrite + Send + Unpin, + MaybeTlsStream: Unpin, +{ + let domain = domain(request)?; + + match connector { + Some(conn) => match conn { + Connector::Rustls(conn) => { + self::encryption::rustls::wrap_stream(stream, domain, mode, Some(conn)).await + } + Connector::Plain => self::encryption::plain::wrap_stream(stream, mode).await, + }, + None => self::encryption::rustls::wrap_stream(stream, domain, mode, None).await, + } +} + +fn domain(request: &tungstenite::handshake::client::Request) -> Result { + match request.uri().host() { + // rustls expects IPv6 addresses without the surrounding [] brackets + Some(d) if d.starts_with('[') && d.ends_with(']') => Ok(d[1..d.len() - 1].to_string()), + Some(d) => Ok(d.to_string()), + None => panic!("No host name"), + } +} diff --git a/nautilus_core/network/src/websocket.rs b/nautilus_core/network/src/websocket.rs index c6da5a32d7bf..cbc669084958 100644 --- a/nautilus_core/network/src/websocket.rs +++ b/nautilus_core/network/src/websocket.rs @@ -29,6 +29,7 @@ use futures_util::{ use hyper::header::HeaderName; use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err}; use pyo3::{prelude::*, types::PyBytes}; +use rustls::crypto::{aws_lc_rs, CryptoProvider}; use tokio::{net::TcpStream, sync::Mutex, task, time::sleep}; use tokio_tungstenite::{ connect_async, @@ -80,6 +81,12 @@ struct WebSocketClientInner { impl WebSocketClientInner { /// Create an inner websocket client. pub async fn connect_url(config: WebSocketConfig) -> Result { + if CryptoProvider::get_default().is_none() { + aws_lc_rs::default_provider() + .install_default() + .expect("Error installing crypto provider"); + } + let WebSocketConfig { url, handler, diff --git a/nautilus_core/network/tokio-tungstenite/.gitignore b/nautilus_core/network/tokio-tungstenite/.gitignore deleted file mode 100644 index a9d37c560c6a..000000000000 --- a/nautilus_core/network/tokio-tungstenite/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target -Cargo.lock diff --git a/nautilus_core/network/tokio-tungstenite/CHANGELOG.md b/nautilus_core/network/tokio-tungstenite/CHANGELOG.md deleted file mode 100644 index a07463c87668..000000000000 --- a/nautilus_core/network/tokio-tungstenite/CHANGELOG.md +++ /dev/null @@ -1,57 +0,0 @@ -# 0.19.0 - -- Allow users to enable/disable Nagle algorithm when using `connect()` helpers. -- Improve the behavior of the `Sink` for the `WebSocketStream`, so it does not return an error when it’s not necessary (when `poll_flush()` is called on a connection that has just been closed). -- Workaround an issue where `rustls` TLS backend expected domain in a certain format and reject IPv6 addresses if they contained square brackets in them. -- Update dependencies and remove unused errors. - -# 0.18.0 - -- Update dependencies (underlying `tungstenite` core). - -# 0.17.2 - -- Make `Origin` header case-sensitive (to keep compatibility with poorely-written servers that don't accept lowercase `Origin` header). -- Make semantics of the reading form the `WebSocketStream` more reasonable (return `None` instead of an error when the stream is normally closed). -- Improve the way `poll_close()` works by properly driving the close of the stream till completion. - -# 0.17.1 - -- Update the `tungstenite` dependency (fixes a panic in `tungstenite` and MSRV), see [`tungstenite`'s changelog for more details](https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md#0172). - -# 0.17.0 - -- Update the dependencies, please refer to the [`tungstenite` changelog](https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md#0170) for the actual changes. - -# 0.16.1 - -- Fix feature selection problem when using TLS. - -# 0.16.0 - -- Add a function to allow to specify the TLS connector when using `connect()` like logic. -- Add support for choosing the right root certificates for the TLS. -- Change the behavior of the `connect()` so that it fails when using TLS without TLS feature. -- Do not project with Unpin. -- Update the dependencies with important [implications / improvements](https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md#0160). - -# 0.15.0 - -- Update the `tungstenite-rs` version to `0.14.0`, - [check `tungstenite-rs` release for more details](https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md#0140). - -# 0.14.0 - -- Support for `rustls` as TLS backend. - - The `tls` feature was renamed to `native-tls` and uses a OS-native TLS implementation. - - A new `native-tls-vendored` feature that uses `native-tls` but forces to build a vendored - version (mostly for `openssl`) instead of linking against the system installation. - - New `rustls-tls` feature flag to enable TLS with `rustls` as backend. - - `stream::Stream` was renamed to `MaybeTlsStream` and wraps a `rustls` TLS stream as well now. - - If both `native-tls` and `rustls-tls` are enabled `native-tls` is used by default. - - A new `Connector` was introduced that is similar to the previous `TlsConnector` but now allows - to control the used TLS backend explicitly (or disable it) in `client_async_tls_with_config`. - -# 0.13.0 - -- Upgrade from Tokio 0.3 to Tokio 1.0.0. diff --git a/nautilus_core/network/tokio-tungstenite/Cargo.toml b/nautilus_core/network/tokio-tungstenite/Cargo.toml deleted file mode 100644 index 408905fcf05e..000000000000 --- a/nautilus_core/network/tokio-tungstenite/Cargo.toml +++ /dev/null @@ -1,66 +0,0 @@ -[package] -name = "tokio-tungstenite" -description = "Tokio binding for Tungstenite, the Lightweight stream-based WebSocket implementation" -categories = ["web-programming::websocket", "network-programming", "asynchronous", "concurrency"] -keywords = ["websocket", "io", "web"] -authors = ["Daniel Abramov ", "Alexey Galakhov "] -license = "MIT" -homepage = "https://github.com/snapview/tokio-tungstenite" -documentation = "https://docs.rs/tokio-tungstenite/0.19.0" -repository = "https://github.com/snapview/tokio-tungstenite" -version = "0.23.1" -edition = "2018" -rust-version = "1.63" -include = ["examples/**/*", "src/**/*", "LICENSE", "README.md", "CHANGELOG.md"] - -[package.metadata.docs.rs] -features = ["native-tls", "__rustls-tls"] - -[features] -default = ["connect", "handshake"] -connect = ["stream", "tokio/net", "handshake"] -handshake = ["tungstenite/handshake"] -native-tls = ["native-tls-crate", "tokio-native-tls", "stream", "tungstenite/native-tls", "handshake"] -native-tls-vendored = ["native-tls", "native-tls-crate/vendored", "tungstenite/native-tls-vendored"] -rustls-tls-native-roots = ["__rustls-tls", "rustls-native-certs"] -rustls-tls-webpki-roots = ["__rustls-tls", "webpki-roots"] -__rustls-tls = ["rustls", "rustls-pki-types", "tokio-rustls", "stream", "tungstenite/__rustls-tls", "handshake"] -stream = [] - -[dependencies] -log = "0.4.22" -futures-util = { version = "0.3.30", default-features = false, features = ["sink", "std"] } -tokio = { workspace = true } - -[dependencies.tungstenite] -version = "0.24.0" -default-features = false - -[dependencies.native-tls-crate] -optional = true -package = "native-tls" -version = "0.2.11" - -[dependencies.rustls] -optional = true -version = "0.22.0" - -[dependencies.rustls-pki-types] -optional = true -version = "1.0" - -[dependencies.rustls-native-certs] -optional = true -version = "0.7.0" - -[dependencies.tokio-native-tls] -optional = true -version = "0.3.1" - -[dependencies.tokio-rustls] -optional = true -version = "0.25.0" - -[dependencies.webpki-roots] -optional = true -version = "0.26.1" diff --git a/nautilus_core/network/tokio-tungstenite/LICENSE b/nautilus_core/network/tokio-tungstenite/LICENSE deleted file mode 100644 index 4bf5242e2dd8..000000000000 --- a/nautilus_core/network/tokio-tungstenite/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2017 Daniel Abramov -Copyright (c) 2017 Alexey Galakhov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/nautilus_core/network/tokio-tungstenite/README.md b/nautilus_core/network/tokio-tungstenite/README.md deleted file mode 100644 index 93aba69f0200..000000000000 --- a/nautilus_core/network/tokio-tungstenite/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# tokio-tungstenite - -Asynchronous WebSockets for Tokio stack. - -[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) -[![Crates.io](https://img.shields.io/crates/v/tokio-tungstenite.svg?maxAge=2592000)](https://crates.io/crates/tokio-tungstenite) -[![Build Status](https://travis-ci.org/snapview/tokio-tungstenite.svg?branch=master)](https://travis-ci.org/snapview/tokio-tungstenite) - -[Documentation](https://docs.rs/tokio-tungstenite) - -## Usage - -Add this in your `Cargo.toml`: - -```toml -[dependencies] -tokio-tungstenite = "*" -``` - -Take a look at the `examples/` directory for client and server examples. You may also want to get familiar with -[Tokio](https://github.com/tokio-rs/tokio) if you don't have any experience with it. - -## What is tokio-tungstenite? - -It is based on [`tungstenite-rs`](https://github.com/snapview/tungstenite-rs) Rust WebSocket library and provides `Tokio` bindings and wrappers for it, so you -can use it with non-blocking/asynchronous `TcpStream`s from and couple it together with other crates from `Tokio` stack. - -## Features - -As with [`tungstenite-rs`](https://github.com/snapview/tungstenite-rs) TLS is supported on all platforms using [`native-tls`](https://github.com/sfackler/rust-native-tls) or [`rustls`](https://github.com/ctz/rustls) through feature flags: `native-tls`, `rustls-tls-native-roots` or `rustls-tls-webpki-roots` feature flags. Neither is enabled by default. See the `Cargo.toml` for more information. If you require support for secure WebSockets (`wss://`) enable one of them. - -## Is it performant? - -In essence, `tokio-tungstenite` is a wrapper for `tungstenite`, so the performance is capped by the performance of `tungstenite`. `tungstenite` -has a decent performance (it has been used in production for real-time communication software, video conferencing, etc), but it's definitely -not the fastest WebSocket library in the world at the moment of writing this note. - -If performance is of a paramount importance for you (especially if you send **large messages**), then you might want to check other libraries -that have been designed to be performant or you could file a PR against `tungstenite` to improve the performance! - -We are aware of changes that both `tungstenite` and `tokio-tungstenite` need in order to fill the gap of ~30% performance difference between `tungstenite` -and more performant libraries like `fastbwebsockets`, but we have not worked on that yet as it was not required for the use case that original authors designed -the library for. In the course of past years we have merged several performance improvements submitted by the awesome community of Rust users who helped to improve -the library! For a quick summary of the pending performance problems/improvements, see [the comment](https://github.com/snapview/tungstenite-rs/issues/352#issuecomment-1537488614). - diff --git a/nautilus_core/network/tokio-tungstenite/rustfmt.toml b/nautilus_core/network/tokio-tungstenite/rustfmt.toml deleted file mode 100644 index 0c9d0486b247..000000000000 --- a/nautilus_core/network/tokio-tungstenite/rustfmt.toml +++ /dev/null @@ -1,7 +0,0 @@ -# This project uses rustfmt to format source code. Run `cargo +nightly fmt [-- --check]. -# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md - -# Break complex but short statements a bit less. -use_small_heuristics = "Max" - -imports_granularity = "Crate" diff --git a/nautilus_core/network/tokio-tungstenite/src/compat.rs b/nautilus_core/network/tokio-tungstenite/src/compat.rs deleted file mode 100644 index 85e28b63814d..000000000000 --- a/nautilus_core/network/tokio-tungstenite/src/compat.rs +++ /dev/null @@ -1,200 +0,0 @@ -#![allow(clippy::all)] - -use log::trace; -use std::{ - io::{Read, Write}, - pin::Pin, - task::{Context, Poll}, -}; - -use futures_util::task; -use std::sync::Arc; -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; -use tungstenite::Error as WsError; - -pub enum ContextWaker { - Read, - Write, -} - -#[derive(Debug)] -pub struct AllowStd { - inner: S, - // We have the problem that external read operations (i.e. the Stream impl) - // can trigger both read (AsyncRead) and write (AsyncWrite) operations on - // the underyling stream. At the same time write operations (i.e. the Sink - // impl) can trigger write operations (AsyncWrite) too. - // Both the Stream and the Sink can be used on two different tasks, but it - // is required that AsyncRead and AsyncWrite are only ever used by a single - // task (or better: with a single waker) at a time. - // - // Doing otherwise would cause only the latest waker to be remembered, so - // in our case either the Stream or the Sink impl would potentially wait - // forever to be woken up because only the other one would've been woken - // up. - // - // To solve this we implement a waker proxy that has two slots (one for - // read, one for write) to store wakers. One waker proxy is always passed - // to the AsyncRead, the other to AsyncWrite so that they will only ever - // have to store a single waker, but internally we dispatch any wakeups to - // up to two actual wakers (one from the Sink impl and one from the Stream - // impl). - // - // write_waker_proxy is always used for AsyncWrite, read_waker_proxy for - // AsyncRead. The read_waker slots of both are used for the Stream impl - // (and handshaking), the write_waker slots for the Sink impl. - write_waker_proxy: Arc, - read_waker_proxy: Arc, -} - -// Internal trait used only in the Handshake module for registering -// the waker for the context used during handshaking. We're using the -// read waker slot for this, but any would do. -// -// Don't ever use this from multiple tasks at the same time! -pub trait SetWaker { - fn set_waker(&self, waker: &task::Waker); -} - -impl SetWaker for AllowStd { - fn set_waker(&self, waker: &task::Waker) { - self.set_waker(ContextWaker::Read, waker); - } -} - -impl AllowStd { - pub(crate) fn new(inner: S, waker: &task::Waker) -> Self { - let res = Self { - inner, - write_waker_proxy: Default::default(), - read_waker_proxy: Default::default(), - }; - - // Register the handshake waker as read waker for both proxies, - // see also the SetWaker trait. - res.write_waker_proxy.read_waker.register(waker); - res.read_waker_proxy.read_waker.register(waker); - - res - } - - // Set the read or write waker for our proxies. - // - // Read: this is only supposed to be called by read (or handshake) operations, i.e. the Stream - // impl on the WebSocketStream. - // Reading can also cause writes to happen, e.g. in case of Message::Ping handling. - // - // Write: this is only supposde to be called by write operations, i.e. the Sink impl on the - // WebSocketStream. - pub(crate) fn set_waker(&self, kind: ContextWaker, waker: &task::Waker) { - match kind { - ContextWaker::Read => { - self.write_waker_proxy.read_waker.register(waker); - self.read_waker_proxy.read_waker.register(waker); - } - ContextWaker::Write => { - self.write_waker_proxy.write_waker.register(waker); - self.read_waker_proxy.write_waker.register(waker); - } - } - } -} - -// Proxy Waker that we pass to the internal AsyncRead/Write of the -// stream underlying the websocket. We have two slots here for the -// actual wakers to allow external read operations to trigger both -// reads and writes, and the same for writes. -#[derive(Debug, Default)] -struct WakerProxy { - read_waker: task::AtomicWaker, - write_waker: task::AtomicWaker, -} - -impl task::ArcWake for WakerProxy { - fn wake_by_ref(arc_self: &Arc) { - arc_self.read_waker.wake(); - arc_self.write_waker.wake(); - } -} - -impl AllowStd -where - S: Unpin, -{ - fn with_context(&mut self, kind: ContextWaker, f: F) -> Poll> - where - F: FnOnce(&mut Context<'_>, Pin<&mut S>) -> Poll>, - { - trace!("{}:{} AllowStd.with_context", file!(), line!()); - let waker = match kind { - ContextWaker::Read => task::waker_ref(&self.read_waker_proxy), - ContextWaker::Write => task::waker_ref(&self.write_waker_proxy), - }; - let mut context = task::Context::from_waker(&waker); - f(&mut context, Pin::new(&mut self.inner)) - } - - pub(crate) fn get_mut(&mut self) -> &mut S { - &mut self.inner - } - - pub(crate) const fn get_ref(&self) -> &S { - &self.inner - } -} - -impl Read for AllowStd -where - S: AsyncRead + Unpin, -{ - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - trace!("{}:{} Read.read", file!(), line!()); - let mut buf = ReadBuf::new(buf); - match self.with_context(ContextWaker::Read, |ctx, stream| { - trace!("{}:{} Read.with_context read -> poll_read", file!(), line!()); - stream.poll_read(ctx, &mut buf) - }) { - Poll::Ready(Ok(())) => Ok(buf.filled().len()), - Poll::Ready(Err(e)) => Err(e), - Poll::Pending => Err(std::io::Error::from(std::io::ErrorKind::WouldBlock)), - } - } -} - -impl Write for AllowStd -where - S: AsyncWrite + Unpin, -{ - fn write(&mut self, buf: &[u8]) -> std::io::Result { - trace!("{}:{} Write.write", file!(), line!()); - match self.with_context(ContextWaker::Write, |ctx, stream| { - trace!("{}:{} Write.with_context write -> poll_write", file!(), line!()); - stream.poll_write(ctx, buf) - }) { - Poll::Ready(r) => r, - Poll::Pending => Err(std::io::Error::from(std::io::ErrorKind::WouldBlock)), - } - } - - fn flush(&mut self) -> std::io::Result<()> { - trace!("{}:{} Write.flush", file!(), line!()); - match self.with_context(ContextWaker::Write, |ctx, stream| { - trace!("{}:{} Write.with_context flush -> poll_flush", file!(), line!()); - stream.poll_flush(ctx) - }) { - Poll::Ready(r) => r, - Poll::Pending => Err(std::io::Error::from(std::io::ErrorKind::WouldBlock)), - } - } -} - -pub fn cvt(r: Result) -> Poll> { - match r { - Ok(v) => Poll::Ready(Ok(v)), - Err(WsError::Io(ref e)) if e.kind() == std::io::ErrorKind::WouldBlock => { - trace!("WouldBlock"); - Poll::Pending - } - Err(e) => Poll::Ready(Err(e)), - } -} diff --git a/nautilus_core/network/tokio-tungstenite/src/connect.rs b/nautilus_core/network/tokio-tungstenite/src/connect.rs deleted file mode 100644 index 5787af153155..000000000000 --- a/nautilus_core/network/tokio-tungstenite/src/connect.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Connection helper. -use tokio::net::TcpStream; - -use tungstenite::{ - error::{Error, UrlError}, - handshake::client::{Request, Response}, - protocol::WebSocketConfig, -}; - -use crate::{domain, stream::MaybeTlsStream, Connector, IntoClientRequest, WebSocketStream}; - -/// Connect to a given URL. -pub async fn connect_async( - request: R, -) -> Result<(WebSocketStream>, Response), Error> -where - R: IntoClientRequest + Unpin, -{ - connect_async_with_config(request, None, false).await -} - -/// The same as `connect_async()` but the one can specify a websocket configuration. -/// Please refer to `connect_async()` for more details. `disable_nagle` specifies if -/// the Nagle's algorithm must be disabled, i.e. `set_nodelay(true)`. If you don't know -/// what the Nagle's algorithm is, better leave it set to `false`. -pub async fn connect_async_with_config( - request: R, - config: Option, - disable_nagle: bool, -) -> Result<(WebSocketStream>, Response), Error> -where - R: IntoClientRequest + Unpin, -{ - connect(request.into_client_request()?, config, disable_nagle, None).await -} - -/// The same as `connect_async()` but the one can specify a websocket configuration, -/// and a TLS connector to use. Please refer to `connect_async()` for more details. -/// `disable_nagle` specifies if the Nagle's algorithm must be disabled, i.e. -/// `set_nodelay(true)`. If you don't know what the Nagle's algorithm is, better -/// leave it to `false`. -#[cfg(any(feature = "native-tls", feature = "__rustls-tls"))] -pub async fn connect_async_tls_with_config( - request: R, - config: Option, - disable_nagle: bool, - connector: Option, -) -> Result<(WebSocketStream>, Response), Error> -where - R: IntoClientRequest + Unpin, -{ - connect(request.into_client_request()?, config, disable_nagle, connector).await -} - -async fn connect( - request: Request, - config: Option, - disable_nagle: bool, - connector: Option, -) -> Result<(WebSocketStream>, Response), Error> { - let domain = domain(&request)?; - let port = request - .uri() - .port_u16() - .or_else(|| match request.uri().scheme_str() { - Some("wss") => Some(443), - Some("ws") => Some(80), - _ => None, - }) - .ok_or(Error::Url(UrlError::UnsupportedUrlScheme))?; - - let addr = format!("{domain}:{port}"); - let socket = TcpStream::connect(addr).await.map_err(Error::Io)?; - - if disable_nagle { - socket.set_nodelay(true)?; - } - - crate::tls::client_async_tls_with_config(request, socket, config, connector).await -} diff --git a/nautilus_core/network/tokio-tungstenite/src/handshake.rs b/nautilus_core/network/tokio-tungstenite/src/handshake.rs deleted file mode 100644 index 6633d91637f1..000000000000 --- a/nautilus_core/network/tokio-tungstenite/src/handshake.rs +++ /dev/null @@ -1,179 +0,0 @@ -#[cfg(feature = "handshake")] -use crate::compat::SetWaker; -use crate::{compat::AllowStd, WebSocketStream}; -use log::trace; -use std::{ - future::Future, - io::{Read, Write}, - pin::Pin, - task::{Context, Poll}, -}; -use tokio::io::{AsyncRead, AsyncWrite}; -use tungstenite::WebSocket; -#[cfg(feature = "handshake")] -use tungstenite::{ - handshake::{ - client::Response, server::Callback, HandshakeError as Error, HandshakeRole, - MidHandshake as WsHandshake, - }, - ClientHandshake, ServerHandshake, -}; - -pub async fn without_handshake(stream: S, f: F) -> WebSocketStream -where - F: FnOnce(AllowStd) -> WebSocket> + Unpin, - S: AsyncRead + AsyncWrite + Unpin, -{ - let start = SkippedHandshakeFuture(Some(SkippedHandshakeFutureInner { f, stream })); - - let ws = start.await; - - WebSocketStream::new(ws) -} - -struct SkippedHandshakeFuture(Option>); -struct SkippedHandshakeFutureInner { - f: F, - stream: S, -} - -impl Future for SkippedHandshakeFuture -where - F: FnOnce(AllowStd) -> WebSocket> + Unpin, - S: Unpin, - AllowStd: Read + Write, -{ - type Output = WebSocket>; - - fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { - let inner = self.get_mut().0.take().expect("future polled after completion"); - trace!("Setting context when skipping handshake"); - let stream = AllowStd::new(inner.stream, ctx.waker()); - - Poll::Ready((inner.f)(stream)) - } -} - -#[cfg(feature = "handshake")] -struct MidHandshake(Option>); - -#[cfg(feature = "handshake")] -enum StartedHandshake { - Done(Role::FinalResult), - Mid(WsHandshake), -} - -#[cfg(feature = "handshake")] -struct StartedHandshakeFuture(Option>); -#[cfg(feature = "handshake")] -struct StartedHandshakeFutureInner { - f: F, - stream: S, -} - -#[cfg(feature = "handshake")] -async fn handshake(stream: S, f: F) -> Result> -where - Role: HandshakeRole + Unpin, - Role::InternalStream: SetWaker + Unpin, - F: FnOnce(AllowStd) -> Result> + Unpin, - S: AsyncRead + AsyncWrite + Unpin, -{ - let start = StartedHandshakeFuture(Some(StartedHandshakeFutureInner { f, stream })); - - match start.await? { - StartedHandshake::Done(r) => Ok(r), - StartedHandshake::Mid(s) => { - let res: Result> = MidHandshake::(Some(s)).await; - res - } - } -} - -#[cfg(feature = "handshake")] -pub async fn client_handshake( - stream: S, - f: F, -) -> Result<(WebSocketStream, Response), Error>>> -where - F: FnOnce( - AllowStd, - ) -> Result< - > as HandshakeRole>::FinalResult, - Error>>, - > + Unpin, - S: AsyncRead + AsyncWrite + Unpin, -{ - let result = handshake(stream, f).await?; - let (s, r) = result; - Ok((WebSocketStream::new(s), r)) -} - -#[cfg(feature = "handshake")] -pub async fn server_handshake( - stream: S, - f: F, -) -> Result, Error, C>>> -where - C: Callback + Unpin, - F: FnOnce( - AllowStd, - ) -> Result< - , C> as HandshakeRole>::FinalResult, - Error, C>>, - > + Unpin, - S: AsyncRead + AsyncWrite + Unpin, -{ - let s: WebSocket> = handshake(stream, f).await?; - Ok(WebSocketStream::new(s)) -} - -#[cfg(feature = "handshake")] -impl Future for StartedHandshakeFuture -where - Role: HandshakeRole, - Role::InternalStream: SetWaker + Unpin, - F: FnOnce(AllowStd) -> Result> + Unpin, - S: Unpin, - AllowStd: Read + Write, -{ - type Output = Result, Error>; - - fn poll(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { - let inner = self.0.take().expect("future polled after completion"); - trace!("Setting ctx when starting handshake"); - let stream = AllowStd::new(inner.stream, ctx.waker()); - - match (inner.f)(stream) { - Ok(r) => Poll::Ready(Ok(StartedHandshake::Done(r))), - Err(Error::Interrupted(mid)) => Poll::Ready(Ok(StartedHandshake::Mid(mid))), - Err(Error::Failure(e)) => Poll::Ready(Err(Error::Failure(e))), - } - } -} - -#[cfg(feature = "handshake")] -impl Future for MidHandshake -where - Role: HandshakeRole + Unpin, - Role::InternalStream: SetWaker + Unpin, -{ - type Output = Result>; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let mut s = self.as_mut().0.take().expect("future polled after completion"); - - let machine = s.get_mut(); - trace!("Setting context in handshake"); - machine.get_mut().set_waker(cx.waker()); - - match s.handshake() { - Ok(stream) => Poll::Ready(Ok(stream)), - Err(Error::Failure(e)) => Poll::Ready(Err(Error::Failure(e))), - Err(Error::Interrupted(mid)) => { - self.0 = Some(mid); - Poll::Pending - } - } - } -} diff --git a/nautilus_core/network/tokio-tungstenite/src/lib.rs b/nautilus_core/network/tokio-tungstenite/src/lib.rs deleted file mode 100644 index 7f9cf96cc142..000000000000 --- a/nautilus_core/network/tokio-tungstenite/src/lib.rs +++ /dev/null @@ -1,423 +0,0 @@ -#![allow(clippy::all)] - -//! Async `WebSocket` usage. -//! -//! This library is an implementation of `WebSocket` handshakes and streams. It -//! is based on the crate which implements all required `WebSocket` protocol -//! logic. So this crate basically just brings tokio support / tokio integration -//! to it. -//! -//! Each `WebSocket` stream implements the required `Stream` and `Sink` traits, -//! so the socket is just a stream of messages coming in and going out. - -#![deny(missing_docs, unused_must_use, unused_mut, unused_imports, unused_import_braces)] - -pub use tungstenite; - -mod compat; -#[cfg(feature = "connect")] -mod connect; -mod handshake; -#[cfg(feature = "stream")] -mod stream; -#[cfg(any(feature = "native-tls", feature = "__rustls-tls", feature = "connect"))] -pub mod tls; - -use std::io::{Read, Write}; - -use compat::{cvt, AllowStd, ContextWaker}; -use futures_util::{ - sink::{Sink, SinkExt}, - stream::{FusedStream, Stream}, -}; -use log::{debug, trace}; -use std::{ - pin::Pin, - task::{Context, Poll}, -}; -use tokio::io::{AsyncRead, AsyncWrite}; - -#[cfg(feature = "handshake")] -use tungstenite::{ - client::IntoClientRequest, - handshake::{ - client::{ClientHandshake, Response}, - server::{Callback, NoCallback}, - HandshakeError, - }, -}; -use tungstenite::{ - error::Error as WsError, - protocol::{Message, Role, WebSocket, WebSocketConfig}, -}; - -#[cfg(any(feature = "native-tls", feature = "__rustls-tls", feature = "connect"))] -pub use tls::Connector; -#[cfg(any(feature = "native-tls", feature = "__rustls-tls"))] -pub use tls::{client_async_tls, client_async_tls_with_config}; - -#[cfg(feature = "connect")] -pub use connect::{connect_async, connect_async_with_config}; - -#[cfg(all(any(feature = "native-tls", feature = "__rustls-tls"), feature = "connect"))] -pub use connect::connect_async_tls_with_config; - -#[cfg(feature = "stream")] -pub use stream::MaybeTlsStream; - -use tungstenite::protocol::CloseFrame; - -/// Creates a `WebSocket` handshake from a request and a stream. -/// For convenience, the user may call this with a url string, a URL, -/// or a `Request`. Calling with `Request` allows the user to add -/// a `WebSocket` protocol or other custom headers. -/// -/// Internally, this custom creates a handshake representation and returns -/// a future representing the resolution of the `WebSocket` handshake. The -/// returned future will resolve to either `WebSocketStream` or `Error` -/// depending on whether the handshake is successful. -/// -/// This is typically used for clients who have already established, for -/// example, a TCP connection to the remote server. -#[cfg(feature = "handshake")] -pub async fn client_async<'a, R, S>( - request: R, - stream: S, -) -> Result<(WebSocketStream, Response), WsError> -where - R: IntoClientRequest + Unpin, - S: AsyncRead + AsyncWrite + Unpin, -{ - client_async_with_config(request, stream, None).await -} - -/// The same as `client_async()` but the one can specify a websocket configuration. -/// Please refer to `client_async()` for more details. -#[cfg(feature = "handshake")] -pub async fn client_async_with_config<'a, R, S>( - request: R, - stream: S, - config: Option, -) -> Result<(WebSocketStream, Response), WsError> -where - R: IntoClientRequest + Unpin, - S: AsyncRead + AsyncWrite + Unpin, -{ - let f = handshake::client_handshake(stream, move |allow_std| { - let request = request.into_client_request()?; - let cli_handshake = ClientHandshake::start(allow_std, request, config)?; - cli_handshake.handshake() - }); - f.await.map_err(|e| match e { - HandshakeError::Failure(e) => e, - e => WsError::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())), - }) -} - -/// Accepts a new `WebSocket` connection with the provided stream. -/// -/// This function will internally call `server::accept` to create a -/// handshake representation and returns a future representing the -/// resolution of the `WebSocket` handshake. The returned future will resolve -/// to either `WebSocketStream` or `Error` depending if it's successful -/// or not. -/// -/// This is typically used after a socket has been accepted from a -/// `TcpListener`. That socket is then passed to this function to perform -/// the server half of the accepting a client's websocket connection. -#[cfg(feature = "handshake")] -pub async fn accept_async(stream: S) -> Result, WsError> -where - S: AsyncRead + AsyncWrite + Unpin, -{ - accept_hdr_async(stream, NoCallback).await -} - -/// The same as `accept_async()` but the one can specify a websocket configuration. -/// Please refer to `accept_async()` for more details. -#[cfg(feature = "handshake")] -pub async fn accept_async_with_config( - stream: S, - config: Option, -) -> Result, WsError> -where - S: AsyncRead + AsyncWrite + Unpin, -{ - accept_hdr_async_with_config(stream, NoCallback, config).await -} - -/// Accepts a new `WebSocket` connection with the provided stream. -/// -/// This function does the same as `accept_async()` but accepts an extra callback -/// for header processing. The callback receives headers of the incoming -/// requests and is able to add extra headers to the reply. -#[cfg(feature = "handshake")] -pub async fn accept_hdr_async(stream: S, callback: C) -> Result, WsError> -where - S: AsyncRead + AsyncWrite + Unpin, - C: Callback + Unpin, -{ - accept_hdr_async_with_config(stream, callback, None).await -} - -/// The same as `accept_hdr_async()` but the one can specify a websocket configuration. -/// Please refer to `accept_hdr_async()` for more details. -#[cfg(feature = "handshake")] -pub async fn accept_hdr_async_with_config( - stream: S, - callback: C, - config: Option, -) -> Result, WsError> -where - S: AsyncRead + AsyncWrite + Unpin, - C: Callback + Unpin, -{ - let f = handshake::server_handshake(stream, move |allow_std| { - tungstenite::accept_hdr_with_config(allow_std, callback, config) - }); - f.await.map_err(|e| match e { - HandshakeError::Failure(e) => e, - e => WsError::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())), - }) -} - -/// A wrapper around an underlying raw stream which implements the `WebSocket` -/// protocol. -/// -/// A `WebSocketStream` represents a handshake that has been completed -/// successfully and both the server and the client are ready for receiving -/// and sending data. Message from a `WebSocketStream` are accessible -/// through the respective `Stream` and `Sink`. Check more information about -/// them in `futures-rs` crate documentation or have a look on the examples -/// and unit tests for this crate. -#[derive(Debug)] -pub struct WebSocketStream { - inner: WebSocket>, - closing: bool, - ended: bool, -} - -impl WebSocketStream { - /// Convert a raw socket into a `WebSocketStream` without performing a - /// handshake. - pub async fn from_raw_socket(stream: S, role: Role, config: Option) -> Self - where - S: AsyncRead + AsyncWrite + Unpin, - { - handshake::without_handshake(stream, move |allow_std| { - WebSocket::from_raw_socket(allow_std, role, config) - }) - .await - } - - /// Convert a raw socket into a `WebSocketStream` without performing a - /// handshake. - pub async fn from_partially_read( - stream: S, - part: Vec, - role: Role, - config: Option, - ) -> Self - where - S: AsyncRead + AsyncWrite + Unpin, - { - handshake::without_handshake(stream, move |allow_std| { - WebSocket::from_partially_read(allow_std, part, role, config) - }) - .await - } - - pub(crate) const fn new(ws: WebSocket>) -> Self { - Self { inner: ws, closing: false, ended: false } - } - - fn with_context(&mut self, ctx: Option<(ContextWaker, &mut Context<'_>)>, f: F) -> R - where - S: Unpin, - F: FnOnce(&mut WebSocket>) -> R, - AllowStd: Read + Write, - { - trace!("{}:{} WebSocketStream.with_context", file!(), line!()); - if let Some((kind, ctx)) = ctx { - self.inner.get_mut().set_waker(kind, ctx.waker()); - } - f(&mut self.inner) - } - - /// Returns a shared reference to the inner stream. - pub fn get_ref(&self) -> &S - where - S: AsyncRead + AsyncWrite + Unpin, - { - self.inner.get_ref().get_ref() - } - - /// Returns a mutable reference to the inner stream. - pub fn get_mut(&mut self) -> &mut S - where - S: AsyncRead + AsyncWrite + Unpin, - { - self.inner.get_mut().get_mut() - } - - /// Returns a reference to the configuration of the tungstenite stream. - pub fn get_config(&self) -> &WebSocketConfig { - self.inner.get_config() - } - - /// Close the underlying web socket - pub async fn close(&mut self, msg: Option>) -> Result<(), WsError> - where - S: AsyncRead + AsyncWrite + Unpin, - { - let msg = msg.map(tungstenite::protocol::CloseFrame::into_owned); - self.send(Message::Close(msg)).await - } -} - -impl Stream for WebSocketStream -where - T: AsyncRead + AsyncWrite + Unpin, -{ - type Item = Result; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - trace!("{}:{} Stream.poll_next", file!(), line!()); - - // The connection has been closed or a critical error has occurred. - // We have already returned the error to the user, the `Stream` is unusable, - // so we assume that the stream has been "fused". - if self.ended { - return Poll::Ready(None); - } - - match futures_util::ready!(self.with_context(Some((ContextWaker::Read, cx)), |s| { - trace!("{}:{} Stream.with_context poll_next -> read()", file!(), line!()); - cvt(s.read()) - })) { - Ok(v) => Poll::Ready(Some(Ok(v))), - Err(e) => { - self.ended = true; - if matches!(e, WsError::AlreadyClosed | WsError::ConnectionClosed) { - Poll::Ready(None) - } else { - Poll::Ready(Some(Err(e))) - } - } - } - } -} - -impl FusedStream for WebSocketStream -where - T: AsyncRead + AsyncWrite + Unpin, -{ - fn is_terminated(&self) -> bool { - self.ended - } -} - -impl Sink for WebSocketStream -where - T: AsyncRead + AsyncWrite + Unpin, -{ - type Error = WsError; - - fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { - match (*self).with_context(None, |s| s.write(item)) { - Ok(()) => Ok(()), - Err(WsError::Io(err)) if err.kind() == std::io::ErrorKind::WouldBlock => { - // the message was accepted and queued - // isn't an error. - Ok(()) - } - Err(e) => { - debug!("websocket start_send error: {e}"); - Err(e) - } - } - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - (*self).with_context(Some((ContextWaker::Write, cx)), |s| cvt(s.flush())).map(|r| { - // WebSocket connection has just been closed. Flushing completed, not an error. - match r { - Err(WsError::ConnectionClosed) => Ok(()), - other => other, - } - }) - } - - fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let res = if self.closing { - // After queueing it, we call `flush` to drive the close handshake to completion. - (*self).with_context(Some((ContextWaker::Write, cx)), tungstenite::WebSocket::flush) - } else { - (*self).with_context(Some((ContextWaker::Write, cx)), |s| s.close(None)) - }; - - match res { - Ok(()) => Poll::Ready(Ok(())), - Err(WsError::ConnectionClosed) => Poll::Ready(Ok(())), - Err(WsError::Io(err)) if err.kind() == std::io::ErrorKind::WouldBlock => { - trace!("WouldBlock"); - self.closing = true; - Poll::Pending - } - Err(e) => { - debug!("websocket close error: {e}"); - Poll::Ready(Err(e)) - } - } - } -} - -/// Get a domain from an URL. -#[cfg(any(feature = "connect", feature = "native-tls", feature = "__rustls-tls"))] -#[inline] -fn domain(request: &tungstenite::handshake::client::Request) -> Result { - match request.uri().host() { - // rustls expects IPv6 addresses without the surrounding [] brackets - #[cfg(feature = "__rustls-tls")] - Some(d) if d.starts_with('[') && d.ends_with(']') => Ok(d[1..d.len() - 1].to_string()), - Some(d) => Ok(d.to_string()), - None => Err(WsError::Url(tungstenite::error::UrlError::NoHostName)), - } -} - -#[cfg(test)] -mod tests { - #[cfg(feature = "connect")] - use crate::stream::MaybeTlsStream; - use crate::{compat::AllowStd, WebSocketStream}; - use std::io::{Read, Write}; - #[cfg(feature = "connect")] - use tokio::io::{AsyncReadExt, AsyncWriteExt}; - - const fn is_read() {} - const fn is_write() {} - #[cfg(feature = "connect")] - const fn is_async_read() {} - #[cfg(feature = "connect")] - const fn is_async_write() {} - const fn is_unpin() {} - - #[test] - const fn web_socket_stream_has_traits() { - is_read::>(); - is_write::>(); - - #[cfg(feature = "connect")] - is_async_read::>(); - #[cfg(feature = "connect")] - is_async_write::>(); - - is_unpin::>(); - #[cfg(feature = "connect")] - is_unpin::>>(); - } -} diff --git a/nautilus_core/network/tokio-tungstenite/src/stream.rs b/nautilus_core/network/tokio-tungstenite/src/stream.rs deleted file mode 100644 index 6e725e928153..000000000000 --- a/nautilus_core/network/tokio-tungstenite/src/stream.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Convenience wrapper for streams to switch between plain TCP and TLS at runtime. -//! -//! There is no dependency on actual TLS implementations. Everything like -//! `native_tls` or `openssl` will work as long as there is a TLS stream supporting standard -//! `Read + Write` traits. -use std::{ - pin::Pin, - task::{Context, Poll}, -}; - -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; - -/// A stream that might be protected with TLS. -#[non_exhaustive] -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub enum MaybeTlsStream { - /// Unencrypted socket stream. - Plain(S), - /// Encrypted socket stream using `native-tls`. - #[cfg(feature = "native-tls")] - NativeTls(tokio_native_tls::TlsStream), - /// Encrypted socket stream using `rustls`. - #[cfg(feature = "__rustls-tls")] - Rustls(tokio_rustls::client::TlsStream), -} - -impl AsyncRead for MaybeTlsStream { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - match self.get_mut() { - Self::Plain(ref mut s) => Pin::new(s).poll_read(cx, buf), - #[cfg(feature = "native-tls")] - Self::NativeTls(s) => Pin::new(s).poll_read(cx, buf), - #[cfg(feature = "__rustls-tls")] - Self::Rustls(s) => Pin::new(s).poll_read(cx, buf), - } - } -} - -impl AsyncWrite for MaybeTlsStream { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - match self.get_mut() { - Self::Plain(ref mut s) => Pin::new(s).poll_write(cx, buf), - #[cfg(feature = "native-tls")] - Self::NativeTls(s) => Pin::new(s).poll_write(cx, buf), - #[cfg(feature = "__rustls-tls")] - Self::Rustls(s) => Pin::new(s).poll_write(cx, buf), - } - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.get_mut() { - Self::Plain(ref mut s) => Pin::new(s).poll_flush(cx), - #[cfg(feature = "native-tls")] - Self::NativeTls(s) => Pin::new(s).poll_flush(cx), - #[cfg(feature = "__rustls-tls")] - Self::Rustls(s) => Pin::new(s).poll_flush(cx), - } - } - - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - match self.get_mut() { - Self::Plain(ref mut s) => Pin::new(s).poll_shutdown(cx), - #[cfg(feature = "native-tls")] - Self::NativeTls(s) => Pin::new(s).poll_shutdown(cx), - #[cfg(feature = "__rustls-tls")] - Self::Rustls(s) => Pin::new(s).poll_shutdown(cx), - } - } -} diff --git a/nautilus_core/network/tokio-tungstenite/src/tls.rs b/nautilus_core/network/tokio-tungstenite/src/tls.rs deleted file mode 100644 index 5190e62fd22b..000000000000 --- a/nautilus_core/network/tokio-tungstenite/src/tls.rs +++ /dev/null @@ -1,243 +0,0 @@ -//! Connection helper. -use tokio::io::{AsyncRead, AsyncWrite}; - -use tungstenite::{ - client::uri_mode, - error::Error, - handshake::client::{Request, Response}, - protocol::WebSocketConfig, - stream::Mode, -}; - -use crate::{client_async_with_config, IntoClientRequest, WebSocketStream}; - -pub use crate::stream::MaybeTlsStream; - -/// A connector that can be used when establishing connections, allowing to control whether -/// `native-tls` or `rustls` is used to create a TLS connection. Or TLS can be disabled with the -/// `Plain` variant. -#[non_exhaustive] -#[derive(Clone)] -pub enum Connector { - /// Plain (non-TLS) connector. - Plain, - /// `native-tls` TLS connector. - #[cfg(feature = "native-tls")] - NativeTls(native_tls_crate::TlsConnector), - /// `rustls` TLS connector. - #[cfg(feature = "__rustls-tls")] - Rustls(std::sync::Arc), -} - -/// Encrypt a stream usin Tls -pub mod encryption { - /// Use native-tls implementaiton to encrypt - #[cfg(feature = "native-tls")] - pub mod native_tls { - use native_tls_crate::TlsConnector; - use tokio_native_tls::TlsConnector as TokioTlsConnector; - - use tokio::io::{AsyncRead, AsyncWrite}; - - use tungstenite::{error::TlsError, stream::Mode, Error}; - - use crate::stream::MaybeTlsStream; - - /// Wraps the stream with tls encryption - pub async fn wrap_stream( - socket: S, - domain: String, - mode: Mode, - tls_connector: Option, - ) -> Result, Error> - where - S: 'static + AsyncRead + AsyncWrite + Send + Unpin, - { - match mode { - Mode::Plain => Ok(MaybeTlsStream::Plain(socket)), - Mode::Tls => { - let try_connector = tls_connector.map_or_else(TlsConnector::new, Ok); - let connector = try_connector.map_err(TlsError::Native)?; - let stream = TokioTlsConnector::from(connector); - let connected = stream.connect(&domain, socket).await; - match connected { - Err(e) => Err(Error::Tls(e.into())), - Ok(s) => Ok(MaybeTlsStream::NativeTls(s)), - } - } - } - } - } - - /// Use rust-tls implementation to encrypt - pub mod rustls { - pub use rustls::ClientConfig; - use rustls::RootCertStore; - use rustls_pki_types::ServerName; - use tokio_rustls::TlsConnector as TokioTlsConnector; - - use std::{convert::TryFrom, sync::Arc}; - use tokio::io::{AsyncRead, AsyncWrite}; - - use tungstenite::{error::TlsError, stream::Mode, Error}; - - use crate::stream::MaybeTlsStream; - - /// Wraps the stream with rust-tls encryption - pub async fn wrap_stream( - socket: S, - domain: String, - mode: Mode, - tls_connector: Option>, - ) -> Result, Error> - where - S: 'static + AsyncRead + AsyncWrite + Send + Unpin, - { - match mode { - Mode::Plain => Ok(MaybeTlsStream::Plain(socket)), - Mode::Tls => { - let config = if let Some(config) = tls_connector { - config - } else { - #[allow(unused_mut)] - let mut root_store = RootCertStore::empty(); - #[cfg(feature = "rustls-tls-native-roots")] - { - let native_certs = rustls_native_certs::load_native_certs()?; - let total_number = native_certs.len(); - let (number_added, number_ignored) = - root_store.add_parsable_certificates(native_certs); - log::debug!("Added {number_added}/{total_number} native root certificates (ignored {number_ignored})"); - } - #[cfg(feature = "rustls-tls-webpki-roots")] - { - root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - } - - Arc::new( - ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(), - ) - }; - let domain = ServerName::try_from(domain.as_str()) - .map_err(|_| TlsError::InvalidDnsName)? - .to_owned(); - let stream = TokioTlsConnector::from(config); - let connected = stream.connect(domain, socket).await; - - match connected { - Err(e) => Err(Error::Io(e)), - Ok(s) => Ok(MaybeTlsStream::Rustls(s)), - } - } - } - } - } - - /// Does not encrypt stream - pub mod plain { - use tokio::io::{AsyncRead, AsyncWrite}; - - use tungstenite::{ - error::{Error, UrlError}, - stream::Mode, - }; - - use crate::stream::MaybeTlsStream; - - /// Keeps the stream unwrapped - pub async fn wrap_stream(socket: S, mode: Mode) -> Result, Error> - where - S: 'static + AsyncRead + AsyncWrite + Send + Unpin, - { - match mode { - Mode::Plain => Ok(MaybeTlsStream::Plain(socket)), - Mode::Tls => Err(Error::Url(UrlError::TlsFeatureNotEnabled)), - } - } - } -} - -/// Creates a `WebSocket` handshake from a request and a stream, -/// upgrading the stream to TLS if required. -#[cfg(any(feature = "native-tls", feature = "__rustls-tls"))] -pub async fn client_async_tls( - request: R, - stream: S, -) -> Result<(WebSocketStream>, Response), Error> -where - R: IntoClientRequest + Unpin, - S: 'static + AsyncRead + AsyncWrite + Send + Unpin, - MaybeTlsStream: Unpin, -{ - client_async_tls_with_config(request, stream, None, None).await -} - -/// Given a domain and mode -pub async fn tcp_tls( - request: &Request, - mode: Mode, - stream: S, - connector: Option, -) -> Result, Error> -where - S: 'static + AsyncRead + AsyncWrite + Send + Unpin, - MaybeTlsStream: Unpin, -{ - #[cfg(any(feature = "native-tls", feature = "__rustls-tls"))] - let domain = crate::domain(request)?; - - match connector { - Some(conn) => match conn { - #[cfg(feature = "native-tls")] - Connector::NativeTls(conn) => { - self::encryption::native_tls::wrap_stream(stream, domain, mode, Some(conn)).await - } - #[cfg(feature = "__rustls-tls")] - Connector::Rustls(conn) => { - self::encryption::rustls::wrap_stream(stream, domain, mode, Some(conn)).await - } - Connector::Plain => self::encryption::plain::wrap_stream(stream, mode).await, - }, - None => { - #[cfg(feature = "native-tls")] - { - self::encryption::native_tls::wrap_stream(stream, domain, mode, None).await - } - #[cfg(all(feature = "__rustls-tls", not(feature = "native-tls")))] - { - self::encryption::rustls::wrap_stream(stream, domain, mode, None).await - } - #[cfg(not(any(feature = "native-tls", feature = "__rustls-tls")))] - { - self::encryption::plain::wrap_stream(stream, mode).await - } - } - } -} - -/// The same as `client_async_tls()` but the one can specify a websocket configuration, -/// and an optional connector. If no connector is specified, a default one will -/// be created. -/// -/// Please refer to `client_async_tls()` for more details. -pub async fn client_async_tls_with_config( - request: R, - stream: S, - config: Option, - connector: Option, -) -> Result<(WebSocketStream>, Response), Error> -where - R: IntoClientRequest + Unpin, - S: 'static + AsyncRead + AsyncWrite + Send + Unpin, - MaybeTlsStream: Unpin, -{ - let request = request.into_client_request()?; - - // Make sure we check domain and mode first. URL must be valid. - let mode = uri_mode(request.uri())?; - - let stream = tcp_tls(&request, mode, stream, connector).await?; - client_async_with_config(request, stream, config).await -} diff --git a/nautilus_core/pyo3/src/lib.rs b/nautilus_core/pyo3/src/lib.rs index a3cf4e25663d..0a41d9c407d9 100644 --- a/nautilus_core/pyo3/src/lib.rs +++ b/nautilus_core/pyo3/src/lib.rs @@ -65,6 +65,12 @@ fn nautilus_pyo3(py: Python<'_>, m: &PyModule) -> PyResult<()> { sys_modules.set_item(format!("{module_name}.{n}"), m.getattr(n)?)?; re_export_module_attributes(m, n)?; + let n = "crypto"; + let submodule = pyo3::wrap_pymodule!(nautilus_adapters::crypto::python::crypto); + m.add_wrapped(submodule)?; + sys_modules.set_item(format!("{module_name}.{n}"), m.getattr(n)?)?; + re_export_module_attributes(m, n)?; + let n = "model"; let submodule = pyo3::wrap_pymodule!(nautilus_model::python::model); m.add_wrapped(submodule)?; diff --git a/nautilus_trader/adapters/binance/common/credentials.py b/nautilus_trader/adapters/binance/common/credentials.py index 549c87cefc96..17f6661a0380 100644 --- a/nautilus_trader/adapters/binance/common/credentials.py +++ b/nautilus_trader/adapters/binance/common/credentials.py @@ -41,3 +41,29 @@ def get_api_secret(account_type: BinanceAccountType, is_testnet: bool) -> str: return get_env_key("BINANCE_API_SECRET") else: return get_env_key("BINANCE_FUTURES_API_SECRET") + + +def get_rsa_private_key(account_type: BinanceAccountType, is_testnet: bool) -> str: + if is_testnet: + if account_type.is_spot_or_margin: + return get_env_key("BINANCE_TESTNET_RSA_PK") + else: + return get_env_key("BINANCE_FUTURES_TESTNET_RSA_PK") + + if account_type.is_spot_or_margin: + return get_env_key("BINANCE_RSA_PK") + else: + return get_env_key("BINANCE_FUTURES_RSA_PK") + + +def get_ed25519_private_key(account_type: BinanceAccountType, is_testnet: bool) -> str: + if is_testnet: + if account_type.is_spot_or_margin: + return get_env_key("BINANCE_TESTNET_ED25519_PK") + else: + return get_env_key("BINANCE_FUTURES_TESTNET_ED25519_PK") + + if account_type.is_spot_or_margin: + return get_env_key("BINANCE_ED25519_PK") + else: + return get_env_key("BINANCE_FUTURES_ED25519_PK") diff --git a/nautilus_trader/adapters/binance/common/enums.py b/nautilus_trader/adapters/binance/common/enums.py index ce0f4e2b3180..c95f837134a6 100644 --- a/nautilus_trader/adapters/binance/common/enums.py +++ b/nautilus_trader/adapters/binance/common/enums.py @@ -39,10 +39,21 @@ from nautilus_trader.model.orders import Order +@unique +class BinanceKeyType(Enum): + """ + Represents a Binance private key cryptographic algorithm type. + """ + + HMAC = "HMAC" + RSA = "RSA" + ED25519 = "Ed25519" + + @unique class BinanceFuturesPositionSide(Enum): """ - Represents a `Binance Futures` position side. + Represents a Binance Futures position side. """ BOTH = "BOTH" diff --git a/nautilus_trader/adapters/binance/common/schemas/account.py b/nautilus_trader/adapters/binance/common/schemas/account.py index 6f867d539958..21a5a8eba36a 100644 --- a/nautilus_trader/adapters/binance/common/schemas/account.py +++ b/nautilus_trader/adapters/binance/common/schemas/account.py @@ -52,9 +52,9 @@ class BinanceUserTrade(msgspec.Struct, frozen=True): """ - HTTP response from `Binance Spot/Margin` `GET /api/v3/myTrades` HTTP response from - `Binance USD-M Futures` `GET /fapi/v1/userTrades` HTTP response from `Binance COIN-M - Futures` `GET /dapi/v1/userTrades` + HTTP response from Binance Spot/Margin `GET /api/v3/myTrades` HTTP response from + Binance USD-M Futures `GET /fapi/v1/userTrades` HTTP response from Binance COIN-M + Futures `GET /dapi/v1/userTrades`. """ commission: str @@ -119,9 +119,9 @@ def parse_to_fill_report( class BinanceOrder(msgspec.Struct, frozen=True): """ - HTTP response from `Binance Spot/Margin` `GET /api/v3/order` HTTP response from - `Binance USD-M Futures` `GET /fapi/v1/order` HTTP response from `Binance COIN-M - Futures` `GET /dapi/v1/order` + HTTP response from Binance Spot/Margin `GET /api/v3/order` HTTP response from + Binance USD-M Futures `GET /fapi/v1/order` HTTP response from Binance COIN-M Futures + `GET /dapi/v1/order`. """ symbol: str diff --git a/nautilus_trader/adapters/binance/common/types.py b/nautilus_trader/adapters/binance/common/types.py index afda2cbf96a6..9c62bdfd2faf 100644 --- a/nautilus_trader/adapters/binance/common/types.py +++ b/nautilus_trader/adapters/binance/common/types.py @@ -370,7 +370,7 @@ def ts_init(self) -> int: @staticmethod def from_dict(values: dict[str, Any]) -> BinanceTicker: """ - Return a `Binance Spot/Margin` ticker parsed from the given values. + Return a Binance Spot/Margin ticker parsed from the given values. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/config.py b/nautilus_trader/adapters/binance/config.py index 168caf548cf0..d2cecff16838 100644 --- a/nautilus_trader/adapters/binance/config.py +++ b/nautilus_trader/adapters/binance/config.py @@ -15,6 +15,7 @@ from nautilus_trader.adapters.binance.common.constants import BINANCE_VENUE from nautilus_trader.adapters.binance.common.enums import BinanceAccountType +from nautilus_trader.adapters.binance.common.enums import BinanceKeyType from nautilus_trader.config import LiveDataClientConfig from nautilus_trader.config import LiveExecClientConfig from nautilus_trader.config import PositiveFloat @@ -38,6 +39,8 @@ class BinanceDataClientConfig(LiveDataClientConfig, frozen=True): The Binance API public key. If ``None`` then will source the `BINANCE_API_SECRET` or `BINANCE_TESTNET_API_SECRET` environment variables. + key_type : BinanceKeyType, default 'HMAC' + The private key cryptographic algorithm type. account_type : BinanceAccountType, default BinanceAccountType.SPOT The account type for the client. base_url_http : str, optional @@ -57,6 +60,7 @@ class BinanceDataClientConfig(LiveDataClientConfig, frozen=True): venue: Venue = BINANCE_VENUE api_key: str | None = None api_secret: str | None = None + key_type: BinanceKeyType = BinanceKeyType.HMAC account_type: BinanceAccountType = BinanceAccountType.SPOT base_url_http: str | None = None base_url_ws: str | None = None @@ -81,6 +85,8 @@ class BinanceExecClientConfig(LiveExecClientConfig, frozen=True): The Binance API public key. If ``None`` then will source the `BINANCE_API_KEY` or `BINANCE_TESTNET_API_KEY` environment variables. + key_type : BinanceKeyType, default 'HMAC' + The private key cryptographic algorithm type. account_type : BinanceAccountType, default BinanceAccountType.SPOT The account type for the client. base_url_http : str, optional @@ -122,6 +128,7 @@ class BinanceExecClientConfig(LiveExecClientConfig, frozen=True): venue: Venue = BINANCE_VENUE api_key: str | None = None api_secret: str | None = None + key_type: BinanceKeyType = BinanceKeyType.HMAC account_type: BinanceAccountType = BinanceAccountType.SPOT base_url_http: str | None = None base_url_ws: str | None = None diff --git a/nautilus_trader/adapters/binance/common/data.py b/nautilus_trader/adapters/binance/data.py similarity index 99% rename from nautilus_trader/adapters/binance/common/data.py rename to nautilus_trader/adapters/binance/data.py index 00da3f6682b6..4e099b930010 100644 --- a/nautilus_trader/adapters/binance/common/data.py +++ b/nautilus_trader/adapters/binance/data.py @@ -141,6 +141,7 @@ def __init__( # Configuration self._binance_account_type = account_type self._use_agg_trade_ticks = config.use_agg_trade_ticks + self._log.info(f"Key type: {config.key_type.value}", LogColor.BLUE) self._log.info(f"Account type: {self._binance_account_type.value}", LogColor.BLUE) self._log.info(f"{config.use_agg_trade_ticks=}", LogColor.BLUE) diff --git a/nautilus_trader/adapters/binance/common/execution.py b/nautilus_trader/adapters/binance/execution.py similarity index 99% rename from nautilus_trader/adapters/binance/common/execution.py rename to nautilus_trader/adapters/binance/execution.py index 4341ec75ec38..1d39726db26a 100644 --- a/nautilus_trader/adapters/binance/common/execution.py +++ b/nautilus_trader/adapters/binance/execution.py @@ -167,6 +167,7 @@ def __init__( self._recv_window = config.recv_window_ms self._max_retries: int = config.max_retries or 0 self._retry_delay: float = config.retry_delay or 1.0 + self._log.info(f"Key type: {config.key_type.value}", LogColor.BLUE) self._log.info(f"Account type: {self._binance_account_type.value}", LogColor.BLUE) self._log.info(f"{config.use_gtd=}", LogColor.BLUE) self._log.info(f"{config.use_reduce_only=}", LogColor.BLUE) diff --git a/nautilus_trader/adapters/binance/factories.py b/nautilus_trader/adapters/binance/factories.py index 3974f999bfe4..720a89c91c37 100644 --- a/nautilus_trader/adapters/binance/factories.py +++ b/nautilus_trader/adapters/binance/factories.py @@ -18,7 +18,10 @@ from nautilus_trader.adapters.binance.common.credentials import get_api_key from nautilus_trader.adapters.binance.common.credentials import get_api_secret +from nautilus_trader.adapters.binance.common.credentials import get_ed25519_private_key +from nautilus_trader.adapters.binance.common.credentials import get_rsa_private_key from nautilus_trader.adapters.binance.common.enums import BinanceAccountType +from nautilus_trader.adapters.binance.common.enums import BinanceKeyType from nautilus_trader.adapters.binance.common.urls import get_http_base_url from nautilus_trader.adapters.binance.common.urls import get_ws_base_url from nautilus_trader.adapters.binance.config import BinanceDataClientConfig @@ -47,8 +50,9 @@ def get_cached_binance_http_client( clock: LiveClock, account_type: BinanceAccountType, - key: str | None = None, - secret: str | None = None, + api_key: str | None = None, + api_secret: str | None = None, + key_type: BinanceKeyType = BinanceKeyType.HMAC, base_url: str | None = None, is_testnet: bool = False, is_us: bool = False, @@ -65,10 +69,12 @@ def get_cached_binance_http_client( The clock for the client. account_type : BinanceAccountType The account type for the client. - key : str, optional + api_key : str, optional The API key for the client. - secret : str, optional + api_secret : str, optional The API secret for the client. + key_type : BinanceKeyType, default 'HMAC' + The private key cryptographic algorithm type. base_url : str, optional The base URL for the API endpoints. is_testnet : bool, default False @@ -83,10 +89,23 @@ def get_cached_binance_http_client( """ global BINANCE_HTTP_CLIENTS - key = key or get_api_key(account_type, is_testnet) - secret = secret or get_api_secret(account_type, is_testnet) + api_key = api_key or get_api_key(account_type, is_testnet) + api_secret = api_secret or get_api_secret(account_type, is_testnet) default_http_base_url = get_http_base_url(account_type, is_testnet, is_us) + match key_type: + case BinanceKeyType.HMAC: + rsa_private_key = None + ed25519_private_key = None + case BinanceKeyType.RSA: + rsa_private_key = get_rsa_private_key(account_type, is_testnet) + ed25519_private_key = None + case BinanceKeyType.ED25519: + rsa_private_key = None + ed25519_private_key = get_ed25519_private_key(account_type, is_testnet) + case _: + raise ValueError(f"invalid `key_type`, was {key_type}") + # Set up rate limit quotas if account_type.is_spot: # Spot @@ -103,12 +122,15 @@ def get_cached_binance_http_client( ("allOrders", Quota.rate_per_minute(int(1200 / 20))), ] - client_key: str = "|".join((account_type.value, key, secret)) + client_key: str = "|".join((account_type.value, api_key, api_secret)) if client_key not in BINANCE_HTTP_CLIENTS: client = BinanceHttpClient( clock=clock, - key=key, - secret=secret, + api_key=api_key, + api_secret=api_secret, + key_type=key_type, + rsa_private_key=rsa_private_key, + ed25519_private_key=ed25519_private_key, base_url=base_url or default_http_base_url, ratelimiter_quotas=ratelimiter_quotas, ratelimiter_default_quota=ratelimiter_default_quota, @@ -127,7 +149,7 @@ def get_cached_binance_spot_instrument_provider( venue: Venue, ) -> BinanceSpotInstrumentProvider: """ - Cache and return an instrument provider for the `Binance Spot/Margin` exchange. + Cache and return an instrument provider for the Binance Spot/Margin exchange. If a cached provider already exists, then that provider will be returned. @@ -169,7 +191,7 @@ def get_cached_binance_futures_instrument_provider( venue: Venue, ) -> BinanceFuturesInstrumentProvider: """ - Cache and return an instrument provider for the `Binance Futures` exchange. + Cache and return an instrument provider for the Binance Futures exchange. If a cached provider already exists, then that provider will be returned. @@ -246,8 +268,9 @@ def create( # type: ignore client: BinanceHttpClient = get_cached_binance_http_client( clock=clock, account_type=config.account_type, - key=config.api_key, - secret=config.api_secret, + api_key=config.api_key, + api_secret=config.api_secret, + key_type=config.key_type, base_url=config.base_url_http, is_testnet=config.testnet, is_us=config.us, @@ -353,8 +376,9 @@ def create( # type: ignore client: BinanceHttpClient = get_cached_binance_http_client( clock=clock, account_type=config.account_type, - key=config.api_key, - secret=config.api_secret, + api_key=config.api_key, + api_secret=config.api_secret, + key_type=config.key_type, base_url=config.base_url_http, is_testnet=config.testnet, is_us=config.us, diff --git a/nautilus_trader/adapters/binance/futures/data.py b/nautilus_trader/adapters/binance/futures/data.py index 44f07ba1fe13..79fcce8f683e 100644 --- a/nautilus_trader/adapters/binance/futures/data.py +++ b/nautilus_trader/adapters/binance/futures/data.py @@ -17,9 +17,9 @@ import msgspec -from nautilus_trader.adapters.binance.common.data import BinanceCommonDataClient from nautilus_trader.adapters.binance.common.enums import BinanceAccountType from nautilus_trader.adapters.binance.config import BinanceDataClientConfig +from nautilus_trader.adapters.binance.data import BinanceCommonDataClient from nautilus_trader.adapters.binance.futures.enums import BinanceFuturesEnumParser from nautilus_trader.adapters.binance.futures.http.market import BinanceFuturesMarketHttpAPI from nautilus_trader.adapters.binance.futures.schemas.market import BinanceFuturesMarkPriceMsg @@ -41,7 +41,7 @@ class BinanceFuturesDataClient(BinanceCommonDataClient): """ - Provides a data client for the `Binance Futures` exchange. + Provides a data client for the Binance Futures exchange. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/futures/enums.py b/nautilus_trader/adapters/binance/futures/enums.py index 0071420eefcd..06e51d80e20a 100644 --- a/nautilus_trader/adapters/binance/futures/enums.py +++ b/nautilus_trader/adapters/binance/futures/enums.py @@ -38,7 +38,7 @@ @unique class BinanceFuturesContractType(Enum): """ - Represents a `Binance Futures` derivatives contract type. + Represents a Binance Futures derivatives contract type. """ PERPETUAL = "PERPETUAL" @@ -53,7 +53,7 @@ class BinanceFuturesContractType(Enum): @unique class BinanceFuturesContractStatus(Enum): """ - Represents a `Binance Futures` contract status. + Represents a Binance Futures contract status. """ PENDING_TRADING = "PENDING_TRADING" @@ -69,7 +69,7 @@ class BinanceFuturesContractStatus(Enum): @unique class BinanceFuturesWorkingType(Enum): """ - Represents a `Binance Futures` working type. + Represents a Binance Futures working type. """ MARK_PRICE = "MARK_PRICE" @@ -79,7 +79,7 @@ class BinanceFuturesWorkingType(Enum): @unique class BinanceFuturesMarginType(Enum): """ - Represents a `Binance Futures` margin type. + Represents a Binance Futures margin type. """ ISOLATED = "isolated" @@ -89,7 +89,7 @@ class BinanceFuturesMarginType(Enum): @unique class BinanceFuturesPositionUpdateReason(Enum): """ - Represents a `Binance Futures` position and balance update reason. + Represents a Binance Futures position and balance update reason. """ DEPOSIT = "DEPOSIT" @@ -114,7 +114,7 @@ class BinanceFuturesPositionUpdateReason(Enum): @unique class BinanceFuturesEventType(Enum): """ - Represents a `Binance Futures` event type. + Represents a Binance Futures event type. """ LISTEN_KEY_EXPIRED = "listenKeyExpired" @@ -122,6 +122,7 @@ class BinanceFuturesEventType(Enum): ACCOUNT_UPDATE = "ACCOUNT_UPDATE" ORDER_TRADE_UPDATE = "ORDER_TRADE_UPDATE" ACCOUNT_CONFIG_UPDATE = "ACCOUNT_CONFIG_UPDATE" + TRADE_LITE = "TRADE_LITE" class BinanceFuturesEnumParser(BinanceEnumParser): diff --git a/nautilus_trader/adapters/binance/futures/execution.py b/nautilus_trader/adapters/binance/futures/execution.py index 979d8e9c9b05..76f5ef1417b3 100644 --- a/nautilus_trader/adapters/binance/futures/execution.py +++ b/nautilus_trader/adapters/binance/futures/execution.py @@ -21,8 +21,8 @@ from nautilus_trader.accounting.accounts.margin import MarginAccount from nautilus_trader.adapters.binance.common.enums import BinanceAccountType from nautilus_trader.adapters.binance.common.enums import BinanceErrorCode -from nautilus_trader.adapters.binance.common.execution import BinanceCommonExecutionClient from nautilus_trader.adapters.binance.config import BinanceExecClientConfig +from nautilus_trader.adapters.binance.execution import BinanceCommonExecutionClient from nautilus_trader.adapters.binance.futures.enums import BinanceFuturesEnumParser from nautilus_trader.adapters.binance.futures.enums import BinanceFuturesEventType from nautilus_trader.adapters.binance.futures.http.account import BinanceFuturesAccountHttpAPI @@ -55,7 +55,7 @@ class BinanceFuturesExecutionClient(BinanceCommonExecutionClient): """ - Provides an execution client for the `Binance Futures` exchange. + Provides an execution client for the Binance Futures exchange. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/futures/http/account.py b/nautilus_trader/adapters/binance/futures/http/account.py index 9c155e617ebc..ea2c5c5c1a61 100644 --- a/nautilus_trader/adapters/binance/futures/http/account.py +++ b/nautilus_trader/adapters/binance/futures/http/account.py @@ -338,7 +338,7 @@ async def get(self, params: GetParameters) -> list[BinanceFuturesPositionRisk]: class BinanceFuturesAccountHttpAPI(BinanceAccountHttpAPI): """ - Provides access to the `Binance Futures` Account/Trade HTTP REST API. + Provides access to the Binance Futures Account/Trade HTTP REST API. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/futures/http/market.py b/nautilus_trader/adapters/binance/futures/http/market.py index bfa5ef9d399e..193eedb6bca0 100644 --- a/nautilus_trader/adapters/binance/futures/http/market.py +++ b/nautilus_trader/adapters/binance/futures/http/market.py @@ -62,7 +62,7 @@ async def get(self) -> BinanceFuturesExchangeInfo: class BinanceFuturesMarketHttpAPI(BinanceMarketHttpAPI): """ - Provides access to the `Binance Futures` HTTP REST API. + Provides access to the Binance Futures HTTP REST API. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/futures/http/user.py b/nautilus_trader/adapters/binance/futures/http/user.py index 6eb3f60b80e9..f35429393d48 100644 --- a/nautilus_trader/adapters/binance/futures/http/user.py +++ b/nautilus_trader/adapters/binance/futures/http/user.py @@ -20,7 +20,7 @@ class BinanceFuturesUserDataHttpAPI(BinanceUserDataHttpAPI): """ - Provides access to the `Binance Futures` User Data HTTP REST API. + Provides access to the Binance Futures User Data HTTP REST API. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/futures/http/wallet.py b/nautilus_trader/adapters/binance/futures/http/wallet.py index 967faec389b9..37362c8e1199 100644 --- a/nautilus_trader/adapters/binance/futures/http/wallet.py +++ b/nautilus_trader/adapters/binance/futures/http/wallet.py @@ -81,7 +81,7 @@ async def get(self, params: GetParameters) -> BinanceFuturesCommissionRate: class BinanceFuturesWalletHttpAPI: """ - Provides access to the `Binance Futures` Wallet HTTP REST API. + Provides access to the Binance Futures Wallet HTTP REST API. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/futures/providers.py b/nautilus_trader/adapters/binance/futures/providers.py index ba42784e7a1c..a8a5e915f7d6 100644 --- a/nautilus_trader/adapters/binance/futures/providers.py +++ b/nautilus_trader/adapters/binance/futures/providers.py @@ -54,7 +54,7 @@ class BinanceFuturesInstrumentProvider(InstrumentProvider): """ - Provides a means of loading instruments from the `Binance Futures` exchange. + Provides a means of loading instruments from the Binance Futures exchange. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/futures/schemas/account.py b/nautilus_trader/adapters/binance/futures/schemas/account.py index 777a264b0f58..3ef879bd8701 100644 --- a/nautilus_trader/adapters/binance/futures/schemas/account.py +++ b/nautilus_trader/adapters/binance/futures/schemas/account.py @@ -37,7 +37,7 @@ class BinanceFuturesBalanceInfo(msgspec.Struct, frozen=True): """ - HTTP response 'inner struct' from `Binance Futures` GET /fapi/v2/account (HMAC + HTTP response 'inner struct' from Binance Futures GET /fapi/v2/account (HMAC SHA256). """ @@ -82,7 +82,7 @@ def parse_to_margin_balance(self) -> MarginBalance: class BinanceFuturesAccountInfo(msgspec.Struct, kw_only=True, frozen=True): """ - HTTP response from `Binance Futures` GET /fapi/v2/account (HMAC SHA256). + HTTP response from Binance Futures GET /fapi/v2/account (HMAC SHA256). """ feeTier: int # account commission tier @@ -158,7 +158,7 @@ def parse_to_position_status_report( class BinanceFuturesDualSidePosition(msgspec.Struct, frozen=True): """ - HTTP response from `Binance Futures` GET /fapi/v1/positionSide/dual (HMAC SHA256). + HTTP response from Binance Futures GET /fapi/v1/positionSide/dual (HMAC SHA256). """ dualSidePosition: bool @@ -166,7 +166,7 @@ class BinanceFuturesDualSidePosition(msgspec.Struct, frozen=True): class BinanceFuturesFeeRates(msgspec.Struct, frozen=True): """ - Represents a `BinanceFutures` fee tier. + Represents a Binance Futures fee tier. https://www.binance.com/en/fee/futureFee diff --git a/nautilus_trader/adapters/binance/futures/schemas/market.py b/nautilus_trader/adapters/binance/futures/schemas/market.py index 45e67b78b750..ce364d177514 100644 --- a/nautilus_trader/adapters/binance/futures/schemas/market.py +++ b/nautilus_trader/adapters/binance/futures/schemas/market.py @@ -42,7 +42,7 @@ class BinanceFuturesAsset(msgspec.Struct, frozen=True): """ - HTTP response 'inner struct' from `Binance Futures` GET /fapi/v1/exchangeInfo. + HTTP response 'inner struct' from Binance Futures GET /fapi/v1/exchangeInfo. """ asset: str @@ -52,7 +52,7 @@ class BinanceFuturesAsset(msgspec.Struct, frozen=True): class BinanceFuturesSymbolInfo(msgspec.Struct, kw_only=True, frozen=True): """ - HTTP response 'inner struct' from `Binance Futures` GET /fapi/v1/exchangeInfo. + HTTP response 'inner struct' from Binance Futures GET /fapi/v1/exchangeInfo. """ symbol: str @@ -101,7 +101,7 @@ def parse_to_quote_currency(self): class BinanceFuturesExchangeInfo(msgspec.Struct, kw_only=True, frozen=True): """ - HTTP response from `Binance Futures` GET /fapi/v1/exchangeInfo. + HTTP response from Binance Futures GET /fapi/v1/exchangeInfo. """ timezone: str @@ -114,7 +114,7 @@ class BinanceFuturesExchangeInfo(msgspec.Struct, kw_only=True, frozen=True): class BinanceFuturesMarkFunding(msgspec.Struct, frozen=True): """ - HTTP response from `Binance Future` GET /fapi/v1/premiumIndex. + HTTP response from Binance Futures GET /fapi/v1/premiumIndex. """ symbol: str @@ -131,7 +131,7 @@ class BinanceFuturesMarkFunding(msgspec.Struct, frozen=True): class BinanceFuturesFundRate(msgspec.Struct, frozen=True): """ - HTTP response from `Binance Future` GET /fapi/v1/fundingRate. + HTTP response from Binance Futures GET /fapi/v1/fundingRate. """ symbol: str @@ -146,7 +146,7 @@ class BinanceFuturesFundRate(msgspec.Struct, frozen=True): class BinanceFuturesTradeData(msgspec.Struct, frozen=True): """ - WebSocket message 'inner struct' for `Binance Futures` Trade Streams. + WebSocket message 'inner struct' for Binance Futures Trade Streams. Fields ------ @@ -190,7 +190,7 @@ def parse_to_trade_tick( class BinanceFuturesTradeMsg(msgspec.Struct, frozen=True): """ - WebSocket message from `Binance Futures` Trade Streams. + WebSocket message from Binance Futures Trade Streams. """ stream: str @@ -199,7 +199,7 @@ class BinanceFuturesTradeMsg(msgspec.Struct, frozen=True): class BinanceFuturesMarkPriceData(msgspec.Struct, frozen=True): """ - WebSocket message 'inner struct' for `Binance Futures` Mark Price Update events. + WebSocket message 'inner struct' for Binance Futures Mark Price Update events. """ e: str # Event type @@ -230,7 +230,7 @@ def parse_to_binance_futures_mark_price_update( class BinanceFuturesMarkPriceMsg(msgspec.Struct, frozen=True): """ - WebSocket message from `Binance Futures` Mark Price Update events. + WebSocket message from Binance Futures Mark Price Update events. """ stream: str diff --git a/nautilus_trader/adapters/binance/futures/schemas/user.py b/nautilus_trader/adapters/binance/futures/schemas/user.py index 27399c11971f..c32f4e5408c7 100644 --- a/nautilus_trader/adapters/binance/futures/schemas/user.py +++ b/nautilus_trader/adapters/binance/futures/schemas/user.py @@ -24,7 +24,7 @@ from nautilus_trader.adapters.binance.common.enums import BinanceOrderStatus from nautilus_trader.adapters.binance.common.enums import BinanceOrderType from nautilus_trader.adapters.binance.common.enums import BinanceTimeInForce -from nautilus_trader.adapters.binance.common.execution import BinanceCommonExecutionClient +from nautilus_trader.adapters.binance.execution import BinanceCommonExecutionClient from nautilus_trader.adapters.binance.futures.enums import BinanceFuturesEventType from nautilus_trader.adapters.binance.futures.enums import BinanceFuturesPositionUpdateReason from nautilus_trader.adapters.binance.futures.enums import BinanceFuturesWorkingType @@ -74,7 +74,7 @@ class BinanceFuturesUserMsgWrapper(msgspec.Struct, frozen=True): class MarginCallPosition(msgspec.Struct, frozen=True): """ - Inner struct position for `Binance Futures` Margin Call events. + Inner struct position for Binance Futures Margin Call events. """ s: str # Symbol @@ -89,7 +89,7 @@ class MarginCallPosition(msgspec.Struct, frozen=True): class BinanceFuturesMarginCallMsg(msgspec.Struct, frozen=True): """ - WebSocket message for `Binance Futures` Margin Call events. + WebSocket message for Binance Futures Margin Call events. """ e: str # Event Type @@ -100,7 +100,7 @@ class BinanceFuturesMarginCallMsg(msgspec.Struct, frozen=True): class BinanceFuturesBalance(msgspec.Struct, frozen=True): """ - Inner struct balance for `Binance Futures` Balance and Position update event. + Inner struct balance for Binance Futures Balance and Position update event. """ a: str # Asset @@ -123,7 +123,7 @@ def parse_to_account_balance(self) -> AccountBalance: class BinanceFuturesPosition(msgspec.Struct, frozen=True): """ - Inner struct position for `Binance Futures` Balance and Position update event. + Inner struct position for Binance Futures Balance and Position update event. """ s: str # Symbol @@ -138,7 +138,7 @@ class BinanceFuturesPosition(msgspec.Struct, frozen=True): class BinanceFuturesAccountUpdateData(msgspec.Struct, frozen=True): """ - WebSocket message for `Binance Futures` Balance and Position Update events. + WebSocket message for Binance Futures Balance and Position Update events. """ m: BinanceFuturesPositionUpdateReason @@ -151,7 +151,7 @@ def parse_to_account_balances(self) -> list[AccountBalance]: class BinanceFuturesAccountUpdateMsg(msgspec.Struct, frozen=True): """ - WebSocket message for `Binance Futures` Balance and Position Update events. + WebSocket message for Binance Futures Balance and Position Update events. """ e: str # Event Type @@ -173,7 +173,7 @@ def handle_account_update(self, exec_client: BinanceCommonExecutionClient): class BinanceFuturesAccountUpdateWrapper(msgspec.Struct, frozen=True): """ - WebSocket message wrapper for `Binance Futures` Balance and Position Update events. + WebSocket message wrapper for Binance Futures Balance and Position Update events. """ stream: str @@ -182,7 +182,7 @@ class BinanceFuturesAccountUpdateWrapper(msgspec.Struct, frozen=True): class BinanceFuturesOrderData(msgspec.Struct, kw_only=True, frozen=True): """ - WebSocket message 'inner struct' for `Binance Futures` Order Update events. + WebSocket message 'inner struct' for Binance Futures Order Update events. Client Order ID 'c': - starts with "autoclose-": liquidation order/ @@ -405,7 +405,7 @@ def handle_order_trade_update( # noqa: C901 (too complex) class BinanceFuturesOrderUpdateMsg(msgspec.Struct, frozen=True): """ - WebSocket message for `Binance Futures` Order Update events. + WebSocket message for Binance Futures Order Update events. """ e: str # Event Type @@ -416,7 +416,7 @@ class BinanceFuturesOrderUpdateMsg(msgspec.Struct, frozen=True): class BinanceFuturesOrderUpdateWrapper(msgspec.Struct, frozen=True): """ - WebSocket message wrapper for `Binance Futures` Order Update events. + WebSocket message wrapper for Binance Futures Order Update events. """ stream: str diff --git a/nautilus_trader/adapters/binance/futures/schemas/wallet.py b/nautilus_trader/adapters/binance/futures/schemas/wallet.py index 4db0cb655b08..68977b9fbda3 100644 --- a/nautilus_trader/adapters/binance/futures/schemas/wallet.py +++ b/nautilus_trader/adapters/binance/futures/schemas/wallet.py @@ -23,7 +23,7 @@ class BinanceFuturesCommissionRate(msgspec.Struct, frozen=True): """ - Schema of a single `Binance Futures` commissionRate. + Schema of a single Binance Futures commissionRate. """ symbol: str diff --git a/nautilus_trader/adapters/binance/futures/types.py b/nautilus_trader/adapters/binance/futures/types.py index bbfcd4dcd94e..551fc34cccc3 100644 --- a/nautilus_trader/adapters/binance/futures/types.py +++ b/nautilus_trader/adapters/binance/futures/types.py @@ -23,7 +23,7 @@ class BinanceFuturesMarkPriceUpdate(Data): """ - Represents a `Binance Futures` mark price and funding rate update. + Represents a Binance Futures mark price and funding rate update. Parameters ---------- @@ -111,7 +111,7 @@ def ts_init(self) -> int: @staticmethod def from_dict(values: dict[str, Any]) -> "BinanceFuturesMarkPriceUpdate": """ - Return a `Binance Futures` mark price update parsed from the given values. + Return a Binance Futures mark price update parsed from the given values. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/http/account.py b/nautilus_trader/adapters/binance/http/account.py index a1783e0710f3..4f4ebe495256 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. # ------------------------------------------------------------------------------------------------- - import msgspec from nautilus_trader.adapters.binance.common.enums import BinanceAccountType diff --git a/nautilus_trader/adapters/binance/http/client.py b/nautilus_trader/adapters/binance/http/client.py index a723aa3dd015..0465a8ae1a89 100644 --- a/nautilus_trader/adapters/binance/http/client.py +++ b/nautilus_trader/adapters/binance/http/client.py @@ -13,23 +13,19 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -import hashlib -import hmac import urllib.parse -from base64 import b64encode from typing import Any import msgspec -from Crypto.Hash import SHA256 -from Crypto.Signature import pkcs1_15 -from nacl.signing import SigningKey import nautilus_trader +from nautilus_trader.adapters.binance.common.enums import BinanceKeyType from nautilus_trader.adapters.binance.http.error import BinanceClientError from nautilus_trader.adapters.binance.http.error import BinanceServerError from nautilus_trader.common.component import LiveClock from nautilus_trader.common.component import Logger from nautilus_trader.common.enums import LogColor +from nautilus_trader.core import nautilus_pyo3 from nautilus_trader.core.nautilus_pyo3 import HttpClient from nautilus_trader.core.nautilus_pyo3 import HttpMethod from nautilus_trader.core.nautilus_pyo3 import HttpResponse @@ -44,51 +40,53 @@ class BinanceHttpClient: ---------- clock : LiveClock The clock for the client. - key : str + api_key : str The Binance API key for requests. - secret : str + api_secret : str The Binance API secret for signed requests. + key_type : BinanceKeyType, default 'HMAC' + The private key cryptographic algorithm type. + rsa_private_key : str, optional + The RSA private key for RSA signing. + ed25519_private_key : str, optional + The Ed25519 private key for Ed25519 signing. base_url : str, optional The base endpoint URL for the client. ratelimiter_quotas : list[tuple[str, Quota]], optional The keyed rate limiter quotas for the client. ratelimiter_quota : Quota, optional The default rate limiter quota for the client. - key_type : str, optional - The type of API key (HMAC, RSA, Ed25519). - rsa_private_key : str, optional - The RSA private key for RSA signing. - ed25519_private_key : str, optional - The Ed25519 private key for Ed25519 signing. """ def __init__( self, clock: LiveClock, - key: str, - secret: str, + api_key: str, + api_secret: str, base_url: str, - ratelimiter_quotas: list[tuple[str, Quota]] | None = None, - ratelimiter_default_quota: Quota | None = None, - key_type: str = "HMAC", + key_type: BinanceKeyType = BinanceKeyType.HMAC, rsa_private_key: str | None = None, ed25519_private_key: str | None = None, + ratelimiter_quotas: list[tuple[str, Quota]] | None = None, + ratelimiter_default_quota: Quota | None = None, ) -> None: self._clock: LiveClock = clock self._log: Logger = Logger(type(self).__name__) - self._key: str = key + self._key: str = api_key self._base_url: str = base_url - self._secret: str = secret - self._key_type: str = key_type + self._secret: str = api_secret + self._key_type: BinanceKeyType = key_type self._rsa_private_key: str | None = rsa_private_key - self._ed25519_private_key: str | None = ed25519_private_key + self._ed25519_private_key: bytes | None = ( + ed25519_private_key.encode() if ed25519_private_key else None + ) self._headers: dict[str, Any] = { "Content-Type": "application/json", "User-Agent": nautilus_trader.USER_AGENT, - "X-MBX-APIKEY": key, + "X-MBX-APIKEY": api_key, } self._client = HttpClient( keyed_quotas=ratelimiter_quotas or [], @@ -137,30 +135,18 @@ def _prepare_params(self, params: dict[str, Any]) -> str: def _get_sign(self, data: str) -> str: match self._key_type: - case "HMAC": - return self._hmac_sign(data) - case "RSA": - return self._rsa_signature(data) - case "Ed25519": - return self._ed25519_signature(data) + case BinanceKeyType.HMAC: + return nautilus_pyo3.hmac_sign(self._secret, data) + case BinanceKeyType.RSA: + if not self._rsa_private_key: + raise ValueError("`rsa_private_key` was None") + return nautilus_pyo3.rsa_signature(self._rsa_private_key, data) + case BinanceKeyType.ED25519: + if not self._ed25519_private_key: + raise ValueError("`ed25519_private_key` was None") + return nautilus_pyo3.ed25519_signature(self._ed25519_private_key, data) case _: - raise ValueError(f"Unsupported key type, was {self._key_type}") - - def _hmac_sign(self, data: str) -> str: - m = hmac.new(self._secret.encode(), data.encode(), hashlib.sha256) - return m.hexdigest() - - def _rsa_signature(self, query_string: str) -> str: - assert self._rsa_private_key - h = SHA256.new(query_string.encode()) - signature = pkcs1_15.new(self._rsa_private_key).sign(h) - return b64encode(signature).decode() - - def _ed25519_signature(self, query_string: str) -> str: - assert self._ed25519_private_key - signing_key = SigningKey(self._ed25519_private_key.encode()) - signed_message = signing_key.sign(query_string.encode()) - return b64encode(signed_message.signature).decode() + raise ValueError(f"Unsupported key type, was '{self._key_type.value}'") async def sign_request( self, diff --git a/nautilus_trader/adapters/binance/http/user.py b/nautilus_trader/adapters/binance/http/user.py index 3af06274c161..88603693f01f 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. # ------------------------------------------------------------------------------------------------- - import msgspec from nautilus_trader.adapters.binance.common.enums import BinanceAccountType diff --git a/nautilus_trader/adapters/binance/spot/data.py b/nautilus_trader/adapters/binance/spot/data.py index a9c8b97d5e1d..787438f646e9 100644 --- a/nautilus_trader/adapters/binance/spot/data.py +++ b/nautilus_trader/adapters/binance/spot/data.py @@ -17,9 +17,9 @@ import msgspec -from nautilus_trader.adapters.binance.common.data import BinanceCommonDataClient from nautilus_trader.adapters.binance.common.enums import BinanceAccountType from nautilus_trader.adapters.binance.config import BinanceDataClientConfig +from nautilus_trader.adapters.binance.data import BinanceCommonDataClient from nautilus_trader.adapters.binance.http.client import BinanceHttpClient from nautilus_trader.adapters.binance.spot.enums import BinanceSpotEnumParser from nautilus_trader.adapters.binance.spot.http.market import BinanceSpotMarketHttpAPI @@ -38,7 +38,7 @@ class BinanceSpotDataClient(BinanceCommonDataClient): """ - Provides a data client for the `Binance Spot/Margin` exchange. + Provides a data client for the Binance Spot/Margin exchange. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/spot/enums.py b/nautilus_trader/adapters/binance/spot/enums.py index 76587394803a..f115e79d342d 100644 --- a/nautilus_trader/adapters/binance/spot/enums.py +++ b/nautilus_trader/adapters/binance/spot/enums.py @@ -34,7 +34,7 @@ @unique class BinanceSpotPermissions(Enum): """ - Represents `Binance Spot/Margin` trading permissions. + Represents Binance Spot/Margin trading permissions. """ SPOT = "SPOT" @@ -76,7 +76,7 @@ class BinanceSpotPermissions(Enum): @unique class BinanceSpotSymbolStatus(Enum): """ - Represents a `Binance Spot/Margin` symbol status. + Represents a Binance Spot/Margin symbol status. """ PRE_TRADING = "PRE_TRADING" @@ -91,7 +91,7 @@ class BinanceSpotSymbolStatus(Enum): @unique class BinanceSpotEventType(Enum): """ - Represents a `Binance Spot/Margin` event type. + Represents a Binance Spot/Margin event type. """ outboundAccountPosition = "outboundAccountPosition" diff --git a/nautilus_trader/adapters/binance/spot/execution.py b/nautilus_trader/adapters/binance/spot/execution.py index c95c8acf0470..8b975efd28db 100644 --- a/nautilus_trader/adapters/binance/spot/execution.py +++ b/nautilus_trader/adapters/binance/spot/execution.py @@ -18,8 +18,8 @@ import msgspec from nautilus_trader.adapters.binance.common.enums import BinanceAccountType -from nautilus_trader.adapters.binance.common.execution import BinanceCommonExecutionClient from nautilus_trader.adapters.binance.config import BinanceExecClientConfig +from nautilus_trader.adapters.binance.execution import BinanceCommonExecutionClient from nautilus_trader.adapters.binance.http.client import BinanceHttpClient from nautilus_trader.adapters.binance.spot.enums import BinanceSpotEnumParser from nautilus_trader.adapters.binance.spot.enums import BinanceSpotEventType @@ -47,7 +47,7 @@ class BinanceSpotExecutionClient(BinanceCommonExecutionClient): """ - Provides an execution client for the `Binance Spot/Margin` exchange. + Provides an execution client for the Binance Spot/Margin exchange. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/spot/http/account.py b/nautilus_trader/adapters/binance/spot/http/account.py index 593294e084d4..5f3979b9fee1 100644 --- a/nautilus_trader/adapters/binance/spot/http/account.py +++ b/nautilus_trader/adapters/binance/spot/http/account.py @@ -524,7 +524,7 @@ async def get(self, params: GetParameters) -> list[BinanceRateLimit]: class BinanceSpotAccountHttpAPI(BinanceAccountHttpAPI): """ - Provides access to the `Binance Spot/Margin` Account/Trade HTTP REST API. + Provides access to the Binance Spot/Margin Account/Trade HTTP REST API. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/spot/http/market.py b/nautilus_trader/adapters/binance/spot/http/market.py index 6c462c54221d..058c9bd8ab3a 100644 --- a/nautilus_trader/adapters/binance/spot/http/market.py +++ b/nautilus_trader/adapters/binance/spot/http/market.py @@ -130,7 +130,7 @@ async def get(self, params: GetParameters) -> BinanceSpotAvgPrice: class BinanceSpotMarketHttpAPI(BinanceMarketHttpAPI): """ - Provides access to the `Binance Spot` Market HTTP REST API. + Provides access to the Binance Spot Market HTTP REST API. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/spot/http/user.py b/nautilus_trader/adapters/binance/spot/http/user.py index 56972488b756..f2d900dd2200 100644 --- a/nautilus_trader/adapters/binance/spot/http/user.py +++ b/nautilus_trader/adapters/binance/spot/http/user.py @@ -20,7 +20,7 @@ class BinanceSpotUserDataHttpAPI(BinanceUserDataHttpAPI): """ - Provides access to the `Binance Spot/Margin` User Data HTTP REST API. + Provides access to the Binance Spot/Margin User Data HTTP REST API. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/spot/http/wallet.py b/nautilus_trader/adapters/binance/spot/http/wallet.py index f3c2f7927479..d63f7a1a220c 100644 --- a/nautilus_trader/adapters/binance/spot/http/wallet.py +++ b/nautilus_trader/adapters/binance/spot/http/wallet.py @@ -83,7 +83,7 @@ async def get(self, params: GetParameters) -> list[BinanceSpotTradeFee]: class BinanceSpotWalletHttpAPI: """ - Provides access to the `Binance Spot/Margin` Wallet HTTP REST API. + Provides access to the Binance Spot/Margin Wallet HTTP REST API. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/spot/providers.py b/nautilus_trader/adapters/binance/spot/providers.py index 9bb91e925c1d..c9c381805905 100644 --- a/nautilus_trader/adapters/binance/spot/providers.py +++ b/nautilus_trader/adapters/binance/spot/providers.py @@ -48,7 +48,7 @@ class BinanceSpotInstrumentProvider(InstrumentProvider): """ - Provides a means of loading instruments from the `Binance Spot/Margin` exchange. + Provides a means of loading instruments from the Binance Spot/Margin exchange. Parameters ---------- diff --git a/nautilus_trader/adapters/binance/spot/schemas/account.py b/nautilus_trader/adapters/binance/spot/schemas/account.py index 53a01edf57cc..1d3243784bb0 100644 --- a/nautilus_trader/adapters/binance/spot/schemas/account.py +++ b/nautilus_trader/adapters/binance/spot/schemas/account.py @@ -31,7 +31,7 @@ class BinanceSpotBalanceInfo(msgspec.Struct, frozen=True): """ - HTTP response 'inner struct' from `Binance Spot/Margin` GET /api/v3/account (HMAC + HTTP response 'inner struct' from Binance Spot/Margin GET /api/v3/account (HMAC SHA256). """ @@ -53,7 +53,7 @@ def parse_to_account_balance(self) -> AccountBalance: class BinanceSpotAccountInfo(msgspec.Struct, frozen=True): """ - HTTP response from `Binance Spot/Margin` GET /api/v3/account (HMAC SHA256). + HTTP response from Binance Spot/Margin GET /api/v3/account (HMAC SHA256). """ makerCommission: int @@ -74,10 +74,10 @@ def parse_to_account_balances(self) -> list[AccountBalance]: class BinanceSpotOrderOco(msgspec.Struct, frozen=True): """ - HTTP response from `Binance Spot/Margin` GET /api/v3/orderList (HMAC SHA256). + HTTP response from Binance Spot/Margin GET /api/v3/orderList (HMAC SHA256). - HTTP response from `Binance Spot/Margin` POST /api/v3/order/oco (HMAC SHA256). HTTP - response from `Binance Spot/Margin` DELETE /api/v3/orderList (HMAC SHA256). + HTTP response from Binance Spot/Margin POST /api/v3/order/oco (HMAC SHA256). HTTP + response from Binance Spot/Margin DELETE /api/v3/orderList (HMAC SHA256). """ diff --git a/nautilus_trader/adapters/binance/spot/schemas/market.py b/nautilus_trader/adapters/binance/spot/schemas/market.py index b1623eef2af7..f232cf7764ba 100644 --- a/nautilus_trader/adapters/binance/spot/schemas/market.py +++ b/nautilus_trader/adapters/binance/spot/schemas/market.py @@ -42,7 +42,7 @@ class BinanceSpotSymbolInfo(msgspec.Struct, frozen=True): """ - HTTP response 'inner struct' from `Binance Spot/Margin` GET /api/v3/exchangeInfo. + HTTP response 'inner struct' from Binance Spot/Margin GET /api/v3/exchangeInfo. """ symbol: str @@ -83,7 +83,7 @@ def parse_to_quote_asset(self): class BinanceSpotExchangeInfo(msgspec.Struct, frozen=True): """ - HTTP response from `Binance Spot/Margin` GET /api/v3/exchangeInfo. + HTTP response from Binance Spot/Margin GET /api/v3/exchangeInfo. """ timezone: str @@ -95,7 +95,7 @@ class BinanceSpotExchangeInfo(msgspec.Struct, frozen=True): class BinanceSpotAvgPrice(msgspec.Struct, frozen=True): """ - HTTP response from `Binance Spot/Margin` GET /api/v3/avgPrice. + HTTP response from Binance Spot/Margin GET /api/v3/avgPrice. """ mins: int @@ -175,7 +175,7 @@ class BinanceSpotOrderBookPartialDepthMsg(msgspec.Struct): class BinanceSpotTradeData(msgspec.Struct): """ - WebSocket message 'inner struct' for `Binance Spot/Margin` Trade Streams. + WebSocket message 'inner struct' for Binance Spot/Margin Trade Streams. Fields ------ diff --git a/nautilus_trader/adapters/binance/spot/schemas/user.py b/nautilus_trader/adapters/binance/spot/schemas/user.py index 3968b9e05370..0fd28578f911 100644 --- a/nautilus_trader/adapters/binance/spot/schemas/user.py +++ b/nautilus_trader/adapters/binance/spot/schemas/user.py @@ -23,7 +23,7 @@ from nautilus_trader.adapters.binance.common.enums import BinanceOrderStatus from nautilus_trader.adapters.binance.common.enums import BinanceOrderType from nautilus_trader.adapters.binance.common.enums import BinanceTimeInForce -from nautilus_trader.adapters.binance.common.execution import BinanceCommonExecutionClient +from nautilus_trader.adapters.binance.execution import BinanceCommonExecutionClient from nautilus_trader.adapters.binance.spot.enums import BinanceSpotEventType from nautilus_trader.core.datetime import millis_to_nanos from nautilus_trader.core.uuid import UUID4 @@ -69,7 +69,7 @@ class BinanceSpotUserMsgWrapper(msgspec.Struct, frozen=True): class BinanceSpotBalance(msgspec.Struct, frozen=True): """ - Inner struct for `Binance Spot/Margin` balances. + Inner struct for Binance Spot/Margin balances. """ a: str # Asset @@ -90,7 +90,7 @@ def parse_to_account_balance(self) -> AccountBalance: class BinanceSpotAccountUpdateMsg(msgspec.Struct, frozen=True): """ - WebSocket message for `Binance Spot/Margin` Account Update events. + WebSocket message for Binance Spot/Margin Account Update events. """ e: str # Event Type @@ -115,7 +115,7 @@ def handle_account_update(self, exec_client: BinanceCommonExecutionClient): class BinanceSpotAccountUpdateWrapper(msgspec.Struct, frozen=True): """ - WebSocket message wrapper for `Binance Spot/Margin` Account Update events. + WebSocket message wrapper for Binance Spot/Margin Account Update events. """ stream: str @@ -124,7 +124,7 @@ class BinanceSpotAccountUpdateWrapper(msgspec.Struct, frozen=True): class BinanceSpotOrderUpdateData(msgspec.Struct, kw_only=True): """ - WebSocket message 'inner struct' for `Binance Spot/Margin` Order Update events. + WebSocket message 'inner struct' for Binance Spot/Margin Order Update events. """ e: BinanceSpotEventType @@ -293,7 +293,7 @@ def handle_execution_report( class BinanceSpotOrderUpdateWrapper(msgspec.Struct, frozen=True): """ - WebSocket message wrapper for `Binance Spot/Margin` Order Update events. + WebSocket message wrapper for Binance Spot/Margin Order Update events. """ stream: str diff --git a/nautilus_trader/adapters/binance/spot/schemas/wallet.py b/nautilus_trader/adapters/binance/spot/schemas/wallet.py index 41a11464dc74..e2ae4ffceb35 100644 --- a/nautilus_trader/adapters/binance/spot/schemas/wallet.py +++ b/nautilus_trader/adapters/binance/spot/schemas/wallet.py @@ -23,7 +23,7 @@ class BinanceSpotTradeFee(msgspec.Struct, frozen=True): """ - Schema of a single `Binance Spot/Margin` tradeFee. + Schema of a single Binance Spot/Margin tradeFee. """ symbol: str diff --git a/nautilus_trader/adapters/dydx/execution.py b/nautilus_trader/adapters/dydx/execution.py index 57f53257a7ab..afd884f1935b 100644 --- a/nautilus_trader/adapters/dydx/execution.py +++ b/nautilus_trader/adapters/dydx/execution.py @@ -1220,19 +1220,28 @@ async def _cancel_order_single( ) await self._cancel_order_single_and_retry( - client_order_id=client_order_id, + order=order, order_id=order_id, good_til_date_secs=good_til_date_secs, ) async def _cancel_order_single_and_retry( self, - client_order_id: ClientOrderId, + order: Order, order_id: DYDXOrderId, good_til_date_secs: int | None, ) -> None: if self._wallet is None: - self._log.error(f"Cannot cancel order {client_order_id!r}: no wallet available.") + reason = f"Cannot cancel order {order.client_order_id!r}: no wallet available." + self._log.error(reason) + self.generate_order_cancel_rejected( + strategy_id=order.strategy_id, + instrument_id=order.instrument_id, + client_order_id=order.client_order_id, + venue_order_id=order.venue_order_id, + reason=reason, + ts_event=self._clock.timestamp_ns(), + ) return is_expired = ( @@ -1242,7 +1251,16 @@ async def _cancel_order_single_and_retry( ) if is_expired: - self._log.warning(f"Cannot cancel order: order {client_order_id!r} is expired") + reason = f"Cannot cancel order: order {order.client_order_id!r} is expired" + self._log.warning(reason) + self.generate_order_cancel_rejected( + strategy_id=order.strategy_id, + instrument_id=order.instrument_id, + client_order_id=order.client_order_id, + venue_order_id=order.venue_order_id, + reason=reason, + ts_event=self._clock.timestamp_ns(), + ) return current_block = await self._grpc_account.latest_block_height() @@ -1250,10 +1268,20 @@ async def _cancel_order_single_and_retry( async with self._retry_manager_pool as retry_manager: await retry_manager.run( name="cancel_order", - details=[client_order_id], + details=[order.client_order_id, order.venue_order_id], func=self._grpc_account.cancel_order, wallet=self._wallet, order_id=order_id, good_til_block=current_block + 10, good_til_block_time=good_til_date_secs, ) + if not retry_manager.result: + self._log.error(f"Failed to cancel order: {retry_manager.message}") + self.generate_order_cancel_rejected( + strategy_id=order.strategy_id, + instrument_id=order.instrument_id, + client_order_id=order.client_order_id, + venue_order_id=order.venue_order_id, + reason=retry_manager.message, + ts_event=self._clock.timestamp_ns(), + ) diff --git a/nautilus_trader/common/providers.py b/nautilus_trader/common/providers.py index 8f5059d07ab2..e60924044015 100644 --- a/nautilus_trader/common/providers.py +++ b/nautilus_trader/common/providers.py @@ -161,7 +161,6 @@ async def initialize(self) -> None: else: self._log.debug("Awaiting loading...") while self._loading: - # Wait 100ms await asyncio.sleep(0.1) # Set async loading flags diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 8f27b396e8f9..1e72d51f91b0 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -3896,3 +3896,8 @@ class DatabentoLiveClient: callback_pyo3: Callable, ) -> Awaitable[None]: ... def close(self) -> None: ... + +# Crypto +def hmac_sign(secret: str, data: str) -> str: ... +def rsa_signature(private_key_pem: str, data: str) -> str: ... +def ed25519_signature(private_key: bytes, data: str) -> str: ... diff --git a/nautilus_trader/execution/emulator.pyx b/nautilus_trader/execution/emulator.pyx index a34c2ce55f7d..2afd99931e48 100644 --- a/nautilus_trader/execution/emulator.pyx +++ b/nautilus_trader/execution/emulator.pyx @@ -45,6 +45,8 @@ from nautilus_trader.execution.messages cimport ModifyOrder from nautilus_trader.execution.messages cimport SubmitOrder from nautilus_trader.execution.messages cimport TradingCommand from nautilus_trader.execution.trailing cimport TrailingStopCalculator +from nautilus_trader.model.book cimport OrderBook +from nautilus_trader.model.data cimport OrderBookDeltas from nautilus_trader.model.data cimport QuoteTick from nautilus_trader.model.data cimport TradeTick from nautilus_trader.model.events.order cimport OrderCanceled @@ -406,6 +408,8 @@ cdef class OrderEmulator(Actor): # Check data subscription if emulation_trigger == TriggerType.DEFAULT or emulation_trigger == TriggerType.BID_ASK: if trigger_instrument_id not in self._subscribed_quotes: + if not trigger_instrument_id.is_synthetic(): + self.subscribe_order_book_deltas(trigger_instrument_id) self.subscribe_quote_ticks(trigger_instrument_id) self._subscribed_quotes.add(trigger_instrument_id) elif emulation_trigger == TriggerType.LAST_TRADE: @@ -793,6 +797,34 @@ cdef class OrderEmulator(Actor): else: self._manager.send_exec_command(command) + cpdef void on_order_book_deltas(self, deltas): + cdef OrderBookDeltas _deltas = deltas # C typing to optimize performance + + if is_logging_initialized(): + self._log.debug(f"Processing {repr(_deltas)}", LogColor.CYAN) + + + cdef MatchingCore matching_core = self._matching_cores.get(_deltas.instrument_id) + if matching_core is None: + self._log.error(f"Cannot handle `OrderBookDeltas`: no matching core for instrument {_deltas.instrument_id}") + return + + cdef OrderBook book = self.cache.order_book(_deltas.instrument_id) + if book is None: + self.log.error(f"Cannot handle `OrderBookDeltas`: no book being maintained for {_deltas.instrument_id}") + return + + cdef Price best_bid = book.best_bid_price() + cdef Price best_ask = book.best_ask_price() + + if best_bid is not None: + matching_core.set_bid_raw(best_bid._mem.raw) + + if best_ask is not None: + matching_core.set_ask_raw(best_ask._mem.raw) + + self._iterate_orders(matching_core) + cpdef void on_quote_tick(self, QuoteTick tick): if is_logging_initialized(): self._log.debug(f"Processing {repr(tick)}", LogColor.CYAN) diff --git a/nautilus_trader/live/retry.py b/nautilus_trader/live/retry.py index f3ae19ae5cc5..9710a703e9a9 100644 --- a/nautilus_trader/live/retry.py +++ b/nautilus_trader/live/retry.py @@ -14,15 +14,23 @@ # ------------------------------------------------------------------------------------------------- import asyncio +from collections.abc import Awaitable from collections.abc import Callable +from typing import Generic, TypeVar from nautilus_trader.common.component import Logger -class RetryManager: +T = TypeVar("T") + + +class RetryManager(Generic[T]): """ Provides retry state management for an HTTP request. + This class is generic over `T`, where `T` is the return type of the + function passed to the `run` method. + Parameters ---------- max_retries : int @@ -64,7 +72,14 @@ def __init__( def __repr__(self) -> str: return f"<{type(self).__name__}(name='{self.name}', details={self.details}) at {hex(id(self))}>" - async def run(self, name: str, details: list[object] | None, func, *args, **kwargs): + async def run( + self, + name: str, + details: list[object] | None, + func: Callable[..., Awaitable[T]], + *args, + **kwargs, + ) -> T | None: """ Execute the given `func` with retry management. @@ -77,13 +92,18 @@ async def run(self, name: str, details: list[object] | None, func, *args, **kwar The name of the operation to run. details : list[object], optional The operation details such as identifiers. - func : Awaitable + func : Callable[..., Awaitable[T]] The function to execute. args : Any Positional arguments to pass to the function `func`. kwargs : Any Keyword arguments to pass to the function `func`. + Returns + ------- + T | None + The result of the executed function, or `None` if the retries fail. + """ self.name = name self.details = details @@ -92,12 +112,12 @@ async def run(self, name: str, details: list[object] | None, func, *args, **kwar while True: if self.cancel_event.is_set(): self._cancel() - return + return None try: - await func(*args, **kwargs) + response = await func(*args, **kwargs) self.result = True - return # Successful request + return response # Successful request except self.exc_types as e: self.log.warning(repr(e)) if ( @@ -108,13 +128,14 @@ async def run(self, name: str, details: list[object] | None, func, *args, **kwar self._log_error() self.result = False self.message = str(e) - return # Operation failed + return None # Operation failed self.retries += 1 self._log_retry() await asyncio.sleep(self.retry_delay_secs) except asyncio.CancelledError: self._cancel() + return None def cancel(self) -> None: """ diff --git a/poetry.lock b/poetry.lock index 6ad4bec69a6d..3260b619030d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -953,19 +953,19 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "filelock" -version = "3.15.4" +version = "3.16.0" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, - {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, + {file = "filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"}, + {file = "filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] -typing = ["typing-extensions (>=4.8)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.3)"] +typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "frozenlist" @@ -1862,19 +1862,19 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"}, + {file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" @@ -2751,13 +2751,13 @@ files = [ [[package]] name = "types-requests" -version = "2.32.0.20240905" +version = "2.32.0.20240907" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" files = [ - {file = "types-requests-2.32.0.20240905.tar.gz", hash = "sha256:e97fd015a5ed982c9ddcd14cc4afba9d111e0e06b797c8f776d14602735e9bd6"}, - {file = "types_requests-2.32.0.20240905-py3-none-any.whl", hash = "sha256:f46ecb55f5e1a37a58be684cf3f013f166da27552732ef2469a0cc8e62a72881"}, + {file = "types-requests-2.32.0.20240907.tar.gz", hash = "sha256:ff33935f061b5e81ec87997e91050f7b4af4f82027a7a7a9d9aaea04a963fdf8"}, + {file = "types_requests-2.32.0.20240907-py3-none-any.whl", hash = "sha256:1d1e79faeaf9d42def77f3c304893dea17a97cae98168ac69f3cb465516ee8da"}, ] [package.dependencies] @@ -2899,13 +2899,13 @@ protobuf = ">=4.23" [[package]] name = "virtualenv" -version = "20.26.3" +version = "20.26.4" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, - {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, + {file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"}, + {file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"}, ] [package.dependencies] @@ -2919,103 +2919,103 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "yarl" -version = "1.9.11" +version = "1.11.0" description = "Yet another URL library" optional = false python-versions = ">=3.8" files = [ - {file = "yarl-1.9.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:79e08c691deae6fcac2fdde2e0515ac561dd3630d7c8adf7b1e786e22f1e193b"}, - {file = "yarl-1.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:752f4b5cf93268dc73c2ae994cc6d684b0dad5118bc87fbd965fd5d6dca20f45"}, - {file = "yarl-1.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:441049d3a449fb8756b0535be72c6a1a532938a33e1cf03523076700a5f87a01"}, - {file = "yarl-1.9.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3dfe17b4aed832c627319da22a33f27f282bd32633d6b145c726d519c89fbaf"}, - {file = "yarl-1.9.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:67abcb7df27952864440c9c85f1c549a4ad94afe44e2655f77d74b0d25895454"}, - {file = "yarl-1.9.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6de3fa29e76fd1518a80e6af4902c44f3b1b4d7fed28eb06913bba4727443de3"}, - {file = "yarl-1.9.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fee45b3bd4d8d5786472e056aa1359cc4dc9da68aded95a10cd7929a0ec661fe"}, - {file = "yarl-1.9.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c59b23886234abeba62087fd97d10fb6b905d9e36e2f3465d1886ce5c0ca30df"}, - {file = "yarl-1.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d93c612b2024ac25a3dc01341fd98fdd19c8c5e2011f3dcd084b3743cba8d756"}, - {file = "yarl-1.9.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4d368e3b9ecd50fa22017a20c49e356471af6ae91c4d788c6e9297e25ddf5a62"}, - {file = "yarl-1.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5b593acd45cdd4cf6664d342ceacedf25cd95263b83b964fddd6c78930ea5211"}, - {file = "yarl-1.9.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:224f8186c220ff00079e64bf193909829144d4e5174bb58665ef0da8bf6955c4"}, - {file = "yarl-1.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:91c478741d7563a12162f7a2db96c0d23d93b0521563f1f1f0ece46ea1702d33"}, - {file = "yarl-1.9.11-cp310-cp310-win32.whl", hash = "sha256:1cdb8f5bb0534986776a43df84031da7ff04ac0cf87cb22ae8a6368231949c40"}, - {file = "yarl-1.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:498439af143b43a2b2314451ffd0295410aa0dcbdac5ee18fc8633da4670b605"}, - {file = "yarl-1.9.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e290de5db4fd4859b4ed57cddfe793fcb218504e65781854a8ac283ab8d5518"}, - {file = "yarl-1.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e5f50a2e26cc2b89186f04c97e0ec0ba107ae41f1262ad16832d46849864f914"}, - {file = "yarl-1.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4a0e724a28d7447e4d549c8f40779f90e20147e94bf949d490402eee09845c6"}, - {file = "yarl-1.9.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85333d38a4fa5997fa2ff6fd169be66626d814b34fa35ec669e8c914ca50a097"}, - {file = "yarl-1.9.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ff184002ee72e4b247240e35d5dce4c2d9a0e81fdbef715dde79ab4718aa541"}, - {file = "yarl-1.9.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:675004040f847c0284827f44a1fa92d8baf425632cc93e7e0aa38408774b07c1"}, - {file = "yarl-1.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30703a7ade2b53f02e09a30685b70cd54f65ed314a8d9af08670c9a5391af1b"}, - {file = "yarl-1.9.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7230007ab67d43cf19200ec15bc6b654e6b85c402f545a6fc565d254d34ff754"}, - {file = "yarl-1.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8c2cf0c7ad745e1c6530fe6521dfb19ca43338239dfcc7da165d0ef2332c0882"}, - {file = "yarl-1.9.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4567cc08f479ad80fb07ed0c9e1bcb363a4f6e3483a490a39d57d1419bf1c4c7"}, - {file = "yarl-1.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:95adc179a02949c4560ef40f8f650a008380766eb253d74232eb9c024747c111"}, - {file = "yarl-1.9.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:755ae9cff06c429632d750aa8206f08df2e3d422ca67be79567aadbe74ae64cc"}, - {file = "yarl-1.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:94f71d54c5faf715e92c8434b4a0b968c4d1043469954d228fc031d51086f143"}, - {file = "yarl-1.9.11-cp311-cp311-win32.whl", hash = "sha256:4ae079573efeaa54e5978ce86b77f4175cd32f42afcaf9bfb8a0677e91f84e4e"}, - {file = "yarl-1.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:9fae7ec5c9a4fe22abb995804e6ce87067dfaf7e940272b79328ce37c8f22097"}, - {file = "yarl-1.9.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:614fa50fd0db41b79f426939a413d216cdc7bab8d8c8a25844798d286a999c5a"}, - {file = "yarl-1.9.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ff64f575d71eacb5a4d6f0696bfe991993d979423ea2241f23ab19ff63f0f9d1"}, - {file = "yarl-1.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c23f6dc3d7126b4c64b80aa186ac2bb65ab104a8372c4454e462fb074197bc6"}, - {file = "yarl-1.9.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8f847cc092c2b85d22e527f91ea83a6cf51533e727e2461557a47a859f96734"}, - {file = "yarl-1.9.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63a5dc2866791236779d99d7a422611d22bb3a3d50935bafa4e017ea13e51469"}, - {file = "yarl-1.9.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c335342d482e66254ae94b1231b1532790afb754f89e2e0c646f7f19d09740aa"}, - {file = "yarl-1.9.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4a8c3dedd081cca134a21179aebe58b6e426e8d1e0202da9d1cafa56e01af3c"}, - {file = "yarl-1.9.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:504d19320c92532cabc3495fb7ed6bb599f3c2bfb45fed432049bf4693dbd6d0"}, - {file = "yarl-1.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b2a8e5eb18181060197e3d5db7e78f818432725c0759bc1e5a9d603d9246389"}, - {file = "yarl-1.9.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f568d70b7187f4002b6b500c0996c37674a25ce44b20716faebe5fdb8bd356e7"}, - {file = "yarl-1.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:735b285ea46ca7e86ad261a462a071d0968aade44e1a3ea2b7d4f3d63b5aab12"}, - {file = "yarl-1.9.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2d1c81c3b92bef0c1c180048e43a5a85754a61b4f69d6f84df8e4bd615bef25d"}, - {file = "yarl-1.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8d6e1c1562b53bd26efd38e886fc13863b8d904d559426777990171020c478a9"}, - {file = "yarl-1.9.11-cp312-cp312-win32.whl", hash = "sha256:aeba4aaa59cb709edb824fa88a27cbbff4e0095aaf77212b652989276c493c00"}, - {file = "yarl-1.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:569309a3efb8369ff5d32edb2a0520ebaf810c3059f11d34477418c90aa878fd"}, - {file = "yarl-1.9.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4915818ac850c3b0413e953af34398775b7a337babe1e4d15f68c8f5c4872553"}, - {file = "yarl-1.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef9610b2f5a73707d4d8bac040f0115ca848e510e3b1f45ca53e97f609b54130"}, - {file = "yarl-1.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:47c0a3dc8076a8dd159de10628dea04215bc7ddaa46c5775bf96066a0a18f82b"}, - {file = "yarl-1.9.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:545f2fbfa0c723b446e9298b5beba0999ff82ce2c126110759e8dac29b5deaf4"}, - {file = "yarl-1.9.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9137975a4ccc163ad5d7a75aad966e6e4e95dedee08d7995eab896a639a0bce2"}, - {file = "yarl-1.9.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b0c70c451d2a86f8408abced5b7498423e2487543acf6fcf618b03f6e669b0a"}, - {file = "yarl-1.9.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce2bd986b1e44528677c237b74d59f215c8bfcdf2d69442aa10f62fd6ab2951c"}, - {file = "yarl-1.9.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d7b717f77846a9631046899c6cc730ea469c0e2fb252ccff1cc119950dbc296"}, - {file = "yarl-1.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3a26a24bbd19241283d601173cea1e5b93dec361a223394e18a1e8e5b0ef20bd"}, - {file = "yarl-1.9.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c189bf01af155ac9882e128d9f3b3ad68a1f2c2f51404afad7201305df4e12b1"}, - {file = "yarl-1.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0cbcc2c54084b2bda4109415631db017cf2960f74f9e8fd1698e1400e4f8aae2"}, - {file = "yarl-1.9.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:30f201bc65941a4aa59c1236783efe89049ec5549dafc8cd2b63cc179d3767b0"}, - {file = "yarl-1.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:922ba3b74f0958a0b5b9c14ff1ef12714a381760c08018f2b9827632783a590c"}, - {file = "yarl-1.9.11-cp313-cp313-win32.whl", hash = "sha256:17107b4b8c43e66befdcbe543fff2f9c93f7a3a9f8e3a9c9ac42bffeba0e8828"}, - {file = "yarl-1.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:0324506afab4f2e176a93cb08b8abcb8b009e1f324e6cbced999a8f5dd9ddb76"}, - {file = "yarl-1.9.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4e4f820fde9437bb47297194f43d29086433e6467fa28fe9876366ad357bd7bb"}, - {file = "yarl-1.9.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dfa9b9d5c9c0dbe69670f5695264452f5e40947590ec3a38cfddc9640ae8ff89"}, - {file = "yarl-1.9.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e700eb26635ce665c018c8cfea058baff9b843ed0cc77aa61849d807bb82a64c"}, - {file = "yarl-1.9.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c305c1bdf10869b5e51facf50bd5b15892884aeae81962ae4ba061fc11217103"}, - {file = "yarl-1.9.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5b7b307140231ea4f7aad5b69355aba2a67f2d7bc34271cffa3c9c324d35b27"}, - {file = "yarl-1.9.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a744bdeda6c86cf3025c94eb0e01ccabe949cf385cd75b6576a3ac9669404b68"}, - {file = "yarl-1.9.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e8ed183c7a8f75e40068333fc185566472a8f6c77a750cf7541e11810576ea5"}, - {file = "yarl-1.9.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1db9a4384694b5d20bdd9cb53f033b0831ac816416ab176c8d0997835015d22"}, - {file = "yarl-1.9.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:70194da6e99713250aa3f335a7fa246b36adf53672a2bcd0ddaa375d04e53dc0"}, - {file = "yarl-1.9.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ddad5cfcda729e22422bb1c85520bdf2770ce6d975600573ac9017fe882f4b7e"}, - {file = "yarl-1.9.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ca35996e0a4bed28fa0640d9512d37952f6b50dea583bcc167d4f0b1e112ac7f"}, - {file = "yarl-1.9.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:61ec0e80970b21a8f3c4b97fa6c6d181c6c6a135dbc7b4a601a78add3feeb209"}, - {file = "yarl-1.9.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9636e4519f6c7558fdccf8f91e6e3b98df2340dc505c4cc3286986d33f2096c2"}, - {file = "yarl-1.9.11-cp38-cp38-win32.whl", hash = "sha256:58081cea14b8feda57c7ce447520e9d0a96c4d010cce54373d789c13242d7083"}, - {file = "yarl-1.9.11-cp38-cp38-win_amd64.whl", hash = "sha256:7d2dee7d6485807c0f64dd5eab9262b7c0b34f760e502243dd83ec09d647d5e1"}, - {file = "yarl-1.9.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d65ad67f981e93ea11f87815f67d086c4f33da4800cf2106d650dd8a0b79dda4"}, - {file = "yarl-1.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:752c0d33b4aacdb147871d0754b88f53922c6dc2aff033096516b3d5f0c02a0f"}, - {file = "yarl-1.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54cc24be98d7f4ff355ca2e725a577e19909788c0db6beead67a0dda70bd3f82"}, - {file = "yarl-1.9.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c82126817492bb2ebc946e74af1ffa10aacaca81bee360858477f96124be39a"}, - {file = "yarl-1.9.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8503989860d7ac10c85cb5b607fec003a45049cf7a5b4b72451e87893c6bb990"}, - {file = "yarl-1.9.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:475e09a67f8b09720192a170ad9021b7abf7827ffd4f3a83826317a705be06b7"}, - {file = "yarl-1.9.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afcac5bda602b74ff701e1f683feccd8cce0d5a21dbc68db81bf9bd8fd93ba56"}, - {file = "yarl-1.9.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaeffcb84faceb2923a94a8a9aaa972745d3c728ab54dd011530cc30a3d5d0c1"}, - {file = "yarl-1.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:51a6f770ac86477cd5c553f88a77a06fe1f6f3b643b053fcc7902ab55d6cbe14"}, - {file = "yarl-1.9.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3fcd056cb7dff3aea5b1ee1b425b0fbaa2fbf6a1c6003e88caf524f01de5f395"}, - {file = "yarl-1.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:21e56c30e39a1833e4e3fd0112dde98c2abcbc4c39b077e6105c76bb63d2aa04"}, - {file = "yarl-1.9.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0a205ec6349879f5e75dddfb63e069a24f726df5330b92ce76c4752a436aac01"}, - {file = "yarl-1.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a5706821e1cf3c70dfea223e4e0958ea354f4e2af9420a1bd45c6b547297fb97"}, - {file = "yarl-1.9.11-cp39-cp39-win32.whl", hash = "sha256:cc295969f8c2172b5d013c0871dccfec7a0e1186cf961e7ea575d47b4d5cbd32"}, - {file = "yarl-1.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:55a67dd29367ce7c08a0541bb602ec0a2c10d46c86b94830a1a665f7fd093dfa"}, - {file = "yarl-1.9.11-py3-none-any.whl", hash = "sha256:c6f6c87665a9e18a635f0545ea541d9640617832af2317d4f5ad389686b4ed3d"}, - {file = "yarl-1.9.11.tar.gz", hash = "sha256:c7548a90cb72b67652e2cd6ae80e2683ee08fde663104528ac7df12d8ef271d2"}, + {file = "yarl-1.11.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0a657db1b9982f3dac0e360614d0e8945d2873da6e681fb7fca23ef1c3eb37f8"}, + {file = "yarl-1.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:65a1a05efca52b102691e64db5fcf973030a1c88fee393804ff91f99c95a6e74"}, + {file = "yarl-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f4cb417d380e2d77961eecec75aaaf6f7ab14e6de26eb3a498f498029a6556a1"}, + {file = "yarl-1.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8aee7c8378c6aa3103b99d1eb9995268ef730fa9f88ea68b9eee4341e204eec9"}, + {file = "yarl-1.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84624db40e2358cfd5cf2558b1aaffd93366d27ee32228a97785f2ec87d44a17"}, + {file = "yarl-1.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a596bb15e036952549871a4ccd2205679902dc7f241e3ced6b2ab2e44c55795"}, + {file = "yarl-1.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9d4d2cc4b076c8ad0175a15ee9482a387b3303c97d4b71062db7356b2ac04c7"}, + {file = "yarl-1.11.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f8bc849004122591104793a576e9c747b0e5d9486d6a30225521b817255748"}, + {file = "yarl-1.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e38176a559edde0cfff4b663791a007a5f9f90c73aee1d6f7ddbcf6bfb7287b3"}, + {file = "yarl-1.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:706ac0f77b45e9e0278ec6c98929764e119d3ce3136792b6475e7ae961da53ec"}, + {file = "yarl-1.11.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:48bac099586cf75ae5837b0ac17a674450d01f451f38afcb02acfc940110b60b"}, + {file = "yarl-1.11.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:540fd5f62fe21f3d1d9efe8af5c4d9dbbb184ce03ce95acb0289500e46215dd2"}, + {file = "yarl-1.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05ab59db0bb64e847972373c5cda8924e6605480f6b13cc04573fa0d87bfc637"}, + {file = "yarl-1.11.0-cp310-cp310-win32.whl", hash = "sha256:ddab47748933ac9cf5f29d6e9e2e2060cff40b2751d02c55129661ea4e577152"}, + {file = "yarl-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:976d02274e6d88b24c7131e7b26a083412b2592f2bbcef53d3b00b2508cad26c"}, + {file = "yarl-1.11.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:39e3087e1ef70862de81e22af9eb299faee580f41673ef92829949022791b521"}, + {file = "yarl-1.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7fd535cc41b81a566ad347081b671ab5c7e5f5b6a15526d85b4e748baf065cf0"}, + {file = "yarl-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f7cc02d8e9a612174869f4b983f159e87659096f7e2dc1fe9effd9902e408739"}, + {file = "yarl-1.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30f391ccf4b1b1e0ba4880075ba337d41a619a5350f67053927f67ebe764bf44"}, + {file = "yarl-1.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c19a0d95943bb2c914b4e71043803be34bc75c08c4a6ca232bdc649a1e9ef1b"}, + {file = "yarl-1.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ead4d89eade0e09b8ef97877664abb0e2e8704787db5564f83658fdee5c36497"}, + {file = "yarl-1.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:195f7791bc23d5f2480efe53f935daf8a61661000dfbfbdd70dbd06397594fff"}, + {file = "yarl-1.11.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01a7905e662665ca8e058635377522bc3c98bdb873be761ff42c86eb72b03914"}, + {file = "yarl-1.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53c80b1927b75aed208d7fd965a3a705dc8c1db4d50b9112418fa0f7784363e6"}, + {file = "yarl-1.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:11af21bbf807688d49b7d4915bb28cbc2e3aa028a2ee194738477eabcc413c65"}, + {file = "yarl-1.11.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:732d56da00ea7a5da4f0d15adbbd22dcb37da7825510aafde40112e53f6baa52"}, + {file = "yarl-1.11.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7bd54d79025b59d1dc5fb26a09734d6a9cc651a04bc381966ed264b28331a168"}, + {file = "yarl-1.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aacd62ff67efd54cb18cea2aa7ae4fb83cfbca19a07055d4777266b70561defe"}, + {file = "yarl-1.11.0-cp311-cp311-win32.whl", hash = "sha256:68e14ae71e5b51c8282ae5db53ccb3baffc40e1551370a8a2361f1c1d8a0bf8c"}, + {file = "yarl-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:3ade2265716667b6bd4123d6f684b5f7cf4a8d83dcf1d5581ac44643466bb00a"}, + {file = "yarl-1.11.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6e73dab98e3c3b5441720153e72a5f28e717aac2d22f1ec4b08ef33417d9987e"}, + {file = "yarl-1.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4a0d090d296ced05edfe29c6ff34869412fa6a97d0928c12b00939c4842884cd"}, + {file = "yarl-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d29e446cfb0a82d3df7745968b9fa286665a9be8b4d68de46bcc32d917cb218e"}, + {file = "yarl-1.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8dc0efcf8266ecfe057b95e01f43eb62516196a4bbf3918fd1dcb8d0dc0dff"}, + {file = "yarl-1.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:202f5ec49ff163dcc767426deb55020a28078e61d6bbe1f80331d92bca53b236"}, + {file = "yarl-1.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8055b0d78ce1cafa657c4b455e22661e8d3b2834de66a0753c3567da47fcc4aa"}, + {file = "yarl-1.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ed3c7f64e820959d7f682ec2f559b4f4df723dc09df619d269853a4214a4b4"}, + {file = "yarl-1.11.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2371510367d39d74997acfdcd1dead17938c79c99365482821627f7838a8eba0"}, + {file = "yarl-1.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e24bb6a8be89ccc3ce8c47e8940fdfcb7429e9efbf65ce6fa3e7d122fcf0bcf0"}, + {file = "yarl-1.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:18ec42da256cfcb9b4cd5d253e04c291f69911a5228d1438a7d431c15ba0ae40"}, + {file = "yarl-1.11.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:418eeb8f228ea36c368bf6782ebd6016ecebfb1a8b90145ef6726ffcbba65ef8"}, + {file = "yarl-1.11.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:07e8cfb1dd7669a129f8fd5df1da65efa73aea77582bde2a3a837412e2863543"}, + {file = "yarl-1.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c458483711d393dad51340505c3fab3194748fd06bab311d2f8b5b7a7349e9a"}, + {file = "yarl-1.11.0-cp312-cp312-win32.whl", hash = "sha256:5b008c3127382503e7a1e12b4c3a3236e3dd833a4c62a066f4a0fbd650c655d2"}, + {file = "yarl-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc94be7472b9f88d7441340534a3ecae05c86ccfec7ba75ce5b6e4778b2bfc6e"}, + {file = "yarl-1.11.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a45e51ba3777031e0b20c1e7ab59114ed4e1884b3c1db48962c1d8d08aefb418"}, + {file = "yarl-1.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:765128029218eade3a01187cdd7f375977cc827505ed31828196c8ae9b622928"}, + {file = "yarl-1.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2516e238daf0339c8ac4dfab9d7cda9afad652ff073517f200d653d5d8371f7e"}, + {file = "yarl-1.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d10be62bee117f05b1ad75a6c2538ca9e5367342dc8a4f3c206c87dadbc1189c"}, + {file = "yarl-1.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50ceaeda771ee3e382291168c90c7ede62b63ecf3e181024bcfeb35c0ea6c84f"}, + {file = "yarl-1.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a601c99fc20fd0eea84e7bc0dc9e7f196f55a0ded67242d724988c754295538"}, + {file = "yarl-1.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42ff79371614764fc0a4ab8eaba9adb493bf9ad856e2a4664f6c754fc907a903"}, + {file = "yarl-1.11.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93fca4c9f88c17ead902b3f3285b2d039fc8f26d117e1441973ba64315109b54"}, + {file = "yarl-1.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e7dddf5f41395c84fc59e0ed5493b24bfeb39fb04823e880b52c8c55085d4695"}, + {file = "yarl-1.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ea501ea07e14ba6364ff2621bfc8b2381e5b1e10353927fa9a607057fd2b98e5"}, + {file = "yarl-1.11.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a4f7e470f2c9c8b8774a5bda72adfb8e9dc4ec32311fe9bdaa4921e36cf6659b"}, + {file = "yarl-1.11.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:361fdb3993431157302b7104d525092b5df4d7d346df5a5ffeee2d1ca8e0d15b"}, + {file = "yarl-1.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e300eaf5e0329ad31b3d53e2f3d26b4b6dff1217207c6ab1d4212967b54b2185"}, + {file = "yarl-1.11.0-cp313-cp313-win32.whl", hash = "sha256:f1e2d4ce72e06e38a16da3e9c24a0520dbc19018a69ef6ed57b6b38527cb275c"}, + {file = "yarl-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:fa9de2f87be58f714a230bd1f3ef3aad1ed65c9931146e3fc55f85fcbe6bacc3"}, + {file = "yarl-1.11.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:24da0b38274727fe9266d09229987e7f0efdb97beb94c0bb2d327d65f112e78d"}, + {file = "yarl-1.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0310eb2e63872de66047e05ad9982f2e53ad6405dc42fa60d7cc670bf6ca8aa8"}, + {file = "yarl-1.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:52433604340a4ab3d1f32281c6eb9ad9b47c99435b4212f763121bf7348c8c00"}, + {file = "yarl-1.11.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e2eb182d59f0845a79434003f94b4f61cd69465248f9388c2e5bf2191c9f7f"}, + {file = "yarl-1.11.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3dd10f0fe0e0f659926c1da791de5bef05fd48974ad74618c9168e302e2b7cc"}, + {file = "yarl-1.11.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:121d3798e4bb35a4321b2422cb887f80ea39f94bf52f0eb5cb2c168bb0043c9b"}, + {file = "yarl-1.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8bbac56c80610dd659ace534765d7bcd2488f6600023f6984f35108b2b3f4f0"}, + {file = "yarl-1.11.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79d420399f0e82e302236a762d8b8ceec89761ce3b30c83ac1d4d6e29f811444"}, + {file = "yarl-1.11.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03a726fb50588307dfe1d233b67535d493fb0bb157bdbfda6bb34e04189f2f57"}, + {file = "yarl-1.11.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9057f5de2fade7440e6db358913bc7ae8de43ba72c83cf95420a1fc1a6c6b59e"}, + {file = "yarl-1.11.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6471d747d0ac8059895e66d32ca8630c8db5b572ca7763150d0927eaa257df67"}, + {file = "yarl-1.11.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:d97cb22ad380850754fa16ef8d490d9340d8573d81f73429f3975e8e87db0586"}, + {file = "yarl-1.11.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fe78dec8caeda1e7b353cbd8aa0cc5a5bc182b22998d64ec8fa9ee59c898ab3b"}, + {file = "yarl-1.11.0-cp38-cp38-win32.whl", hash = "sha256:7ff371002fbbb79613269d76a2932c99979dac15fac30107064ef70d25f35474"}, + {file = "yarl-1.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:4fa9d762eee63eed767895d68b994c58e29f809292a4d0fca483e9cc6fdc22c8"}, + {file = "yarl-1.11.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4ae63bc65e5bf8843bd1eca46e75eaa9eb157e0312fb362123181512892daad8"}, + {file = "yarl-1.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3d1bd3262e00043907e0a6d7d4f7b7a4815281acc25699a2384552870c79f1f0"}, + {file = "yarl-1.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c58656c2e0b41b5d325130b8da4f8e216aad10029e7de5c523a6be25faa9fe8"}, + {file = "yarl-1.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9425c333575fce5e0fb414b766492c6ba4aa335ef910a7540dbdefe58a78232e"}, + {file = "yarl-1.11.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dc66e2420e1e282105071934883bbb9c37c16901b5b8aa0a8aee370b477eac6"}, + {file = "yarl-1.11.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2949067359d1ef5bf3228c7f1deb102c209832a13df5419239f99449bc1d3fa9"}, + {file = "yarl-1.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006fe73f851cf20b9986b3b4cc15239795bd5da9c3fda76bb3e043da5bec4ff"}, + {file = "yarl-1.11.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:969ad4ee3892e893471b6572bbf2bbb091f93e7c81de25d6b3a5c0a5126e5ccb"}, + {file = "yarl-1.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c9fbe9dc6ee8bfe1af34137e3add6f0e49799dd5467dd6af189d27616879161e"}, + {file = "yarl-1.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69a45c711fea9b783b592a75f26f6dc59b2e4a923b97bf6eec357566fcb1d922"}, + {file = "yarl-1.11.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1a29b82c42a7791ffe53ee6dfbf29acc61ea7ec05643dcacc50510ed6187b897"}, + {file = "yarl-1.11.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ed0c090f00c3fc024f7b0799cab9dd7c419fcd8f1a00634d1f9952bab7e7bfb2"}, + {file = "yarl-1.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:31df9d9b3fe6e15decee629fc7976a5fb21eaa39e290f60e57e1d422827194c6"}, + {file = "yarl-1.11.0-cp39-cp39-win32.whl", hash = "sha256:fcb7c36ba8b663a5900e6d40533f0e698ba0f38f744aad5410d4e38129e41a70"}, + {file = "yarl-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c6c0d640bad721834a737e25267fb71d296684ada21ca7d5ad2e63da7b73f1b7"}, + {file = "yarl-1.11.0-py3-none-any.whl", hash = "sha256:03717a6627e55934b2a1d9caf24f299b461a2e8d048a90920f42ad5c20ae1b82"}, + {file = "yarl-1.11.0.tar.gz", hash = "sha256:f86f4f4a57a29ef08fa70c4667d04c5e3ba513500da95586208b285437cb9592"}, ] [package.dependencies] @@ -3024,7 +3024,6 @@ multidict = ">=4.0" [extras] betfair = ["betfair_parser"] -binance = ["pycryptodome"] docker = ["docker"] dydx = ["bech32", "bip-utils", "ecdsa", "pycryptodome", "v4-proto"] ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] @@ -3032,4 +3031,4 @@ ib = ["async-timeout", "defusedxml", "nautilus_ibapi"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "2b86479eb0d1b262df26ea21b66f037dd9eae4b44d522c1faeabbbc0eb9ef9ca" +content-hash = "7205906c621b25dbbffbbc5ceb62e4167cc15305d7708092c75480a4a7bac9af" diff --git a/pyproject.toml b/pyproject.toml index 00d25ed79fde..d08dc7e41779 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautilus_trader" -version = "1.200.0" +version = "1.201.0" description = "A high-performance algorithmic trading platform and event-driven backtester" authors = ["Nautech Systems "] license = "LGPL-3.0-or-later" @@ -77,7 +77,6 @@ pycryptodome = {version = "^3.20.0", optional = true} [tool.poetry.extras] betfair = ["betfair_parser"] -binance = ["pycryptodome"] docker = ["docker"] dydx = ["v4-proto", "bech32", "ecdsa", "bip-utils", "pycryptodome"] ib = ["nautilus_ibapi", "async-timeout", "defusedxml"] diff --git a/tests/integration_tests/adapters/binance/conftest.py b/tests/integration_tests/adapters/binance/conftest.py index 1fdb36c852a3..6ad25c38faa8 100644 --- a/tests/integration_tests/adapters/binance/conftest.py +++ b/tests/integration_tests/adapters/binance/conftest.py @@ -43,8 +43,8 @@ def live_logger(): def binance_http_client(loop, live_clock): client = BinanceHttpClient( clock=live_clock, - key="SOME_BINANCE_API_KEY", - secret="SOME_BINANCE_API_SECRET", + api_key="SOME_BINANCE_API_KEY", + api_secret="SOME_BINANCE_API_SECRET", base_url="https://api.binance.com/", # Spot/Margin ) return client diff --git a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_account.py b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_account.py index b36765bc6df6..ec184b3cda17 100644 --- a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_account.py +++ b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_account.py @@ -30,8 +30,8 @@ async def test_binance_spot_account_http_client(): client = get_cached_binance_http_client( clock=clock, account_type=BinanceAccountType.USDT_FUTURE, - key=os.getenv("BINANCE_FUTURES_TESTNET_API_KEY"), - secret=os.getenv("BINANCE_FUTURES_TESTNET_API_SECRET"), + api_key=os.getenv("BINANCE_FUTURES_TESTNET_API_KEY"), + api_secret=os.getenv("BINANCE_FUTURES_TESTNET_API_SECRET"), is_testnet=True, ) diff --git a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_instrument_provider.py b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_instrument_provider.py index 48c80f3aec85..02b7c912acd6 100644 --- a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_instrument_provider.py +++ b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_instrument_provider.py @@ -33,8 +33,8 @@ async def test_binance_futures_testnet_instrument_provider(): client = get_cached_binance_http_client( clock=clock, account_type=BinanceAccountType.USDT_FUTURE, - key=os.getenv("BINANCE_FUTURES_TESTNET_API_KEY"), - secret=os.getenv("BINANCE_FUTURES_TESTNET_API_SECRET"), + api_key=os.getenv("BINANCE_FUTURES_TESTNET_API_KEY"), + api_secret=os.getenv("BINANCE_FUTURES_TESTNET_API_SECRET"), is_testnet=True, ) diff --git a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_market.py b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_market.py index b11e13cac513..37fc1cea8cfd 100644 --- a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_market.py +++ b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_market.py @@ -32,8 +32,8 @@ async def test_binance_futures_testnet_market_http_client(): client = get_cached_binance_http_client( clock=clock, account_type=account_type, - key=os.getenv("BINANCE_FUTURES_TESTNET_API_KEY"), - secret=os.getenv("BINANCE_FUTURES_TESTNET_API_SECRET"), + api_key=os.getenv("BINANCE_FUTURES_TESTNET_API_KEY"), + api_secret=os.getenv("BINANCE_FUTURES_TESTNET_API_SECRET"), is_testnet=True, ) diff --git a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_wallet.py b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_wallet.py index 1454fa49b0ee..c4a74107ec11 100644 --- a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_wallet.py +++ b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_futures_testnet_wallet.py @@ -31,8 +31,8 @@ async def test_binance_futures_testnet_wallet_http_client(): client = get_cached_binance_http_client( clock=clock, account_type=BinanceAccountType.USDT_FUTURE, - key=os.getenv("BINANCE_FUTURES_TESTNET_API_KEY"), - secret=os.getenv("BINANCE_FUTURES_TESTNET_API_SECRET"), + api_key=os.getenv("BINANCE_FUTURES_TESTNET_API_KEY"), + api_secret=os.getenv("BINANCE_FUTURES_TESTNET_API_SECRET"), is_testnet=True, ) diff --git a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_spot_account.py b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_spot_account.py index 33645f08db7b..d8ec6a82ea6f 100644 --- a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_spot_account.py +++ b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_spot_account.py @@ -30,8 +30,8 @@ async def test_binance_spot_account_http_client(): client = get_cached_binance_http_client( clock=clock, account_type=BinanceAccountType.SPOT, - key=os.getenv("BINANCE_API_KEY"), - secret=os.getenv("BINANCE_API_SECRET"), + api_key=os.getenv("BINANCE_API_KEY"), + api_secret=os.getenv("BINANCE_API_SECRET"), ) http_account = BinanceSpotAccountHttpAPI(client=client) diff --git a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_spot_instrument_provider.py b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_spot_instrument_provider.py index 031e86174489..4e690d4151cf 100644 --- a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_spot_instrument_provider.py +++ b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_spot_instrument_provider.py @@ -30,8 +30,8 @@ async def test_binance_spot_instrument_provider(): client = get_cached_binance_http_client( clock=clock, account_type=BinanceAccountType.SPOT, - key=os.getenv("BINANCE_API_KEY"), - secret=os.getenv("BINANCE_API_SECRET"), + api_key=os.getenv("BINANCE_API_KEY"), + api_secret=os.getenv("BINANCE_API_SECRET"), is_testnet=True, # <-- add this argument to use the testnet ) diff --git a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_spot_user.py b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_spot_user.py index 7662bb0adf43..cacc0a3c9d05 100644 --- a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_spot_user.py +++ b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_spot_user.py @@ -31,8 +31,8 @@ async def test_binance_spot_account_http_client(): client = get_cached_binance_http_client( clock=clock, account_type=BinanceAccountType.SPOT, - key=os.getenv("BINANCE_API_KEY"), - secret=os.getenv("BINANCE_API_SECRET"), + api_key=os.getenv("BINANCE_API_KEY"), + api_secret=os.getenv("BINANCE_API_SECRET"), ) user = BinanceSpotUserDataHttpAPI(client=client) diff --git a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_wallet.py b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_wallet.py index d9d36d0d3149..fa0bca7bac53 100644 --- a/tests/integration_tests/adapters/binance/sandbox/sandbox_http_wallet.py +++ b/tests/integration_tests/adapters/binance/sandbox/sandbox_http_wallet.py @@ -31,8 +31,8 @@ async def test_binance_spot_wallet_http_client(): client = get_cached_binance_http_client( clock=clock, account_type=BinanceAccountType.SPOT, - key=os.getenv("BINANCE_API_KEY"), - secret=os.getenv("BINANCE_API_SECRET"), + api_key=os.getenv("BINANCE_API_KEY"), + api_secret=os.getenv("BINANCE_API_SECRET"), ) wallet = BinanceSpotWalletHttpAPI(clock=clock, client=client) diff --git a/tests/integration_tests/adapters/binance/sandbox/sandbox_ws_spot_user.py b/tests/integration_tests/adapters/binance/sandbox/sandbox_ws_spot_user.py index 30f02d834758..62e3335e73f0 100644 --- a/tests/integration_tests/adapters/binance/sandbox/sandbox_ws_spot_user.py +++ b/tests/integration_tests/adapters/binance/sandbox/sandbox_ws_spot_user.py @@ -32,8 +32,8 @@ async def test_binance_websocket_client(): client = get_cached_binance_http_client( clock=clock, account_type=BinanceAccountType.SPOT, - key=os.getenv("BINANCE_API_KEY"), - secret=os.getenv("BINANCE_API_SECRET"), + api_key=os.getenv("BINANCE_API_KEY"), + api_secret=os.getenv("BINANCE_API_SECRET"), ) user = BinanceSpotUserDataHttpAPI(client=client) diff --git a/tests/integration_tests/adapters/binance/test_data_spot.py b/tests/integration_tests/adapters/binance/test_data_spot.py index d661c58b543d..62cd2141e921 100644 --- a/tests/integration_tests/adapters/binance/test_data_spot.py +++ b/tests/integration_tests/adapters/binance/test_data_spot.py @@ -65,8 +65,8 @@ def setup(self): self.http_client = BinanceHttpClient( clock=self.clock, - key="SOME_BINANCE_API_KEY", - secret="SOME_BINANCE_API_SECRET", + api_key="SOME_BINANCE_API_KEY", + api_secret="SOME_BINANCE_API_SECRET", base_url="https://api.binance.com/", # Spot/Margin ) diff --git a/tests/integration_tests/adapters/binance/test_execution_futures.py b/tests/integration_tests/adapters/binance/test_execution_futures.py index 0a59b600d576..2f6ff88b3f51 100644 --- a/tests/integration_tests/adapters/binance/test_execution_futures.py +++ b/tests/integration_tests/adapters/binance/test_execution_futures.py @@ -71,8 +71,8 @@ def setup(self): self.http_client = BinanceHttpClient( clock=self.clock, - key="SOME_BINANCE_API_KEY", - secret="SOME_BINANCE_API_SECRET", + api_key="SOME_BINANCE_API_KEY", + api_secret="SOME_BINANCE_API_SECRET", base_url="https://api.binance.com/", # Spot/Margin ) diff --git a/tests/integration_tests/adapters/binance/test_execution_spot.py b/tests/integration_tests/adapters/binance/test_execution_spot.py index d7079c85c094..b9902309f454 100644 --- a/tests/integration_tests/adapters/binance/test_execution_spot.py +++ b/tests/integration_tests/adapters/binance/test_execution_spot.py @@ -71,8 +71,8 @@ def setup(self): self.http_client = BinanceHttpClient( clock=self.clock, - key="SOME_BINANCE_API_KEY", - secret="SOME_BINANCE_API_SECRET", + api_key="SOME_BINANCE_API_KEY", + api_secret="SOME_BINANCE_API_SECRET", base_url="https://api.binance.com/", # Spot/Margin ) diff --git a/tests/integration_tests/adapters/binance/test_http_account.py b/tests/integration_tests/adapters/binance/test_http_account.py index 262c7800ccc9..3c7698a5287f 100644 --- a/tests/integration_tests/adapters/binance/test_http_account.py +++ b/tests/integration_tests/adapters/binance/test_http_account.py @@ -33,8 +33,8 @@ def setup(self): self.clock = LiveClock() self.client = BinanceHttpClient( clock=self.clock, - key="SOME_BINANCE_API_KEY", - secret="SOME_BINANCE_API_SECRET", + api_key="SOME_BINANCE_API_KEY", + api_secret="SOME_BINANCE_API_SECRET", base_url="https://api.binance.com/", # Spot/Margin ) @@ -42,8 +42,8 @@ def setup(self): self.futures_client = BinanceHttpClient( clock=self.clock, - key="SOME_BINANCE_FUTURES_API_KEY", - secret="SOME_BINANCE_FUTURES_API_SECRET", + api_key="SOME_BINANCE_FUTURES_API_KEY", + api_secret="SOME_BINANCE_FUTURES_API_SECRET", base_url="https://fapi.binance.com/", # Futures ) diff --git a/tests/integration_tests/adapters/binance/test_http_market.py b/tests/integration_tests/adapters/binance/test_http_market.py index 9387d2fde3bd..ed9b1d21ab16 100644 --- a/tests/integration_tests/adapters/binance/test_http_market.py +++ b/tests/integration_tests/adapters/binance/test_http_market.py @@ -27,8 +27,8 @@ def setup(self): clock = LiveClock() self.client = BinanceHttpClient( clock=clock, - key="SOME_BINANCE_API_KEY", - secret="SOME_BINANCE_API_SECRET", + api_key="SOME_BINANCE_API_KEY", + api_secret="SOME_BINANCE_API_SECRET", base_url="https://api.binance.com/", # Spot/Margin ) diff --git a/tests/integration_tests/adapters/binance/test_http_user.py b/tests/integration_tests/adapters/binance/test_http_user.py index 95b734d9176a..fc546db6cec9 100644 --- a/tests/integration_tests/adapters/binance/test_http_user.py +++ b/tests/integration_tests/adapters/binance/test_http_user.py @@ -29,8 +29,8 @@ def setup(self): clock = LiveClock() self.client = BinanceHttpClient( clock=clock, - key="SOME_BINANCE_API_KEY", - secret="SOME_BINANCE_API_SECRET", + api_key="SOME_BINANCE_API_KEY", + api_secret="SOME_BINANCE_API_SECRET", base_url="https://api.binance.com/", # Spot/Margin ) self.test_symbol = "ETHUSDT" diff --git a/tests/integration_tests/adapters/binance/test_http_wallet.py b/tests/integration_tests/adapters/binance/test_http_wallet.py index 074067268d0e..45f94078b176 100644 --- a/tests/integration_tests/adapters/binance/test_http_wallet.py +++ b/tests/integration_tests/adapters/binance/test_http_wallet.py @@ -31,8 +31,8 @@ def setup(self): clock = LiveClock() self.client = BinanceHttpClient( clock=clock, - key="SOME_BINANCE_API_KEY", - secret="SOME_BINANCE_API_SECRET", + api_key="SOME_BINANCE_API_KEY", + api_secret="SOME_BINANCE_API_SECRET", base_url="https://api.binance.com/", # Spot/Margin ) diff --git a/tests/unit_tests/execution/test_emulator.py b/tests/unit_tests/execution/test_emulator.py index 804c54dedb1b..088e484fe010 100644 --- a/tests/unit_tests/execution/test_emulator.py +++ b/tests/unit_tests/execution/test_emulator.py @@ -17,6 +17,7 @@ import pytest +from nautilus_trader.backtest.data_client import BacktestMarketDataClient from nautilus_trader.cache.cache import Cache from nautilus_trader.common.component import MessageBus from nautilus_trader.common.component import TestClock @@ -124,6 +125,13 @@ def setup(self) -> None: ) self.venue = Venue("BINANCE") + self.data_client = BacktestMarketDataClient( + client_id=ClientId(self.venue.value), + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + ) + self.exec_client = MockExecutionClient( client_id=ClientId(self.venue.value), venue=self.venue, @@ -136,6 +144,7 @@ def setup(self) -> None: update = TestEventStubs.margin_account_state(account_id=AccountId("BINANCE-001")) self.portfolio.update_account(update) + self.data_engine.register_client(self.data_client) self.exec_engine.register_client(self.exec_client) self.strategy = Strategy() @@ -634,6 +643,56 @@ def test_submit_stop_limit_order_then_triggered_releases_limit_order( assert isinstance(order.events[3], OrderReleased) assert self.exec_client.calls == ["_start", "submit_order"] + @pytest.mark.parametrize( + ("order_side", "trigger_price"), + [ + [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_from_deltas_releases_limit_order( + self, + order_side: OrderSide, + trigger_price: TriggerType, + ) -> None: + # Arrange + order = self.strategy.order_factory.stop_limit( + instrument_id=ETHUSDT_PERP_BINANCE.id, + order_side=order_side, + quantity=Quantity.from_int(10), + price=trigger_price, + trigger_price=trigger_price, + emulation_trigger=TriggerType.DEFAULT, + ) + + self.strategy.submit_order(order) + + order1 = TestDataStubs.order(price=5_000, side=OrderSide.BUY) + delta1 = TestDataStubs.order_book_delta(order=order1) + + order2 = TestDataStubs.order(price=5_000, side=OrderSide.SELL) + delta2 = TestDataStubs.order_book_delta(order=order2) + + deltas = TestDataStubs.order_book_deltas( + instrument_id=ETHUSDT_PERP_BINANCE.id, + deltas=[delta1, delta2], + ) + + # Act + self.data_engine.process(deltas) + + # Assert + order = self.cache.order(order.client_order_id) # Recover transformed order from cache + assert order.order_type == OrderType.LIMIT + assert order.emulation_trigger == TriggerType.NO_TRIGGER + assert order.is_active_local + assert len(order.events) == 4 + assert isinstance(order.events[0], OrderInitialized) + assert isinstance(order.events[1], OrderEmulated) + assert isinstance(order.events[2], OrderInitialized) + assert isinstance(order.events[3], OrderReleased) + assert self.exec_client.calls == ["_start", "submit_order"] + @pytest.mark.parametrize( ("order_side", "trigger_price"), [ diff --git a/version.json b/version.json index ee915e792d80..ae60999f64ca 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "", - "message": "v1.200.0", + "message": "v1.201.0", "color": "orange" }