From 762d7238a80916761a49f61bd2214ffaedc67e31 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Nov 2024 21:06:42 +1100 Subject: [PATCH 01/78] Bump version --- RELEASES.md | 21 ++++++++++++++++++++- nautilus_core/Cargo.lock | 38 +++++++++++++++++++------------------- nautilus_core/Cargo.toml | 2 +- pyproject.toml | 2 +- version.json | 2 +- 5 files changed, 42 insertions(+), 23 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 813a9f2e075d..5b75de258ae4 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,11 +1,30 @@ +# NautilusTrader 1.207.0 Beta + +Released on TBD (UTC). + +### Enhancements +None + +### Internal Improvements +- Upgraded `datafusion` crate to v0.43.0 (#2056), thanks @twitu + +### Breaking Changes +None + +### Fixes +None + +--- + # NautilusTrader 1.206.0 Beta Released on 17th November 2024 (UTC). ### Enhancements +- Added `TardisDataClient` providing live data streams from a Tardis Machine WebSocket server +- Added `TardisInstrumentProvider` providing instrument definitions from Tardis through the HTTP instrument metadata API - Added `Portfolio.realized_pnl(...)` method for per instrument realized PnL (based on positions) - Added `Portfolio.realized_pnls(...)` method for per venue realized PnL (based on positions) -- Added `TardisInstrumentProvider` - Added `PolymarketExecClientConfig.generate_order_history_from_trades` config setting (default False and not currently recommended) - Added configuration warning for `InstrumentProvider` (to warn when node starts with no instrument loading) - Implemented Tardis optional [symbol normalization](https://nautilustrader.io/docs/nightly/integrations/tardis/#symbology-and-normalization) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 7c16b4acbd37..5adc6e26d972 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2852,7 +2852,7 @@ dependencies = [ [[package]] name = "nautilus-adapters" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "arrow", @@ -2897,7 +2897,7 @@ dependencies = [ [[package]] name = "nautilus-analysis" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "criterion", @@ -2910,7 +2910,7 @@ dependencies = [ [[package]] name = "nautilus-backtest" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "cbindgen", @@ -2932,7 +2932,7 @@ dependencies = [ [[package]] name = "nautilus-cli" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "clap", @@ -2948,7 +2948,7 @@ dependencies = [ [[package]] name = "nautilus-common" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "bytes", @@ -2980,7 +2980,7 @@ dependencies = [ [[package]] name = "nautilus-core" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "bytes", @@ -3002,7 +3002,7 @@ dependencies = [ [[package]] name = "nautilus-cryptography" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "base64", @@ -3021,7 +3021,7 @@ dependencies = [ [[package]] name = "nautilus-data" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "chrono", @@ -3042,7 +3042,7 @@ dependencies = [ [[package]] name = "nautilus-execution" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "criterion", @@ -3061,7 +3061,7 @@ dependencies = [ [[package]] name = "nautilus-indicators" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "log", @@ -3074,7 +3074,7 @@ dependencies = [ [[package]] name = "nautilus-infrastructure" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "async-stream", @@ -3105,7 +3105,7 @@ dependencies = [ [[package]] name = "nautilus-model" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "cbindgen", @@ -3136,7 +3136,7 @@ dependencies = [ [[package]] name = "nautilus-network" -version = "0.36.0" +version = "0.37.0" dependencies = [ "axum", "bytes", @@ -3164,7 +3164,7 @@ dependencies = [ [[package]] name = "nautilus-persistence" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "binary-heap-plus", @@ -3189,7 +3189,7 @@ dependencies = [ [[package]] name = "nautilus-portfolio" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "criterion", @@ -3214,7 +3214,7 @@ dependencies = [ [[package]] name = "nautilus-pyo3" -version = "0.36.0" +version = "0.37.0" dependencies = [ "nautilus-adapters", "nautilus-common", @@ -3232,7 +3232,7 @@ dependencies = [ [[package]] name = "nautilus-risk" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "criterion", @@ -3251,7 +3251,7 @@ dependencies = [ [[package]] name = "nautilus-serialization" -version = "0.36.0" +version = "0.37.0" dependencies = [ "arrow", "criterion", @@ -3266,7 +3266,7 @@ dependencies = [ [[package]] name = "nautilus-test-kit" -version = "0.36.0" +version = "0.37.0" dependencies = [ "anyhow", "axum", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 5efec5873728..e5dfb90a0902 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -24,7 +24,7 @@ members = [ [workspace.package] rust-version = "1.82.0" -version = "0.36.0" +version = "0.37.0" edition = "2021" authors = ["Nautech Systems "] description = "A high-performance algorithmic trading platform and event-driven backtester" diff --git a/pyproject.toml b/pyproject.toml index 2db00275b91b..46390cf55646 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautilus_trader" -version = "1.206.0" +version = "1.207.0" description = "A high-performance algorithmic trading platform and event-driven backtester" authors = ["Nautech Systems "] license = "LGPL-3.0-or-later" diff --git a/version.json b/version.json index e8e12dded037..13e50e00215a 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "", - "message": "v1.206.0", + "message": "v1.207.0", "color": "orange" } From 3896902cfcd118960ff1c5799e6d79f7efd93188 Mon Sep 17 00:00:00 2001 From: Ishan Bhanuka Date: Sun, 17 Nov 2024 05:13:22 -0500 Subject: [PATCH 02/78] Upgrade datafusion (#2056) --- nautilus_core/Cargo.lock | 100 ++++++++++-------- nautilus_core/persistence/Cargo.toml | 2 +- .../serialization/src/arrow/trade.rs | 25 ++++- 3 files changed, 80 insertions(+), 47 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 5adc6e26d972..c51d14485710 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -1255,9 +1255,9 @@ dependencies = [ [[package]] name = "datafusion" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae5f2abc725737d6e87b6d348a5aa2d0a77e4cf873045f004546da946e6e619" +checksum = "cbba0799cf6913b456ed07a94f0f3b6e12c62a5d88b10809e2284a0f2b915c05" dependencies = [ "ahash 0.8.11", "arrow", @@ -1311,9 +1311,9 @@ dependencies = [ [[package]] name = "datafusion-catalog" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998761705551f11ffa4ee692cc285b44eb1def6e0d28c4eaf5041b9e2810dc1e" +checksum = "7493c5c2d40eec435b13d92e5703554f4efc7059451fcb8d3a79580ff0e45560" dependencies = [ "arrow-schema", "async-trait", @@ -1326,9 +1326,9 @@ dependencies = [ [[package]] name = "datafusion-common" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11986f191e88d950f10a5cc512a598afba27d92e04a0201215ad60785005115a" +checksum = "24953049ebbd6f8964f91f60aa3514e121b5e81e068e33b60e77815ab369b25c" dependencies = [ "ahash 0.8.11", "arrow", @@ -1338,6 +1338,7 @@ dependencies = [ "chrono", "half", "hashbrown 0.14.5", + "indexmap", "instant", "libc", "num_cpus", @@ -1351,9 +1352,9 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "694c9d7ea1b82f95768215c4cb5c2d5c613690624e832a7ee64be563139d582f" +checksum = "f06df4ef76872e11c924d3c814fd2a8dd09905ed2e2195f71c857d78abd19685" dependencies = [ "log", "tokio", @@ -1361,9 +1362,9 @@ dependencies = [ [[package]] name = "datafusion-execution" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b4cedcd98151e0a297f34021b6b232ff0ebc0f2f18ea5e7446b5ebda99b1a1" +checksum = "6bbdcb628d690f3ce5fea7de81642b514486d58ff9779a51f180a69a4eadb361" dependencies = [ "arrow", "chrono", @@ -1382,9 +1383,9 @@ dependencies = [ [[package]] name = "datafusion-expr" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8dd114dc0296cacaee98ad3165724529fcca9a65b2875abcd447b9cc02b2b74" +checksum = "8036495980e3131f706b7d33ab00b4492d73dc714e3cb74d11b50f9602a73246" dependencies = [ "ahash 0.8.11", "arrow", @@ -1394,7 +1395,9 @@ dependencies = [ "datafusion-common", "datafusion-expr-common", "datafusion-functions-aggregate-common", + "datafusion-functions-window-common", "datafusion-physical-expr-common", + "indexmap", "paste", "serde_json", "sqlparser", @@ -1404,20 +1407,21 @@ dependencies = [ [[package]] name = "datafusion-expr-common" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d1ba2bb018218d9260bbd7de6a46a20f61b93d4911dba8aa07735625004c4fb" +checksum = "4da0f3cb4669f9523b403d6b5a0ec85023e0ab3bf0183afd1517475b3e64fdd2" dependencies = [ "arrow", "datafusion-common", + "itertools 0.13.0", "paste", ] [[package]] name = "datafusion-functions" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "547cb780a4ac51fd8e52c0fb9188bc16cea4e35aebf6c454bda0b82a7a417304" +checksum = "f52c4012648b34853e40a2c6bcaa8772f837831019b68aca384fb38436dba162" dependencies = [ "arrow", "arrow-buffer", @@ -1438,9 +1442,9 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e68cf5aa7ebcac08bd04bb709a9a6d4963eafd227da62b628133bc509c40f5a0" +checksum = "e5b8bb624597ba28ed7446df4a9bd7c7a7bde7c578b6b527da3f47371d5f6741" dependencies = [ "ahash 0.8.11", "arrow", @@ -1452,16 +1456,16 @@ dependencies = [ "datafusion-physical-expr", "datafusion-physical-expr-common", "half", + "indexmap", "log", "paste", - "sqlparser", ] [[package]] name = "datafusion-functions-aggregate-common" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2285d080dfecdfb8605b0ab2f1a41e2473208dc8e9bd6f5d1dbcfe97f517e6f" +checksum = "6fb06208fc470bc8cf1ce2d9a1159d42db591f2c7264a8c1776b53ad8f675143" dependencies = [ "ahash 0.8.11", "arrow", @@ -1473,21 +1477,34 @@ dependencies = [ [[package]] name = "datafusion-functions-window" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e78d30ebd6e9f74d4aeddec32744f5a18b5f9584591bc586fb5259c4848bac5" +checksum = "5ae23356c634e54c59f7c51acb7a5b9f6240ffb2cf997049a1a24a8a88598dbe" dependencies = [ "datafusion-common", "datafusion-expr", + "datafusion-functions-window-common", + "datafusion-physical-expr", "datafusion-physical-expr-common", "log", + "paste", +] + +[[package]] +name = "datafusion-functions-window-common" +version = "43.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b3d6ff7794acea026de36007077a06b18b89e4f9c3fea7f2215f9f7dd9059b" +dependencies = [ + "datafusion-common", + "datafusion-physical-expr-common", ] [[package]] name = "datafusion-optimizer" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be172c44bf344df707e0c041fa3f41e6dc5fb0976f539c68bc442bca150ee58c" +checksum = "bec6241eb80c595fa0e1a8a6b69686b5cf3bd5fdacb8319582a0943b0bd788aa" dependencies = [ "arrow", "async-trait", @@ -1505,9 +1522,9 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b86b7fa0b8161c49b0f005b0df193fc6d9b65ceec675f155422cda5d1583ca" +checksum = "3370357b8fc75ec38577700644e5d1b0bc78f38babab99c0b8bd26bafb3e4335" dependencies = [ "ahash 0.8.11", "arrow", @@ -1516,30 +1533,26 @@ dependencies = [ "arrow-ord", "arrow-schema", "arrow-string", - "base64", "chrono", "datafusion-common", - "datafusion-execution", "datafusion-expr", "datafusion-expr-common", "datafusion-functions-aggregate-common", "datafusion-physical-expr-common", "half", "hashbrown 0.14.5", - "hex", "indexmap", "itertools 0.13.0", "log", "paste", "petgraph", - "regex", ] [[package]] name = "datafusion-physical-expr-common" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "242ba8a26351d9ca16295814c46743b0d1b00ec372174bdfbba991d0953dd596" +checksum = "b8b7734d94bf2fa6f6e570935b0ddddd8421179ce200065be97874e13d46a47b" dependencies = [ "ahash 0.8.11", "arrow", @@ -1551,13 +1564,15 @@ dependencies = [ [[package]] name = "datafusion-physical-optimizer" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ca088eb904bf1cfc9c5e5653110c70a6eaba43164085a9d180b35b77ce3b8b" +checksum = "7eee8c479522df21d7b395640dff88c5ed05361852dce6544d7c98e9dbcebffe" dependencies = [ + "arrow", "arrow-schema", "datafusion-common", "datafusion-execution", + "datafusion-expr-common", "datafusion-physical-expr", "datafusion-physical-plan", "itertools 0.13.0", @@ -1565,9 +1580,9 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4989a53b824abc759685eb643f4d604c2fc2fea4e2c309ac3473bea263ecbbeb" +checksum = "17e1fc2e2c239d14e8556f2622b19a726bf6bc6962cc00c71fc52626274bee24" dependencies = [ "ahash 0.8.11", "arrow", @@ -1581,8 +1596,8 @@ dependencies = [ "datafusion-common-runtime", "datafusion-execution", "datafusion-expr", - "datafusion-functions-aggregate", "datafusion-functions-aggregate-common", + "datafusion-functions-window-common", "datafusion-physical-expr", "datafusion-physical-expr-common", "futures", @@ -1600,15 +1615,16 @@ dependencies = [ [[package]] name = "datafusion-sql" -version = "42.2.0" +version = "43.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b9b75b9da10ed656073ac0553708f17eb8fa5a7b065ef9848914c93150ab9e" +checksum = "63e3a4ed41dbee20a5d947a59ca035c225d67dc9cbe869c10f66dcdf25e7ce51" dependencies = [ "arrow", "arrow-array", "arrow-schema", "datafusion-common", "datafusion-expr", + "indexmap", "log", "regex", "sqlparser", @@ -4852,9 +4868,9 @@ dependencies = [ [[package]] name = "sqlparser" -version = "0.50.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5b515a2bd5168426033e9efbfd05500114833916f1d5c268f938b4ee130ac" +checksum = "5fe11944a61da0da3f592e19a45ebe5ab92dc14a779907ff1f08fbb797bfefc7" dependencies = [ "log", "sqlparser_derive", diff --git a/nautilus_core/persistence/Cargo.toml b/nautilus_core/persistence/Cargo.toml index fb3b9ec194ce..45243957e0c5 100644 --- a/nautilus_core/persistence/Cargo.toml +++ b/nautilus_core/persistence/Cargo.toml @@ -24,7 +24,7 @@ tokio = { workspace = true } thiserror = { workspace = true } binary-heap-plus = "0.5.0" compare = "0.1.0" -datafusion = { version = "42.2.0", default-features = false, features = ["compression", "regex_expressions", "unicode_expressions", "pyarrow"] } +datafusion = { version = "43.0.0", default-features = false, features = ["compression", "regex_expressions", "unicode_expressions", "pyarrow"] } [dev-dependencies] nautilus-test-kit = { path = "../test_kit" } diff --git a/nautilus_core/serialization/src/arrow/trade.rs b/nautilus_core/serialization/src/arrow/trade.rs index eb7e5faef50d..b90178c5162f 100644 --- a/nautilus_core/serialization/src/arrow/trade.rs +++ b/nautilus_core/serialization/src/arrow/trade.rs @@ -16,7 +16,7 @@ use std::{collections::HashMap, str::FromStr, sync::Arc}; use arrow::{ - array::{Int64Array, StringArray, StringBuilder, UInt64Array, UInt8Array}, + array::{Int64Array, StringArray, StringBuilder, StringViewArray, UInt64Array, UInt8Array}, datatypes::{DataType, Field, Schema}, error::ArrowError, record_batch::RecordBatch, @@ -130,10 +130,27 @@ impl DecodeFromRecordBatch for TradeTick { let size_values = extract_column::(cols, "size", 1, DataType::UInt64)?; let aggressor_side_values = extract_column::(cols, "aggressor_side", 2, DataType::UInt8)?; - let trade_id_values = extract_column::(cols, "trade_id", 3, DataType::Utf8)?; let ts_event_values = extract_column::(cols, "ts_event", 4, DataType::UInt64)?; let ts_init_values = extract_column::(cols, "ts_init", 5, DataType::UInt64)?; + // Datafusion reads trade_ids as StringView + let trade_id_values: Vec = if record_batch + .schema() + .field_with_name("trade_id")? + .data_type() + == &DataType::Utf8View + { + extract_column::(cols, "trade_id", 3, DataType::Utf8View)? + .iter() + .map(|id| TradeId::from(id.unwrap())) + .collect() + } else { + extract_column::(cols, "trade_id", 3, DataType::Utf8)? + .iter() + .map(|id| TradeId::from(id.unwrap())) + .collect() + }; + let result: Result, EncodingError> = (0..record_batch.num_rows()) .map(|i| { let price = Price::from_raw(price_values.value(i), price_precision); @@ -146,7 +163,7 @@ impl DecodeFromRecordBatch for TradeTick { format!("Invalid enum value, was {aggressor_side_value}"), ) })?; - let trade_id = TradeId::from(trade_id_values.value(i)); + let trade_id = trade_id_values[i]; let ts_event = ts_event_values.value(i).into(); let ts_init = ts_init_values.value(i).into(); @@ -184,7 +201,7 @@ mod tests { use std::sync::Arc; use arrow::{ - array::{Array, Int64Array, StringArray, UInt64Array, UInt8Array}, + array::{Array, Int64Array, UInt64Array, UInt8Array}, record_batch::RecordBatch, }; use rstest::rstest; From 855aee09eb2c1bfd3afc8d6b68057f29183387ff Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 17 Nov 2024 21:59:22 +1100 Subject: [PATCH 03/78] Update release notes --- RELEASES.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 5b75de258ae4..5fe1fafadf40 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -6,7 +6,7 @@ Released on TBD (UTC). None ### Internal Improvements -- Upgraded `datafusion` crate to v0.43.0 (#2056), thanks @twitu +- Upgraded `datafusion` crate to v43.0.0 (#2056), thanks @twitu ### Breaking Changes None @@ -25,7 +25,6 @@ Released on 17th November 2024 (UTC). - Added `TardisInstrumentProvider` providing instrument definitions from Tardis through the HTTP instrument metadata API - Added `Portfolio.realized_pnl(...)` method for per instrument realized PnL (based on positions) - Added `Portfolio.realized_pnls(...)` method for per venue realized PnL (based on positions) -- Added `PolymarketExecClientConfig.generate_order_history_from_trades` config setting (default False and not currently recommended) - Added configuration warning for `InstrumentProvider` (to warn when node starts with no instrument loading) - Implemented Tardis optional [symbol normalization](https://nautilustrader.io/docs/nightly/integrations/tardis/#symbology-and-normalization) - Implemented `WebSocketClient` reconnection retries (#2044), thanks @davidsblom From 984e323e799e8f55fafa74a0a4f623b8837f59d9 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Nov 2024 06:45:58 +1100 Subject: [PATCH 04/78] Cleanup GitHub workflows --- .github/workflows/build-wheels.yml | 2 +- .github/workflows/build.yml | 6 +++--- .github/workflows/coverage.yml | 2 +- .github/workflows/release.yml | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 19551acb0c4a..d6b0ae8dac3b 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -60,7 +60,7 @@ jobs: version: ${{ env.POETRY_VERSION }} - name: Install build dependencies - run: python -m pip install --upgrade pip setuptools wheel msgspec + run: python -m pip install --upgrade pip setuptools wheel - name: Set poetry cache-dir run: echo "POETRY_CACHE_DIR=$(poetry config cache-dir)" >> $GITHUB_ENV diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 95ab43d084a0..97b1dea6a158 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,7 +85,7 @@ jobs: version: ${{ env.POETRY_VERSION }} - name: Install build dependencies - run: python -m pip install --upgrade pip setuptools wheel pre-commit msgspec + run: python -m pip install --upgrade pip setuptools wheel pre-commit # ta-lib Python install currently broken # https://github.com/TA-Lib/ta-lib-python/issues/655 @@ -221,7 +221,7 @@ jobs: version: ${{ env.POETRY_VERSION }} - name: Install build dependencies - run: python -m pip install --upgrade pip setuptools wheel pre-commit msgspec + run: python -m pip install --upgrade pip setuptools wheel pre-commit - name: Cached pre-commit id: cached-pre-commit @@ -324,7 +324,7 @@ jobs: version: ${{ env.POETRY_VERSION }} - name: Install build dependencies - run: python -m pip install --upgrade pip setuptools wheel pre-commit msgspec + run: python -m pip install --upgrade pip setuptools wheel pre-commit - name: Cached pre-commit id: cached-pre-commit diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8892cb4bc785..301b36d33a37 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -77,7 +77,7 @@ jobs: version: ${{ env.POETRY_VERSION }} - name: Install build dependencies - run: python -m pip install --upgrade pip setuptools wheel pre-commit msgspec + run: python -m pip install --upgrade pip setuptools wheel pre-commit # ta-lib Python install currently broken # https://github.com/TA-Lib/ta-lib-python/issues/655 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e3668b2caa6..c769498694a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,7 +70,7 @@ jobs: version: ${{ env.POETRY_VERSION }} - name: Install build dependencies - run: python -m pip install --upgrade pip setuptools wheel pre-commit msgspec + run: python -m pip install --upgrade pip setuptools wheel pre-commit - name: Test pip installation run: pip install . @@ -128,7 +128,7 @@ jobs: version: ${{ env.POETRY_VERSION }} - name: Install build dependencies - run: python -m pip install --upgrade pip setuptools wheel pre-commit msgspec + run: python -m pip install --upgrade pip setuptools wheel pre-commit - name: Set poetry cache-dir run: echo "POETRY_CACHE_DIR=$(poetry config cache-dir)" >> $GITHUB_ENV @@ -217,7 +217,7 @@ jobs: version: ${{ env.POETRY_VERSION }} - name: Install build dependencies - run: python -m pip install --upgrade pip setuptools wheel pre-commit msgspec + run: python -m pip install --upgrade pip setuptools wheel pre-commit - name: Set poetry cache-dir run: echo "POETRY_CACHE_DIR=$(poetry config cache-dir)" >> $GITHUB_ENV @@ -324,7 +324,7 @@ jobs: version: ${{ env.POETRY_VERSION }} - name: Install build dependencies - run: python -m pip install --upgrade pip setuptools wheel pre-commit msgspec + run: python -m pip install --upgrade pip setuptools wheel pre-commit - name: Set poetry cache-dir run: echo "POETRY_CACHE_DIR=$(poetry config cache-dir)" >> $GITHUB_ENV From 92acf5c9651fd7585d529eb6a7aa2ca32d0559c7 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Nov 2024 07:19:26 +1100 Subject: [PATCH 05/78] Suppress Rust logging when off --- examples/live/databento/databento_subscriber.py | 4 ++-- nautilus_trader/system/kernel.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/live/databento/databento_subscriber.py b/examples/live/databento/databento_subscriber.py index 0251619edc68..350a247c73e2 100644 --- a/examples/live/databento/databento_subscriber.py +++ b/examples/live/databento/databento_subscriber.py @@ -49,7 +49,7 @@ # InstrumentId.from_str("ES.FUT.GLBX"), # InstrumentId.from_str("CL.FUT.GLBX"), # InstrumentId.from_str("LO.OPT.GLBX"), - # InstrumentId.from_str("AAPL.XNAS"), + # InstrumentId.from_str("AAPL.IEXG"), ] # Configure the trading node @@ -171,7 +171,7 @@ def on_start(self) -> None: # self.request_data(status_data_type, client_id=DATABENTO_CLIENT_ID) # from nautilus_trader.model.data import BarType - # self.request_bars(BarType.from_str(f"{instrument_id}-1-MINUTE-LAST-EXTERNAL")) + # self.request_bars(BarType.from_str(f"{instrument_id}-1-SECOND-LAST-EXTERNAL")) # # Imbalance # from nautilus_trader.adapters.databento import DatabentoImbalance diff --git a/nautilus_trader/system/kernel.py b/nautilus_trader/system/kernel.py index 9c926bd4a964..11a95a4d3af4 100644 --- a/nautilus_trader/system/kernel.py +++ b/nautilus_trader/system/kernel.py @@ -15,6 +15,7 @@ import asyncio import concurrent.futures +import os import platform import signal import socket @@ -170,6 +171,9 @@ def __init__( # noqa (too complex) logging: LoggingConfig = config.logging or LoggingConfig() if not is_logging_initialized(): + if "RUST_LOG" not in os.environ: + os.environ["RUST_LOG"] = "off" + if not logging.bypass_logging: if logging.clear_log_file and logging.log_directory and logging.log_file_name: file_path = Path( @@ -184,6 +188,7 @@ def __init__( # noqa (too complex) if logging.use_pyo3: set_logging_pyo3(True) + # Initialize tracing for async Rust nautilus_pyo3.init_tracing() From 8d678c831fd1affdb1aecb8d18f54f30bda800e5 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Nov 2024 07:39:53 +1100 Subject: [PATCH 06/78] Rename TriggerType::LastTrade to LastPrice More standard terminology and no enum namespace conflicts. --- RELEASES.md | 2 +- docs/concepts/orders.md | 10 +++--- docs/concepts/strategies.md | 2 +- .../crypto_ema_cross_ethusdt_trailing_stop.py | 2 +- .../crypto_ema_cross_with_binance_provider.py | 2 +- ...es_testnet_ema_cross_with_trailing_stop.py | 2 +- .../live/bybit/bybit_ema_cross_stop_entry.py | 2 +- .../bybit_ema_cross_with_trailing_stop.py | 2 +- nautilus_core/model/src/enums.rs | 22 ++++++------- nautilus_core/model/src/python/enums.rs | 4 +-- .../binance/common/schemas/account.py | 2 +- nautilus_trader/adapters/binance/execution.py | 6 ++-- .../adapters/binance/futures/enums.py | 2 +- .../adapters/binance/spot/schemas/user.py | 2 +- .../adapters/bybit/common/enums.py | 2 +- .../interactive_brokers/parsing/execution.py | 2 +- nautilus_trader/adapters/okx/common/enums.py | 2 +- nautilus_trader/core/includes/model.h | 32 +++++++++---------- nautilus_trader/core/nautilus_pyo3.pyi | 6 ++-- nautilus_trader/core/rust/model.pxd | 22 ++++++------- nautilus_trader/execution/emulator.pyx | 4 +-- nautilus_trader/execution/trailing.pyx | 2 +- tests/acceptance_tests/test_backtest.py | 2 +- ..._crypto_ema_cross_ethusdt_trailing_stop.py | 2 +- .../backtest/test_exchange_trailing_stops.py | 18 +++++------ tests/unit_tests/execution/test_emulator.py | 16 +++++----- .../execution/test_emulator_list.py | 6 ++-- tests/unit_tests/model/test_enums.py | 4 +-- tests/unit_tests/model/test_orders.py | 10 +++--- tests/unit_tests/risk/test_engine.py | 2 +- .../unit_tests/serialization/test_msgpack.py | 2 +- 31 files changed, 98 insertions(+), 98 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 5fe1fafadf40..87748c2ad41c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,7 +9,7 @@ None - Upgraded `datafusion` crate to v43.0.0 (#2056), thanks @twitu ### Breaking Changes -None +- Renamed `TriggerType.LAST_TRADE` to `LAST_PRICE` ### Fixes None diff --git a/docs/concepts/orders.md b/docs/concepts/orders.md index 409a53c7939b..d51b1e618527 100644 --- a/docs/concepts/orders.md +++ b/docs/concepts/orders.md @@ -245,7 +245,7 @@ order: StopMarketOrder = self.order_factory.stop_market( order_side=OrderSide.SELL, quantity=Quantity.from_int(1), trigger_price=Price.from_int(100_000), - trigger_type=TriggerType.LAST_TRADE, # <-- optional (default DEFAULT) + trigger_type=TriggerType.LAST_PRICE, # <-- optional (default DEFAULT) time_in_force=TimeInForce.GTC, # <-- optional (default GTC) expire_time=None, # <-- optional (default None) reduce_only=False, # <-- optional (default False) @@ -328,7 +328,7 @@ See the `MarketToLimitOrder` [API Reference](../api_reference/model/orders.md#cl ### Market-If-Touched A _Market-If-Touched_ order is a conditional order which once triggered will immediately -place a _Market_ order. This order type is often used to enter a new position on a stop price in the market orders direction, +place a _Market_ order. This order type is often used to enter a new position on a stop price, or to take profits for an existing position, either as a SELL order against LONG positions, or as a BUY order against SHORT positions. @@ -349,7 +349,7 @@ order: MarketIfTouchedOrder = self.order_factory.market_if_touched( order_side=OrderSide.SELL, quantity=Quantity.from_int(10), trigger_price=Price.from_str("10_000.00"), - trigger_type=TriggerType.LAST_TRADE, # <-- optional (default DEFAULT) + trigger_type=TriggerType.LAST_PRICE, # <-- optional (default DEFAULT) time_in_force=TimeInForce.GTC, # <-- optional (default GTC) expire_time=None, # <-- optional (default None) reduce_only=False, # <-- optional (default False) @@ -386,7 +386,7 @@ order: StopLimitOrder = self.order_factory.limit_if_touched( quantity=Quantity.from_int(5), price=Price.from_str("30_100"), trigger_price=Price.from_str("30_150"), - trigger_type=TriggerType.LAST_TRADE, # <-- optional (default DEFAULT) + trigger_type=TriggerType.LAST_PRICE, # <-- optional (default DEFAULT) time_in_force=TimeInForce.GTD, # <-- optional (default GTC) expire_time=pd.Timestamp("2022-06-06T12:00"), post_only=True, # <-- optional (default False) @@ -425,7 +425,7 @@ order: TrailingStopMarketOrder = self.order_factory.trailing_stop_market( order_side=OrderSide.SELL, quantity=Quantity.from_int(10), trigger_price=Price.from_str("5_000"), - trigger_type=TriggerType.LAST_TRADE, # <-- optional (default DEFAULT) + trigger_type=TriggerType.LAST_PRICE, # <-- optional (default DEFAULT) trailing_offset=Decimal(100), trailing_offset_type=TrailingOffsetType.BASIS_POINTS, time_in_force=TimeInForce.GTC, # <-- optional (default GTC) diff --git a/docs/concepts/strategies.md b/docs/concepts/strategies.md index 686e0dd623b0..35c0ad3835ae 100644 --- a/docs/concepts/strategies.md +++ b/docs/concepts/strategies.md @@ -400,7 +400,7 @@ def buy(self) -> None: order_side=OrderSide.BUY, quantity=self.instrument.make_qty(self.trade_size), price=self.instrument.make_price(5000.00), - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, ) self.submit_order(order) diff --git a/examples/backtest/crypto_ema_cross_ethusdt_trailing_stop.py b/examples/backtest/crypto_ema_cross_ethusdt_trailing_stop.py index 35cdfb279ccd..9a461066c343 100755 --- a/examples/backtest/crypto_ema_cross_ethusdt_trailing_stop.py +++ b/examples/backtest/crypto_ema_cross_ethusdt_trailing_stop.py @@ -73,7 +73,7 @@ atr_period=20, trailing_atr_multiple=3.0, trailing_offset_type="PRICE", - trigger_type="LAST_TRADE", + trigger_type="LAST_PRICE", ) # Instantiate and add your strategy strategy = EMACrossTrailingStop(config=config) diff --git a/examples/backtest/crypto_ema_cross_with_binance_provider.py b/examples/backtest/crypto_ema_cross_with_binance_provider.py index 7bc35db84f61..dbe97926ad71 100644 --- a/examples/backtest/crypto_ema_cross_with_binance_provider.py +++ b/examples/backtest/crypto_ema_cross_with_binance_provider.py @@ -114,7 +114,7 @@ async def create_provider(): atr_period=20, trailing_atr_multiple=3.0, trailing_offset_type="PRICE", - trigger_type="LAST_TRADE", + trigger_type="LAST_PRICE", ) # Instantiate and add your strategy strategy = EMACrossTrailingStop(config=config) diff --git a/examples/live/binance/binance_futures_testnet_ema_cross_with_trailing_stop.py b/examples/live/binance/binance_futures_testnet_ema_cross_with_trailing_stop.py index fc4a914c0f53..3b95bc549240 100644 --- a/examples/live/binance/binance_futures_testnet_ema_cross_with_trailing_stop.py +++ b/examples/live/binance/binance_futures_testnet_ema_cross_with_trailing_stop.py @@ -92,7 +92,7 @@ atr_period=20, trailing_atr_multiple=3.0, trailing_offset_type="BASIS_POINTS", - trigger_type="LAST_TRADE", + trigger_type="LAST_PRICE", trade_size=Decimal("0.010"), ) # Instantiate your strategy diff --git a/examples/live/bybit/bybit_ema_cross_stop_entry.py b/examples/live/bybit/bybit_ema_cross_stop_entry.py index e1080f4af411..c837bb9a5513 100644 --- a/examples/live/bybit/bybit_ema_cross_stop_entry.py +++ b/examples/live/bybit/bybit_ema_cross_stop_entry.py @@ -98,7 +98,7 @@ trailing_atr_multiple=3.0, trailing_offset=Decimal("0.010"), trailing_offset_type="BASIS_POINTS", - trigger_type="LAST_TRADE", + trigger_type="LAST_PRICE", trade_size=trade_size, ) # Instantiate your strategy diff --git a/examples/live/bybit/bybit_ema_cross_with_trailing_stop.py b/examples/live/bybit/bybit_ema_cross_with_trailing_stop.py index 40c526ab2909..8068d7436a43 100644 --- a/examples/live/bybit/bybit_ema_cross_with_trailing_stop.py +++ b/examples/live/bybit/bybit_ema_cross_with_trailing_stop.py @@ -97,7 +97,7 @@ atr_period=20, trailing_atr_multiple=3.0, trailing_offset_type="BASIS_POINTS", - trigger_type="LAST_TRADE", + trigger_type="LAST_PRICE", trade_size=trade_size, ) # Instantiate your strategy diff --git a/nautilus_core/model/src/enums.rs b/nautilus_core/model/src/enums.rs index 5c875d7368bb..5ef5930cd9f7 100644 --- a/nautilus_core/model/src/enums.rs +++ b/nautilus_core/model/src/enums.rs @@ -1164,22 +1164,22 @@ pub enum TriggerType { NoTrigger = 0, /// The default trigger type set by the trading venue. Default = 1, - /// Based on the top-of-book quoted prices for the instrument. - BidAsk = 2, /// Based on the last traded price for the instrument. - LastTrade = 3, + LastPrice = 2, + /// Based on the mark price for the instrument. + MarkPrice = 3, + /// Based on the index price for the instrument. + IndexPrice = 4, + /// Based on the top-of-book quoted prices for the instrument. + BidAsk = 5, /// Based on a 'double match' of the last traded price for the instrument - DoubleLast = 4, + DoubleLast = 6, /// Based on a 'double match' of the bid/ask price for the instrument - DoubleBidAsk = 5, + DoubleBidAsk = 7, /// Based on both the [`TriggerType::LastTrade`] and [`TriggerType::BidAsk`]. - LastOrBidAsk = 6, + LastOrBidAsk = 8, /// Based on the mid-point of the [`TriggerType::BidAsk`]. - MidPoint = 7, - /// Based on the mark price for the instrument. - MarkPrice = 8, - /// Based on the index price for the instrument. - IndexPrice = 9, + MidPoint = 9, } enum_strum_serde!(AccountType); diff --git a/nautilus_core/model/src/python/enums.rs b/nautilus_core/model/src/python/enums.rs index 5f073310bdb2..dd8b4536d436 100644 --- a/nautilus_core/model/src/python/enums.rs +++ b/nautilus_core/model/src/python/enums.rs @@ -2028,9 +2028,9 @@ impl TriggerType { } #[classattr] - #[pyo3(name = "LAST_TRADE")] + #[pyo3(name = "LAST_PRICE")] fn py_last_trade() -> Self { - Self::LastTrade + Self::LastPrice } #[classattr] diff --git a/nautilus_trader/adapters/binance/common/schemas/account.py b/nautilus_trader/adapters/binance/common/schemas/account.py index 7f9cbcbe9279..0ea36c0d20d3 100644 --- a/nautilus_trader/adapters/binance/common/schemas/account.py +++ b/nautilus_trader/adapters/binance/common/schemas/account.py @@ -193,7 +193,7 @@ def parse_to_order_status_report( if self.workingType is not None: trigger_type = enum_parser.parse_binance_trigger_type(self.workingType) elif trigger_price > 0: - trigger_type = TriggerType.LAST_TRADE + trigger_type = TriggerType.LAST_PRICE trailing_offset = None trailing_offset_type = TrailingOffsetType.NO_TRAILING_OFFSET diff --git a/nautilus_trader/adapters/binance/execution.py b/nautilus_trader/adapters/binance/execution.py index eac860e1a213..cbeaee6e26b3 100644 --- a/nautilus_trader/adapters/binance/execution.py +++ b/nautilus_trader/adapters/binance/execution.py @@ -750,7 +750,7 @@ async def _submit_stop_limit_order( ) -> None: if self._binance_account_type.is_spot_or_margin: working_type = None - elif order.trigger_type in (TriggerType.DEFAULT, TriggerType.LAST_TRADE): + elif order.trigger_type in (TriggerType.DEFAULT, TriggerType.LAST_PRICE): working_type = "CONTRACT_PRICE" elif order.trigger_type == TriggerType.MARK_PRICE: working_type = "MARK_PRICE" @@ -801,7 +801,7 @@ async def _submit_stop_market_order( ) -> None: if self._binance_account_type.is_spot_or_margin: working_type = None - elif order.trigger_type in (TriggerType.DEFAULT, TriggerType.LAST_TRADE): + elif order.trigger_type in (TriggerType.DEFAULT, TriggerType.LAST_PRICE): working_type = "CONTRACT_PRICE" elif order.trigger_type == TriggerType.MARK_PRICE: working_type = "MARK_PRICE" @@ -833,7 +833,7 @@ async def _submit_trailing_stop_market_order( order: TrailingStopMarketOrder, position_side: BinanceFuturesPositionSide | None, ) -> None: - if order.trigger_type in (TriggerType.DEFAULT, TriggerType.LAST_TRADE): + if order.trigger_type in (TriggerType.DEFAULT, TriggerType.LAST_PRICE): working_type = "CONTRACT_PRICE" elif order.trigger_type == TriggerType.MARK_PRICE: working_type = "MARK_PRICE" diff --git a/nautilus_trader/adapters/binance/futures/enums.py b/nautilus_trader/adapters/binance/futures/enums.py index 06e51d80e20a..3c8b934fa066 100644 --- a/nautilus_trader/adapters/binance/futures/enums.py +++ b/nautilus_trader/adapters/binance/futures/enums.py @@ -181,7 +181,7 @@ def parse_internal_order_type(self, order: Order) -> BinanceOrderType: def parse_binance_trigger_type(self, trigger_type: str) -> TriggerType: if trigger_type == BinanceFuturesWorkingType.CONTRACT_PRICE.value: - return TriggerType.LAST_TRADE + return TriggerType.LAST_PRICE elif trigger_type == BinanceFuturesWorkingType.MARK_PRICE.value: return TriggerType.MARK_PRICE else: diff --git a/nautilus_trader/adapters/binance/spot/schemas/user.py b/nautilus_trader/adapters/binance/spot/schemas/user.py index 0fd28578f911..858958e2a8a6 100644 --- a/nautilus_trader/adapters/binance/spot/schemas/user.py +++ b/nautilus_trader/adapters/binance/spot/schemas/user.py @@ -193,7 +193,7 @@ def parse_to_order_status_report( order_status=OrderStatus.ACCEPTED, price=price, trigger_price=trigger_price, - trigger_type=TriggerType.LAST_TRADE, + trigger_type=TriggerType.LAST_PRICE, trailing_offset=None, trailing_offset_type=TrailingOffsetType.NO_TRAILING_OFFSET, quantity=Quantity.from_str(self.q), diff --git a/nautilus_trader/adapters/bybit/common/enums.py b/nautilus_trader/adapters/bybit/common/enums.py index 33c4a8c381ea..aae1e54c292a 100644 --- a/nautilus_trader/adapters/bybit/common/enums.py +++ b/nautilus_trader/adapters/bybit/common/enums.py @@ -452,7 +452,7 @@ def __init__(self) -> None: # fmt: on self.bybit_to_nautilus_trigger_type = { BybitTriggerType.NONE: TriggerType.NO_TRIGGER, - BybitTriggerType.LAST_PRICE: TriggerType.LAST_TRADE, + BybitTriggerType.LAST_PRICE: TriggerType.LAST_PRICE, BybitTriggerType.MARK_PRICE: TriggerType.MARK_PRICE, BybitTriggerType.INDEX_PRICE: TriggerType.INDEX_PRICE, } diff --git a/nautilus_trader/adapters/interactive_brokers/parsing/execution.py b/nautilus_trader/adapters/interactive_brokers/parsing/execution.py index 1fdebeb8f498..41643bf763e1 100644 --- a/nautilus_trader/adapters/interactive_brokers/parsing/execution.py +++ b/nautilus_trader/adapters/interactive_brokers/parsing/execution.py @@ -27,7 +27,7 @@ MAP_TRIGGER_METHOD: dict[int, int] = { TriggerType.DEFAULT: 0, TriggerType.DOUBLE_BID_ASK: 1, - TriggerType.LAST_TRADE: 2, + TriggerType.LAST_PRICE: 2, TriggerType.DOUBLE_LAST: 3, TriggerType.BID_ASK: 4, TriggerType.LAST_OR_BID_ASK: 7, diff --git a/nautilus_trader/adapters/okx/common/enums.py b/nautilus_trader/adapters/okx/common/enums.py index 7736582bc761..f28d280224f2 100644 --- a/nautilus_trader/adapters/okx/common/enums.py +++ b/nautilus_trader/adapters/okx/common/enums.py @@ -406,7 +406,7 @@ def __init__(self) -> None: self.okx_to_nautilus_trigger_type = { OKXTriggerType.NONE: TriggerType.NO_TRIGGER, - OKXTriggerType.LAST: TriggerType.LAST_TRADE, + OKXTriggerType.LAST: TriggerType.LAST_PRICE, OKXTriggerType.MARK: TriggerType.MARK_PRICE, OKXTriggerType.INDEX: TriggerType.INDEX_PRICE, } diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index 9711a63537a4..c318e2d71859 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -756,37 +756,37 @@ typedef enum TriggerType { */ DEFAULT = 1, /** - * Based on the top-of-book quoted prices for the instrument. + * Based on the last traded price for the instrument. */ - BID_ASK = 2, + LAST_PRICE = 2, /** - * Based on the last traded price for the instrument. + * Based on the mark price for the instrument. */ - LAST_TRADE = 3, + MARK_PRICE = 3, /** - * Based on a 'double match' of the last traded price for the instrument + * Based on the index price for the instrument. */ - DOUBLE_LAST = 4, + INDEX_PRICE = 4, /** - * Based on a 'double match' of the bid/ask price for the instrument + * Based on the top-of-book quoted prices for the instrument. */ - DOUBLE_BID_ASK = 5, + BID_ASK = 5, /** - * Based on both the [`TriggerType::LastTrade`] and [`TriggerType::BidAsk`]. + * Based on a 'double match' of the last traded price for the instrument */ - LAST_OR_BID_ASK = 6, + DOUBLE_LAST = 6, /** - * Based on the mid-point of the [`TriggerType::BidAsk`]. + * Based on a 'double match' of the bid/ask price for the instrument */ - MID_POINT = 7, + DOUBLE_BID_ASK = 7, /** - * Based on the mark price for the instrument. + * Based on both the [`TriggerType::LastTrade`] and [`TriggerType::BidAsk`]. */ - MARK_PRICE = 8, + LAST_OR_BID_ASK = 8, /** - * Based on the index price for the instrument. + * Based on the mid-point of the [`TriggerType::BidAsk`]. */ - INDEX_PRICE = 9, + MID_POINT = 9, } TriggerType; /** diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 0ae39e2b4f2d..eeb0518c1816 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -1040,14 +1040,14 @@ class TrailingOffsetType(Enum): class TriggerType(Enum): DEFAULT = "DEFAULT" + LAST_PRICE = "LAST_PRICE" + MARK_PRICE = "MARK_PRICE" + INDEX_PRICE = "INDEX_PRICE" BID_ASK = "BID_ASK" - LAST_TRADE = "LAST_TRADE" DOUBLE_LAST = "DOUBLE_LAST" DOUBLE_BID_ASK = "DOUBLE_BID_ASK" LAST_OR_BID_ASK = "LAST_OR_BID_ASK" MID_POINT = "MID_POINT" - MARK_PRICE = "MARK_PRICE" - INDEX_PRICE = "INDEX_PRICE" class MovingAverageType(Enum): SIMPLE = "SIMPLE" diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index 5689e0778c52..2f18c93d1ad3 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -399,22 +399,22 @@ cdef extern from "../includes/model.h": NO_TRIGGER # = 0, # The default trigger type set by the trading venue. DEFAULT # = 1, - # Based on the top-of-book quoted prices for the instrument. - BID_ASK # = 2, # Based on the last traded price for the instrument. - LAST_TRADE # = 3, + LAST_PRICE # = 2, + # Based on the mark price for the instrument. + MARK_PRICE # = 3, + # Based on the index price for the instrument. + INDEX_PRICE # = 4, + # Based on the top-of-book quoted prices for the instrument. + BID_ASK # = 5, # Based on a 'double match' of the last traded price for the instrument - DOUBLE_LAST # = 4, + DOUBLE_LAST # = 6, # Based on a 'double match' of the bid/ask price for the instrument - DOUBLE_BID_ASK # = 5, + DOUBLE_BID_ASK # = 7, # Based on both the [`TriggerType::LastTrade`] and [`TriggerType::BidAsk`]. - LAST_OR_BID_ASK # = 6, + LAST_OR_BID_ASK # = 8, # Based on the mid-point of the [`TriggerType::BidAsk`]. - MID_POINT # = 7, - # Based on the mark price for the instrument. - MARK_PRICE # = 8, - # Based on the index price for the instrument. - INDEX_PRICE # = 9, + MID_POINT # = 9, # Represents a discrete price level in an order book. # diff --git a/nautilus_trader/execution/emulator.pyx b/nautilus_trader/execution/emulator.pyx index 2afd99931e48..bec0d7d55d56 100644 --- a/nautilus_trader/execution/emulator.pyx +++ b/nautilus_trader/execution/emulator.pyx @@ -75,7 +75,7 @@ from nautilus_trader.model.orders.market cimport MarketOrder from nautilus_trader.portfolio.base cimport PortfolioFacade -cdef set SUPPORTED_TRIGGERS = {TriggerType.DEFAULT, TriggerType.BID_ASK, TriggerType.LAST_TRADE} +cdef set SUPPORTED_TRIGGERS = {TriggerType.DEFAULT, TriggerType.BID_ASK, TriggerType.LAST_PRICE} cdef class OrderEmulator(Actor): @@ -412,7 +412,7 @@ cdef class OrderEmulator(Actor): 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: + elif emulation_trigger == TriggerType.LAST_PRICE: if trigger_instrument_id not in self._subscribed_trades: self.subscribe_trade_ticks(trigger_instrument_id) self._subscribed_trades.add(trigger_instrument_id) diff --git a/nautilus_trader/execution/trailing.pyx b/nautilus_trader/execution/trailing.pyx index 2dce4aaf44f9..b7c40bbb5eaa 100644 --- a/nautilus_trader/execution/trailing.pyx +++ b/nautilus_trader/execution/trailing.pyx @@ -61,7 +61,7 @@ cdef class TrailingStopCalculator: Price temp_price if ( order.trigger_type == TriggerType.DEFAULT - or order.trigger_type == TriggerType.LAST_TRADE + or order.trigger_type == TriggerType.LAST_PRICE or order.trigger_type == TriggerType.MARK_PRICE ): if last is None: diff --git a/tests/acceptance_tests/test_backtest.py b/tests/acceptance_tests/test_backtest.py index 5b5ecdc18fa3..453e794330ac 100644 --- a/tests/acceptance_tests/test_backtest.py +++ b/tests/acceptance_tests/test_backtest.py @@ -259,7 +259,7 @@ def test_run_ema_cross_stop_entry_trail_strategy(self): trailing_atr_multiple=3.0, trailing_offset_type="PRICE", trailing_offset=Decimal("0.01"), - trigger_type="LAST_TRADE", + trigger_type="LAST_PRICE", ) strategy = EMACrossStopEntry(config=config) self.engine.add_strategy(strategy) diff --git a/tests/mem_leak_tests/tracemalloc_crypto_ema_cross_ethusdt_trailing_stop.py b/tests/mem_leak_tests/tracemalloc_crypto_ema_cross_ethusdt_trailing_stop.py index 139f66d5184e..11ff53035e56 100644 --- a/tests/mem_leak_tests/tracemalloc_crypto_ema_cross_ethusdt_trailing_stop.py +++ b/tests/mem_leak_tests/tracemalloc_crypto_ema_cross_ethusdt_trailing_stop.py @@ -76,7 +76,7 @@ def run(*args, **kwargs): atr_period=20, trailing_atr_multiple=3.0, trailing_offset_type="PRICE", - trigger_type="LAST_TRADE", + trigger_type="LAST_PRICE", ) # Instantiate and add your strategy strategy = EMACrossTrailingStop(config=config) diff --git a/tests/unit_tests/backtest/test_exchange_trailing_stops.py b/tests/unit_tests/backtest/test_exchange_trailing_stops.py index 73af954a7132..e31a4ed1ba71 100644 --- a/tests/unit_tests/backtest/test_exchange_trailing_stops.py +++ b/tests/unit_tests/backtest/test_exchange_trailing_stops.py @@ -191,7 +191,7 @@ def test_trailing_stop_market_order_last_when_no_quote_ticks_raises_runtime_erro quantity=Quantity.from_int(200_000), trailing_offset_type=TrailingOffsetType.PRICE, trailing_offset=Decimal("1.0"), - trigger_type=TriggerType.LAST_TRADE, + trigger_type=TriggerType.LAST_PRICE, ) self.strategy.submit_order(trailing_stop) @@ -288,14 +288,14 @@ def test_trailing_stop_market_order_bid_ask_with_no_trigger_updates_order( OrderSide.BUY, TrailingOffsetType.PRICE, Decimal("1.0"), - TriggerType.LAST_TRADE, + TriggerType.LAST_PRICE, Price.from_str("15.000"), ], [ OrderSide.SELL, TrailingOffsetType.PRICE, Decimal("1.0"), - TriggerType.LAST_TRADE, + TriggerType.LAST_PRICE, Price.from_str("13.000"), ], [ @@ -543,7 +543,7 @@ def test_trailing_stop_limit_order_bid_ask_with_no_trigger_updates_order( OrderSide.BUY, TrailingOffsetType.PRICE, Decimal("1.0"), - TriggerType.LAST_TRADE, + TriggerType.LAST_PRICE, Price.from_str("15.000"), Price.from_str("15.000"), ], @@ -551,7 +551,7 @@ def test_trailing_stop_limit_order_bid_ask_with_no_trigger_updates_order( OrderSide.SELL, TrailingOffsetType.PRICE, Decimal("1.0"), - TriggerType.LAST_TRADE, + TriggerType.LAST_PRICE, Price.from_str("13.000"), Price.from_str("13.000"), ], @@ -575,7 +575,7 @@ def test_trailing_stop_limit_order_bid_ask_with_no_trigger_updates_order( OrderSide.BUY, TrailingOffsetType.BASIS_POINTS, Decimal("100"), - TriggerType.LAST_TRADE, + TriggerType.LAST_PRICE, Price.from_str("14.140"), Price.from_str("14.140"), ], @@ -583,7 +583,7 @@ def test_trailing_stop_limit_order_bid_ask_with_no_trigger_updates_order( OrderSide.SELL, TrailingOffsetType.BASIS_POINTS, Decimal("100"), - TriggerType.LAST_TRADE, + TriggerType.LAST_PRICE, Price.from_str("13.860"), Price.from_str("13.860"), ], @@ -871,7 +871,7 @@ def test_trailing_stop_limit_order_buy_last_ticks_when_offset_activated_updates_ trailing_offset_type=TrailingOffsetType.TICKS, trailing_offset=Decimal("20"), limit_offset=Decimal("20"), - trigger_type=TriggerType.LAST_TRADE, + trigger_type=TriggerType.LAST_PRICE, ) self.strategy.submit_order(trailing_stop) self.exchange.process(0) @@ -929,7 +929,7 @@ def test_trailing_stop_limit_order_sell_last_ticks_when_offset_activated_updates trailing_offset_type=TrailingOffsetType.TICKS, trailing_offset=Decimal("20"), limit_offset=Decimal("20"), - trigger_type=TriggerType.LAST_TRADE, + trigger_type=TriggerType.LAST_PRICE, ) self.strategy.submit_order(trailing_stop) self.exchange.process(0) diff --git a/tests/unit_tests/execution/test_emulator.py b/tests/unit_tests/execution/test_emulator.py index 088e484fe010..c554635a6c0e 100644 --- a/tests/unit_tests/execution/test_emulator.py +++ b/tests/unit_tests/execution/test_emulator.py @@ -351,7 +351,7 @@ def test_submit_order_with_emulation_trigger_last_subscribes_to_data(self) -> No order_side=OrderSide.BUY, quantity=Quantity.from_int(10), price=ETHUSDT_PERP_BINANCE.make_price(2_000), - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, ) # Act @@ -372,7 +372,7 @@ def test_emulator_restart_reactivates_emulated_orders(self) -> None: order_side=OrderSide.BUY, quantity=Quantity.from_int(10), price=ETHUSDT_PERP_BINANCE.make_price(2_000), - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, ) self.strategy.submit_order(order) @@ -396,7 +396,7 @@ def test_cancel_all_with_emulated_order_cancels_order(self) -> None: order_side=OrderSide.BUY, quantity=Quantity.from_int(10), price=ETHUSDT_PERP_BINANCE.make_price(2_000), - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, ) self.strategy.submit_order(order) @@ -414,7 +414,7 @@ def test_cancel_all_buy_orders_with_emulated_orders_cancels_buy_order(self) -> N order_side=OrderSide.BUY, quantity=Quantity.from_int(10), price=ETHUSDT_PERP_BINANCE.make_price(2_000), - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, ) order2 = self.strategy.order_factory.limit( @@ -422,7 +422,7 @@ def test_cancel_all_buy_orders_with_emulated_orders_cancels_buy_order(self) -> N order_side=OrderSide.SELL, quantity=Quantity.from_int(10), price=ETHUSDT_PERP_BINANCE.make_price(2_010), - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, ) self.strategy.submit_order(order1) @@ -444,7 +444,7 @@ def test_cancel_all_sell_orders_with_emulated_orders_cancels_sell_order(self) -> order_side=OrderSide.BUY, quantity=Quantity.from_int(10), price=ETHUSDT_PERP_BINANCE.make_price(2_000), - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, ) order2 = self.strategy.order_factory.limit( @@ -452,7 +452,7 @@ def test_cancel_all_sell_orders_with_emulated_orders_cancels_sell_order(self) -> order_side=OrderSide.SELL, quantity=Quantity.from_int(10), price=ETHUSDT_PERP_BINANCE.make_price(2_010), - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, ) self.strategy.submit_order(order1) @@ -485,7 +485,7 @@ def test_submit_limit_order_last_then_triggered_releases_market_order( order_side=order_side, quantity=Quantity.from_int(10), price=trigger_price, - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, ) self.strategy.submit_order(order) diff --git a/tests/unit_tests/execution/test_emulator_list.py b/tests/unit_tests/execution/test_emulator_list.py index 8c60b214eb04..e35a8417264b 100644 --- a/tests/unit_tests/execution/test_emulator_list.py +++ b/tests/unit_tests/execution/test_emulator_list.py @@ -176,7 +176,7 @@ def test_submit_stop_order_bulk_then_emulates(self) -> None: order_side=OrderSide.BUY, quantity=ETHUSDT_PERP_BINANCE.make_qty(10), trigger_price=ETHUSDT_PERP_BINANCE.make_price(5000.0), - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, ) stop2 = self.strategy.order_factory.stop_market( @@ -184,7 +184,7 @@ def test_submit_stop_order_bulk_then_emulates(self) -> None: order_side=OrderSide.BUY, quantity=ETHUSDT_PERP_BINANCE.make_qty(10), trigger_price=ETHUSDT_PERP_BINANCE.make_price(5010.0), - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, ) stop3 = self.strategy.order_factory.stop_market( @@ -192,7 +192,7 @@ def test_submit_stop_order_bulk_then_emulates(self) -> None: order_side=OrderSide.BUY, quantity=ETHUSDT_PERP_BINANCE.make_qty(10), trigger_price=ETHUSDT_PERP_BINANCE.make_price(5020.0), - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, ) order_list = OrderList( diff --git a/tests/unit_tests/model/test_enums.py b/tests/unit_tests/model/test_enums.py index b8ebe864ad12..157009d4a2c1 100644 --- a/tests/unit_tests/model/test_enums.py +++ b/tests/unit_tests/model/test_enums.py @@ -1036,7 +1036,7 @@ class TestTriggerType: [ [TriggerType.NO_TRIGGER, "NO_TRIGGER"], [TriggerType.DEFAULT, "DEFAULT"], - [TriggerType.LAST_TRADE, "LAST_TRADE"], + [TriggerType.LAST_PRICE, "LAST_PRICE"], [TriggerType.BID_ASK, "BID_ASK"], [TriggerType.DOUBLE_LAST, "DOUBLE_LAST"], [TriggerType.DOUBLE_BID_ASK, "DOUBLE_BID_ASK"], @@ -1058,7 +1058,7 @@ def test_trigger_type_to_str(self, enum, expected): [ ["NO_TRIGGER", TriggerType.NO_TRIGGER], ["DEFAULT", TriggerType.DEFAULT], - ["LAST_TRADE", TriggerType.LAST_TRADE], + ["LAST_PRICE", TriggerType.LAST_PRICE], ["BID_ASK", TriggerType.BID_ASK], ["DOUBLE_LAST", TriggerType.DOUBLE_LAST], ["DOUBLE_BID_ASK", TriggerType.DOUBLE_BID_ASK], diff --git a/tests/unit_tests/model/test_orders.py b/tests/unit_tests/model/test_orders.py index 2fd39dbeb7b5..aeaff3e9d308 100644 --- a/tests/unit_tests/model/test_orders.py +++ b/tests/unit_tests/model/test_orders.py @@ -890,7 +890,7 @@ def test_initialize_limit_if_touched_order(self): Quantity.from_int(100_000), Price.from_str("1.00000"), Price.from_str("1.10010"), - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, tags=["ENTRY"], ) @@ -909,11 +909,11 @@ def test_initialize_limit_if_touched_order(self): assert isinstance(order.init_event, OrderInitialized) assert ( str(order) - == "LimitIfTouchedOrder(BUY 100_000 AUD/USD.SIM LIMIT_IF_TOUCHED @ 1.10010-STOP[DEFAULT] 1.00000-LIMIT GTC EMULATED[LAST_TRADE], status=INITIALIZED, client_order_id=O-19700101-000000-000-001-1, venue_order_id=None, position_id=None, tags=['ENTRY'])" # noqa + == "LimitIfTouchedOrder(BUY 100_000 AUD/USD.SIM LIMIT_IF_TOUCHED @ 1.10010-STOP[DEFAULT] 1.00000-LIMIT GTC EMULATED[LAST_PRICE], status=INITIALIZED, client_order_id=O-19700101-000000-000-001-1, venue_order_id=None, position_id=None, tags=['ENTRY'])" # noqa ) assert ( repr(order) - == "LimitIfTouchedOrder(BUY 100_000 AUD/USD.SIM LIMIT_IF_TOUCHED @ 1.10010-STOP[DEFAULT] 1.00000-LIMIT GTC EMULATED[LAST_TRADE], status=INITIALIZED, client_order_id=O-19700101-000000-000-001-1, venue_order_id=None, position_id=None, tags=['ENTRY'])" # noqa + == "LimitIfTouchedOrder(BUY 100_000 AUD/USD.SIM LIMIT_IF_TOUCHED @ 1.10010-STOP[DEFAULT] 1.00000-LIMIT GTC EMULATED[LAST_PRICE], status=INITIALIZED, client_order_id=O-19700101-000000-000-001-1, venue_order_id=None, position_id=None, tags=['ENTRY'])" # noqa ) def test_limit_if_touched_order_to_dict(self): @@ -925,7 +925,7 @@ def test_limit_if_touched_order_to_dict(self): Price.from_str("1.00000"), Price.from_str("1.10010"), trigger_type=TriggerType.MARK_PRICE, - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, trigger_instrument_id=TestIdStubs.usdjpy_id(), tags=["STOP_LOSS"], ) @@ -964,7 +964,7 @@ def test_limit_if_touched_order_to_dict(self): "is_reduce_only": False, "is_quote_quantity": False, "display_qty": None, - "emulation_trigger": "LAST_TRADE", + "emulation_trigger": "LAST_PRICE", "trigger_instrument_id": "USD/JPY.SIM", "contingency_type": "NO_CONTINGENCY", "order_list_id": None, diff --git a/tests/unit_tests/risk/test_engine.py b/tests/unit_tests/risk/test_engine.py index cff681a1cb1b..b415c47336f4 100644 --- a/tests/unit_tests/risk/test_engine.py +++ b/tests/unit_tests/risk/test_engine.py @@ -1778,7 +1778,7 @@ def test_submit_order_for_emulation_sends_command_to_emulator(self): OrderSide.BUY, Quantity.from_int(1_000), Price.from_str("1.00000"), - emulation_trigger=TriggerType.LAST_TRADE, + emulation_trigger=TriggerType.LAST_PRICE, ) # Act diff --git a/tests/unit_tests/serialization/test_msgpack.py b/tests/unit_tests/serialization/test_msgpack.py index 9c435f0f7a22..006779d35a5a 100644 --- a/tests/unit_tests/serialization/test_msgpack.py +++ b/tests/unit_tests/serialization/test_msgpack.py @@ -353,7 +353,7 @@ def test_pack_and_unpack_stop_limit_orders_with_expiration(self): Quantity(100_000, precision=0), price=Price(1.00000, precision=5), trigger_price=Price(1.00010, precision=5), - trigger_type=TriggerType.LAST_TRADE, + trigger_type=TriggerType.LAST_PRICE, time_in_force=TimeInForce.GTD, expire_time_ns=1_000_000_000 * 60, init_id=UUID4(), From 77152d1b533ab39e437b2981024b80a93a069d2a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Nov 2024 07:57:30 +1100 Subject: [PATCH 07/78] Scaffold AccountsManager in Rust --- nautilus_core/portfolio/src/lib.rs | 1 + nautilus_core/portfolio/src/manager.rs | 116 +++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 nautilus_core/portfolio/src/manager.rs diff --git a/nautilus_core/portfolio/src/lib.rs b/nautilus_core/portfolio/src/lib.rs index 9362283191fd..156b325b5c35 100644 --- a/nautilus_core/portfolio/src/lib.rs +++ b/nautilus_core/portfolio/src/lib.rs @@ -15,6 +15,7 @@ //! Provides a generic `Portfolio` for all environments. +pub mod manager; pub mod portfolio; // Re-exports diff --git a/nautilus_core/portfolio/src/manager.rs b/nautilus_core/portfolio/src/manager.rs new file mode 100644 index 000000000000..8e58e39fe6be --- /dev/null +++ b/nautilus_core/portfolio/src/manager.rs @@ -0,0 +1,116 @@ +// ------------------------------------------------------------------------------------------------- +// 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. +// ------------------------------------------------------------------------------------------------- + +//! Provides account management functionality. + +// Under development +#![allow(dead_code)] +#![allow(unused_variables)] + +use std::{cell::RefCell, rc::Rc}; + +use nautilus_common::{cache::Cache, clock::Clock}; +use nautilus_core::nanos::UnixNanos; +use nautilus_model::{ + accounts::{any::AccountAny, cash::CashAccount, margin::MarginAccount}, + enums::OrderSideSpecified, + events::{account::state::AccountState, order::OrderFilled}, + instruments::any::InstrumentAny, + orders::any::OrderAny, + position::Position, + types::money::Money, +}; +use rust_decimal::Decimal; + +pub struct AccountsManager { + clock: Rc>, + cache: Rc>, +} + +impl AccountsManager { + pub fn update_balances( + &self, + account: AccountAny, + instrument: InstrumentAny, + fill: OrderFilled, + ) -> AccountState { + todo!() + } + + pub fn update_orders( + &self, + account: AccountAny, + instrument: InstrumentAny, + orders_open: &[OrderAny], + ts_event: UnixNanos, + ) -> AccountState { + todo!() + } + + pub fn update_positions( + &self, + account: MarginAccount, + instrument: InstrumentAny, + positions: &[Position], + ts_event: UnixNanos, + ) -> AccountState { + todo!() + } + + fn update_balance_locked( + &self, + account: CashAccount, + instrument: InstrumentAny, + fill: OrderFilled, + ) -> AccountState { + todo!() + } + + fn update_margin_init( + &self, + account: MarginAccount, + instrument: InstrumentAny, + orders_open: &[OrderAny], + ts_event: UnixNanos, + ) -> AccountState { + todo!() + } + + fn update_balance_single_currency(&self, account: AccountAny, fill: OrderFilled, pnl: Money) { + todo!() + } + + fn update_balance_multi_currency( + &self, + account: AccountAny, + fill: OrderFilled, + pnls: &[Money], + ) { + todo!() + } + + fn generate_account_state(&self, account: AccountAny, ts_event: UnixNanos) -> AccountState { + todo!() + } + + fn calculate_xrate_to_base( + &self, + account: AccountAny, + instrument: InstrumentAny, + side: OrderSideSpecified, + ) -> Decimal { + todo!() + } +} From 2f016af567d303fad337e37365316f47dfed5191 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Nov 2024 08:13:17 +1100 Subject: [PATCH 08/78] Refine BookSnapshotter --- nautilus_core/data/src/engine/book.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/nautilus_core/data/src/engine/book.rs b/nautilus_core/data/src/engine/book.rs index 9accef65911d..2f7da4941823 100644 --- a/nautilus_core/data/src/engine/book.rs +++ b/nautilus_core/data/src/engine/book.rs @@ -122,16 +122,21 @@ impl BookSnapshotter { pub fn snapshot(&self, event: TimeEvent) { let cache = self.cache.borrow(); + let mut msgbus = self.msgbus.borrow_mut(); if self.snap_info.is_composite { - let msgbus = self.msgbus.borrow_mut(); let topic = self.snap_info.topic; let underlying = self.snap_info.root; for instrument in cache.instruments(&self.snap_info.venue, Some(&underlying)) { - self.publish_order_book(&instrument.id(), &topic, &cache); + self.publish_order_book(&instrument.id(), &topic, &cache, &mut msgbus); } } else { - self.publish_order_book(&self.snap_info.instrument_id, &self.snap_info.topic, &cache); + self.publish_order_book( + &self.snap_info.instrument_id, + &self.snap_info.topic, + &cache, + &mut msgbus, + ); } } @@ -139,9 +144,9 @@ impl BookSnapshotter { &self, instrument_id: &InstrumentId, topic: &Ustr, - cache: &Ref<'_, Cache>, + cache: &Ref, + msgbus: &mut MessageBus, ) { - // TODO: Optimize: We shouldn't need to keep fetching the message bus mut ref every time let book = cache .order_book(instrument_id) .unwrap_or_else(|| panic!("OrderBook for {instrument_id} was not in cache")); @@ -151,6 +156,6 @@ impl BookSnapshotter { return; } - self.msgbus.borrow_mut().publish(topic, book as &dyn Any); + msgbus.publish(topic, book as &dyn Any); } } From 64576b19fb81f55a5020578dd2b9f234b4c4ed0d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Nov 2024 09:40:11 +1100 Subject: [PATCH 09/78] Continue DataEngine in Rust --- nautilus_core/common/src/testing.rs | 17 +++++++++ nautilus_core/data/src/engine/mod.rs | 50 ++++++++++++-------------- nautilus_core/data/src/engine/tests.rs | 12 ------- nautilus_core/model/src/data/deltas.rs | 6 ++++ 4 files changed, 46 insertions(+), 39 deletions(-) diff --git a/nautilus_core/common/src/testing.rs b/nautilus_core/common/src/testing.rs index 02b85ed36db6..87afc1a48c36 100644 --- a/nautilus_core/common/src/testing.rs +++ b/nautilus_core/common/src/testing.rs @@ -21,6 +21,23 @@ use std::{ time::{Duration, Instant}, }; +use log::LevelFilter; +use nautilus_core::uuid::UUID4; +use nautilus_model::identifiers::TraderId; + +use crate::logging::{init_logging, logger::LoggerConfig, writer::FileWriterConfig}; + +pub fn init_logger_for_testing(stdout_level: LevelFilter) { + let mut config = LoggerConfig::default(); + config.stdout_level = stdout_level; + init_logging( + TraderId::default(), + UUID4::new(), + config, + FileWriterConfig::default(), + ); +} + /// Repeatedly evaluates a condition with a delay until it becomes true or a timeout occurs. /// /// * `condition`: A closure that represents the condition to be met. This closure should return `true` diff --git a/nautilus_core/data/src/engine/mod.rs b/nautilus_core/data/src/engine/mod.rs index b4af0045db41..f7511994d92f 100644 --- a/nautilus_core/data/src/engine/mod.rs +++ b/nautilus_core/data/src/engine/mod.rs @@ -45,7 +45,6 @@ use std::{ cell::{Ref, RefCell}, collections::{HashMap, HashSet, VecDeque}, num::NonZeroU64, - ops::Deref, rc::Rc, sync::Arc, }; @@ -102,7 +101,7 @@ pub struct DataEngine { bar_aggregators: Vec>, // TODO: dyn for now synthetic_quote_feeds: HashMap>, synthetic_trade_feeds: HashMap>, - buffered_deltas_map: HashMap>, + buffered_deltas_map: HashMap>, // TODO: Use OrderBookDeltas? msgbus_priority: u8, command_queue: VecDeque, config: DataEngineConfig, @@ -342,38 +341,28 @@ impl DataEngine { } pub fn execute(&mut self, cmd: SubscriptionCommand) { - match cmd.action { + let result = match cmd.action { Action::Subscribe => match cmd.data_type.type_name() { stringify!(OrderBookDelta) => self.handle_subscribe_book_deltas(&cmd), stringify!(OrderBook) => self.handle_subscribe_book_snapshots(&cmd), stringify!(Bar) => self.handle_subscribe_bars(&cmd), - type_name => Err(anyhow::anyhow!( - "Cannot handle subscription, type `{type_name}` is unrecognized" - )), + _ => Ok(()), // No other actions for engine }, Action::Unsubscribe => match cmd.data_type.type_name() { stringify!(OrderBookDelta) => self.handle_unsubscribe_book_deltas(&cmd), stringify!(OrderBook) => self.handle_unsubscribe_book_snapshots(&cmd), stringify!(Bar) => self.handle_unsubscribe_bars(&cmd), - type_name => Err(anyhow::anyhow!( - "Cannot handle subscription, type `{type_name}` is unrecognized" - )), + _ => Ok(()), // No other actions for engine }, + }; + + if let Err(e) = result { + log::error!("{e}"); + return; } - .unwrap_or_else(|e| log::error!("{e}")); if let Some(client) = self.get_client_mut(&cmd.client_id, &cmd.venue) { - client.execute(cmd.clone()); - - // TBD if we want to do the below instead - // if client.handles_order_book_deltas { - // client.subscribe_order_book_deltas(instrument_id, book_type, depth)?; - // } else if client.handles_order_book_snapshots { - // client.subscribe_order_book_snapshots(instrument_id, book_type, depth)?; - // } else { - // anyhow::bail!("Cannot subscribe order book for {instrument_id}: client does not handle book subscriptions"); - // } - // client.execute(command); + client.execute(cmd); } else { log::error!( "Cannot handle command: no client found for {}", @@ -405,7 +394,7 @@ impl DataEngine { pub fn process_data(&mut self, data: Data) { match data { Data::Delta(delta) => self.handle_delta(delta), - Data::Deltas(deltas) => self.handle_deltas(deltas.deref().clone()), // TODO: Optimize + Data::Deltas(deltas) => self.handle_deltas(deltas.into_inner()), Data::Depth10(depth) => self.handle_depth10(depth), Data::Quote(quote) => self.handle_quote(quote), Data::Trade(trade) => self.handle_trade(trade), @@ -472,10 +461,13 @@ impl DataEngine { return; // Not the last delta for event } - // TODO: Improve efficiency, the FFI API will go along with Cython - OrderBookDeltas::new(delta.instrument_id, buffer_deltas.clone()) + // SAFETY: We know the deltas exists already + let deltas = self + .buffered_deltas_map + .remove(&delta.instrument_id) + .unwrap(); + OrderBookDeltas::new(delta.instrument_id, deltas) } else { - // TODO: Improve efficiency, the FFI API will go along with Cython OrderBookDeltas::new(delta.instrument_id, vec![delta]) }; @@ -503,8 +495,12 @@ impl DataEngine { return; } - // TODO: Improve efficiency, the FFI API will go along with Cython - OrderBookDeltas::new(deltas.instrument_id, buffer_deltas.clone()) + // SAFETY: We know the deltas exists already + let buffer_deltas = self + .buffered_deltas_map + .remove(&deltas.instrument_id) + .unwrap(); + OrderBookDeltas::new(deltas.instrument_id, buffer_deltas) } else { deltas }; diff --git a/nautilus_core/data/src/engine/tests.rs b/nautilus_core/data/src/engine/tests.rs index b6a991bd89ef..cf2ef8a3cbfc 100644 --- a/nautilus_core/data/src/engine/tests.rs +++ b/nautilus_core/data/src/engine/tests.rs @@ -56,18 +56,6 @@ use crate::{ mocks::MockDataClient, }; -// TODO: Used for development -// fn init_logger(stdout_level: LevelFilter) { -// let mut config = LoggerConfig::default(); -// config.stdout_level = stdout_level; -// init_logging( -// TraderId::default(), -// UUID4::new(), -// config, -// FileWriterConfig::default(), -// ); -// } - #[fixture] fn trader_id() -> TraderId { TraderId::default() diff --git a/nautilus_core/model/src/data/deltas.rs b/nautilus_core/model/src/data/deltas.rs index 2f3673bc7b64..65f4d59f9d43 100644 --- a/nautilus_core/model/src/data/deltas.rs +++ b/nautilus_core/model/src/data/deltas.rs @@ -147,6 +147,12 @@ impl OrderBookDeltas_API { pub fn new(deltas: OrderBookDeltas) -> Self { Self(Box::new(deltas)) } + + /// Consumes the wrapper and returns the inner `OrderBookDeltas`. + #[must_use] + pub fn into_inner(self) -> OrderBookDeltas { + *self.0 + } } impl Deref for OrderBookDeltas_API { From e41fce47025e1756e78835b5f69196997f955188 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Nov 2024 11:00:09 +1100 Subject: [PATCH 10/78] Add BarSpecification aggregation checks --- nautilus_core/model/src/data/bar.rs | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/nautilus_core/model/src/data/bar.rs b/nautilus_core/model/src/data/bar.rs index 3a59895d31cd..1a433a25fa0e 100644 --- a/nautilus_core/model/src/data/bar.rs +++ b/nautilus_core/model/src/data/bar.rs @@ -149,6 +149,55 @@ impl BarSpecification { ), } } + + /// Return a value indicating whether the aggregation method is time-driven: + /// - [`BarAggregation::Millisecond`] + /// - [`BarAggregation::Second`] + /// - [`BarAggregation::Minute`] + /// - [`BarAggregation::Hour`] + /// - [`BarAggregation::Day`] + /// - [`BarAggregation::Month`] + pub fn is_time_aggregated(&self) -> bool { + matches!( + self.aggregation, + BarAggregation::Millisecond + | BarAggregation::Second + | BarAggregation::Minute + | BarAggregation::Hour + | BarAggregation::Day + | BarAggregation::Month + ) + } + + /// Return a value indicating whether the aggregation method is threshold-driven: + /// - [`BarAggregation::Tick`] + /// - [`BarAggregation::TickImbalance`] + /// - [`BarAggregation::Volume`] + /// - [`BarAggregation::VolumeImbalance`] + /// - [`BarAggregation::Value`] + /// - [`BarAggregation::ValueImbalance`] + pub fn is_threshold_aggregated(&self) -> bool { + matches!( + self.aggregation, + BarAggregation::Tick + | BarAggregation::TickImbalance + | BarAggregation::Volume + | BarAggregation::VolumeImbalance + | BarAggregation::Value + | BarAggregation::ValueImbalance + ) + } + + /// Return a value indicating whether the aggregation method is information-driven: + /// - [`BarAggregation::TickRuns`] + /// - [`BarAggregation::VolumeRuns`] + /// - [`BarAggregation::ValueRuns`] + pub fn is_information_aggregated(&self) -> bool { + matches!( + self.aggregation, + BarAggregation::TickRuns | BarAggregation::VolumeRuns | BarAggregation::ValueRuns + ) + } } impl Display for BarSpecification { From 913ef5588a5eaf94b71111ef5c39659d86e8a074 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Nov 2024 11:00:53 +1100 Subject: [PATCH 11/78] Continue DataEngine in Rust --- nautilus_core/common/src/testing.rs | 5 +- nautilus_core/data/src/engine/mod.rs | 92 +++++++++++++++++++++++--- nautilus_core/data/src/engine/tests.rs | 16 +++-- 3 files changed, 98 insertions(+), 15 deletions(-) diff --git a/nautilus_core/common/src/testing.rs b/nautilus_core/common/src/testing.rs index 87afc1a48c36..8b5c24bb03c4 100644 --- a/nautilus_core/common/src/testing.rs +++ b/nautilus_core/common/src/testing.rs @@ -21,15 +21,14 @@ use std::{ time::{Duration, Instant}, }; -use log::LevelFilter; use nautilus_core::uuid::UUID4; use nautilus_model::identifiers::TraderId; use crate::logging::{init_logging, logger::LoggerConfig, writer::FileWriterConfig}; -pub fn init_logger_for_testing(stdout_level: LevelFilter) { +pub fn init_logger_for_testing(stdout_level: Option) { let mut config = LoggerConfig::default(); - config.stdout_level = stdout_level; + config.stdout_level = stdout_level.unwrap_or(log::LevelFilter::Trace); init_logging( TraderId::default(), UUID4::new(), diff --git a/nautilus_core/data/src/engine/mod.rs b/nautilus_core/data/src/engine/mod.rs index f7511994d92f..8d2e69193839 100644 --- a/nautilus_core/data/src/engine/mod.rs +++ b/nautilus_core/data/src/engine/mod.rs @@ -77,7 +77,7 @@ use nautilus_model::{ trade::TradeTick, Data, DataType, }, - enums::{BookType, RecordFlag}, + enums::{AggregationSource, BookType, PriceType, RecordFlag}, identifiers::{ClientId, InstrumentId, Venue}, instruments::{any::InstrumentAny, synthetic::SyntheticInstrument}, orderbook::book::OrderBook, @@ -98,7 +98,7 @@ pub struct DataEngine { book_intervals: HashMap>, book_updaters: HashMap>, book_snapshotters: HashMap>, - bar_aggregators: Vec>, // TODO: dyn for now + bar_aggregators: HashMap>, synthetic_quote_feeds: HashMap>, synthetic_trade_feeds: HashMap>, buffered_deltas_map: HashMap>, // TODO: Use OrderBookDeltas? @@ -127,7 +127,7 @@ impl DataEngine { book_intervals: HashMap::new(), book_updaters: HashMap::new(), book_snapshotters: HashMap::new(), - bar_aggregators: Vec::new(), + bar_aggregators: HashMap::new(), synthetic_quote_feeds: HashMap::new(), synthetic_trade_feeds: HashMap::new(), buffered_deltas_map: HashMap::new(), @@ -579,7 +579,7 @@ impl DataEngine { ) -> anyhow::Result<()> { let instrument_id = command.data_type.instrument_id().ok_or_else(|| { anyhow::anyhow!( - "Invalid order book deltas subscription: did not contain an `instrument_id`, {}", + "Invalid order book deltas subscription: did not contain an 'instrument_id', {}", command.data_type ) })?; @@ -608,7 +608,7 @@ impl DataEngine { ) -> anyhow::Result<()> { let instrument_id = command.data_type.instrument_id().ok_or_else(|| { anyhow::anyhow!( - "Invalid order book snapshots subscription: did not contain an `instrument_id`, {}", + "Invalid order book snapshots subscription: did not contain an 'instrument_id', {}", command.data_type ) })?; @@ -677,7 +677,23 @@ impl DataEngine { } fn handle_subscribe_bars(&mut self, command: &SubscriptionCommand) -> anyhow::Result<()> { - // TODO: Handle aggregators + let bar_type = command.data_type.bar_type(); + + match bar_type.aggregation_source() { + AggregationSource::Internal => { + if !self.bar_aggregators.contains_key(&bar_type.standard()) { + self.start_bar_aggregator(bar_type)?; + } + } + AggregationSource::External => { + if bar_type.instrument_id().is_synthetic() { + anyhow::bail!( + "Cannot subscribe for externally aggregated synthetic instrument bar data" + ); + }; + } + } + Ok(()) } @@ -687,7 +703,7 @@ impl DataEngine { ) -> anyhow::Result<()> { let instrument_id = command.data_type.instrument_id().ok_or_else(|| { anyhow::anyhow!( - "Invalid order book snapshots subscription: did not contain an `instrument_id`, {}", + "Invalid order book snapshots subscription: did not contain an 'instrument_id', {}", command.data_type ) })?; @@ -718,7 +734,7 @@ impl DataEngine { ) -> anyhow::Result<()> { let instrument_id = command.data_type.instrument_id().ok_or_else(|| { anyhow::anyhow!( - "Invalid order book snapshots subscription: did not contain an `instrument_id`, {}", + "Invalid order book snapshots subscription: did not contain an 'instrument_id', {}", command.data_type ) })?; @@ -860,6 +876,66 @@ impl DataEngine { Ok(()) } + + fn start_bar_aggregator(&mut self, bar_type: BarType) -> anyhow::Result<()> { + let instrument = self + .cache + .borrow() + .instrument(&bar_type.instrument_id()) + .ok_or_else(|| { + anyhow::anyhow!( + "Cannot start bar aggregation: no instrument found for {}", + bar_type.instrument_id(), + ) + })?; + + // Create aggregator + // TODO: Determine how to handle generic Clock vs dyn Clock + // let aggregator = if bar_type.spec().is_time_aggregated() { + // TimeBarAggregator::new( + // instrument, + // bar_type, + // |b| self.handle_bar(b), + // false, + // self.clock, + // self.config.time_bars_build_with_no_updates, + // self.config.time_bars_timestamp_on_close, + // &self.config.time_bars_interval_type, + // ) + // }; + + Ok(()) + } + + fn stop_bar_aggregator(&mut self, bar_type: BarType) -> anyhow::Result<()> { + let aggregator = self + .bar_aggregators + .remove(&bar_type.standard()) + .ok_or_else(|| { + anyhow::anyhow!("Cannot stop bar aggregator: no aggregator to stop for {bar_type}") + })?; + + // TODO: If its a `TimeBarAggregator` then call `.stop()` + // if let Some(aggregator) = (aggregator as &dyn BarAggregator) + // .as_any() + // .downcast_ref::>() + // { + // aggregator.stop(); + // }; + + if bar_type.is_composite() { + let composite_bar_type = bar_type.composite(); + // TODO: Unsubscribe the `aggregator.handle_bar` + } else if bar_type.spec().price_type == PriceType::Last { + // TODO: Unsubscribe `aggregator.handle_trade_tick` + todo!() + } else { + // TODO: Unsubscribe `aggregator.handle_quote_tick` + todo!() + }; + + Ok(()) + } } pub struct SubscriptionCommandHandler { diff --git a/nautilus_core/data/src/engine/tests.rs b/nautilus_core/data/src/engine/tests.rs index cf2ef8a3cbfc..79135b467a3d 100644 --- a/nautilus_core/data/src/engine/tests.rs +++ b/nautilus_core/data/src/engine/tests.rs @@ -32,6 +32,7 @@ use nautilus_common::{ switchboard::MessagingSwitchboard, MessageBus, }, + testing::init_logger_for_testing, }; use nautilus_core::{nanos::UnixNanos, uuid::UUID4}; use nautilus_model::{ @@ -470,14 +471,13 @@ fn test_execute_subscribe_trade_ticks( #[rstest] fn test_execute_subscribe_bars( + audusd_sim: CurrencyPair, msgbus: Rc>, switchboard: MessagingSwitchboard, data_engine: Rc>, data_client: DataClientAdapter, ) { - let client_id = data_client.client_id; - let venue = data_client.venue; - data_engine.borrow_mut().register_client(data_client, None); + init_logger_for_testing(None); // TODO: Remove once initial development completed let endpoint = switchboard.data_engine_execute; let handler = ShareableMessageHandler(Rc::new(SubscriptionCommandHandler { @@ -486,7 +486,14 @@ fn test_execute_subscribe_bars( })); msgbus.borrow_mut().register(endpoint, handler); - let bar_type = BarType::from("AUDUSD.SIM-1-MINUTE-LAST-INTERNAL"); + let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim); + data_engine.borrow_mut().process(&audusd_sim as &dyn Any); + + let client_id = data_client.client_id; + let venue = data_client.venue; + data_engine.borrow_mut().register_client(data_client, None); + + let bar_type = BarType::from("AUD/USD.SIM-1-MINUTE-LAST-INTERNAL"); let metadata = indexmap! { "bar_type".to_string() => bar_type.to_string(), }; @@ -515,6 +522,7 @@ fn test_execute_subscribe_bars( msgbus.borrow().send(&endpoint, &cmd as &dyn Any); data_engine.borrow_mut().run(); + assert_eq!(audusd_sim.id(), bar_type.instrument_id()); assert!(!data_engine.borrow().subscribed_bars().contains(&bar_type)); } From 2606768a0934cf33ed2f22415ffe4154f0a441e1 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Nov 2024 11:05:32 +1100 Subject: [PATCH 12/78] Fix clippy lints --- nautilus_core/portfolio/src/manager.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nautilus_core/portfolio/src/manager.rs b/nautilus_core/portfolio/src/manager.rs index 8e58e39fe6be..c1edae0f75bc 100644 --- a/nautilus_core/portfolio/src/manager.rs +++ b/nautilus_core/portfolio/src/manager.rs @@ -40,6 +40,7 @@ pub struct AccountsManager { } impl AccountsManager { + #[must_use] pub fn update_balances( &self, account: AccountAny, @@ -49,6 +50,7 @@ impl AccountsManager { todo!() } + #[must_use] pub fn update_orders( &self, account: AccountAny, @@ -59,6 +61,7 @@ impl AccountsManager { todo!() } + #[must_use] pub fn update_positions( &self, account: MarginAccount, From ae07595ca50593c7844f2f2281381827da13fce6 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Nov 2024 11:50:53 +1100 Subject: [PATCH 13/78] Improve live engines error logging --- RELEASES.md | 1 + nautilus_trader/live/data_engine.py | 16 ++++++++-------- nautilus_trader/live/execution_engine.py | 8 ++++---- nautilus_trader/live/risk_engine.py | 8 ++++---- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 87748c2ad41c..378c113a6497 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -6,6 +6,7 @@ Released on TBD (UTC). None ### Internal Improvements +- Improve live engines error logging (will now log all exceptions rather than just `RuntimeError`) - Upgraded `datafusion` crate to v43.0.0 (#2056), thanks @twitu ### Breaking Changes diff --git a/nautilus_trader/live/data_engine.py b/nautilus_trader/live/data_engine.py index 8d05a5c04a63..b013c3d99ec1 100644 --- a/nautilus_trader/live/data_engine.py +++ b/nautilus_trader/live/data_engine.py @@ -390,8 +390,8 @@ async def _run_cmd_queue(self) -> None: self._execute_command(command) except asyncio.CancelledError: self._log.warning("DataCommand message queue canceled") - except RuntimeError as e: - self._log.error(f"RuntimeError: {e}") + except Exception as e: + self._log.error(repr(e)) finally: stopped_msg = "DataCommand message queue stopped" if not self._cmd_queue.empty(): @@ -411,8 +411,8 @@ async def _run_req_queue(self) -> None: self._handle_request(request) except asyncio.CancelledError: self._log.warning("DataRequest message queue canceled") - except RuntimeError as e: - self._log.error(f"RuntimeError: {e}") + except Exception as e: + self._log.error(repr(e)) finally: stopped_msg = "DataRequest message queue stopped" if not self._req_queue.empty(): @@ -432,8 +432,8 @@ async def _run_res_queue(self) -> None: self._handle_response(response) except asyncio.CancelledError: self._log.warning("DataResponse message queue canceled") - except RuntimeError as e: - self._log.error(f"RuntimeError: {e}") + except Exception as e: + self._log.error(repr(e)) finally: stopped_msg = "DataResponse message queue stopped" if not self._res_queue.empty(): @@ -451,8 +451,8 @@ async def _run_data_queue(self) -> None: self._handle_data(data) except asyncio.CancelledError: self._log.warning("Data message queue canceled") - except RuntimeError as e: - self._log.error(f"RuntimeError: {e}") + except Exception as e: + self._log.error(repr(e)) finally: stopped_msg = "Data message queue stopped" if not self._data_queue.empty(): diff --git a/nautilus_trader/live/execution_engine.py b/nautilus_trader/live/execution_engine.py index e256dfc82ccb..8c87ac331ec0 100644 --- a/nautilus_trader/live/execution_engine.py +++ b/nautilus_trader/live/execution_engine.py @@ -398,8 +398,8 @@ async def _run_cmd_queue(self) -> None: self._execute_command(command) except asyncio.CancelledError: self._log.warning("Command message queue canceled") - except RuntimeError as e: - self._log.error(f"RuntimeError: {e}") + except Exception as e: + self._log.error(repr(e)) finally: stopped_msg = "Command message queue stopped" if not self._cmd_queue.empty(): @@ -419,8 +419,8 @@ async def _run_evt_queue(self) -> None: self._handle_event(event) except asyncio.CancelledError: self._log.warning("Event message queue canceled") - except RuntimeError as e: - self._log.error(f"RuntimeError: {e}") + except Exception as e: + self._log.error(repr(e)) finally: stopped_msg = "Event message queue stopped" if not self._evt_queue.empty(): diff --git a/nautilus_trader/live/risk_engine.py b/nautilus_trader/live/risk_engine.py index 6a14dfbfbc0c..61581828ee47 100644 --- a/nautilus_trader/live/risk_engine.py +++ b/nautilus_trader/live/risk_engine.py @@ -244,8 +244,8 @@ async def _run_cmd_queue(self) -> None: self._execute_command(command) except asyncio.CancelledError: self._log.warning("Command message queue canceled") - except RuntimeError as e: - self._log.error(f"RuntimeError: {e}") + except Exception as e: + self._log.error(repr(e)) finally: stopped_msg = "Command message queue stopped" if not self._cmd_queue.empty(): @@ -265,8 +265,8 @@ async def _run_evt_queue(self) -> None: self._handle_event(event) except asyncio.CancelledError: self._log.warning("Event message queue canceled") - except RuntimeError as e: - self._log.error(f"RuntimeError: {e}") + except Exception as e: + self._log.error(repr(e)) finally: stopped_msg = "Event message queue stopped" if not self._evt_queue.empty(): From 8850ee23c924eaa4184e5842d319a9eca20753e1 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 18 Nov 2024 17:14:59 +1100 Subject: [PATCH 14/78] Improve HttpClient for use from Rust --- nautilus_core/network/benches/test_client.rs | 4 +- nautilus_core/network/src/http.rs | 124 ++++++++++++++----- nautilus_core/network/src/python/http.rs | 33 ++--- 3 files changed, 102 insertions(+), 59 deletions(-) diff --git a/nautilus_core/network/benches/test_client.rs b/nautilus_core/network/benches/test_client.rs index 0245d149f734..142db6a06b8c 100644 --- a/nautilus_core/network/benches/test_client.rs +++ b/nautilus_core/network/benches/test_client.rs @@ -13,8 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::collections::HashMap; - use nautilus_network::http::InnerHttpClient; use reqwest::Method; @@ -30,7 +28,7 @@ async fn main() { reqs.push(client.send_request( Method::GET, "http://127.0.0.1:3000".to_string(), - HashMap::new(), + None, None, None, )); diff --git a/nautilus_core/network/src/http.rs b/nautilus_core/network/src/http.rs index ff5a4b074489..1f110273e624 100644 --- a/nautilus_core/network/src/http.rs +++ b/nautilus_core/network/src/http.rs @@ -18,12 +18,13 @@ use std::{collections::HashMap, hash::Hash, sync::Arc, time::Duration}; use bytes::Bytes; +use futures_util::{stream, StreamExt}; use reqwest::{ header::{HeaderMap, HeaderName}, Method, Response, Url, }; -use crate::ratelimiter::{clock::MonotonicClock, RateLimiter}; +use crate::ratelimiter::{clock::MonotonicClock, quota::Quota, RateLimiter}; /// Represents the HTTP methods supported by the `HttpClient`. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -70,23 +71,6 @@ pub struct HttpResponse { pub(crate) body: Bytes, } -/// A high-performance HTTP client with rate limiting and timeout capabilities. -/// -/// This struct is designed to handle HTTP requests efficiently, providing -/// support for rate limiting, timeouts, and custom headers. The client is -/// built on top of `reqwest` and can be used for both synchronous and -/// asynchronous HTTP requests. -#[cfg_attr( - feature = "python", - pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.network") -)] -pub struct HttpClient { - /// The rate limiter to control the request rate. - pub(crate) rate_limiter: Arc>, - /// The underlying HTTP client used to make requests. - pub(crate) client: InnerHttpClient, -} - /// Represents errors that can occur when using the `HttpClient`. /// /// This enum provides variants for general HTTP errors and timeout errors, @@ -116,6 +100,85 @@ impl From for HttpClientError { } } +/// A high-performance HTTP client with rate limiting and timeout capabilities. +/// +/// This struct is designed to handle HTTP requests efficiently, providing +/// support for rate limiting, timeouts, and custom headers. The client is +/// built on top of `reqwest` and can be used for both synchronous and +/// asynchronous HTTP requests. +#[cfg_attr( + feature = "python", + pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.network") +)] +pub struct HttpClient { + /// The rate limiter to control the request rate. + pub(crate) rate_limiter: Arc>, + /// The underlying HTTP client used to make requests. + pub(crate) client: Arc, +} + +impl HttpClient { + /// Creates a new [`HttpClient`] instance. + #[must_use] + pub fn new( + header_keys: Vec, + keyed_quotas: Vec<(String, Quota)>, + default_quota: Option, + ) -> Self { + let rate_limiter = Arc::new(RateLimiter::new_with_quota(default_quota, keyed_quotas)); + let client = InnerHttpClient { + client: reqwest::Client::new(), + header_keys, + }; + + Self { + rate_limiter, + client: Arc::new(client), + } + } + + /// Send an HTTP request. + /// + /// `method`: The HTTP method to call. + /// `url`: The request is sent to this url. + /// `headers`: The header key value pairs in the request. + /// `body`: The bytes sent in the body of request. + /// `keys`: The keys used for rate limiting the request. + /// + /// # Example + /// + /// When a request is made the URL should be split into all relevant keys within it. + /// + /// For request /foo/bar, should pass keys ["foo/bar", "foo"] for rate limiting. + #[allow(clippy::too_many_arguments)] + pub async fn request( + &self, + method: Method, + url: String, + headers: Option>, + body: Option, + keys: Option>, + timeout_secs: Option, + ) -> Result { + let client = self.client.clone(); + let rate_limiter = self.rate_limiter.clone(); + + // Check keys for rate limiting quota + let keys = keys.unwrap_or_default(); + let tasks = keys.iter().map(|key| rate_limiter.until_key_ready(key)); + + stream::iter(tasks) + .for_each(|key| async move { + key.await; + }) + .await; + + client + .send_request(method, url, headers, body, timeout_secs) + .await + } +} + /// A high-performance `HttpClient` for HTTP requests. /// /// The client is backed by a hyper Client which keeps connections alive and @@ -142,10 +205,11 @@ impl InnerHttpClient { &self, method: Method, url: String, - headers: HashMap, - body: Option>, + headers: Option>, + body: Option, timeout_secs: Option, ) -> Result { + let headers = headers.unwrap_or_default(); let reqwest_url = Url::parse(url.as_str()) .map_err(|e| HttpClientError::from(format!("URL parse error: {e}")))?; @@ -169,7 +233,7 @@ impl InnerHttpClient { let request = match body { Some(b) => request_builder - .body(b) + .body(b.to_vec()) .build() .map_err(HttpClientError::from)?, None => request_builder.build().map_err(HttpClientError::from)?, @@ -277,13 +341,7 @@ mod tests { let client = InnerHttpClient::default(); let response = client - .send_request( - reqwest::Method::GET, - format!("{url}/get"), - HashMap::new(), - None, - None, - ) + .send_request(reqwest::Method::GET, format!("{url}/get"), None, None, None) .await .unwrap(); @@ -301,7 +359,7 @@ mod tests { .send_request( reqwest::Method::POST, format!("{url}/post"), - HashMap::new(), + None, None, None, ) @@ -329,13 +387,13 @@ mod tests { ); let body_string = serde_json::to_string(&body).unwrap(); - let body_bytes = body_string.into_bytes(); + let body_bytes = Bytes::from(body_string.into_bytes()); let response = client .send_request( reqwest::Method::POST, format!("{url}/post"), - HashMap::new(), + None, Some(body_bytes), None, ) @@ -355,7 +413,7 @@ mod tests { .send_request( reqwest::Method::PATCH, format!("{url}/patch"), - HashMap::new(), + None, None, None, ) @@ -375,7 +433,7 @@ mod tests { .send_request( reqwest::Method::DELETE, format!("{url}/delete"), - HashMap::new(), + None, None, None, ) diff --git a/nautilus_core/network/src/python/http.rs b/nautilus_core/network/src/python/http.rs index cf1f23ee5a08..7c37865dd726 100644 --- a/nautilus_core/network/src/python/http.rs +++ b/nautilus_core/network/src/python/http.rs @@ -16,7 +16,6 @@ use std::{ collections::{hash_map::DefaultHasher, HashMap}, hash::{Hash, Hasher}, - sync::Arc, }; use bytes::Bytes; @@ -24,8 +23,8 @@ use futures_util::{stream, StreamExt}; use pyo3::{create_exception, exceptions::PyException, prelude::*, types::PyBytes}; use crate::{ - http::{HttpClient, HttpClientError, HttpMethod, HttpResponse, InnerHttpClient}, - ratelimiter::{quota::Quota, RateLimiter}, + http::{HttpClient, HttpClientError, HttpMethod, HttpResponse}, + ratelimiter::quota::Quota, }; // Python exception class for generic HTTP errors. @@ -86,7 +85,7 @@ impl HttpResponse { #[pymethods] impl HttpClient { - /// Create a new HttpClient. + /// Creates a new HttpClient. /// /// `header_keys`: The key value pairs for the given `header_keys` are retained from the responses. /// `keyed_quota`: A list of string quota pairs that gives quota for specific key values. @@ -115,18 +114,7 @@ impl HttpClient { keyed_quotas: Vec<(String, Quota)>, default_quota: Option, ) -> Self { - let client = reqwest::Client::new(); - let rate_limiter = Arc::new(RateLimiter::new_with_quota(default_quota, keyed_quotas)); - - let client = InnerHttpClient { - client, - header_keys, - }; - - Self { - rate_limiter, - client, - } + Self::new(header_keys, keyed_quotas, default_quota) } /// Send an HTTP request. @@ -155,14 +143,13 @@ impl HttpClient { timeout_secs: Option, py: Python<'py>, ) -> PyResult> { - let headers = headers.unwrap_or_default(); - let body_vec = body.map(|py_bytes| py_bytes.as_bytes().to_vec()); - let keys = keys.unwrap_or_default(); let client = self.client.clone(); let rate_limiter = self.rate_limiter.clone(); - let method = method.into(); + let keys = keys.unwrap_or_default(); + let body = body.map(|py_bytes| Bytes::from(py_bytes.as_bytes().to_vec())); + pyo3_async_runtimes::tokio::future_into_py(py, async move { - // Check keys for rate limiting quota + // TODO: Consolidate rate limiting let tasks = keys.iter().map(|key| rate_limiter.until_key_ready(key)); stream::iter(tasks) .for_each(|key| async move { @@ -170,9 +157,9 @@ impl HttpClient { }) .await; client - .send_request(method, url, headers, body_vec, timeout_secs) + .send_request(method.into(), url, headers, body, timeout_secs) .await - .map_err(super::super::http::HttpClientError::into_py_err) + .map_err(HttpClientError::into_py_err) }) } } From 8b901f0f012921e55bb7dc12c399c05348a865eb Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 19 Nov 2024 07:30:26 +1100 Subject: [PATCH 15/78] Refine HttpClient --- RELEASES.md | 1 + nautilus_core/network/src/http.rs | 35 +++++++------------- nautilus_core/network/src/python/http.rs | 15 ++------- nautilus_core/network/src/ratelimiter/mod.rs | 12 +++++++ 4 files changed, 28 insertions(+), 35 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 378c113a6497..6b026c360efe 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -7,6 +7,7 @@ None ### Internal Improvements - Improve live engines error logging (will now log all exceptions rather than just `RuntimeError`) +- Refined `HttpClient` for use directly from Rust - Upgraded `datafusion` crate to v43.0.0 (#2056), thanks @twitu ### Breaking Changes diff --git a/nautilus_core/network/src/http.rs b/nautilus_core/network/src/http.rs index 1f110273e624..0a2bc503e48c 100644 --- a/nautilus_core/network/src/http.rs +++ b/nautilus_core/network/src/http.rs @@ -18,7 +18,6 @@ use std::{collections::HashMap, hash::Hash, sync::Arc, time::Duration}; use bytes::Bytes; -use futures_util::{stream, StreamExt}; use reqwest::{ header::{HeaderMap, HeaderName}, Method, Response, Url, @@ -111,10 +110,10 @@ impl From for HttpClientError { pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.network") )] pub struct HttpClient { + /// The underlying HTTP client used to make requests. + pub(crate) client: InnerHttpClient, /// The rate limiter to control the request rate. pub(crate) rate_limiter: Arc>, - /// The underlying HTTP client used to make requests. - pub(crate) client: Arc, } impl HttpClient { @@ -125,15 +124,15 @@ impl HttpClient { keyed_quotas: Vec<(String, Quota)>, default_quota: Option, ) -> Self { - let rate_limiter = Arc::new(RateLimiter::new_with_quota(default_quota, keyed_quotas)); let client = InnerHttpClient { client: reqwest::Client::new(), - header_keys, + header_keys: Arc::new(header_keys), }; + let rate_limiter = Arc::new(RateLimiter::new_with_quota(default_quota, keyed_quotas)); Self { + client, rate_limiter, - client: Arc::new(client), } } @@ -156,24 +155,14 @@ impl HttpClient { method: Method, url: String, headers: Option>, - body: Option, + body: Option>, keys: Option>, timeout_secs: Option, ) -> Result { - let client = self.client.clone(); let rate_limiter = self.rate_limiter.clone(); - // Check keys for rate limiting quota - let keys = keys.unwrap_or_default(); - let tasks = keys.iter().map(|key| rate_limiter.until_key_ready(key)); - - stream::iter(tasks) - .for_each(|key| async move { - key.await; - }) - .await; - - client + rate_limiter.await_keys_ready(keys).await; + self.client .send_request(method, url, headers, body, timeout_secs) .await } @@ -190,7 +179,7 @@ impl HttpClient { #[derive(Clone)] pub struct InnerHttpClient { pub(crate) client: reqwest::Client, - pub(crate) header_keys: Vec, + pub(crate) header_keys: Arc>, } impl InnerHttpClient { @@ -206,7 +195,7 @@ impl InnerHttpClient { method: Method, url: String, headers: Option>, - body: Option, + body: Option>, timeout_secs: Option, ) -> Result { let headers = headers.unwrap_or_default(); @@ -233,7 +222,7 @@ impl InnerHttpClient { let request = match body { Some(b) => request_builder - .body(b.to_vec()) + .body(b) .build() .map_err(HttpClientError::from)?, None => request_builder.build().map_err(HttpClientError::from)?, @@ -387,7 +376,7 @@ mod tests { ); let body_string = serde_json::to_string(&body).unwrap(); - let body_bytes = Bytes::from(body_string.into_bytes()); + let body_bytes = body_string.into_bytes(); let response = client .send_request( diff --git a/nautilus_core/network/src/python/http.rs b/nautilus_core/network/src/python/http.rs index 7c37865dd726..9101daf19647 100644 --- a/nautilus_core/network/src/python/http.rs +++ b/nautilus_core/network/src/python/http.rs @@ -19,8 +19,7 @@ use std::{ }; use bytes::Bytes; -use futures_util::{stream, StreamExt}; -use pyo3::{create_exception, exceptions::PyException, prelude::*, types::PyBytes}; +use pyo3::{create_exception, exceptions::PyException, prelude::*}; use crate::{ http::{HttpClient, HttpClientError, HttpMethod, HttpResponse}, @@ -138,24 +137,16 @@ impl HttpClient { method: HttpMethod, url: String, headers: Option>, - body: Option>, + body: Option>, keys: Option>, timeout_secs: Option, py: Python<'py>, ) -> PyResult> { let client = self.client.clone(); let rate_limiter = self.rate_limiter.clone(); - let keys = keys.unwrap_or_default(); - let body = body.map(|py_bytes| Bytes::from(py_bytes.as_bytes().to_vec())); pyo3_async_runtimes::tokio::future_into_py(py, async move { - // TODO: Consolidate rate limiting - let tasks = keys.iter().map(|key| rate_limiter.until_key_ready(key)); - stream::iter(tasks) - .for_each(|key| async move { - key.await; - }) - .await; + rate_limiter.await_keys_ready(keys).await; client .send_request(method.into(), url, headers, body, timeout_secs) .await diff --git a/nautilus_core/network/src/ratelimiter/mod.rs b/nautilus_core/network/src/ratelimiter/mod.rs index 724087ee8b8b..d2687e751ab9 100644 --- a/nautilus_core/network/src/ratelimiter/mod.rs +++ b/nautilus_core/network/src/ratelimiter/mod.rs @@ -29,6 +29,7 @@ use std::{ }; use dashmap::DashMap; +use futures_util::StreamExt; use tokio::time::sleep; use self::{ @@ -191,6 +192,17 @@ where } } } + + pub async fn await_keys_ready(&self, keys: Option>) { + let keys = keys.unwrap_or_default(); + let tasks = keys.iter().map(|key| self.until_key_ready(key)); + + futures::stream::iter(tasks) + .for_each_concurrent(None, |key_future| async move { + key_future.await; + }) + .await; + } } //////////////////////////////////////////////////////////////////////////////// From d4b192d3efc9988cf94c327a91f0d0c0e524bcb4 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 19 Nov 2024 16:05:15 +1100 Subject: [PATCH 16/78] Improve make clean target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 49534108e09b..0970a1982dc1 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,7 @@ build-wheel-debug: .PHONY: clean clean: - git clean -fxd + git clean -fxd -e tests/test_data/large/ .PHONY: format format: From e7f8879d090ffb88464be8b8f7760e488f1878fa Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 19 Nov 2024 16:09:13 +1100 Subject: [PATCH 17/78] Update dependencies --- nautilus_core/Cargo.lock | 4 +- poetry.lock | 503 +++++++++++++++++++++------------------ 2 files changed, 270 insertions(+), 237 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index c51d14485710..ab4699baf81a 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -4464,9 +4464,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.40" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags 2.6.0", "errno", diff --git a/poetry.lock b/poetry.lock index 437afddfa0ed..c438720e2225 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,87 +13,87 @@ files = [ [[package]] name = "aiohttp" -version = "3.11.2" +version = "3.11.3" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" files = [ - {file = "aiohttp-3.11.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:783741f534c14957fbe657d62a34b947ec06db23d45a2fd4a8aeb73d9c84d7e6"}, - {file = "aiohttp-3.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:435f7a08d8aa42371a94e7c141205a9cb092ba551084b5e0c57492e6673601a3"}, - {file = "aiohttp-3.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c681f34e2814bc6e1eef49752b338061b94a42c92734d0be9513447d3f83718c"}, - {file = "aiohttp-3.11.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73a664478ae1ea011b5a710fb100b115ca8b2146864fa0ce4143ff944df714b8"}, - {file = "aiohttp-3.11.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1d06c8fd8b453c3e553c956bd3b8395100401060430572174bb7876dd95ad49"}, - {file = "aiohttp-3.11.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b1f4844909321ef2c1cee50ddeccbd6018cd8c8d1ddddda3f553e94a5859497"}, - {file = "aiohttp-3.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdc6f8dce09281ae534eaf08a54f0d38612398375f28dad733a8885f3bf9b978"}, - {file = "aiohttp-3.11.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2d942421cf3a1d1eceae8fa192f1fbfb74eb9d3e207d35ad2696bd2ce2c987c"}, - {file = "aiohttp-3.11.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:08ebe7a1d6c1e5ca766d68407280d69658f5f98821c2ba6c41c63cabfed159af"}, - {file = "aiohttp-3.11.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2793d3297f3e49015140e6d3ea26142c967e07998e2fb00b6ee8d041138fbc4e"}, - {file = "aiohttp-3.11.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4a23475d8d5c56e447b7752a1e2ac267c1f723f765e406c81feddcd16cdc97bc"}, - {file = "aiohttp-3.11.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:556564d89e2f4a6e8fe000894c03e4e84cf0b6cfa5674e425db122633ee244d1"}, - {file = "aiohttp-3.11.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:57993f406ce3f114b2a6756d7809be3ffd0cc40f33e8f8b9a4aa1b027fd4e3eb"}, - {file = "aiohttp-3.11.2-cp310-cp310-win32.whl", hash = "sha256:177b000efaf8d2f7012c649e8aee5b0bf488677b1162be5e7511aa4f9d567607"}, - {file = "aiohttp-3.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:ff5d22eece44528023254b595c670dfcf9733ac6af74c4b6cb4f6a784dc3870c"}, - {file = "aiohttp-3.11.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:50e0aee4adc9abcd2109c618a8d1b2c93b85ac277b24a003ab147d91e068b06d"}, - {file = "aiohttp-3.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9aa4e68f1e4f303971ec42976fb170204fb5092de199034b57199a1747e78a2d"}, - {file = "aiohttp-3.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d84930b4145991214602372edd7305fc76b700220db79ac0dd57d3afd0f0a1ca"}, - {file = "aiohttp-3.11.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ec8afd362356b8798c8caa806e91deb3f0602d8ffae8e91d2d3ced2a90c35e"}, - {file = "aiohttp-3.11.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb0544a0e8294a5a5e20d3cacdaaa9a911d7c0a9150f5264aef36e7d8fdfa07e"}, - {file = "aiohttp-3.11.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7b0a1618060e3f5aa73d3526ca2108a16a1b6bf86612cd0bb2ddcbef9879d06"}, - {file = "aiohttp-3.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d878a0186023ac391861958035174d0486f3259cabf8fd94e591985468da3ea"}, - {file = "aiohttp-3.11.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e33a7eddcd07545ccf5c3ab230f60314a17dc33e285475e8405e26e21f02660"}, - {file = "aiohttp-3.11.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4d7fad8c456d180a6d2f44c41cfab4b80e2e81451815825097db48b8293f59d5"}, - {file = "aiohttp-3.11.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d954ba0eae7f33884d27dc00629ca4389d249eb8d26ca07c30911257cae8c96"}, - {file = "aiohttp-3.11.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:afa55e863224e664a782effa62245df73fdfc55aee539bed6efacf35f6d4e4b7"}, - {file = "aiohttp-3.11.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:10a5f91c319d9d4afba812f72984816b5fcd20742232ff7ecc1610ffbf3fc64d"}, - {file = "aiohttp-3.11.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6e8e19a80ba194db5c06915a9df23c0c06e0e9ca9a4db9386a6056cca555a027"}, - {file = "aiohttp-3.11.2-cp311-cp311-win32.whl", hash = "sha256:9c8d1db4f65bbc9d75b7b271d68fb996f1c8c81a525263862477d93611856c2d"}, - {file = "aiohttp-3.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:2adb967454e10e69478ba4a8d8afbba48a7c7a8619216b7c807f8481cc66ddfb"}, - {file = "aiohttp-3.11.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f833a80d9de9307d736b6af58c235b17ef7f90ebea7b9c49cd274dec7a66a2f1"}, - {file = "aiohttp-3.11.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:382f853516664d2ebfc75dc01da4a10fdef5edcb335fe7b45cf471ce758ecb18"}, - {file = "aiohttp-3.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3a2bcf6c81639a165da93469e1e0aff67c956721f3fa9c0560f07dd1e505116"}, - {file = "aiohttp-3.11.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de3b4d5fb5d69749104b880a157f38baeea7765c93d9cd3837cedd5b84729e10"}, - {file = "aiohttp-3.11.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a90a0dc4b054b5af299a900bf950fe8f9e3e54322bc405005f30aa5cacc5c98"}, - {file = "aiohttp-3.11.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32334f35824811dd20a12cc90825d000e6b50faaeaa71408d42269151a66140d"}, - {file = "aiohttp-3.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cba0b8d25aa2d450762f3dd6df85498f5e7c3ad0ddeb516ef2b03510f0eea32"}, - {file = "aiohttp-3.11.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bbb2dbc2701ab7e9307ca3a8fa4999c5b28246968e0a0202a5afabf48a42e22"}, - {file = "aiohttp-3.11.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97fba98fc5d9ccd3d33909e898d00f2494d6a9eec7cbda3d030632e2c8bb4d00"}, - {file = "aiohttp-3.11.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0ebdf5087e2ce903d8220cc45dcece90c2199ae4395fd83ca616fcc81010db2c"}, - {file = "aiohttp-3.11.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:122768e3ae9ce74f981b46edefea9c6e5a40aea38aba3ac50168e6370459bf20"}, - {file = "aiohttp-3.11.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5587da333b7d280a312715b843d43e734652aa382cba824a84a67c81f75b338b"}, - {file = "aiohttp-3.11.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85de9904bc360fd29a98885d2bfcbd4e02ab33c53353cb70607f2bea2cb92468"}, - {file = "aiohttp-3.11.2-cp312-cp312-win32.whl", hash = "sha256:b470de64d17156c37e91effc109d3b032b39867000e2c126732fe01d034441f9"}, - {file = "aiohttp-3.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:3f617a48b70f4843d54f52440ea1e58da6bdab07b391a3a6aed8d3b311a4cc04"}, - {file = "aiohttp-3.11.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5d90b5a3b0f32a5fecf5dd83d828713986c019585f5cddf40d288ff77f366615"}, - {file = "aiohttp-3.11.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d23854e5867650d40cba54d49956aad8081452aa80b2cf0d8c310633f4f48510"}, - {file = "aiohttp-3.11.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:486273d3b5af75a80c31c311988931bdd2a4b96a74d5c7f422bad948f99988ef"}, - {file = "aiohttp-3.11.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9075313f8e41b481e4cb10af405054564b0247dc335db5398ed05f8ec38787e2"}, - {file = "aiohttp-3.11.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44b69c69c194ffacbc50165911cf023a4b1b06422d1e1199d3aea82eac17004e"}, - {file = "aiohttp-3.11.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b339d91ac9060bd6ecdc595a82dc151045e5d74f566e0864ef3f2ba0887fec42"}, - {file = "aiohttp-3.11.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64e8f5178958a9954043bc8cd10a5ae97352c3f2fc99aa01f2aebb0026010910"}, - {file = "aiohttp-3.11.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3129151378f858cdc4a0a4df355c9a0d060ab49e2eea7e62e9f085bac100551b"}, - {file = "aiohttp-3.11.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:14eb6c628432720e41b4fab1ada879d56cfe7034159849e083eb536b4c2afa99"}, - {file = "aiohttp-3.11.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e57a10aacedcf24666f4c90d03e599f71d172d1c5e00dcf48205c445806745b0"}, - {file = "aiohttp-3.11.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:66e58a2e8c7609a3545c4b38fb8b01a6b8346c4862e529534f7674c5265a97b8"}, - {file = "aiohttp-3.11.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9b6d15adc9768ff167614ca853f7eeb6ee5f1d55d5660e3af85ce6744fed2b82"}, - {file = "aiohttp-3.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2914061f5ca573f990ec14191e6998752fa8fe50d518e3405410353c3f44aa5d"}, - {file = "aiohttp-3.11.2-cp313-cp313-win32.whl", hash = "sha256:1c2496182e577042e0e07a328d91c949da9e77a2047c7291071e734cd7a6e780"}, - {file = "aiohttp-3.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:cccb2937bece1310c5c0163d0406aba170a2e5fb1f0444d7b0e7fdc9bd6bb713"}, - {file = "aiohttp-3.11.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:994cb893936dd2e1803655ae8667a45066bfd53360b148e22b4e3325cc5ea7a3"}, - {file = "aiohttp-3.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3666c750b73ce463a413692e3a57c60f7089e2d9116a2aa5a0f0eaf2ae325148"}, - {file = "aiohttp-3.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6ad9a7d2a3a0f235184426425f80bd3b26c66b24fd5fddecde66be30c01ebe6e"}, - {file = "aiohttp-3.11.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c979fc92aba66730b66099cd5becb42d869a26c0011119bc1c2478408a8bf7a"}, - {file = "aiohttp-3.11.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:766d0ebf8703d28f854f945982aa09224d5a27a29594c70d921c43c3930fe7ac"}, - {file = "aiohttp-3.11.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79efd1ee3827b2f16797e14b1e45021206c3271249b4d0025014466d416d7413"}, - {file = "aiohttp-3.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d6e069b882c1fdcbe5577dc4be372eda705180197140577a4cddb648c29d22e"}, - {file = "aiohttp-3.11.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e9a766c346b2ed7e88937919d84ed64b4ef489dad1d8939f806ee52901dc142"}, - {file = "aiohttp-3.11.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2b02a68b9445c70d7f5c8b578c5f5e5866b1d67ca23eb9e8bc8658ae9e3e2c74"}, - {file = "aiohttp-3.11.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:374baefcb1b6275f350da605951f5f02487a9bc84a574a7d5b696439fabd49a3"}, - {file = "aiohttp-3.11.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d2f991c18132f3e505c108147925372ffe4549173b7c258cf227df1c5977a635"}, - {file = "aiohttp-3.11.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:34f37c59b12bc3afc52bab6fcd9cd3be82ff01c4598a84cbea934ccb3a9c54a0"}, - {file = "aiohttp-3.11.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:33af11eca7bb0f5c6ffaf5e7d9d2336c2448f9c6279b93abdd6f3c35f9ee321f"}, - {file = "aiohttp-3.11.2-cp39-cp39-win32.whl", hash = "sha256:83a70e22e0f6222effe7f29fdeba6c6023f9595e59a0479edacfbd7de4b77bb7"}, - {file = "aiohttp-3.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:c28c1677ea33ccb8b14330560094cc44d3ff4fad617a544fd18beb90403fe0f1"}, - {file = "aiohttp-3.11.2.tar.gz", hash = "sha256:68d1f46f9387db3785508f5225d3acbc5825ca13d9c29f2b5cce203d5863eb79"}, + {file = "aiohttp-3.11.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:acb0eb91b7a9ce435728d009388dcbf82d3e394b00596c3eda2402644ce42c33"}, + {file = "aiohttp-3.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3cd3d29e2e0b9726629031d73b08027048deaa856cefda343f3db34da9d6fb04"}, + {file = "aiohttp-3.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63df79e626ad76d3170f2cc87727ebe360c4c56c3dd01d80e1c22fbea18b3368"}, + {file = "aiohttp-3.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ad9cdb478e835d1809d7a86b16c88fb430d6e8f06940e0616586258ec1c4eed"}, + {file = "aiohttp-3.11.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cc0f81fcd9690371d9c89b6675cd12da6abf8bb841cda5b78379fc72ba95cf55"}, + {file = "aiohttp-3.11.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18e391196b233b1e9daef38d14dccbfc3a62934fc1a5cbc711fbf0aaaf12afb2"}, + {file = "aiohttp-3.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb3753b764b6122491db64f5c99c0acf481f6ac54a3062f9041e6e9099337fe3"}, + {file = "aiohttp-3.11.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a2fed9db26ff62d0926169c2d9dba5f1029f75559728dd0ae80e7085e16e056"}, + {file = "aiohttp-3.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:22678102ad8e27536bb5338daa6a8fef268a27498d45793f66c5a90836278376"}, + {file = "aiohttp-3.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ba8bd24b1a08e22baf35e7f1deadee409118cdf086ade14d3a7c0c7cfebc828d"}, + {file = "aiohttp-3.11.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7d16a132673163bec662f77e056e2d7f5c9472560e4346f6847860eabc2e75b3"}, + {file = "aiohttp-3.11.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7b6d71c6eed09fb6c893ca40c49487f46fe440f8a5697e9942715d1a28433b19"}, + {file = "aiohttp-3.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44d15ef64678e9318969a707a6d444621988980a60e917cc8a7dab1dd763bd7c"}, + {file = "aiohttp-3.11.3-cp310-cp310-win32.whl", hash = "sha256:fe842fe3504b3d76be4af8ae5865389eae5cd4d0a7afd631e8d73971628ab525"}, + {file = "aiohttp-3.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:158e47fb9bd16c964762c1661be934d5781423ac7b92d57ccba3cdaef9aa7c16"}, + {file = "aiohttp-3.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a5b836e843e702db8ebeacc6dd8a5137a578dc23b4367e63dc11231fcefe7088"}, + {file = "aiohttp-3.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc53bddadf5fd0a03c6520855cc4e4050ae737f7697d799bac81a0ef8a85f865"}, + {file = "aiohttp-3.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:59912e8f1dc74ffe7cdbf84199131cf60b940ecbd1cd672e88090321875b2ea2"}, + {file = "aiohttp-3.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62aaf76d9077397e34a7c25028ad570d05d11c2f5ec9e42262326d22395cda7d"}, + {file = "aiohttp-3.11.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b10b85ae6e6d7d4e57ef1fd1a3fbfa92bc14854f172957ecf12688f965c7efce"}, + {file = "aiohttp-3.11.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ee52b10fa0ee52ad9741406e18d2d1fce9bc4622566066239d35aaa68323427"}, + {file = "aiohttp-3.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0dcb32dc5d83372e1407675879121e2aabeaa6c633283a8837fcdb363bc5d49"}, + {file = "aiohttp-3.11.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86b3ece9d0c1a2d881b61fa6f4c2b72c5af7e74dbffb73a61fd604be5f51c2e2"}, + {file = "aiohttp-3.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e15fd83a3d252039d35849ccb8f9ee6a774124a0ae49934c8deb472a7b95e5a8"}, + {file = "aiohttp-3.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8998fd80aa1e10b0da45c1edacdb7e7433d4fe9b28fc0d28d69370d733d13bc5"}, + {file = "aiohttp-3.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c3d721538094108db57dde5c3b3af0e157c0a1db6c9f3f55c84f2736f697481c"}, + {file = "aiohttp-3.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:95635838422669e3a0220c74fe9678c838e2cb0dae91bcabfdd3557f11dfe16a"}, + {file = "aiohttp-3.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a79e638ff960068b5a891fe67672d94c9a906fa01043db88789349090333983"}, + {file = "aiohttp-3.11.3-cp311-cp311-win32.whl", hash = "sha256:39554727dc67c170ed84ca4d85937c4955a08dba65d887e48f075b0e3fb746f2"}, + {file = "aiohttp-3.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:16225e7bb046880631e58d3e2ecba19c020be8e873d517ee42a1be8a126b70f0"}, + {file = "aiohttp-3.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0665d51a9580a7df74a281cb3730526865db299742fce115a2ce3033817f7fca"}, + {file = "aiohttp-3.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8e9735209839fbcf81b0b1bfe16d5bd0d5497a5077c2c601f3a347ad34a1436e"}, + {file = "aiohttp-3.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c06fed93bb81f0fe5c2b1a6a131310449e0dfdd0c89ede4b9400e0b5270680e3"}, + {file = "aiohttp-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e7bd606a02bcdb8764a3a6d1493131515a146e44f6e8788f1472445b7ff5280"}, + {file = "aiohttp-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c28332611f7aebd69f0fc183b41b21af2422846ac3dbfa7888ec40962cb8b09"}, + {file = "aiohttp-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce5dd859f71f95235d473bf948a416a981cb37c3247f10a6ca7e630e7ea28e37"}, + {file = "aiohttp-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:969ee33c80399ab6e2627b576073456825234e1384d672dcced9f52e918091b1"}, + {file = "aiohttp-3.11.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270fbf3a5df1ae5fa1f18d26111c0b4cd8c04d84c79b1fe513139a635b5c5285"}, + {file = "aiohttp-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:791d4e2328f06a0373db3ed2b4d353c8f2f3ef7521cacf6e66278033ed2fd192"}, + {file = "aiohttp-3.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8eb02a8a783cc56aa1445df1ccffbf187b66fda103ece7a13e19b3ae33e093f7"}, + {file = "aiohttp-3.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:728bb2b31652c718b03bf9936b4008af0d26a31b8cc632c57450298dcfb82e08"}, + {file = "aiohttp-3.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:10d7b73c974460d198df5cab9f5ebfd40d4b425a52affe05deb9c3ae78664cf5"}, + {file = "aiohttp-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf1f571854b65229b7497de399762a4560894e3f077dd645ab5fdc001eb527ac"}, + {file = "aiohttp-3.11.3-cp312-cp312-win32.whl", hash = "sha256:29828b71745c5562ab73f1513d558e5afca980d83fab42cf87015a50e6076967"}, + {file = "aiohttp-3.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:c1fce3416981cd97a17b0bccebb225c31f82f1e3bbabf04a78547f26c332d619"}, + {file = "aiohttp-3.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:610b39400c761767d5da3d9960811f616f623bba34a9f491dc89029d2a49cc82"}, + {file = "aiohttp-3.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:231c4f1d5f79482e5641febdcb946742c66398deb63dce384da870e59cc884ba"}, + {file = "aiohttp-3.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd0a1856b23b7598e9cd6ff46a08251165f1253b2c922cf3ce07634a1250afb8"}, + {file = "aiohttp-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a003c5ac1e11674bfd7b057afb4c04465d601ea99a927c5eeedcb824b6fb95f1"}, + {file = "aiohttp-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26155e39749dd26cf8795dab6c93ccbe4e58d654a670c520d26bb45786325359"}, + {file = "aiohttp-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4265069ae8d55bf978001402824c18297d1b01aabf4b34931299442249d98113"}, + {file = "aiohttp-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1255eb1438cb35a65af81762964808c8a513a4e686f93319c97d5f351b4f8dad"}, + {file = "aiohttp-3.11.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1468ff557bb154bd500648042e860cd1cc05192e037dd661fff2ce81aeea3bdc"}, + {file = "aiohttp-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:634b474fdcb889a42751cb1095686a3c43d4fca34c560aa5d167353adda7288a"}, + {file = "aiohttp-3.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7ee89599796e220fd8e391d8688b22b63452b772b5b2baffda0f24e2ab258444"}, + {file = "aiohttp-3.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ec2082ffa6e75b41c760b37901bd84711bcee306a5f2fc9fff9d4d290e9a6047"}, + {file = "aiohttp-3.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6a312f5a7fe17a133e605c2db93bd928aad5d1988a7fba5d16f634ac7f5443a0"}, + {file = "aiohttp-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4187859ad7be3de2b319eb84d8865623e2dd907eb0cb825f8c9709afb36947b8"}, + {file = "aiohttp-3.11.3-cp313-cp313-win32.whl", hash = "sha256:cac27ab70c62e043208231ef4cd2f241ee7001355e968f7e474c9007c0e92400"}, + {file = "aiohttp-3.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:66ec2a0c2e6c6fc5f50c1309e3f06280008ba6b13473f465a279e37934c7e9b1"}, + {file = "aiohttp-3.11.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3971b5af634c00c6e1387ac0ed30f713a4abd78aa1b008d0986071011377e042"}, + {file = "aiohttp-3.11.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:aba99bb1f22470e07e2dd29bac108aee1f7278cbcb38f2e67970171feda5c0d2"}, + {file = "aiohttp-3.11.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e22331096df5fa2ce2a6f6bc063c82c867fbe00f465311f7355212e89032145a"}, + {file = "aiohttp-3.11.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:117796a65d59c159642924bf989da00d6c4dc3faf323d86003546f157f14d6e4"}, + {file = "aiohttp-3.11.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ba1ae2c0aa10b8eb946e343d56e58e021550c4fe093cfee4a4aa1eb1bad6cbb"}, + {file = "aiohttp-3.11.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f15f42bfcbbfa4b8a0c0685161e4b1f91c7b24ee47f6d2076e0824bcfafa481"}, + {file = "aiohttp-3.11.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c33562307c6e5dfdea5f380c90ba46c555536edc153b99e0fcce6478f51e386c"}, + {file = "aiohttp-3.11.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed4bbe2f39bc65bd80dc063833877bde77e7032896fd648b799c4dc8489bb3ba"}, + {file = "aiohttp-3.11.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:faae49269ecaf640b1cbc22d896b2540d487564c1b62538a72c54a96573ffb34"}, + {file = "aiohttp-3.11.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:30abc2f6c004a07b9ffa8086c3b739eddc41c54e5b3825ad18bf6d38e01a1fe2"}, + {file = "aiohttp-3.11.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:38aaa713e303b0852e110694d7ef0c6162ffa0c4fe1d70729f3c35f9bda8f752"}, + {file = "aiohttp-3.11.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0c400c7e60f08aa541be32cb1aec82f9c130e326c9fe5a98dda246f67ea64ff5"}, + {file = "aiohttp-3.11.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:91a7dad146005a849a56f4c4448c7ad988b51d08d699ec1605a116cd87073a06"}, + {file = "aiohttp-3.11.3-cp39-cp39-win32.whl", hash = "sha256:3d1b50a83d4f054d4eb7a76f82ecfae3305c6339420cdd2958d9f2cb94581c4d"}, + {file = "aiohttp-3.11.3-cp39-cp39-win_amd64.whl", hash = "sha256:b7816289d4d01b5c6ddccb70d85c3c35cad4d26ae4eeadeee72919da6c7bad99"}, + {file = "aiohttp-3.11.3.tar.gz", hash = "sha256:0fbd111a0f1c254dd2cc54bdf6307e5b04fc3d20f3d36fd53c9bcb25bcebb96e"}, ] [package.dependencies] @@ -2713,83 +2713,116 @@ pytest = "*" [[package]] name = "py-sr25519-bindings" -version = "0.2.0" -description = "Python bindings for sr25519 library" +version = "0.2.1" +description = "Python bindings for schnorrkel RUST crate" optional = true -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "py_sr25519_bindings-0.2.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:86cc1a571852a4f2ade827ebf211e066b23ab805d3e864cbe213a3d8cd53f7d5"}, - {file = "py_sr25519_bindings-0.2.0-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:453c9088e39dd04b07bf3ada6c473a5349c4dfd965009a35124b2c807117eda8"}, - {file = "py_sr25519_bindings-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f12122a18b688e4a4bf0e74d3969d9e3f6f83d2b01fe88ab5f19c969e95192a2"}, - {file = "py_sr25519_bindings-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2815ecc958f6edbad79fee76899bd33b8950caa7106c1db08c828ec90e16fa7"}, - {file = "py_sr25519_bindings-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfe52e73d7f0237820f7a935397d5004733a1d890464701f2c3c71be6033c442"}, - {file = "py_sr25519_bindings-0.2.0-cp310-cp310-manylinux_2_28_armv7l.whl", hash = "sha256:df7e2fad636831919bfa479cd4b6fffdd429cde778da72b1834c1434dadaf982"}, - {file = "py_sr25519_bindings-0.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f4ebeb2aac26a39160f2fad8ffc40ff98da835af57618c0446637bf182b9c927"}, - {file = "py_sr25519_bindings-0.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:942a6b52e871d6e152dda80a60ed338dccedc69b6375e080e496bf886f2556c0"}, - {file = "py_sr25519_bindings-0.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b24307c34a06209d0e34ca15ab4c0275617538dfdac1eac8aa25e792fa9f4108"}, - {file = "py_sr25519_bindings-0.2.0-cp310-none-win32.whl", hash = "sha256:2e06a2d1119a2ad063f11448bb27ec4f4ba77416043d98ae28ef30624cf0e12d"}, - {file = "py_sr25519_bindings-0.2.0-cp310-none-win_amd64.whl", hash = "sha256:16b36d9fe8bda873ab8376f3a4d0894b8d4ab2d702665afc3ab3ca69f0dc9495"}, - {file = "py_sr25519_bindings-0.2.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:54e8c41081a4c23eca4b19f52de2514c48ddec6f49844dff7ad4cfac0bc11712"}, - {file = "py_sr25519_bindings-0.2.0-cp311-cp311-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6c73bd1a87849db9cd0e664b2d2e14208183dd8d11ac083d70e688fc28283a71"}, - {file = "py_sr25519_bindings-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47d21382ea24f7f25e72cdddaca2f013ce46cc7983bcfebc611c795cea177eff"}, - {file = "py_sr25519_bindings-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1448cf55bbf6f52d2e24766a8a84ba6d77100a991897e8519711ccd7409830"}, - {file = "py_sr25519_bindings-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:392b8b9875c89c9302930ad3d59567b62176f33adeee96a55ff61ba17fb7aac2"}, - {file = "py_sr25519_bindings-0.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7b56b5cbbfb36b41ddfa462989a03386590ac036f3a755ef64fffeb2fed88654"}, - {file = "py_sr25519_bindings-0.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8f06ea3237e06666e3a4ff4719b4fba415472943831b229428753c37d5ecd1b4"}, - {file = "py_sr25519_bindings-0.2.0-cp311-none-win_amd64.whl", hash = "sha256:d62af30b2022f5fa787e46c06823c35a21abe791bf55012f498f9ba8e4baabc8"}, - {file = "py_sr25519_bindings-0.2.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ceafa0c31b49f2128461eb2c6ea18dc5d0bfae7218a100be7153f271e46bac49"}, - {file = "py_sr25519_bindings-0.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c8dedb8525556591738a64310875df70ea67886e5a40f2799bd96ef8848936cf"}, - {file = "py_sr25519_bindings-0.2.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ce149796923696f5cfc6263f135674a14fe2d513fd35b2bfa73226b940aff648"}, - {file = "py_sr25519_bindings-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20d71ca3ba22f98f4c208d509f735fe4eb5aa9e3547a507733a95828adde6cab"}, - {file = "py_sr25519_bindings-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8e20ee0856e8a60682566df955b81e7631670136607da627ab6892df34790d"}, - {file = "py_sr25519_bindings-0.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0efd5487e3f6d6053cfc4a891b10f729d69263897270d0354f409ee2106fc9b7"}, - {file = "py_sr25519_bindings-0.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ae7f2164d510458740145d20912d5d7a5c45e8fcde7cebd4057f60811ecc276f"}, - {file = "py_sr25519_bindings-0.2.0-cp312-none-win32.whl", hash = "sha256:92382456c6f176c07e0d554c71d483853387885ce17714f8a4b50fdcf7552297"}, - {file = "py_sr25519_bindings-0.2.0-cp312-none-win_amd64.whl", hash = "sha256:48ee4e14a77f815f3996beecb7d7abf422b756e9163ee4df739c1aded8a3e8ba"}, - {file = "py_sr25519_bindings-0.2.0-cp36-cp36m-macosx_10_7_x86_64.whl", hash = "sha256:c3de899a1e911b8945f09e6389f8d2df68924c12c78e3e66fedb15f1e4ff56ad"}, - {file = "py_sr25519_bindings-0.2.0-cp36-cp36m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:758761b605f90e4238304df7520155a3358b13cc55ee18c5113632da17343163"}, - {file = "py_sr25519_bindings-0.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f63580a224607e68b861eb03421465091c3104b6309e5fca7448f5aa6dbda60"}, - {file = "py_sr25519_bindings-0.2.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b999075b76cae8e84d5f19f2c8f28d3f24c93ba858ad49e58bcf22afe0406b"}, - {file = "py_sr25519_bindings-0.2.0-cp36-cp36m-manylinux_2_28_armv7l.whl", hash = "sha256:5102c94e97d316009ad4482f24d9a933fc0b7eb0bb88e6a784a820cd1bd25827"}, - {file = "py_sr25519_bindings-0.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b477b18940f472d4e25e141f19503a6e55aadff31b4822228a491c9638096baf"}, - {file = "py_sr25519_bindings-0.2.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:7e69bf7bdc9920013c1a2bea25a8b02df9588d9856cb20270f4d8d95b8e83f52"}, - {file = "py_sr25519_bindings-0.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:dc436a34e17037833c3909062722ee3d46e28288972c87f619d163d00054d68e"}, - {file = "py_sr25519_bindings-0.2.0-cp36-none-win32.whl", hash = "sha256:fc27c847dd4df727388aaadc3870aeb472f2d5c35717536d319792fe08f6120a"}, - {file = "py_sr25519_bindings-0.2.0-cp36-none-win_amd64.whl", hash = "sha256:0441381c2a6f532831d560a1f2ae8a917c7190cf27f5428d9b0528fa28a72e2d"}, - {file = "py_sr25519_bindings-0.2.0-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:e1471134450e6189b7e38d245ab16b06f3de900b6d07aa66b1e6973cdbc00d01"}, - {file = "py_sr25519_bindings-0.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:302bd20e75d900d98e7961934b03958e8acc8784eed594ab48f9bb298396c734"}, - {file = "py_sr25519_bindings-0.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e09ac91f4b2e2b9c50e268f6ee292d9fa447c5b7cc6327cfeae7750d716f49e"}, - {file = "py_sr25519_bindings-0.2.0-cp37-cp37m-manylinux_2_28_armv7l.whl", hash = "sha256:28b904739147c4f797627bd3b44d1e64d061533253abd1882c6d3b8944e7bbd8"}, - {file = "py_sr25519_bindings-0.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0931ac85331aae33bef67460a3cce554ef5c1f7dfec0ebe2f5b9ea57c5bee65c"}, - {file = "py_sr25519_bindings-0.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cd8da64f9e42ff973b394ed9164f1e9a454279a058eed08ac8d006fcbd61093b"}, - {file = "py_sr25519_bindings-0.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:297ad50e3cace5c89dbf5bd916b714aac3ebe6bc76f85382dac228cbeb71449e"}, - {file = "py_sr25519_bindings-0.2.0-cp37-none-win32.whl", hash = "sha256:422d62ca74ebe5065eca88607552b9a5f1dc4abff0c597cc3793dd8adfb8c4ea"}, - {file = "py_sr25519_bindings-0.2.0-cp37-none-win_amd64.whl", hash = "sha256:d1b0ed9a4dded60f671f34fdd81c974dad159e98f43bcab21833f984e05920f9"}, - {file = "py_sr25519_bindings-0.2.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:37f11ffee535c624bf5ddc6109c2cdca9a2dbd10f7d310bcd1dd97f6121c532f"}, - {file = "py_sr25519_bindings-0.2.0-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:4e1b553a6b1cc1b0aa9da2d7157329713cc7f299acb12a052d326f9b594b145c"}, - {file = "py_sr25519_bindings-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f0b6dcf1328027dba1f9236bd3432cc3cce1de55a12c1a3a4ea7a8dc3ab3e857"}, - {file = "py_sr25519_bindings-0.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:458c7e6d7447bd267a6f870a8801e995d710952566a0a52634f408bf804cf27a"}, - {file = "py_sr25519_bindings-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d64253d7d08fd6073e7b79bba9cff78687e76698cc210d3c6f236b90766b9421"}, - {file = "py_sr25519_bindings-0.2.0-cp38-cp38-manylinux_2_28_armv7l.whl", hash = "sha256:a9aac20a196416b8daf764704a9cee71ddee16bc705d12b5c6bcb6f51e81ac6e"}, - {file = "py_sr25519_bindings-0.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e162687189cf765f602178aa195a2be4284107622141ff746e92e14e266cf3b7"}, - {file = "py_sr25519_bindings-0.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d44ab4d150c9bdd1641ccad49942ecf2d0ef61bd66a7da41094bb4a9cbaca529"}, - {file = "py_sr25519_bindings-0.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:251ff9cef5dafd96ff241b77ff471912c40249b6df31e71c6c32de6a26a8dbc6"}, - {file = "py_sr25519_bindings-0.2.0-cp38-none-win32.whl", hash = "sha256:ca9794f9d4fc37cdc8cbb6724d5432a064d22c26ecde312928154b6bc691f4d3"}, - {file = "py_sr25519_bindings-0.2.0-cp38-none-win_amd64.whl", hash = "sha256:6406cb0aeb5cbb8cfaa37d59d15d7640c0d812a1cbb55657bee52fd3d9e92aa9"}, - {file = "py_sr25519_bindings-0.2.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:b9da73447c8f5b8392a8229c2b65d742709c6aa2d0c6b32e39b635fb245145f1"}, - {file = "py_sr25519_bindings-0.2.0-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:7f00236a802d6d3f3705713d5352ba968c0ce353a20519c445e66ce19869bfdc"}, - {file = "py_sr25519_bindings-0.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d473199c0dbad846b0723c6663b1b6a04040ccdca700cb1609acac3e621f2087"}, - {file = "py_sr25519_bindings-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44bede0dd42f75cf849d3ccb4e443d6425218035bc00a6330b11dc2cc1146f3b"}, - {file = "py_sr25519_bindings-0.2.0-cp39-cp39-manylinux_2_28_armv7l.whl", hash = "sha256:a8e462d2442726d9db07854dc2eb640b1a8a548948b1ff3aa580771ab739bab8"}, - {file = "py_sr25519_bindings-0.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:55b1f67fdaeab91481fda54432dffdf87ed516d26461d31e70911c7ea55d6164"}, - {file = "py_sr25519_bindings-0.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ec11493d59075ba75fe0bc0312d502ffdc45b641a46fb084bf8b04906597688b"}, - {file = "py_sr25519_bindings-0.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:101ee46368da149ad332aea225d4ff2907dffce574e8f8f7fe56f5c29211f333"}, - {file = "py_sr25519_bindings-0.2.0-cp39-none-win32.whl", hash = "sha256:909f13f63f67f1e5595d4d495cf8a3c95e392626c08f94550cbf8f0e8ea1c743"}, - {file = "py_sr25519_bindings-0.2.0-cp39-none-win_amd64.whl", hash = "sha256:b3f86e4aad6c2b8ff74af76f38fde7fbaf9dd83bc4a7c259709092008c3b8e5d"}, - {file = "py_sr25519_bindings-0.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38db0ee90bd676b9df7ddd03fcb2113b5a5e9d9c984d82426728acc0e9d54277"}, - {file = "py_sr25519_bindings-0.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dfe767069d5c5e8a313e77b6bd681ea4f6b5988b09b6b4c9399e255fe4a7c53"}, - {file = "py_sr25519_bindings-0.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8951d1a6e310a682a6253d547e44a9e7a606476dbc18dea3f121d98bdb81042"}, - {file = "py_sr25519_bindings-0.2.0.tar.gz", hash = "sha256:0c2fe92b7cdcebf6c5611a90054f8ba6ea90b68b8832896d2dc565537bc40b0c"}, + {file = "py_sr25519_bindings-0.2.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10489c399768dc4ac91c90a6c8da60aeb77a48b21a81944244d41b0d4c4be2f"}, + {file = "py_sr25519_bindings-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8358a7b3048765008a79733447dfdcafdce3f66859c98634055fee6868252e12"}, + {file = "py_sr25519_bindings-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:202af5a516614907ddaef073104ae6d0a98ec96743d11cb87faa09d2b235a6b4"}, + {file = "py_sr25519_bindings-0.2.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0b0d977c9ba6063d7807dda84264f10b1951736ba528b4d4078e5c9989051b1"}, + {file = "py_sr25519_bindings-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e6c46cbbb87eb9db3c7deebd71c296d67c0725d9379ee737255e22c15c64bae"}, + {file = "py_sr25519_bindings-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9368e9ca0bc1c967db0dd5cfc401f23d364064e99a48d21ea12a068612ccce7e"}, + {file = "py_sr25519_bindings-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9f1ade92569b0281ff24476bd93333865370d86746b2d7949545f1ca70ac4e14"}, + {file = "py_sr25519_bindings-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7286da1662afc300038441620092a0ae527430f7c50b0768e826d46893dd5095"}, + {file = "py_sr25519_bindings-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1afbf451ecb78d5a1fa3be0f1cafb914aa2d4464ce15374bbff495cc384b1947"}, + {file = "py_sr25519_bindings-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:873c0ec12fed805f4086e36ebbb673c95af09e4007ea66d5a9bbd2cc29dfa076"}, + {file = "py_sr25519_bindings-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5917f8584cf6a81e32f03547d9fbd8c783db2372d49bd9ff8c5c57d969ea1039"}, + {file = "py_sr25519_bindings-0.2.1-cp310-none-win32.whl", hash = "sha256:09f184393e01d0d2b62d3782a6d18dd0824a225444e0171c08e03f8cf3920e7b"}, + {file = "py_sr25519_bindings-0.2.1-cp310-none-win_amd64.whl", hash = "sha256:2d548a8ea057c6f150572059475761101ba8ef15e3b349d2d0cb108652f6aaf8"}, + {file = "py_sr25519_bindings-0.2.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4941e6e0e180f7e72565043ed3ba7190455c9feaa2ab9ee6038904f2b4bb6c5b"}, + {file = "py_sr25519_bindings-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b63d7cf5bb4d9b986d7f7012c80b92be70311dc9b75862f7880e03b71a29543d"}, + {file = "py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6752bf3b109446d99f3a368e3ba805812fc5bc09e52ef1c82f5a47e43b19973"}, + {file = "py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0368dcdf5ec8d2bb9c13273c78c3c5b033211d37a70a2f1d2080f29a7d118340"}, + {file = "py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2618b02b4a3babac07b8bb61fe9550f911f038bb079665682ca76b2e664e5258"}, + {file = "py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab1bc4dc524efefaecf3a85f4a0ff05c1ca9509d4d64056199984550f3c98b3"}, + {file = "py_sr25519_bindings-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7ccdc89d5e3ae0dd163c8150ec76b6bb3291c1cec9746eb79e9544b3423f35f9"}, + {file = "py_sr25519_bindings-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ae6545c414cfa5d7207c9c77aaa576bb374982fb2105a7a9c2764afa5621f6d4"}, + {file = "py_sr25519_bindings-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7046774e39e0166d3c12632969c9d1713e6ad9ca8206bbe82923ba6935b0a01f"}, + {file = "py_sr25519_bindings-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cba9a8821176895b080ea761e5ab9cd8727660bf401478a6532a30ae3429573d"}, + {file = "py_sr25519_bindings-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c31aba05819e5b6b26746dc1b078cf680bd471f135c55e376e95c7774e22e936"}, + {file = "py_sr25519_bindings-0.2.1-cp311-none-win32.whl", hash = "sha256:d4bfb9c9a5c46563ccf12e74862ee95d2961556ba7aca62c9e4d6e4f7c37b4e0"}, + {file = "py_sr25519_bindings-0.2.1-cp311-none-win_amd64.whl", hash = "sha256:4f0d5c065d5e6122e53e771035aa335534363b451358b408d211df1c46773617"}, + {file = "py_sr25519_bindings-0.2.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:01ef73c0b3d3f703b54ee69c0f5ff4aa54b4233212c466fd497c7a84d170963a"}, + {file = "py_sr25519_bindings-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7ce8ac85e5ea82825a863f3f6f071e5ead610d7675820eb8ffe772267445ec0b"}, + {file = "py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f59ac8c03c8ef819db063627f4a8247aab0db11d88b21562abbe371612cf66ab"}, + {file = "py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2c11fc77b57308e3ada9a40e7c343027129b582d3091ebd992c99b1832ac8c1"}, + {file = "py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92af2831d6896f0b3fef792d1f2da780fabf6c78dac12535b394cbdb51c0d257"}, + {file = "py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc99f7f310b7641e510810c1d6a6b51792ab2ccefac3ab288445a9fcbc9a8265"}, + {file = "py_sr25519_bindings-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1dc4995a352a6e5851a41cb0ea37d8c9083d173515b7fd2f381b014f57dc1cda"}, + {file = "py_sr25519_bindings-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f103dc5c420057c4447bd6ebf28b2b68ff3ab8da85a5f7ff39c405293de80c78"}, + {file = "py_sr25519_bindings-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:902ee675497b8d356a2abe2abc4278cd76c503f76d06ef2bcd797c1df59e84b7"}, + {file = "py_sr25519_bindings-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5dd9748f4bd9a3bc4d5c1245f6edcc723075b1470b4c36add4474df4c53604e8"}, + {file = "py_sr25519_bindings-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c24bc55699d12948571969c26e65138a942bdaca062171288c40c44b9a4f266"}, + {file = "py_sr25519_bindings-0.2.1-cp312-none-win32.whl", hash = "sha256:d4799c9a8f280abdfe564d397bad45da380275c8d22604e059bd7b3d5af404b5"}, + {file = "py_sr25519_bindings-0.2.1-cp312-none-win_amd64.whl", hash = "sha256:0746befd71d1766d8747910cfeb2cec2be2c859c3b3618eda1dc3cb4a1b85175"}, + {file = "py_sr25519_bindings-0.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfb80d71c010654638873e594e348a0add78dba66d089ef07d02998712744e80"}, + {file = "py_sr25519_bindings-0.2.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:761e48147e3b1e65b9c5ed3f547e600126f02d6b8e99aa99eb8faeb2c69166c2"}, + {file = "py_sr25519_bindings-0.2.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a14ce5fa0759710d45848cc98b49a10f7db3f1002726b61c57b9cdaf91c2f5f"}, + {file = "py_sr25519_bindings-0.2.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d731da6f49ee67dcd90ed25a393f9027e7a0caece837b1a66ffba10e63861356"}, + {file = "py_sr25519_bindings-0.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:507ae0d8894307642056f99df4acf2da9fe11153fd6e9d9e255d1d05db1b348e"}, + {file = "py_sr25519_bindings-0.2.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:a3dad5382f60696161e91d6dd2d9381e9de61af1bf5699084939780d86115e12"}, + {file = "py_sr25519_bindings-0.2.1-cp37-cp37m-musllinux_1_2_armv7l.whl", hash = "sha256:f913f2ddb478232a7a716603b47d276a4ab29230a4d3e87406523a0f1ae5c191"}, + {file = "py_sr25519_bindings-0.2.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:4844e2999a1d0ac5e9a166a2cc3557aedce6144b886bc9efd7b3f2e081feca97"}, + {file = "py_sr25519_bindings-0.2.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:f81ad03ff65d92c9a6deba451c922204af97dbc9a0d0680a91495ad523944929"}, + {file = "py_sr25519_bindings-0.2.1-cp37-none-win32.whl", hash = "sha256:87ac70b8424b91ba5a446a6e6dcf33d55eb4acadf1cec393294ec740d26aa7c6"}, + {file = "py_sr25519_bindings-0.2.1-cp37-none-win_amd64.whl", hash = "sha256:593b639e25a6d334a25c4b51ab2eeb80f13d510433a42abf5f2302876e637435"}, + {file = "py_sr25519_bindings-0.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5059645a99bcb77a8cadacd1a5b01dc3041b3f684595e47669a484dc6e862bc1"}, + {file = "py_sr25519_bindings-0.2.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ce9bef8a02542a1f80560137a67e011f74e0cd77b168214d2e564225f73aa01"}, + {file = "py_sr25519_bindings-0.2.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:007e6b548bfbf4bf4d0daa30784c7e03935bf47081cc9a3095cf52712ae64c72"}, + {file = "py_sr25519_bindings-0.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5b65131461d87062c75f2076a2c99aea4072e4886275e87e8616b3433e5c456"}, + {file = "py_sr25519_bindings-0.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:db721395bb9c7d61392ab3957781450cba281b814c94f1888bb576891d3016d1"}, + {file = "py_sr25519_bindings-0.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b590397aaf2f222a5768f0b74bf08315ef105bc70c50f9bf5f3e6b97458d772d"}, + {file = "py_sr25519_bindings-0.2.1-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:a5b43cdf722f40f042ed05607bca7032055df4cdc413f52746e972ec393aa82f"}, + {file = "py_sr25519_bindings-0.2.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:46033ed3fe67ad11fa0f46f19483175a83185a02af6eb93d7391e81b3219c5a8"}, + {file = "py_sr25519_bindings-0.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4e3c1d51ae59b1bf295f1c5af21adc1acab60a7a018e081873f124456492db88"}, + {file = "py_sr25519_bindings-0.2.1-cp38-none-win32.whl", hash = "sha256:6b34f32efccb5a26c14f4ec1666f2821760981a709e04a486357bc0a152f5d94"}, + {file = "py_sr25519_bindings-0.2.1-cp38-none-win_amd64.whl", hash = "sha256:9ab1d3c8c3458a74217b849ffed3e03c98e746d488c9cf9b773f55ad8d3031ad"}, + {file = "py_sr25519_bindings-0.2.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:89014247bb398acf99e508a0eff7b1dee8cea4b1d441ceeee8de275b1944812f"}, + {file = "py_sr25519_bindings-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3ce7c463b73864909391bfad078b1c88ebbc1eb84f58336c605cbcaf3cecd2f"}, + {file = "py_sr25519_bindings-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a2c28840138ba0a4e6f8c6953821cbd1d80d2e52404ff9722030a22d26addd"}, + {file = "py_sr25519_bindings-0.2.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b0cea045676c3c482423232d19b6aac2458925416fcceec0a37c938f8bc9c00d"}, + {file = "py_sr25519_bindings-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cedbcc9779630c7cd364a66e686aa5c2ad0dd81fbb95edb689a6f63eb3323d6"}, + {file = "py_sr25519_bindings-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d064e6154554e18f3c40349c7df01297d812da5f6c4bcb825fa9f4fe2dd402d"}, + {file = "py_sr25519_bindings-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6156c50f92b705d89f82b0dcb51eb0eaf0f22fba9fa51648a5e0c8274b0e0502"}, + {file = "py_sr25519_bindings-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a915deadf311592c9d7dc6cf6b0550830aeb08c5029cb06e882c32dcb560125b"}, + {file = "py_sr25519_bindings-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:8b56ceec5f83dd9c4b809f3be3ef4262d1e833d1ed8f16d7d8283fb2c5ae1a75"}, + {file = "py_sr25519_bindings-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:73948c2b022287ff478a276b725a98a3bea34920cfe0edbedc0154f9a6125061"}, + {file = "py_sr25519_bindings-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a8bc937794b947b9da2f20fa0d8f5002d20d2bfc2656a21ef834e1af2d3fdca4"}, + {file = "py_sr25519_bindings-0.2.1-cp39-none-win32.whl", hash = "sha256:d27b882546d5ad78f71c1ec48033267a0dd812fb1583881c39a75b3180a7e80b"}, + {file = "py_sr25519_bindings-0.2.1-cp39-none-win_amd64.whl", hash = "sha256:5ad0d7b14339452072773bae6d4570684895658a046279bebd3410941846ea65"}, + {file = "py_sr25519_bindings-0.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50f8b34fed2c98814dcd414379ef43bf63cd4c05d7d90b83c590cca60fe804d6"}, + {file = "py_sr25519_bindings-0.2.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:141b0f8fb99cb249984f7c9ec67dd1768aae4d137d47ea0eca027d669503e132"}, + {file = "py_sr25519_bindings-0.2.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45cfef18bdfde67d445650a388bfafecbd1844a64c19087e9e4267548998c100"}, + {file = "py_sr25519_bindings-0.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:639410c0258a543bb84b0518616af724716737054ac5c78daa4d956d17841b17"}, + {file = "py_sr25519_bindings-0.2.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a98e5a395445046f37fc4e365556ce06fa344e3b711de0564ac3fd2b351a1b3e"}, + {file = "py_sr25519_bindings-0.2.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:7935b79a91aa72db42b5015117018554980c320256e63bc930b8bd148a0765a4"}, + {file = "py_sr25519_bindings-0.2.1-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:cedf5d0669c23ddab8804982f665c7e99b13e8452db78128f231217b8528c31a"}, + {file = "py_sr25519_bindings-0.2.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:9ea24db07992f756409729adad1e3ec9aa0a9d4fece5da90768a56ac1563f0f4"}, + {file = "py_sr25519_bindings-0.2.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e8b7e42cd4a5177dd83bbcdef77591fd72d3da02616545011ebcdd872f8cc39d"}, + {file = "py_sr25519_bindings-0.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0db53685c6282b29d118ccac3bdaad257723494c07c38dc9a4f31027dc41885"}, + {file = "py_sr25519_bindings-0.2.1-pp37-pypy37_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6e42e6edca45b9f116c97068416eb96c6606498289c056731dc08b645592b1ec"}, + {file = "py_sr25519_bindings-0.2.1-pp37-pypy37_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:927d773693d41f6fb9644148649d78875ac27d21dcfd3436502d68c5cc6b0f30"}, + {file = "py_sr25519_bindings-0.2.1-pp37-pypy37_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d169fd6a803a80c3554562c38894d942da8a408a43685b723bcd3a79ce884ee4"}, + {file = "py_sr25519_bindings-0.2.1-pp37-pypy37_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:37f48ba05b3306b2aa9b97b6b91361c83467ce8b77348c2ecb28090fae193d6d"}, + {file = "py_sr25519_bindings-0.2.1-pp37-pypy37_pp73-musllinux_1_2_i686.whl", hash = "sha256:9c9385e98e166cb293dda2a0691b511d770a5ffa0d7fe8495fd558387cbe06fe"}, + {file = "py_sr25519_bindings-0.2.1-pp37-pypy37_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:0855eb9ad70f3673d88e25cae4d799aff0e9fcfb6cbd24a8a41e1c7915f5f5a8"}, + {file = "py_sr25519_bindings-0.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1662dc8161fbb4e5220a89e8f4fd42a1ce5d71471e5d5a9398ed07ced12d2dc"}, + {file = "py_sr25519_bindings-0.2.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c3bca18a20ea6f12f662f4a38e8132f952f3ec77e4a1e0b4654a5fc0aeb54eb"}, + {file = "py_sr25519_bindings-0.2.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b857902b2c74269ae4fb9d1ac915993bbd55291351f6b8bb2bb6a08b5631bb5"}, + {file = "py_sr25519_bindings-0.2.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:04f44054b3244e13c1f9440b616251ff200fe679ce7d934783f214065a22f78e"}, + {file = "py_sr25519_bindings-0.2.1-pp38-pypy38_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:7f8e2c3e0ecb086648c64274a98d6663bece7aaafbee8b7e229fc3f024d4ffb5"}, + {file = "py_sr25519_bindings-0.2.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:23edd08e0866b5ccf9fdbafa7e6a0646070b37ad6869723252136a2c47b5b5fc"}, + {file = "py_sr25519_bindings-0.2.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5bfd4d91329889109d8d5cbd22fa4138e778cb7522704f45f451b23a5573b1aa"}, + {file = "py_sr25519_bindings-0.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:858b80041b18fdde666427ec9843303931ab2184cdf698285e8d34f3f6c4fad0"}, + {file = "py_sr25519_bindings-0.2.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9a848030227b8099c26c4f38b35fbae55cb78e0d3fab69804bf220e60a85455"}, + {file = "py_sr25519_bindings-0.2.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de7afdcc714fd83fc3636b9cea6c2ef6515e59f97410e73210276c3e0e64a28b"}, + {file = "py_sr25519_bindings-0.2.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9333d891f1305f686f6ef4b9aef204df3090d037056e9f6e1276165c29ef70c2"}, + {file = "py_sr25519_bindings-0.2.1-pp39-pypy39_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:970e0635104f2d5e771de3b8863eb0f7d04617c164d49d17e02ecc60c3a97182"}, + {file = "py_sr25519_bindings-0.2.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:995e2c49dd0df3adb7907b2dc5a30d4df64160023205d89256b88a956c64637c"}, + {file = "py_sr25519_bindings-0.2.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4177bf68b73080ca0a21cf1231877dbec4f4485ee22bc97b7d447a0e29fe9c30"}, + {file = "py_sr25519_bindings-0.2.1.tar.gz", hash = "sha256:1b96d3dde43adcf86ab427a9fd72b2c6291dca36eb40747df631588c16f01c1a"}, ] [[package]] @@ -3975,93 +4008,93 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "yarl" -version = "1.17.1" +version = "1.17.2" description = "Yet another URL library" optional = false python-versions = ">=3.9" files = [ - {file = "yarl-1.17.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1794853124e2f663f0ea54efb0340b457f08d40a1cef78edfa086576179c91"}, - {file = "yarl-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fbea1751729afe607d84acfd01efd95e3b31db148a181a441984ce9b3d3469da"}, - {file = "yarl-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ee427208c675f1b6e344a1f89376a9613fc30b52646a04ac0c1f6587c7e46ec"}, - {file = "yarl-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b74ff4767d3ef47ffe0cd1d89379dc4d828d4873e5528976ced3b44fe5b0a21"}, - {file = "yarl-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62a91aefff3d11bf60e5956d340eb507a983a7ec802b19072bb989ce120cd948"}, - {file = "yarl-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:846dd2e1243407133d3195d2d7e4ceefcaa5f5bf7278f0a9bda00967e6326b04"}, - {file = "yarl-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e844be8d536afa129366d9af76ed7cb8dfefec99f5f1c9e4f8ae542279a6dc3"}, - {file = "yarl-1.17.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc7c92c1baa629cb03ecb0c3d12564f172218fb1739f54bf5f3881844daadc6d"}, - {file = "yarl-1.17.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ae3476e934b9d714aa8000d2e4c01eb2590eee10b9d8cd03e7983ad65dfbfcba"}, - {file = "yarl-1.17.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c7e177c619342e407415d4f35dec63d2d134d951e24b5166afcdfd1362828e17"}, - {file = "yarl-1.17.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64cc6e97f14cf8a275d79c5002281f3040c12e2e4220623b5759ea7f9868d6a5"}, - {file = "yarl-1.17.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:84c063af19ef5130084db70ada40ce63a84f6c1ef4d3dbc34e5e8c4febb20822"}, - {file = "yarl-1.17.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:482c122b72e3c5ec98f11457aeb436ae4aecca75de19b3d1de7cf88bc40db82f"}, - {file = "yarl-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:380e6c38ef692b8fd5a0f6d1fa8774d81ebc08cfbd624b1bca62a4d4af2f9931"}, - {file = "yarl-1.17.1-cp310-cp310-win32.whl", hash = "sha256:16bca6678a83657dd48df84b51bd56a6c6bd401853aef6d09dc2506a78484c7b"}, - {file = "yarl-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:561c87fea99545ef7d692403c110b2f99dced6dff93056d6e04384ad3bc46243"}, - {file = "yarl-1.17.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cbad927ea8ed814622305d842c93412cb47bd39a496ed0f96bfd42b922b4a217"}, - {file = "yarl-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fca4b4307ebe9c3ec77a084da3a9d1999d164693d16492ca2b64594340999988"}, - {file = "yarl-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff5c6771c7e3511a06555afa317879b7db8d640137ba55d6ab0d0c50425cab75"}, - {file = "yarl-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b29beab10211a746f9846baa39275e80034e065460d99eb51e45c9a9495bcca"}, - {file = "yarl-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a52a1ffdd824fb1835272e125385c32fd8b17fbdefeedcb4d543cc23b332d74"}, - {file = "yarl-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58c8e9620eb82a189c6c40cb6b59b4e35b2ee68b1f2afa6597732a2b467d7e8f"}, - {file = "yarl-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d216e5d9b8749563c7f2c6f7a0831057ec844c68b4c11cb10fc62d4fd373c26d"}, - {file = "yarl-1.17.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:881764d610e3269964fc4bb3c19bb6fce55422828e152b885609ec176b41cf11"}, - {file = "yarl-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8c79e9d7e3d8a32d4824250a9c6401194fb4c2ad9a0cec8f6a96e09a582c2cc0"}, - {file = "yarl-1.17.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:299f11b44d8d3a588234adbe01112126010bd96d9139c3ba7b3badd9829261c3"}, - {file = "yarl-1.17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cc7d768260f4ba4ea01741c1b5fe3d3a6c70eb91c87f4c8761bbcce5181beafe"}, - {file = "yarl-1.17.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:de599af166970d6a61accde358ec9ded821234cbbc8c6413acfec06056b8e860"}, - {file = "yarl-1.17.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2b24ec55fad43e476905eceaf14f41f6478780b870eda5d08b4d6de9a60b65b4"}, - {file = "yarl-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9fb815155aac6bfa8d86184079652c9715c812d506b22cfa369196ef4e99d1b4"}, - {file = "yarl-1.17.1-cp311-cp311-win32.whl", hash = "sha256:7615058aabad54416ddac99ade09a5510cf77039a3b903e94e8922f25ed203d7"}, - {file = "yarl-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:14bc88baa44e1f84164a392827b5defb4fa8e56b93fecac3d15315e7c8e5d8b3"}, - {file = "yarl-1.17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:327828786da2006085a4d1feb2594de6f6d26f8af48b81eb1ae950c788d97f61"}, - {file = "yarl-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc353841428d56b683a123a813e6a686e07026d6b1c5757970a877195f880c2d"}, - {file = "yarl-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73df5b6e8fabe2ddb74876fb82d9dd44cbace0ca12e8861ce9155ad3c886139"}, - {file = "yarl-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bdff5e0995522706c53078f531fb586f56de9c4c81c243865dd5c66c132c3b5"}, - {file = "yarl-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06157fb3c58f2736a5e47c8fcbe1afc8b5de6fb28b14d25574af9e62150fcaac"}, - {file = "yarl-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1654ec814b18be1af2c857aa9000de7a601400bd4c9ca24629b18486c2e35463"}, - {file = "yarl-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6595c852ca544aaeeb32d357e62c9c780eac69dcd34e40cae7b55bc4fb1147"}, - {file = "yarl-1.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:459e81c2fb920b5f5df744262d1498ec2c8081acdcfe18181da44c50f51312f7"}, - {file = "yarl-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e48cdb8226644e2fbd0bdb0a0f87906a3db07087f4de77a1b1b1ccfd9e93685"}, - {file = "yarl-1.17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d9b6b28a57feb51605d6ae5e61a9044a31742db557a3b851a74c13bc61de5172"}, - {file = "yarl-1.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e594b22688d5747b06e957f1ef822060cb5cb35b493066e33ceac0cf882188b7"}, - {file = "yarl-1.17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5f236cb5999ccd23a0ab1bd219cfe0ee3e1c1b65aaf6dd3320e972f7ec3a39da"}, - {file = "yarl-1.17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a2a64e62c7a0edd07c1c917b0586655f3362d2c2d37d474db1a509efb96fea1c"}, - {file = "yarl-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d0eea830b591dbc68e030c86a9569826145df485b2b4554874b07fea1275a199"}, - {file = "yarl-1.17.1-cp312-cp312-win32.whl", hash = "sha256:46ddf6e0b975cd680eb83318aa1d321cb2bf8d288d50f1754526230fcf59ba96"}, - {file = "yarl-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:117ed8b3732528a1e41af3aa6d4e08483c2f0f2e3d3d7dca7cf538b3516d93df"}, - {file = "yarl-1.17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5d1d42556b063d579cae59e37a38c61f4402b47d70c29f0ef15cee1acaa64488"}, - {file = "yarl-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0167540094838ee9093ef6cc2c69d0074bbf84a432b4995835e8e5a0d984374"}, - {file = "yarl-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2f0a6423295a0d282d00e8701fe763eeefba8037e984ad5de44aa349002562ac"}, - {file = "yarl-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b078134f48552c4d9527db2f7da0b5359abd49393cdf9794017baec7506170"}, - {file = "yarl-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d401f07261dc5aa36c2e4efc308548f6ae943bfff20fcadb0a07517a26b196d8"}, - {file = "yarl-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5f1ac7359e17efe0b6e5fec21de34145caef22b260e978336f325d5c84e6938"}, - {file = "yarl-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f63d176a81555984e91f2c84c2a574a61cab7111cc907e176f0f01538e9ff6e"}, - {file = "yarl-1.17.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e275792097c9f7e80741c36de3b61917aebecc08a67ae62899b074566ff8556"}, - {file = "yarl-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81713b70bea5c1386dc2f32a8f0dab4148a2928c7495c808c541ee0aae614d67"}, - {file = "yarl-1.17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:aa46dce75078fceaf7cecac5817422febb4355fbdda440db55206e3bd288cfb8"}, - {file = "yarl-1.17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1ce36ded585f45b1e9bb36d0ae94765c6608b43bd2e7f5f88079f7a85c61a4d3"}, - {file = "yarl-1.17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2d374d70fdc36f5863b84e54775452f68639bc862918602d028f89310a034ab0"}, - {file = "yarl-1.17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2d9f0606baaec5dd54cb99667fcf85183a7477f3766fbddbe3f385e7fc253299"}, - {file = "yarl-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0341e6d9a0c0e3cdc65857ef518bb05b410dbd70d749a0d33ac0f39e81a4258"}, - {file = "yarl-1.17.1-cp313-cp313-win32.whl", hash = "sha256:2e7ba4c9377e48fb7b20dedbd473cbcbc13e72e1826917c185157a137dac9df2"}, - {file = "yarl-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:949681f68e0e3c25377462be4b658500e85ca24323d9619fdc41f68d46a1ffda"}, - {file = "yarl-1.17.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8994b29c462de9a8fce2d591028b986dbbe1b32f3ad600b2d3e1c482c93abad6"}, - {file = "yarl-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f9cbfbc5faca235fbdf531b93aa0f9f005ec7d267d9d738761a4d42b744ea159"}, - {file = "yarl-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b40d1bf6e6f74f7c0a567a9e5e778bbd4699d1d3d2c0fe46f4b717eef9e96b95"}, - {file = "yarl-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5efe0661b9fcd6246f27957f6ae1c0eb29bc60552820f01e970b4996e016004"}, - {file = "yarl-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5c4804e4039f487e942c13381e6c27b4b4e66066d94ef1fae3f6ba8b953f383"}, - {file = "yarl-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5d6a6c9602fd4598fa07e0389e19fe199ae96449008d8304bf5d47cb745462e"}, - {file = "yarl-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4c9156c4d1eb490fe374fb294deeb7bc7eaccda50e23775b2354b6a6739934"}, - {file = "yarl-1.17.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6324274b4e0e2fa1b3eccb25997b1c9ed134ff61d296448ab8269f5ac068c4c"}, - {file = "yarl-1.17.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d8a8b74d843c2638f3864a17d97a4acda58e40d3e44b6303b8cc3d3c44ae2d29"}, - {file = "yarl-1.17.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7fac95714b09da9278a0b52e492466f773cfe37651cf467a83a1b659be24bf71"}, - {file = "yarl-1.17.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c180ac742a083e109c1a18151f4dd8675f32679985a1c750d2ff806796165b55"}, - {file = "yarl-1.17.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:578d00c9b7fccfa1745a44f4eddfdc99d723d157dad26764538fbdda37209857"}, - {file = "yarl-1.17.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1a3b91c44efa29e6c8ef8a9a2b583347998e2ba52c5d8280dbd5919c02dfc3b5"}, - {file = "yarl-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a7ac5b4984c468ce4f4a553df281450df0a34aefae02e58d77a0847be8d1e11f"}, - {file = "yarl-1.17.1-cp39-cp39-win32.whl", hash = "sha256:7294e38f9aa2e9f05f765b28ffdc5d81378508ce6dadbe93f6d464a8c9594473"}, - {file = "yarl-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:eb6dce402734575e1a8cc0bb1509afca508a400a57ce13d306ea2c663bad1138"}, - {file = "yarl-1.17.1-py3-none-any.whl", hash = "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06"}, - {file = "yarl-1.17.1.tar.gz", hash = "sha256:067a63fcfda82da6b198fa73079b1ca40b7c9b7994995b6ee38acda728b64d47"}, + {file = "yarl-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:93771146ef048b34201bfa382c2bf74c524980870bb278e6df515efaf93699ff"}, + {file = "yarl-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8281db240a1616af2f9c5f71d355057e73a1409c4648c8949901396dc0a3c151"}, + {file = "yarl-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:170ed4971bf9058582b01a8338605f4d8c849bd88834061e60e83b52d0c76870"}, + {file = "yarl-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc61b005f6521fcc00ca0d1243559a5850b9dd1e1fe07b891410ee8fe192d0c0"}, + {file = "yarl-1.17.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:871e1b47eec7b6df76b23c642a81db5dd6536cbef26b7e80e7c56c2fd371382e"}, + {file = "yarl-1.17.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a58a2f2ca7aaf22b265388d40232f453f67a6def7355a840b98c2d547bd037f"}, + {file = "yarl-1.17.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:736bb076f7299c5c55dfef3eb9e96071a795cb08052822c2bb349b06f4cb2e0a"}, + {file = "yarl-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fd51299e21da709eabcd5b2dd60e39090804431292daacbee8d3dabe39a6bc0"}, + {file = "yarl-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:358dc7ddf25e79e1cc8ee16d970c23faee84d532b873519c5036dbb858965795"}, + {file = "yarl-1.17.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:50d866f7b1a3f16f98603e095f24c0eeba25eb508c85a2c5939c8b3870ba2df8"}, + {file = "yarl-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8b9c4643e7d843a0dca9cd9d610a0876e90a1b2cbc4c5ba7930a0d90baf6903f"}, + {file = "yarl-1.17.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d63123bfd0dce5f91101e77c8a5427c3872501acece8c90df457b486bc1acd47"}, + {file = "yarl-1.17.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:4e76381be3d8ff96a4e6c77815653063e87555981329cf8f85e5be5abf449021"}, + {file = "yarl-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:734144cd2bd633a1516948e477ff6c835041c0536cef1d5b9a823ae29899665b"}, + {file = "yarl-1.17.2-cp310-cp310-win32.whl", hash = "sha256:26bfb6226e0c157af5da16d2d62258f1ac578d2899130a50433ffee4a5dfa673"}, + {file = "yarl-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:76499469dcc24759399accd85ec27f237d52dec300daaca46a5352fcbebb1071"}, + {file = "yarl-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:792155279dc093839e43f85ff7b9b6493a8eaa0af1f94f1f9c6e8f4de8c63500"}, + {file = "yarl-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38bc4ed5cae853409cb193c87c86cd0bc8d3a70fd2268a9807217b9176093ac6"}, + {file = "yarl-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4a8c83f6fcdc327783bdc737e8e45b2e909b7bd108c4da1892d3bc59c04a6d84"}, + {file = "yarl-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6d5fed96f0646bfdf698b0a1cebf32b8aae6892d1bec0c5d2d6e2df44e1e2d"}, + {file = "yarl-1.17.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:782ca9c58f5c491c7afa55518542b2b005caedaf4685ec814fadfcee51f02493"}, + {file = "yarl-1.17.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff6af03cac0d1a4c3c19e5dcc4c05252411bf44ccaa2485e20d0a7c77892ab6e"}, + {file = "yarl-1.17.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a3f47930fbbed0f6377639503848134c4aa25426b08778d641491131351c2c8"}, + {file = "yarl-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1fa68a3c921365c5745b4bd3af6221ae1f0ea1bf04b69e94eda60e57958907f"}, + {file = "yarl-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:187df91395c11e9f9dc69b38d12406df85aa5865f1766a47907b1cc9855b6303"}, + {file = "yarl-1.17.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:93d1c8cc5bf5df401015c5e2a3ce75a5254a9839e5039c881365d2a9dcfc6dc2"}, + {file = "yarl-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:11d86c6145ac5c706c53d484784cf504d7d10fa407cb73b9d20f09ff986059ef"}, + {file = "yarl-1.17.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c42774d1d1508ec48c3ed29e7b110e33f5e74a20957ea16197dbcce8be6b52ba"}, + {file = "yarl-1.17.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8e589379ef0407b10bed16cc26e7392ef8f86961a706ade0a22309a45414d7"}, + {file = "yarl-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1056cadd5e850a1c026f28e0704ab0a94daaa8f887ece8dfed30f88befb87bb0"}, + {file = "yarl-1.17.2-cp311-cp311-win32.whl", hash = "sha256:be4c7b1c49d9917c6e95258d3d07f43cfba2c69a6929816e77daf322aaba6628"}, + {file = "yarl-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:ac8eda86cc75859093e9ce390d423aba968f50cf0e481e6c7d7d63f90bae5c9c"}, + {file = "yarl-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dd90238d3a77a0e07d4d6ffdebc0c21a9787c5953a508a2231b5f191455f31e9"}, + {file = "yarl-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c74f0b0472ac40b04e6d28532f55cac8090e34c3e81f118d12843e6df14d0909"}, + {file = "yarl-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d486ddcaca8c68455aa01cf53d28d413fb41a35afc9f6594a730c9779545876"}, + {file = "yarl-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25b7e93f5414b9a983e1a6c1820142c13e1782cc9ed354c25e933aebe97fcf2"}, + {file = "yarl-1.17.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a0baff7827a632204060f48dca9e63fbd6a5a0b8790c1a2adfb25dc2c9c0d50"}, + {file = "yarl-1.17.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:460024cacfc3246cc4d9f47a7fc860e4fcea7d1dc651e1256510d8c3c9c7cde0"}, + {file = "yarl-1.17.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5870d620b23b956f72bafed6a0ba9a62edb5f2ef78a8849b7615bd9433384171"}, + {file = "yarl-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2941756754a10e799e5b87e2319bbec481ed0957421fba0e7b9fb1c11e40509f"}, + {file = "yarl-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9611b83810a74a46be88847e0ea616794c406dbcb4e25405e52bff8f4bee2d0a"}, + {file = "yarl-1.17.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cd7e35818d2328b679a13268d9ea505c85cd773572ebb7a0da7ccbca77b6a52e"}, + {file = "yarl-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6b981316fcd940f085f646b822c2ff2b8b813cbd61281acad229ea3cbaabeb6b"}, + {file = "yarl-1.17.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:688058e89f512fb7541cb85c2f149c292d3fa22f981d5a5453b40c5da49eb9e8"}, + {file = "yarl-1.17.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56afb44a12b0864d17b597210d63a5b88915d680f6484d8d202ed68ade38673d"}, + {file = "yarl-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:17931dfbb84ae18b287279c1f92b76a3abcd9a49cd69b92e946035cff06bcd20"}, + {file = "yarl-1.17.2-cp312-cp312-win32.whl", hash = "sha256:ff8d95e06546c3a8c188f68040e9d0360feb67ba8498baf018918f669f7bc39b"}, + {file = "yarl-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:4c840cc11163d3c01a9d8aad227683c48cd3e5be5a785921bcc2a8b4b758c4f3"}, + {file = "yarl-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3294f787a437cb5d81846de3a6697f0c35ecff37a932d73b1fe62490bef69211"}, + {file = "yarl-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1e7fedb09c059efee2533119666ca7e1a2610072076926fa028c2ba5dfeb78c"}, + {file = "yarl-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:da9d3061e61e5ae3f753654813bc1cd1c70e02fb72cf871bd6daf78443e9e2b1"}, + {file = "yarl-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91c012dceadc695ccf69301bfdccd1fc4472ad714fe2dd3c5ab4d2046afddf29"}, + {file = "yarl-1.17.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f11fd61d72d93ac23718d393d2a64469af40be2116b24da0a4ca6922df26807e"}, + {file = "yarl-1.17.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46c465ad06971abcf46dd532f77560181387b4eea59084434bdff97524444032"}, + {file = "yarl-1.17.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef6eee1a61638d29cd7c85f7fd3ac7b22b4c0fabc8fd00a712b727a3e73b0685"}, + {file = "yarl-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4434b739a8a101a837caeaa0137e0e38cb4ea561f39cb8960f3b1e7f4967a3fc"}, + {file = "yarl-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:752485cbbb50c1e20908450ff4f94217acba9358ebdce0d8106510859d6eb19a"}, + {file = "yarl-1.17.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:17791acaa0c0f89323c57da7b9a79f2174e26d5debbc8c02d84ebd80c2b7bff8"}, + {file = "yarl-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5c6ea72fe619fee5e6b5d4040a451d45d8175f560b11b3d3e044cd24b2720526"}, + {file = "yarl-1.17.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db5ac3871ed76340210fe028f535392f097fb31b875354bcb69162bba2632ef4"}, + {file = "yarl-1.17.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7a1606ba68e311576bcb1672b2a1543417e7e0aa4c85e9e718ba6466952476c0"}, + {file = "yarl-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9bc27dd5cfdbe3dc7f381b05e6260ca6da41931a6e582267d5ca540270afeeb2"}, + {file = "yarl-1.17.2-cp313-cp313-win32.whl", hash = "sha256:52492b87d5877ec405542f43cd3da80bdcb2d0c2fbc73236526e5f2c28e6db28"}, + {file = "yarl-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:8e1bf59e035534ba4077f5361d8d5d9194149f9ed4f823d1ee29ef3e8964ace3"}, + {file = "yarl-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c556fbc6820b6e2cda1ca675c5fa5589cf188f8da6b33e9fc05b002e603e44fa"}, + {file = "yarl-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f44a4247461965fed18b2573f3a9eb5e2c3cad225201ee858726cde610daca"}, + {file = "yarl-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a3ede8c248f36b60227eb777eac1dbc2f1022dc4d741b177c4379ca8e75571a"}, + {file = "yarl-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2654caaf5584449d49c94a6b382b3cb4a246c090e72453493ea168b931206a4d"}, + {file = "yarl-1.17.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d41c684f286ce41fa05ab6af70f32d6da1b6f0457459a56cf9e393c1c0b2217"}, + {file = "yarl-1.17.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2270d590997445a0dc29afa92e5534bfea76ba3aea026289e811bf9ed4b65a7f"}, + {file = "yarl-1.17.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18662443c6c3707e2fc7fad184b4dc32dd428710bbe72e1bce7fe1988d4aa654"}, + {file = "yarl-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75ac158560dec3ed72f6d604c81090ec44529cfb8169b05ae6fcb3e986b325d9"}, + {file = "yarl-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1fee66b32e79264f428dc8da18396ad59cc48eef3c9c13844adec890cd339db5"}, + {file = "yarl-1.17.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:585ce7cd97be8f538345de47b279b879e091c8b86d9dbc6d98a96a7ad78876a3"}, + {file = "yarl-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c019abc2eca67dfa4d8fb72ba924871d764ec3c92b86d5b53b405ad3d6aa56b0"}, + {file = "yarl-1.17.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c6e659b9a24d145e271c2faf3fa6dd1fcb3e5d3f4e17273d9e0350b6ab0fe6e2"}, + {file = "yarl-1.17.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:d17832ba39374134c10e82d137e372b5f7478c4cceeb19d02ae3e3d1daed8721"}, + {file = "yarl-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bc3003710e335e3f842ae3fd78efa55f11a863a89a72e9a07da214db3bf7e1f8"}, + {file = "yarl-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f5ffc6b7ace5b22d9e73b2a4c7305740a339fbd55301d52735f73e21d9eb3130"}, + {file = "yarl-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:48e424347a45568413deec6f6ee2d720de2cc0385019bedf44cd93e8638aa0ed"}, + {file = "yarl-1.17.2-py3-none-any.whl", hash = "sha256:dd7abf4f717e33b7487121faf23560b3a50924f80e4bef62b22dab441ded8f3b"}, + {file = "yarl-1.17.2.tar.gz", hash = "sha256:753eaaa0c7195244c84b5cc159dc8204b7fd99f716f11198f999f2332a86b178"}, ] [package.dependencies] From 75294301406dbef70b7e6a09dfdfbefa6d95d155 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 19 Nov 2024 16:10:18 +1100 Subject: [PATCH 18/78] Update pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 651cc8a2650e..68277cedbb0e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -129,6 +129,6 @@ repos: ] - repo: https://github.com/kynan/nbstripout - rev: 0.8.0 + rev: 0.8.1 hooks: - id: nbstripout From b6e1b2bc1e4e1ec171e85829ca5dee4c5b6317cd Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 19 Nov 2024 16:34:37 +1100 Subject: [PATCH 19/78] Fix missing venue -> exchange mappings for Tardis --- RELEASES.md | 2 +- nautilus_trader/adapters/tardis/common.py | 24 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 6b026c360efe..ef4cbd1e5e9f 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -14,7 +14,7 @@ None - Renamed `TriggerType.LAST_TRADE` to `LAST_PRICE` ### Fixes -None +- Fixed missing venue -> exchange mappings for Tardis integration --- diff --git a/nautilus_trader/adapters/tardis/common.py b/nautilus_trader/adapters/tardis/common.py index eb41482b095c..f40f6fe1a8e7 100644 --- a/nautilus_trader/adapters/tardis/common.py +++ b/nautilus_trader/adapters/tardis/common.py @@ -64,6 +64,30 @@ def infer_tardis_exchange_str(instrument: Instrument) -> str: # noqa: C901 (too return "bybit-options" else: return "bybit" + case "CRYPTO_COM": + if isinstance(instrument, CurrencyPair): + return "crypto-com" + else: + return "crypto-com-derivatives" + case "GATEIO": + if isinstance(instrument, CurrencyPair): + return "gate-io" + else: + return "gate-io-futures" + case "HUOBI": + if isinstance(instrument, CurrencyPair): + return "huobi" + elif isinstance(instrument, CryptoPerpetual): + return "huobi-dm-linear-swap" + elif isinstance(instrument, OptionsContract): + return "huobi-dm-options" + else: + return "huobi-dm-swap" + case "KRAKEN": + if isinstance(instrument, CurrencyPair): + return "kraken" + else: + return "kraken-futures" case "OKX": if isinstance(instrument, CurrencyPair): return "okex" From a88af082ef1857f17e83309c614b493442b8a2eb Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 19 Nov 2024 18:01:49 +1100 Subject: [PATCH 20/78] Add historical bar requests for TardisDataClient --- docs/integrations/tardis.md | 2 + examples/live/tardis/tardis_subscriber.py | 4 +- .../adapters/src/tardis/python/machine.rs | 14 +++- nautilus_trader/adapters/tardis/data.py | 76 ++++++++++++++++++- nautilus_trader/core/nautilus_pyo3.pyi | 2 +- 5 files changed, 92 insertions(+), 6 deletions(-) diff --git a/docs/integrations/tardis.md b/docs/integrations/tardis.md index 18936a5e8c6c..9ecf084ef456 100644 --- a/docs/integrations/tardis.md +++ b/docs/integrations/tardis.md @@ -425,6 +425,8 @@ you must filter for the desired venues using an `InstrumentProviderConfig`: ```python from nautilus_trader.config import InstrumentProviderConfig +# See supported venues https://nautilustrader.io/docs/nightly/integrations/tardis#venues +venues = {"BINANCE", "BYBIT"} filters = {"venues": frozenset(venues)} instrument_provider_config = InstrumentProviderConfig(load_all=True, filters=filters) ``` diff --git a/examples/live/tardis/tardis_subscriber.py b/examples/live/tardis/tardis_subscriber.py index 5937735bee58..fa810ebeb6e3 100644 --- a/examples/live/tardis/tardis_subscriber.py +++ b/examples/live/tardis/tardis_subscriber.py @@ -156,7 +156,7 @@ def on_start(self) -> None: self.subscribe_trade_ticks(instrument_id, client_id=self.client_id) # from nautilus_trader.model.data import BarType - # bar_type = BarType.from_str(f"{instrument_id}-10-TICK-LAST-EXTERNAL") + # bar_type = BarType.from_str(f"{instrument_id}-1-SECOND-LAST-EXTERNAL") # self.subscribe_bars(bar_type, client_id=self.client_id) # self.subscribe_instrument_status(instrument_id) @@ -173,7 +173,7 @@ def on_start(self) -> None: # self.request_data(status_data_type) # from nautilus_trader.model.data import BarType - # self.request_bars(BarType.from_str(f"{instrument_id}-1-MINUTE-LAST-EXTERNAL")) + # self.request_bars(BarType.from_str(f"{instrument_id}-1-SECOND-LAST-EXTERNAL")) def on_stop(self) -> None: """ diff --git a/nautilus_core/adapters/src/tardis/python/machine.rs b/nautilus_core/adapters/src/tardis/python/machine.rs index 60a83a526b1a..3fd115890707 100644 --- a/nautilus_core/adapters/src/tardis/python/machine.rs +++ b/nautilus_core/adapters/src/tardis/python/machine.rs @@ -87,13 +87,25 @@ impl TardisMachineClient { #[pyo3(name = "replay")] fn py_replay<'py>( &self, + instruments: Vec, options: Vec, callback: PyObject, py: Python<'py>, ) -> PyResult> { + let map = if !instruments.is_empty() { + let mut instrument_map: HashMap> = + HashMap::new(); + for inst in instruments { + let key = inst.as_tardis_instrument_key(); + instrument_map.insert(key, Arc::new(inst.clone())); + } + instrument_map + } else { + self.instruments.clone() + }; + let base_url = self.base_url.clone(); let replay_signal = self.replay_signal.clone(); - let map = self.instruments.clone(); pyo3_async_runtimes::tokio::future_into_py(py, async move { let stream = replay_normalized(&base_url, options, replay_signal) diff --git a/nautilus_trader/adapters/tardis/data.py b/nautilus_trader/adapters/tardis/data.py index 98ce53d88e5c..ef5ec16b9bbc 100644 --- a/nautilus_trader/adapters/tardis/data.py +++ b/nautilus_trader/adapters/tardis/data.py @@ -20,6 +20,7 @@ from nautilus_trader.adapters.tardis.common import convert_nautilus_bar_type_to_tardis_data_type from nautilus_trader.adapters.tardis.common import convert_nautilus_data_type_to_tardis_data_type from nautilus_trader.adapters.tardis.common import create_instrument_info +from nautilus_trader.adapters.tardis.common import create_replay_normalized_request_options from nautilus_trader.adapters.tardis.common import create_stream_normalized_request_options from nautilus_trader.adapters.tardis.common import get_ws_client_key from nautilus_trader.adapters.tardis.config import TardisDataClientConfig @@ -32,6 +33,7 @@ from nautilus_trader.core import nautilus_pyo3 from nautilus_trader.core.uuid import UUID4 from nautilus_trader.live.data_client import LiveMarketDataClient +from nautilus_trader.model.data import Bar from nautilus_trader.model.data import BarType from nautilus_trader.model.data import DataType from nautilus_trader.model.data import OrderBookDelta @@ -40,6 +42,7 @@ from nautilus_trader.model.data import TradeTick from nautilus_trader.model.data import capsule_to_data from nautilus_trader.model.enums import BookType +from nautilus_trader.model.enums import PriceType from nautilus_trader.model.identifiers import ClientId from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.identifiers import Venue @@ -413,10 +416,79 @@ async def _request_bars( start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, ) -> None: - self._log.error( - f"Cannot request historical bars for {bar_type}: not supported in this version", + if bar_type.is_internally_aggregated(): + self._log.error( + f"Cannot request {bar_type} bars: " + f"only historical bars with EXTERNAL aggregation available from Tardis", + ) + return + + if bar_type.spec.price_type != PriceType.LAST: + self._log.error( + f"Cannot request {bar_type} bars: " + f"only historical bars for LAST price type available through Tardis", + ) + return + + instrument = self._cache.instrument(bar_type.instrument_id) + if instrument is None: + self._log.error(f"Cannot request bars: no instrument for {bar_type.instrument_id}") + return + + instrument_info = create_instrument_info(instrument) + tardis_exchange_str = instrument_info.exchange + raw_symbol_str = instrument.raw_symbol.value + tardis_data_type = convert_nautilus_bar_type_to_tardis_data_type(bar_type) + + self._log.info( + f"Subscribing replay: exchange={tardis_exchange_str}, raw_symbol={raw_symbol_str}, data_type={tardis_data_type}", + LogColor.MAGENTA, ) + date_now_utc = self._clock.utc_now().date() + + replay_request = create_replay_normalized_request_options( + exchange=tardis_exchange_str, + symbols=[raw_symbol_str], + from_date=start.date() if start is not None else date_now_utc - pd.Timedelta(days=1), + to_date=end.date() if end is not None else date_now_utc, + data_types=[tardis_data_type], + ) + + bar_capsules: list[object] = [] + + await asyncio.ensure_future( + self._ws_client.replay( + instruments=[instrument_info], + options=[replay_request], + callback=bar_capsules.append, + ), + ) + + self._log.info( + f"Streamed {len(bar_capsules):,} {bar_type} bars from replay", + LogColor.MAGENTA, + ) + + if limit: + bar_capsules = bar_capsules[-limit:] + + # Convert capsules to bars and apply time filters in one pass + bars: list[Bar] = [ + bar + for pycapsule in bar_capsules + if (bar := capsule_to_data(pycapsule)) + and (start is None or bar.ts_event >= start) + and (end is None or bar.ts_event <= end) + ] + + self._log.info( + f"Sending response with {len(bars):,} bars after filtering", + LogColor.MAGENTA, + ) + + self._handle_bars(bar_type, bars, None, correlation_id) + def _handle_msg( self, pycapsule: object, diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index eeb0518c1816..5e896dc92e29 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -4152,7 +4152,7 @@ class TardisMachineClient: def __init__(self, base_url: str | None = None, normalize_symbols: bool = True) -> None: ... def is_closed(self) -> bool: ... def close(self) -> None: ... - def replay(self, options: list[ReplayNormalizedRequestOptions], callback: Callable) -> Awaitable[None]: ... + def replay(self, instruments: list[InstrumentMiniInfo], options: list[ReplayNormalizedRequestOptions], callback: Callable) -> Awaitable[None]: ... def stream(self, instruments: list[InstrumentMiniInfo], options: list[StreamNormalizedRequestOptions], callback: Callable,) -> Awaitable[None]: ... # noqa async def run_tardis_machine_replay(config_filepath: str, output_path: str | None = None) -> None: ... From 78adb6af5b2d6249627637d1baac90342190a4f4 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 20 Nov 2024 07:24:11 +1100 Subject: [PATCH 21/78] Improve Cache behavior when adding more recent data --- RELEASES.md | 1 + nautilus_trader/cache/cache.pyx | 26 +++++--------- nautilus_trader/test_kit/stubs/data.py | 6 ++-- tests/unit_tests/cache/test_data.py | 50 ++++++++++++++++++++++++-- 4 files changed, 60 insertions(+), 23 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index ef4cbd1e5e9f..09420d10edfa 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,6 +4,7 @@ Released on TBD (UTC). ### Enhancements None +- Improved `Cache` behavior when adding more recent quotes, trades, or bars (now adds to cache) ### Internal Improvements - Improve live engines error logging (will now log all exceptions rather than just `RuntimeError`) diff --git a/nautilus_trader/cache/cache.pyx b/nautilus_trader/cache/cache.pyx index 92f7e8c9bb52..905f21b3e7f2 100644 --- a/nautilus_trader/cache/cache.pyx +++ b/nautilus_trader/cache/cache.pyx @@ -1223,14 +1223,12 @@ cdef class Cache(CacheFacade): # The instrument_id was not registered cached_ticks = deque(maxlen=self.tick_capacity) self._quote_ticks[instrument_id] = cached_ticks - elif len(cached_ticks) > 0: - # Currently the simple solution for multiple consumers requesting - # ticks at system spool up is just to add only if the cache is empty. - self._log.debug("Cache already contains ticks") - return cdef QuoteTick tick for tick in ticks: + if cached_ticks and tick.ts_event <= cached_ticks[0].ts_event: + # Only add more recent data to cache + continue cached_ticks.appendleft(tick) cpdef void add_trade_ticks(self, list ticks): @@ -1257,17 +1255,14 @@ cdef class Cache(CacheFacade): cached_ticks = self._trade_ticks.get(instrument_id) if not cached_ticks: - # The instrument_id was not registered cached_ticks = deque(maxlen=self.tick_capacity) self._trade_ticks[instrument_id] = cached_ticks - elif len(cached_ticks) > 0: - # Currently the simple solution for multiple consumers requesting - # ticks at system spool up is just to add only if the cache is empty. - self._log.debug("Cache already contains ticks") - return cdef TradeTick tick for tick in ticks: + if cached_ticks and tick.ts_event <= cached_ticks[0].ts_event: + # Only add more recent data to cache + continue cached_ticks.appendleft(tick) cpdef void add_bars(self, list bars): @@ -1294,17 +1289,14 @@ cdef class Cache(CacheFacade): cached_bars = self._bars.get(bar_type) if not cached_bars: - # The instrument_id was not registered cached_bars = deque(maxlen=self.bar_capacity) self._bars[bar_type] = cached_bars - elif len(cached_bars) > 0: - # Currently the simple solution for multiple consumers requesting - # bars at system spool up is just to add only if the cache is empty. - self._log.debug("Cache already contains bars") - return cdef Bar bar for bar in bars: + if cached_bars and bar.ts_event <= cached_bars[0].ts_event: + # Only add more recent data to cache + continue cached_bars.appendleft(bar) bar = bars[-1] diff --git a/nautilus_trader/test_kit/stubs/data.py b/nautilus_trader/test_kit/stubs/data.py index 93548c7bca88..139c24ffe370 100644 --- a/nautilus_trader/test_kit/stubs/data.py +++ b/nautilus_trader/test_kit/stubs/data.py @@ -192,7 +192,7 @@ def bartype_adabtc_binance_1min_last() -> BarType: return BarType(TestIdStubs.adabtc_binance_id(), TestDataStubs.bar_spec_1min_last()) @staticmethod - def bar_5decimal() -> Bar: + def bar_5decimal(ts_event=0, ts_init=0) -> Bar: return Bar( bar_type=TestDataStubs.bartype_audusd_1min_bid(), open=Price.from_str("1.00002"), @@ -200,8 +200,8 @@ def bar_5decimal() -> Bar: low=Price.from_str("1.00001"), close=Price.from_str("1.00003"), volume=Quantity.from_int(1_000_000), - ts_event=0, - ts_init=0, + ts_event=ts_event, + ts_init=ts_init, ) @staticmethod diff --git a/tests/unit_tests/cache/test_data.py b/tests/unit_tests/cache/test_data.py index 463a8e70ee09..8ccf03968ff4 100644 --- a/tests/unit_tests/cache/test_data.py +++ b/tests/unit_tests/cache/test_data.py @@ -240,7 +240,7 @@ def test_quote_ticks_when_one_tick_returns_expected_list(self): # Assert assert result == [tick] - def test_add_quote_ticks_when_already_ticks_does_not_add(self): + def test_add_quote_ticks_when_identical_ticks_does_not_add(self): # Arrange tick = TestDataStubs.quote_tick() @@ -253,6 +253,20 @@ def test_add_quote_ticks_when_already_ticks_does_not_add(self): # Assert assert result == [tick] + def test_add_quote_ticks_when_older_quotes(self): + # Arrange + tick1 = TestDataStubs.quote_tick() + self.cache.add_quote_tick(tick1) + + tick2 = TestDataStubs.quote_tick(ts_event=1, ts_init=1) + + # Act + self.cache.add_quote_ticks([tick2]) + result = self.cache.quote_ticks(tick1.instrument_id) + + # Assert + assert result == [tick2, tick1] + def test_trade_ticks_when_one_tick_returns_expected_list(self): # Arrange tick = TestDataStubs.trade_tick() @@ -265,7 +279,7 @@ def test_trade_ticks_when_one_tick_returns_expected_list(self): # Assert assert result == [tick] - def test_add_trade_ticks_when_already_ticks_does_not_add(self): + def test_add_trade_ticks_when_identical_ticks_does_not_add(self): # Arrange tick = TestDataStubs.trade_tick() @@ -278,6 +292,21 @@ def test_add_trade_ticks_when_already_ticks_does_not_add(self): # Assert assert result == [tick] + def test_add_trade_ticks_when_older_trades(self): + # Arrange + tick1 = TestDataStubs.trade_tick() + self.cache.add_trade_tick(tick1) + + tick2 = TestDataStubs.trade_tick(ts_event=1, ts_init=1) + self.cache.add_trade_tick(tick2) + + # Act + self.cache.add_trade_ticks([tick1]) + result = self.cache.trade_ticks(tick1.instrument_id) + + # Assert + assert result == [tick2, tick1] + def test_bars_when_one_bar_returns_expected_list(self): # Arrange bar = TestDataStubs.bar_5decimal() @@ -290,7 +319,7 @@ def test_bars_when_one_bar_returns_expected_list(self): # Assert assert result == [bar] - def test_add_bars_when_already_bars_does_not_add(self): + def test_add_bars_when_already_identical_bar_does_not_add(self): # Arrange bar = TestDataStubs.bar_5decimal() @@ -303,6 +332,21 @@ def test_add_bars_when_already_bars_does_not_add(self): # Assert assert result == [bar] + def test_add_bars_when_older_cached_bars(self): + # Arrange + bar1 = TestDataStubs.bar_5decimal() + self.cache.add_bar(bar1) + + bar2 = TestDataStubs.bar_5decimal(ts_event=1) + self.cache.add_bar(bar2) + + # Act + self.cache.add_bars([bar2]) + result = self.cache.bars(bar1.bar_type) + + # Assert + assert result == [bar2, bar1] + def test_instrument_when_no_instrument_returns_none(self): # Arrange, Act result = self.cache.instrument(AUDUSD_SIM.id) From 719919bca34719a11a7a6176609d122ea8e80a28 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 20 Nov 2024 08:09:52 +1100 Subject: [PATCH 22/78] Improve symbol normalization for Tardis --- RELEASES.md | 3 ++- examples/live/tardis/tardis_subscriber.py | 2 ++ nautilus_core/adapters/src/tardis/parse.rs | 10 ++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 09420d10edfa..886217259c4b 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -7,7 +7,8 @@ None - Improved `Cache` behavior when adding more recent quotes, trades, or bars (now adds to cache) ### Internal Improvements -- Improve live engines error logging (will now log all exceptions rather than just `RuntimeError`) +- Improved live engines error logging (will now log all exceptions rather than just `RuntimeError`) +- Improved symbol normalization for Tardis - Refined `HttpClient` for use directly from Rust - Upgraded `datafusion` crate to v43.0.0 (#2056), thanks @twitu diff --git a/examples/live/tardis/tardis_subscriber.py b/examples/live/tardis/tardis_subscriber.py index fa810ebeb6e3..a9a39c0995e5 100644 --- a/examples/live/tardis/tardis_subscriber.py +++ b/examples/live/tardis/tardis_subscriber.py @@ -51,6 +51,8 @@ # InstrumentId.from_str("BTCUSDT.BINANCE"), # InstrumentId.from_str("XBTUSDT.BITMEX"), # InstrumentId.from_str("ETHUSDT.BITMEX"), + # InstrumentId.from_str("BTC_USDT.GATEIO"), + # InstrumentId.from_str("BTC_USDT-PERP.GATEIO"), ] # See supported venues https://nautilustrader.io/docs/nightly/integrations/tardis#venues diff --git a/nautilus_core/adapters/src/tardis/parse.rs b/nautilus_core/adapters/src/tardis/parse.rs index e16391d14f98..9b827d56bd29 100644 --- a/nautilus_core/adapters/src/tardis/parse.rs +++ b/nautilus_core/adapters/src/tardis/parse.rs @@ -77,6 +77,16 @@ pub fn normalize_symbol_str( symbol.push_str("-PERP"); } + Exchange::GateIoFutures if instrument_type == InstrumentType::Perpetual => { + symbol.push_str("-PERP"); + } + + Exchange::HuobiDmSwap | Exchange::HuobiDmLinearSwap + if instrument_type == InstrumentType::Perpetual => + { + symbol.push_str("-PERP"); + } + _ => {} } From c8812dafc7a0a97228a350b8f72799bd91e484af Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 20 Nov 2024 18:10:38 +1100 Subject: [PATCH 23/78] Update dependencies --- nautilus_core/Cargo.lock | 12 +-- poetry.lock | 154 +++++++++++++++++++-------------------- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index ab4699baf81a..b753c1c8aa41 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2063,9 +2063,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", @@ -2236,9 +2236,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", @@ -4579,9 +4579,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] diff --git a/poetry.lock b/poetry.lock index c438720e2225..3090708c3298 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,87 +13,87 @@ files = [ [[package]] name = "aiohttp" -version = "3.11.3" +version = "3.11.6" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" files = [ - {file = "aiohttp-3.11.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:acb0eb91b7a9ce435728d009388dcbf82d3e394b00596c3eda2402644ce42c33"}, - {file = "aiohttp-3.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3cd3d29e2e0b9726629031d73b08027048deaa856cefda343f3db34da9d6fb04"}, - {file = "aiohttp-3.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63df79e626ad76d3170f2cc87727ebe360c4c56c3dd01d80e1c22fbea18b3368"}, - {file = "aiohttp-3.11.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ad9cdb478e835d1809d7a86b16c88fb430d6e8f06940e0616586258ec1c4eed"}, - {file = "aiohttp-3.11.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cc0f81fcd9690371d9c89b6675cd12da6abf8bb841cda5b78379fc72ba95cf55"}, - {file = "aiohttp-3.11.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18e391196b233b1e9daef38d14dccbfc3a62934fc1a5cbc711fbf0aaaf12afb2"}, - {file = "aiohttp-3.11.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb3753b764b6122491db64f5c99c0acf481f6ac54a3062f9041e6e9099337fe3"}, - {file = "aiohttp-3.11.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a2fed9db26ff62d0926169c2d9dba5f1029f75559728dd0ae80e7085e16e056"}, - {file = "aiohttp-3.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:22678102ad8e27536bb5338daa6a8fef268a27498d45793f66c5a90836278376"}, - {file = "aiohttp-3.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ba8bd24b1a08e22baf35e7f1deadee409118cdf086ade14d3a7c0c7cfebc828d"}, - {file = "aiohttp-3.11.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7d16a132673163bec662f77e056e2d7f5c9472560e4346f6847860eabc2e75b3"}, - {file = "aiohttp-3.11.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7b6d71c6eed09fb6c893ca40c49487f46fe440f8a5697e9942715d1a28433b19"}, - {file = "aiohttp-3.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:44d15ef64678e9318969a707a6d444621988980a60e917cc8a7dab1dd763bd7c"}, - {file = "aiohttp-3.11.3-cp310-cp310-win32.whl", hash = "sha256:fe842fe3504b3d76be4af8ae5865389eae5cd4d0a7afd631e8d73971628ab525"}, - {file = "aiohttp-3.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:158e47fb9bd16c964762c1661be934d5781423ac7b92d57ccba3cdaef9aa7c16"}, - {file = "aiohttp-3.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a5b836e843e702db8ebeacc6dd8a5137a578dc23b4367e63dc11231fcefe7088"}, - {file = "aiohttp-3.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc53bddadf5fd0a03c6520855cc4e4050ae737f7697d799bac81a0ef8a85f865"}, - {file = "aiohttp-3.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:59912e8f1dc74ffe7cdbf84199131cf60b940ecbd1cd672e88090321875b2ea2"}, - {file = "aiohttp-3.11.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62aaf76d9077397e34a7c25028ad570d05d11c2f5ec9e42262326d22395cda7d"}, - {file = "aiohttp-3.11.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b10b85ae6e6d7d4e57ef1fd1a3fbfa92bc14854f172957ecf12688f965c7efce"}, - {file = "aiohttp-3.11.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ee52b10fa0ee52ad9741406e18d2d1fce9bc4622566066239d35aaa68323427"}, - {file = "aiohttp-3.11.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0dcb32dc5d83372e1407675879121e2aabeaa6c633283a8837fcdb363bc5d49"}, - {file = "aiohttp-3.11.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86b3ece9d0c1a2d881b61fa6f4c2b72c5af7e74dbffb73a61fd604be5f51c2e2"}, - {file = "aiohttp-3.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e15fd83a3d252039d35849ccb8f9ee6a774124a0ae49934c8deb472a7b95e5a8"}, - {file = "aiohttp-3.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8998fd80aa1e10b0da45c1edacdb7e7433d4fe9b28fc0d28d69370d733d13bc5"}, - {file = "aiohttp-3.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c3d721538094108db57dde5c3b3af0e157c0a1db6c9f3f55c84f2736f697481c"}, - {file = "aiohttp-3.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:95635838422669e3a0220c74fe9678c838e2cb0dae91bcabfdd3557f11dfe16a"}, - {file = "aiohttp-3.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8a79e638ff960068b5a891fe67672d94c9a906fa01043db88789349090333983"}, - {file = "aiohttp-3.11.3-cp311-cp311-win32.whl", hash = "sha256:39554727dc67c170ed84ca4d85937c4955a08dba65d887e48f075b0e3fb746f2"}, - {file = "aiohttp-3.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:16225e7bb046880631e58d3e2ecba19c020be8e873d517ee42a1be8a126b70f0"}, - {file = "aiohttp-3.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0665d51a9580a7df74a281cb3730526865db299742fce115a2ce3033817f7fca"}, - {file = "aiohttp-3.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8e9735209839fbcf81b0b1bfe16d5bd0d5497a5077c2c601f3a347ad34a1436e"}, - {file = "aiohttp-3.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c06fed93bb81f0fe5c2b1a6a131310449e0dfdd0c89ede4b9400e0b5270680e3"}, - {file = "aiohttp-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e7bd606a02bcdb8764a3a6d1493131515a146e44f6e8788f1472445b7ff5280"}, - {file = "aiohttp-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c28332611f7aebd69f0fc183b41b21af2422846ac3dbfa7888ec40962cb8b09"}, - {file = "aiohttp-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce5dd859f71f95235d473bf948a416a981cb37c3247f10a6ca7e630e7ea28e37"}, - {file = "aiohttp-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:969ee33c80399ab6e2627b576073456825234e1384d672dcced9f52e918091b1"}, - {file = "aiohttp-3.11.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270fbf3a5df1ae5fa1f18d26111c0b4cd8c04d84c79b1fe513139a635b5c5285"}, - {file = "aiohttp-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:791d4e2328f06a0373db3ed2b4d353c8f2f3ef7521cacf6e66278033ed2fd192"}, - {file = "aiohttp-3.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8eb02a8a783cc56aa1445df1ccffbf187b66fda103ece7a13e19b3ae33e093f7"}, - {file = "aiohttp-3.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:728bb2b31652c718b03bf9936b4008af0d26a31b8cc632c57450298dcfb82e08"}, - {file = "aiohttp-3.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:10d7b73c974460d198df5cab9f5ebfd40d4b425a52affe05deb9c3ae78664cf5"}, - {file = "aiohttp-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf1f571854b65229b7497de399762a4560894e3f077dd645ab5fdc001eb527ac"}, - {file = "aiohttp-3.11.3-cp312-cp312-win32.whl", hash = "sha256:29828b71745c5562ab73f1513d558e5afca980d83fab42cf87015a50e6076967"}, - {file = "aiohttp-3.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:c1fce3416981cd97a17b0bccebb225c31f82f1e3bbabf04a78547f26c332d619"}, - {file = "aiohttp-3.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:610b39400c761767d5da3d9960811f616f623bba34a9f491dc89029d2a49cc82"}, - {file = "aiohttp-3.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:231c4f1d5f79482e5641febdcb946742c66398deb63dce384da870e59cc884ba"}, - {file = "aiohttp-3.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd0a1856b23b7598e9cd6ff46a08251165f1253b2c922cf3ce07634a1250afb8"}, - {file = "aiohttp-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a003c5ac1e11674bfd7b057afb4c04465d601ea99a927c5eeedcb824b6fb95f1"}, - {file = "aiohttp-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26155e39749dd26cf8795dab6c93ccbe4e58d654a670c520d26bb45786325359"}, - {file = "aiohttp-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4265069ae8d55bf978001402824c18297d1b01aabf4b34931299442249d98113"}, - {file = "aiohttp-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1255eb1438cb35a65af81762964808c8a513a4e686f93319c97d5f351b4f8dad"}, - {file = "aiohttp-3.11.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1468ff557bb154bd500648042e860cd1cc05192e037dd661fff2ce81aeea3bdc"}, - {file = "aiohttp-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:634b474fdcb889a42751cb1095686a3c43d4fca34c560aa5d167353adda7288a"}, - {file = "aiohttp-3.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7ee89599796e220fd8e391d8688b22b63452b772b5b2baffda0f24e2ab258444"}, - {file = "aiohttp-3.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ec2082ffa6e75b41c760b37901bd84711bcee306a5f2fc9fff9d4d290e9a6047"}, - {file = "aiohttp-3.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6a312f5a7fe17a133e605c2db93bd928aad5d1988a7fba5d16f634ac7f5443a0"}, - {file = "aiohttp-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4187859ad7be3de2b319eb84d8865623e2dd907eb0cb825f8c9709afb36947b8"}, - {file = "aiohttp-3.11.3-cp313-cp313-win32.whl", hash = "sha256:cac27ab70c62e043208231ef4cd2f241ee7001355e968f7e474c9007c0e92400"}, - {file = "aiohttp-3.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:66ec2a0c2e6c6fc5f50c1309e3f06280008ba6b13473f465a279e37934c7e9b1"}, - {file = "aiohttp-3.11.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3971b5af634c00c6e1387ac0ed30f713a4abd78aa1b008d0986071011377e042"}, - {file = "aiohttp-3.11.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:aba99bb1f22470e07e2dd29bac108aee1f7278cbcb38f2e67970171feda5c0d2"}, - {file = "aiohttp-3.11.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e22331096df5fa2ce2a6f6bc063c82c867fbe00f465311f7355212e89032145a"}, - {file = "aiohttp-3.11.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:117796a65d59c159642924bf989da00d6c4dc3faf323d86003546f157f14d6e4"}, - {file = "aiohttp-3.11.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ba1ae2c0aa10b8eb946e343d56e58e021550c4fe093cfee4a4aa1eb1bad6cbb"}, - {file = "aiohttp-3.11.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f15f42bfcbbfa4b8a0c0685161e4b1f91c7b24ee47f6d2076e0824bcfafa481"}, - {file = "aiohttp-3.11.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c33562307c6e5dfdea5f380c90ba46c555536edc153b99e0fcce6478f51e386c"}, - {file = "aiohttp-3.11.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed4bbe2f39bc65bd80dc063833877bde77e7032896fd648b799c4dc8489bb3ba"}, - {file = "aiohttp-3.11.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:faae49269ecaf640b1cbc22d896b2540d487564c1b62538a72c54a96573ffb34"}, - {file = "aiohttp-3.11.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:30abc2f6c004a07b9ffa8086c3b739eddc41c54e5b3825ad18bf6d38e01a1fe2"}, - {file = "aiohttp-3.11.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:38aaa713e303b0852e110694d7ef0c6162ffa0c4fe1d70729f3c35f9bda8f752"}, - {file = "aiohttp-3.11.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0c400c7e60f08aa541be32cb1aec82f9c130e326c9fe5a98dda246f67ea64ff5"}, - {file = "aiohttp-3.11.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:91a7dad146005a849a56f4c4448c7ad988b51d08d699ec1605a116cd87073a06"}, - {file = "aiohttp-3.11.3-cp39-cp39-win32.whl", hash = "sha256:3d1b50a83d4f054d4eb7a76f82ecfae3305c6339420cdd2958d9f2cb94581c4d"}, - {file = "aiohttp-3.11.3-cp39-cp39-win_amd64.whl", hash = "sha256:b7816289d4d01b5c6ddccb70d85c3c35cad4d26ae4eeadeee72919da6c7bad99"}, - {file = "aiohttp-3.11.3.tar.gz", hash = "sha256:0fbd111a0f1c254dd2cc54bdf6307e5b04fc3d20f3d36fd53c9bcb25bcebb96e"}, + {file = "aiohttp-3.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7510b3ca2275691875ddf072a5b6cd129278d11fe09301add7d292fc8d3432de"}, + {file = "aiohttp-3.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfab0d2c3380c588fc925168533edb21d3448ad76c3eadc360ff963019161724"}, + {file = "aiohttp-3.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf02dba0f342f3a8228f43fae256aafc21c4bc85bffcf537ce4582e2b1565188"}, + {file = "aiohttp-3.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92daedf7221392e7a7984915ca1b0481a94c71457c2f82548414a41d65555e70"}, + {file = "aiohttp-3.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2274a7876e03429e3218589a6d3611a194bdce08c3f1e19962e23370b47c0313"}, + {file = "aiohttp-3.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a2e1eae2d2f62f3660a1591e16e543b2498358593a73b193006fb89ee37abc6"}, + {file = "aiohttp-3.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:978ec3fb0a42efcd98aae608f58c6cfcececaf0a50b4e86ee3ea0d0a574ab73b"}, + {file = "aiohttp-3.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51f87b27d9219ed4e202ed8d6f1bb96f829e5eeff18db0d52f592af6de6bdbf"}, + {file = "aiohttp-3.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:04d1a02a669d26e833c8099992c17f557e3b2fdb7960a0c455d7b1cbcb05121d"}, + {file = "aiohttp-3.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3679d5fcbc7f1ab518ab4993f12f80afb63933f6afb21b9b272793d398303b98"}, + {file = "aiohttp-3.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a4b24e03d04893b5c8ec9cd5f2f11dc9c8695c4e2416d2ac2ce6c782e4e5ffa5"}, + {file = "aiohttp-3.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d9abdfd35ecff1c95f270b7606819a0e2de9e06fa86b15d9080de26594cf4c23"}, + {file = "aiohttp-3.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b5c3e7928a0ad80887a5eba1c1da1830512ddfe7394d805badda45c03db3109"}, + {file = "aiohttp-3.11.6-cp310-cp310-win32.whl", hash = "sha256:913dd9e9378f3c38aeb5c4fb2b8383d6490bc43f3b427ae79f2870651ae08f22"}, + {file = "aiohttp-3.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:4ac26d482c2000c3a59bf757a77adc972828c9d4177b4bd432a46ba682ca7271"}, + {file = "aiohttp-3.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26ac4c960ea8debf557357a172b3ef201f2236a462aefa1bc17683a75483e518"}, + {file = "aiohttp-3.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b1f13ebc99fb98c7c13057b748f05224ccc36d17dee18136c695ef23faaf4ff"}, + {file = "aiohttp-3.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4679f1a47516189fab1774f7e45a6c7cac916224c91f5f94676f18d0b64ab134"}, + {file = "aiohttp-3.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74491fdb3d140ff561ea2128cb7af9ba0a360067ee91074af899c9614f88a18f"}, + {file = "aiohttp-3.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f51e1a90412d387e62aa2d243998c5eddb71373b199d811e6ed862a9f34f9758"}, + {file = "aiohttp-3.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72ab89510511c3bb703d0bb5504787b11e0ed8be928ed2a7cf1cda9280628430"}, + {file = "aiohttp-3.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6681c9e046d99646e8059266688374a063da85b2e4c0ebfa078cda414905d080"}, + {file = "aiohttp-3.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a17f8a6d3ab72cbbd137e494d1a23fbd3ea973db39587941f32901bb3c5c350"}, + {file = "aiohttp-3.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:867affc7612a314b95f74d93aac550ce0909bc6f0b6c658cc856890f4d326542"}, + {file = "aiohttp-3.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:00d894ebd609d5a423acef885bd61e7f6a972153f99c5b3ea45fc01fe909196c"}, + {file = "aiohttp-3.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:614c87be9d0d64477d1e4b663bdc5d1534fc0a7ebd23fb08347ab9fd5fe20fd7"}, + {file = "aiohttp-3.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:533ed46cf772f28f3bffae81c0573d916a64dee590b5dfaa3f3d11491da05b95"}, + {file = "aiohttp-3.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:589884cfbc09813afb1454816b45677e983442e146183143f988f7f5a040791a"}, + {file = "aiohttp-3.11.6-cp311-cp311-win32.whl", hash = "sha256:1da63633ba921669eec3d7e080459d4ceb663752b3dafb2f31f18edd248d2170"}, + {file = "aiohttp-3.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:d778ddda09622e7d83095cc8051698a0084c155a1474bfee9bac27d8613dbc31"}, + {file = "aiohttp-3.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:943a952df105a5305257984e7a1f5c2d0fd8564ff33647693c4d07eb2315446d"}, + {file = "aiohttp-3.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d24ec28b7658970a1f1d98608d67f88376c7e503d9d45ff2ba1949c09f2b358c"}, + {file = "aiohttp-3.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6720e809a660fdb9bec7c168c582e11cfedce339af0a5ca847a5d5b588dce826"}, + {file = "aiohttp-3.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4252d30da0ada6e6841b325869c7ef5104b488e8dd57ec439892abbb8d7b3615"}, + {file = "aiohttp-3.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f65f43ff01b238aa0b5c47962c83830a49577efe31bd37c1400c3d11d8a32835"}, + {file = "aiohttp-3.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc5933f6c9b26404444d36babb650664f984b8e5fa0694540e7b7315d11a4ff"}, + {file = "aiohttp-3.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bf546ba0c029dfffc718c4b67748687fd4f341b07b7c8f1719d6a3a46164798"}, + {file = "aiohttp-3.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c351d05bbeae30c088009c0bb3b17dda04fd854f91cc6196c448349cc98f71c3"}, + {file = "aiohttp-3.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:10499079b063576fad1597898de3f9c0a2ce617c19cc7cd6b62fdcff6b408bf7"}, + {file = "aiohttp-3.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:442ee82eda47dd59798d6866ce020fb8d02ea31ac9ac82b3d719ed349e6a9d52"}, + {file = "aiohttp-3.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:86fce9127bc317119b34786d9e9ae8af4508a103158828a535f56d201da6ab19"}, + {file = "aiohttp-3.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:973d26a5537ce5d050302eb3cd876457451745b1da0624cbb483217970e12567"}, + {file = "aiohttp-3.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:532b8f038a4e001137d3600cea5d3439d1881df41bdf44d0f9651264d562fdf0"}, + {file = "aiohttp-3.11.6-cp312-cp312-win32.whl", hash = "sha256:4863c59f748dbe147da82b389931f2a676aebc9d3419813ed5ca32d057c9cb32"}, + {file = "aiohttp-3.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:5d7f481f82c18ac1f7986e31ba6eea9be8b2e2c86f1ef035b6866179b6c5dd68"}, + {file = "aiohttp-3.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:40f502350496ba4c6820816d3164f8a0297b9aa4e95d910da31beb189866a9df"}, + {file = "aiohttp-3.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9072669b0bffb40f1f6977d0b5e8a296edc964f9cefca3a18e68649c214d0ce3"}, + {file = "aiohttp-3.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:518160ecf4e6ffd61715bc9173da0925fcce44ae6c7ca3d3f098fe42585370fb"}, + {file = "aiohttp-3.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f69cc1b45115ac44795b63529aa5caa9674be057f11271f65474127b24fc1ce6"}, + {file = "aiohttp-3.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6be90a6beced41653bda34afc891617c6d9e8276eef9c183f029f851f0a3c3d"}, + {file = "aiohttp-3.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00c22fe2486308770d22ef86242101d7b0f1e1093ce178f2358f860e5149a551"}, + {file = "aiohttp-3.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2607ebb783e3aeefa017ec8f34b506a727e6b6ab2c4b037d65f0bc7151f4430a"}, + {file = "aiohttp-3.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f761d6819870c2a8537f75f3e2fc610b163150cefa01f9f623945840f601b2c"}, + {file = "aiohttp-3.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e44d1bc6c88f5234115011842219ba27698a5f2deee245c963b180080572aaa2"}, + {file = "aiohttp-3.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e0cb6a1b1f499cb2aa0bab1c9f2169ad6913c735b7447e058e0c29c9e51c0b5"}, + {file = "aiohttp-3.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a76b4d4ca34254dca066acff2120811e2a8183997c135fcafa558280f2cc53f3"}, + {file = "aiohttp-3.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69051c1e45fb18c0ae4d39a075532ff0b015982e7997f19eb5932eb4a3e05c17"}, + {file = "aiohttp-3.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aff2ed18274c0bfe0c1d772781c87d5ca97ae50f439729007cec9644ee9b15fe"}, + {file = "aiohttp-3.11.6-cp313-cp313-win32.whl", hash = "sha256:2fbea25f2d44df809a46414a8baafa5f179d9dda7e60717f07bded56300589b3"}, + {file = "aiohttp-3.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:f77bc29a465c0f9f6573d1abe656d385fa673e34efe615bd4acc50899280ee47"}, + {file = "aiohttp-3.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:de6123b298d17bca9e53581f50a275b36e10d98e8137eb743ce69ee766dbdfe9"}, + {file = "aiohttp-3.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a10200f705f4fff00e148b7f41e5d1d929c7cd4ac523c659171a0ea8284cd6fb"}, + {file = "aiohttp-3.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7776ef6901b54dd557128d96c71e412eec0c39ebc07567e405ac98737995aad"}, + {file = "aiohttp-3.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e5c2a55583cd91936baf73d223807bb93ace6eb1fe54424782690f2707162ab"}, + {file = "aiohttp-3.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b032bd6cf7422583bf44f233f4a1489fee53c6d35920123a208adc54e2aba41e"}, + {file = "aiohttp-3.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fe2d99acbc5cf606f75d7347bf3a027c24c27bc052d470fb156f4cfcea5739"}, + {file = "aiohttp-3.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84a79c366375c2250934d1238abe5d5ea7754c823a1c7df0c52bf0a2bfded6a9"}, + {file = "aiohttp-3.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c33cbbe97dc94a34d1295a7bb68f82727bcbff2b284f73ae7e58ecc05903da97"}, + {file = "aiohttp-3.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:19e4fb9ac727834b003338dcdd27dcfe0de4fb44082b01b34ed0ab67c3469fc9"}, + {file = "aiohttp-3.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a97f6b2afbe1d27220c0c14ea978e09fb4868f462ef3d56d810d206bd2e057a2"}, + {file = "aiohttp-3.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c3f7afeea03a9bc49be6053dfd30809cd442cc12627d6ca08babd1c1f9e04ccf"}, + {file = "aiohttp-3.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0d10967600ce5bb69ddcb3e18d84b278efb5199d8b24c3c71a4959c2f08acfd0"}, + {file = "aiohttp-3.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:60f2f631b9fe7aa321fa0f0ff3f5d8b9f7f9b72afd4eecef61c33cf1cfea5d58"}, + {file = "aiohttp-3.11.6-cp39-cp39-win32.whl", hash = "sha256:4d2b75333deb5c5f61bac5a48bba3dbc142eebbd3947d98788b6ef9cc48628ae"}, + {file = "aiohttp-3.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:8908c235421972a2e02abcef87d16084aabfe825d14cc9a1debd609b3cfffbea"}, + {file = "aiohttp-3.11.6.tar.gz", hash = "sha256:fd9f55c1b51ae1c20a1afe7216a64a88d38afee063baa23c7fce03757023c999"}, ] [package.dependencies] From 6d5e9705f5e24ad1c85ab5287366d09c6691276a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 20 Nov 2024 18:42:21 +1100 Subject: [PATCH 24/78] Add bar subscription for example --- examples/live/databento/databento_subscriber.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/live/databento/databento_subscriber.py b/examples/live/databento/databento_subscriber.py index 350a247c73e2..a42989830957 100644 --- a/examples/live/databento/databento_subscriber.py +++ b/examples/live/databento/databento_subscriber.py @@ -49,6 +49,7 @@ # InstrumentId.from_str("ES.FUT.GLBX"), # InstrumentId.from_str("CL.FUT.GLBX"), # InstrumentId.from_str("LO.OPT.GLBX"), + # InstrumentId.from_str("AAPL.XNAS"), # InstrumentId.from_str("AAPL.IEXG"), ] @@ -157,7 +158,9 @@ def on_start(self) -> None: self.subscribe_quote_ticks(instrument_id, client_id=DATABENTO_CLIENT_ID) self.subscribe_trade_ticks(instrument_id, client_id=DATABENTO_CLIENT_ID) + # self.subscribe_bars(BarType.from_str(f"{instrument_id}-1-SECOND-LAST-EXTERNAL")) # self.subscribe_instrument_status(instrument_id, client_id=DATABENTO_CLIENT_ID) + # self.request_quote_ticks(instrument_id) # self.request_trade_ticks(instrument_id) From cbbe657799e793e3bc0113753c558b832316e33a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 20 Nov 2024 18:55:18 +1100 Subject: [PATCH 25/78] Refine Tardis symbology normalization --- docs/integrations/tardis.md | 6 ++++-- nautilus_core/adapters/src/tardis/parse.rs | 6 ------ nautilus_trader/adapters/tardis/data.py | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/integrations/tardis.md b/docs/integrations/tardis.md index 9ecf084ef456..e449196af3fb 100644 --- a/docs/integrations/tardis.md +++ b/docs/integrations/tardis.md @@ -77,8 +77,9 @@ This includes the following: ## Symbology and normalization -The Tardis integration seamlessly integrates with NautilusTrader's crypto exchange adapters through consistent symbol normalization. -Each exchange's raw symbols are normalized to follow Nautilus symbology conventions as detailed below: +The Tardis integration ensures seamless compatibility with NautilusTrader’s crypto exchange adapters +by consistently normalizing symbols. Typically, NautilusTrader uses the native exchange naming conventions +provided by Tardis. However, for certain exchanges, raw symbols are adjusted to adhere to the Nautilus symbology normalization, as outlined below: ### Common rules @@ -91,6 +92,7 @@ Each exchange's raw symbols are normalized to follow Nautilus symbology conventi - **Binance**: Nautilus appends the suffix `-PERP` to all perpetual symbols. - **Bybit**: Nautilus uses specific product category suffixes, including `-SPOT`, `-LINEAR`, `-INVERSE`, `-OPTION`. - **dYdX**: Nautilus appends the suffix `-PERP` to all perpetual symbols. +- **Gate.io**: Nautilus appends the suffix `-PERP` to all perpetual symbols. For detailed symbology documentation per exchange: diff --git a/nautilus_core/adapters/src/tardis/parse.rs b/nautilus_core/adapters/src/tardis/parse.rs index 9b827d56bd29..a93c556b0b97 100644 --- a/nautilus_core/adapters/src/tardis/parse.rs +++ b/nautilus_core/adapters/src/tardis/parse.rs @@ -81,12 +81,6 @@ pub fn normalize_symbol_str( symbol.push_str("-PERP"); } - Exchange::HuobiDmSwap | Exchange::HuobiDmLinearSwap - if instrument_type == InstrumentType::Perpetual => - { - symbol.push_str("-PERP"); - } - _ => {} } diff --git a/nautilus_trader/adapters/tardis/data.py b/nautilus_trader/adapters/tardis/data.py index ef5ec16b9bbc..9240461c61d6 100644 --- a/nautilus_trader/adapters/tardis/data.py +++ b/nautilus_trader/adapters/tardis/data.py @@ -465,7 +465,7 @@ async def _request_bars( ), ) - self._log.info( + self._log.debug( f"Streamed {len(bar_capsules):,} {bar_type} bars from replay", LogColor.MAGENTA, ) @@ -482,7 +482,7 @@ async def _request_bars( and (end is None or bar.ts_event <= end) ] - self._log.info( + self._log.debug( f"Sending response with {len(bars):,} bars after filtering", LogColor.MAGENTA, ) From d52a87c13ed824d35dee00dfdda5d2f180ad10a4 Mon Sep 17 00:00:00 2001 From: Ishan Bhanuka Date: Wed, 20 Nov 2024 14:19:50 -0500 Subject: [PATCH 26/78] Efficiently clean up expired timers in clocks (#2064) --- nautilus_core/common/src/clock.rs | 35 +++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/nautilus_core/common/src/clock.rs b/nautilus_core/common/src/clock.rs index 9db2c08573d6..ab75f29913a8 100644 --- a/nautilus_core/common/src/clock.rs +++ b/nautilus_core/common/src/clock.rs @@ -155,12 +155,15 @@ impl TestClock { self.time.set_time(to_time_ns); } - let mut events: Vec = self - .timers - .iter_mut() - .filter(|(_, timer)| !timer.is_expired()) - .flat_map(|(_, timer)| timer.advance(to_time_ns)) - .collect(); + // Iterate and advance timers and collect events. Only retain alive timers. + let mut events: Vec = Vec::new(); + self.timers.retain(|_, timer| { + timer.advance(to_time_ns).for_each(|event| { + events.push(event); + }); + + !timer.is_expired() + }); events.sort_by(|a, b| a.ts_event.cmp(&b.ts_event)); events @@ -182,13 +185,14 @@ impl TestClock { self.time.set_time(to_time_ns); - self.timers - .iter_mut() - .filter(|(_, timer)| !timer.is_expired()) - .flat_map(|(_, timer)| timer.advance(to_time_ns)) - .for_each(|event| { + // Iterate and advance timers and push events to heap. Only retain alive timers. + self.timers.retain(|_, timer| { + timer.advance(to_time_ns).for_each(|event| { self.heap.push(event); }); + + !timer.is_expired() + }); } /// Matches `TimeEvent` objects with their corresponding event handlers. @@ -398,6 +402,11 @@ impl LiveClock { pub const fn get_timers(&self) -> &HashMap { &self.timers } + + // Clean up expired timers. Retain only live ones + fn clear_expired_timers(&mut self) { + self.timers.retain(|_, timer| !timer.is_expired()); + } } impl Default for LiveClock { @@ -510,6 +519,8 @@ impl Clock for LiveClock { ); timer.start(); + + self.clear_expired_timers(); self.timers.insert(Ustr::from(name), timer); } @@ -552,6 +563,8 @@ impl Clock for LiveClock { self.heap.clone(), ); timer.start(); + + self.clear_expired_timers(); self.timers.insert(Ustr::from(name), timer); } From d61c42d8167205cc026d40c88c91e5fcc650fd60 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 21 Nov 2024 06:16:42 +1100 Subject: [PATCH 27/78] Update dependencies --- nautilus_core/Cargo.lock | 18 +++++++++--------- nautilus_core/Cargo.toml | 2 +- poetry.lock | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index b753c1c8aa41..e95d341aa6e3 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -506,7 +506,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "tokio", "tower", "tower-layer", @@ -529,7 +529,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "tower-layer", "tower-service", "tracing", @@ -2574,9 +2574,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" [[package]] name = "jobserver" @@ -4285,7 +4285,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper 1.0.2", "system-configuration", "tokio", "tokio-native-tls", @@ -5168,9 +5168,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] @@ -5735,9 +5735,9 @@ checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-normalization" diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index e5dfb90a0902..06b09cd73f7e 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -44,7 +44,7 @@ heck = "0.5.0" hex = "0.4.3" indexmap = { version = "2.6.0", features = ["serde"] } itertools = "0.13.0" -itoa = "1.0.11" +itoa = "1.0.13" once_cell = "1.20.2" log = { version = "0.4.22", features = ["std", "kv_unstable", "serde", "release_max_level_debug"] } parquet = "53.2.0" # Keep in line with datafusion diff --git a/poetry.lock b/poetry.lock index 3090708c3298..6672e1ea5688 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3568,23 +3568,23 @@ files = [ [[package]] name = "setuptools" -version = "75.5.0" +version = "75.6.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" files = [ - {file = "setuptools-75.5.0-py3-none-any.whl", hash = "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829"}, - {file = "setuptools-75.5.0.tar.gz", hash = "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef"}, + {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, + {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] -core = ["importlib-metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] [[package]] name = "six" From b312e128d97cd478c6dd4a2168595815c3e2962a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 21 Nov 2024 06:20:30 +1100 Subject: [PATCH 28/78] Update release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index 886217259c4b..b2785370988a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -11,6 +11,7 @@ None - Improved symbol normalization for Tardis - Refined `HttpClient` for use directly from Rust - Upgraded `datafusion` crate to v43.0.0 (#2056), thanks @twitu +- Efficiently clean up expired timers in clocks (#2064), thanks @twitu ### Breaking Changes - Renamed `TriggerType.LAST_TRADE` to `LAST_PRICE` From 35cf1a9fe3517328689313fa22375f08123cee22 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 21 Nov 2024 08:14:21 +1100 Subject: [PATCH 29/78] Add connect_stream for WebSocketClient --- nautilus_core/network/src/websocket.rs | 52 ++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/nautilus_core/network/src/websocket.rs b/nautilus_core/network/src/websocket.rs index 2fc2f37fd6da..fe3fa1ce2549 100644 --- a/nautilus_core/network/src/websocket.rs +++ b/nautilus_core/network/src/websocket.rs @@ -323,6 +323,58 @@ pub struct WebSocketClient { } impl WebSocketClient { + /// Creates a websocket client that returns a stream for reading messages. + pub async fn connect_stream( + url: String, + headers: Vec<(String, String)>, + heartbeat: Option, + heartbeat_msg: Option, + max_reconnection_tries: Option, + keyed_quotas: Vec<(String, Quota)>, + default_quota: Option, + ) -> Result<(MessageReader, Self), Error> { + let (ws_stream, _) = connect_async(url.clone().into_client_request()?).await?; + let (writer, reader) = ws_stream.split(); + let writer = Arc::new(Mutex::new(writer)); + + let disconnect_mode = Arc::new(AtomicBool::new(false)); + let rate_limiter = Arc::new(RateLimiter::new_with_quota(default_quota, keyed_quotas)); + + // Create config with minimal no-op Python handler so we incrementally + // move towards a more Rust-native approach. + let config = { + let handler = Python::with_gil(|py| Arc::new(py.None())); + WebSocketConfig { + url, + handler, + headers, + heartbeat, + heartbeat_msg, + ping_handler: None, + max_reconnection_tries, + } + }; + + let inner = WebSocketClientInner::connect_url(config).await?; + let controller_task = Self::spawn_controller_task( + inner, + disconnect_mode.clone(), + None, // no post_reconnection + None, // no post_disconnection + max_reconnection_tries, + ); + + Ok(( + reader, + Self { + rate_limiter, + writer: writer.clone(), + controller_task, + disconnect_mode, + }, + )) + } + /// Creates a websocket client. /// /// Creates an inner client and controller task to reconnect or disconnect From f153b991a446083d9f768fa8cc088ae6255e048d Mon Sep 17 00:00:00 2001 From: David Blom Date: Thu, 21 Nov 2024 11:35:59 +0100 Subject: [PATCH 30/78] Add max_reconnection_tries to data client config for dYdX (#2066) Add support for stop limit and stop market orders --- nautilus_trader/adapters/dydx/config.py | 4 ++++ nautilus_trader/adapters/dydx/data.py | 1 + nautilus_trader/adapters/dydx/execution.py | 12 +++++++++++- nautilus_trader/adapters/dydx/grpc/order_builder.py | 10 ++++++++-- nautilus_trader/adapters/dydx/websocket/client.py | 6 ++++++ nautilus_trader/core/nautilus_pyo3.pyi | 1 + 6 files changed, 31 insertions(+), 3 deletions(-) diff --git a/nautilus_trader/adapters/dydx/config.py b/nautilus_trader/adapters/dydx/config.py index 1c4c96604fcd..fa2b21c00983 100644 --- a/nautilus_trader/adapters/dydx/config.py +++ b/nautilus_trader/adapters/dydx/config.py @@ -36,12 +36,16 @@ class DYDXDataClientConfig(LiveDataClientConfig, frozen=True): If the client is connecting to the dYdX testnet API. update_instruments_interval_mins: PositiveInt or None, default 60 The interval (minutes) between reloading instruments from the venue. + max_reconnection_tries: int, default 3 + The number of retries to reconnect the websocket connection if the + connection is broken. """ wallet_address: str | None = None is_testnet: bool = False update_instruments_interval_mins: PositiveInt | None = 60 + max_ws_reconnection_tries: int | None = 3 class DYDXExecClientConfig(LiveExecClientConfig, frozen=True): diff --git a/nautilus_trader/adapters/dydx/data.py b/nautilus_trader/adapters/dydx/data.py index 1a9217eb30f8..815097081515 100644 --- a/nautilus_trader/adapters/dydx/data.py +++ b/nautilus_trader/adapters/dydx/data.py @@ -133,6 +133,7 @@ def __init__( handler_reconnect=None, base_url=ws_base_url, loop=loop, + max_reconnection_tries=config.max_ws_reconnection_tries, ) # HTTP API diff --git a/nautilus_trader/adapters/dydx/execution.py b/nautilus_trader/adapters/dydx/execution.py index 3f7aef32d61f..44e0a4bf1e9c 100644 --- a/nautilus_trader/adapters/dydx/execution.py +++ b/nautilus_trader/adapters/dydx/execution.py @@ -974,7 +974,7 @@ async def _submit_order_list(self, command: SubmitOrderList) -> None: for order in command.order_list.orders: await self._submit_order_single(order=order) - async def _submit_order_single(self, order) -> None: + async def _submit_order_single(self, order: Order) -> None: """ Submit a single order. """ @@ -1057,6 +1057,8 @@ async def _submit_order_single(self, order) -> None: order_type_map = { OrderType.LIMIT: DYDXGRPCOrderType.LIMIT, OrderType.MARKET: DYDXGRPCOrderType.MARKET, + OrderType.STOP_MARKET: DYDXGRPCOrderType.STOP_MARKET, + OrderType.STOP_LIMIT: DYDXGRPCOrderType.STOP_LIMIT, } order_side_map = { OrderSide.NO_ORDER_SIDE: DYDXOrder.Side.SIDE_UNSPECIFIED, @@ -1071,11 +1073,18 @@ async def _submit_order_single(self, order) -> None: } price = 0 + trigger_price = None if order.order_type == OrderType.LIMIT: price = order.price.as_double() elif order.order_type == OrderType.MARKET: price = 0 + elif order.order_type == OrderType.STOP_LIMIT: + price = order.price.as_double() + trigger_price = order.trigger_price.as_double() + elif order.order_type == OrderType.STOP_MARKET: + price = 0 + trigger_price = order.trigger_price.as_double() else: rejection_reason = ( f"Cannot submit order: order type `{order.order_type}` not (yet) supported" @@ -1100,6 +1109,7 @@ async def _submit_order_single(self, order) -> None: post_only=order.is_post_only, good_til_block=good_til_block, good_til_block_time=good_til_date_secs, + trigger_price=trigger_price, ) await self._place_order(order_msg=order_msg, order=order) diff --git a/nautilus_trader/adapters/dydx/grpc/order_builder.py b/nautilus_trader/adapters/dydx/grpc/order_builder.py index 63bb53020bc8..54054969ae78 100644 --- a/nautilus_trader/adapters/dydx/grpc/order_builder.py +++ b/nautilus_trader/adapters/dydx/grpc/order_builder.py @@ -240,7 +240,7 @@ def create_order( good_til_block: int | None = None, good_til_block_time: int | None = None, execution: OrderExecution = OrderExecution.DEFAULT, - conditional_order_trigger_subticks: int = 0, + trigger_price: float | None = None, ) -> Order: """ Create a new Order instance. @@ -271,7 +271,9 @@ def create_order( not yet filled. execution : OrderExecution, default OrderExecution.DEFAULT OrderExecution enum: DEFAULT, IOC, FOK or POST_ONLY - conditional_order_trigger_subticks : int, default value is 0. + trigger_price : float, optional. + The price of the conditional limit order. Only applicable to STOP_LIMIT, + STOP_MARKET, TAKE_PROFIT_MARKET or TAKE_PROFIT_LIMIT orders. """ order_time_in_force = OrderHelper.calculate_time_in_force( @@ -282,6 +284,10 @@ def create_order( ) client_metadata = OrderHelper.calculate_client_metadata(order_type) condition_type = OrderHelper.calculate_condition_type(order_type) + conditional_order_trigger_subticks = 0 + + if trigger_price is not None: + conditional_order_trigger_subticks = self.calculate_subticks(trigger_price) return Order( order_id=order_id, diff --git a/nautilus_trader/adapters/dydx/websocket/client.py b/nautilus_trader/adapters/dydx/websocket/client.py index 15f293efe38c..3fd82886e0b9 100644 --- a/nautilus_trader/adapters/dydx/websocket/client.py +++ b/nautilus_trader/adapters/dydx/websocket/client.py @@ -52,6 +52,9 @@ class DYDXWebsocketClient: The event loop for the client. subscription_rate_limit_per_second : int, default 2 The maximum number of subscription message to send to the venue. + max_reconnection_tries: int, default 3 + The number of retries to reconnect the websocket connection if the + connection is broken. """ @@ -63,6 +66,7 @@ def __init__( handler_reconnect: Callable[..., Awaitable[None]] | None, loop: asyncio.AbstractEventLoop, subscription_rate_limit_per_second: int = 2, + max_reconnection_tries: int | None = 3, ) -> None: """ Provide a dYdX streaming WebSocket client. @@ -77,6 +81,7 @@ def __init__( self._is_running = False self._subscriptions: set[tuple[str, str]] = set() self._subscription_rate_limit_per_second = subscription_rate_limit_per_second + self._max_reconnection_tries = max_reconnection_tries self._msg_timestamp = self._clock.utc_now() self._msg_timeout_secs: int = 60 self._reconnect_task: asyncio.Task | None = None @@ -145,6 +150,7 @@ async def connect(self) -> None: heartbeat=10, headers=[], ping_handler=self._handle_ping, + max_reconnection_tries=self._max_reconnection_tries, ) client = await WebSocketClient.connect( config=config, diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 5e896dc92e29..473ea6fac5ab 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -2770,6 +2770,7 @@ class WebSocketConfig: heartbeat: int | None = None, heartbeat_msg: str | None = None, ping_handler: Callable[..., Any] | None = None, + max_reconnection_tries: int | None = None, ) -> None: ... class WebSocketClient: From 46b5a40086e0bea1ebbf9a8b4cb36bdea266217a Mon Sep 17 00:00:00 2001 From: David Blom Date: Thu, 21 Nov 2024 20:58:33 +0100 Subject: [PATCH 31/78] Fix account balance and order status parsing for dYdX (#2067) --- nautilus_trader/adapters/dydx/common/enums.py | 2 +- nautilus_trader/adapters/dydx/schemas/ws.py | 22 +------------------ .../adapters/dydx/test_parsing.py | 2 +- .../adapters/dydx/test_websocket_schema.py | 10 ++++----- 4 files changed, 8 insertions(+), 28 deletions(-) diff --git a/nautilus_trader/adapters/dydx/common/enums.py b/nautilus_trader/adapters/dydx/common/enums.py index 5849706a1270..19303887951c 100644 --- a/nautilus_trader/adapters/dydx/common/enums.py +++ b/nautilus_trader/adapters/dydx/common/enums.py @@ -244,7 +244,7 @@ def __init__(self) -> None: DYDXOrderStatus.OPEN: OrderStatus.ACCEPTED, DYDXOrderStatus.FILLED: OrderStatus.FILLED, DYDXOrderStatus.CANCELED: OrderStatus.CANCELED, - DYDXOrderStatus.BEST_EFFORT_CANCELED: OrderStatus.PENDING_CANCEL, + DYDXOrderStatus.BEST_EFFORT_CANCELED: OrderStatus.CANCELED, DYDXOrderStatus.BEST_EFFORT_OPENED: OrderStatus.SUBMITTED, } diff --git a/nautilus_trader/adapters/dydx/schemas/ws.py b/nautilus_trader/adapters/dydx/schemas/ws.py index 53d3a8d79407..401fb7f65837 100644 --- a/nautilus_trader/adapters/dydx/schemas/ws.py +++ b/nautilus_trader/adapters/dydx/schemas/ws.py @@ -19,7 +19,6 @@ # ruff: noqa: N815 import datetime -from collections import defaultdict from decimal import Decimal import msgspec @@ -548,31 +547,12 @@ def parse_to_account_balances(self) -> list[AccountBalance]: """ Create an account balance report. """ - orders: dict[str, list[DYDXOrderResponse]] = defaultdict(list) - - # Orders with state BEST_EFFORT_CANCELED are not considered to be - # included in the locked account balance. - if self.orders is not None: - for order in self.orders: - if ( - order.status - in ( - DYDXOrderStatus.OPEN, - DYDXOrderStatus.BEST_EFFORT_OPENED, - ) - and order.type == DYDXOrderType.LIMIT - ): - orders[order.quote_currency()].append(order) - account_balances = [] if self.subaccount is not None: for asset_position in self.subaccount.assetPositions.values(): + # Only valid for a margin account locked = Decimal(0) - - for order in orders[asset_position.symbol]: - locked += Decimal(order.size) * Decimal(order.price) - account_balance = asset_position.parse_to_account_balance(locked=locked) account_balances.append(account_balance) diff --git a/tests/integration_tests/adapters/dydx/test_parsing.py b/tests/integration_tests/adapters/dydx/test_parsing.py index d30a02f7b5b4..f4f48d9098bc 100644 --- a/tests/integration_tests/adapters/dydx/test_parsing.py +++ b/tests/integration_tests/adapters/dydx/test_parsing.py @@ -144,7 +144,7 @@ def test_parse_nautilus_order_side( (DYDXOrderStatus.OPEN, OrderStatus.ACCEPTED), (DYDXOrderStatus.FILLED, OrderStatus.FILLED), (DYDXOrderStatus.CANCELED, OrderStatus.CANCELED), - (DYDXOrderStatus.BEST_EFFORT_CANCELED, OrderStatus.PENDING_CANCEL), + (DYDXOrderStatus.BEST_EFFORT_CANCELED, OrderStatus.CANCELED), (DYDXOrderStatus.BEST_EFFORT_OPENED, OrderStatus.SUBMITTED), ], ) diff --git a/tests/integration_tests/adapters/dydx/test_websocket_schema.py b/tests/integration_tests/adapters/dydx/test_websocket_schema.py index dab60ef941af..4515ffbe8a65 100644 --- a/tests/integration_tests/adapters/dydx/test_websocket_schema.py +++ b/tests/integration_tests/adapters/dydx/test_websocket_schema.py @@ -205,8 +205,8 @@ def test_account_parse_to_account_balances() -> None: expected_result = [ AccountBalance( total=Money(Decimal("11.62332500"), Currency.from_str("USDC")), - locked=Money(Decimal("10.00590000"), Currency.from_str("USDC")), - free=Money(Decimal("1.61742500"), Currency.from_str("USDC")), + locked=Money(Decimal("0"), Currency.from_str("USDC")), + free=Money(Decimal("11.62332500"), Currency.from_str("USDC")), ), ] @@ -228,8 +228,8 @@ def test_account_parse_to_account_balances_order_best_effort_canceled() -> None: expected_result = [ AccountBalance( total=Money(Decimal("11.62332500"), Currency.from_str("USDC")), - locked=Money(Decimal("10.00590000"), Currency.from_str("USDC")), - free=Money(Decimal("1.61742500"), Currency.from_str("USDC")), + locked=Money(Decimal("0"), Currency.from_str("USDC")), + free=Money(Decimal("11.62332500"), Currency.from_str("USDC")), ), ] @@ -660,7 +660,7 @@ def test_account_channel_data_order_best_effort_canceled() -> None: order_side=OrderSide.SELL, order_type=OrderType.LIMIT, time_in_force=TimeInForce.IOC, - order_status=OrderStatus.PENDING_CANCEL, + order_status=OrderStatus.CANCELED, price=Price(2519.4, 4), quantity=Quantity(0.003, 5), filled_qty=Quantity(0, 5), From fe5cd995286d0c58f96a921d52b30c33da25086f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Nov 2024 07:03:21 +1100 Subject: [PATCH 32/78] Update release notes --- RELEASES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index b2785370988a..a95f720cbd83 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -3,8 +3,9 @@ Released on TBD (UTC). ### Enhancements -None - Improved `Cache` behavior when adding more recent quotes, trades, or bars (now adds to cache) +- Added `STOP_MARKET` and `STOP_LIMIT` order support for dYdX (#2066), thanks @davidsblom +- Added `max_reconnection_tries` to data client config for dYdX (#2066), thanks @davidsblom ### Internal Improvements - Improved live engines error logging (will now log all exceptions rather than just `RuntimeError`) @@ -18,6 +19,7 @@ None ### Fixes - Fixed missing venue -> exchange mappings for Tardis integration +- Fixed account balance and order status parsing for dYdX (#2067), thanks @davidsblom --- From f0181c96a197edb784a90ba6edcef0b11e871595 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Nov 2024 07:06:39 +1100 Subject: [PATCH 33/78] Update dependencies --- nautilus_core/Cargo.lock | 204 ++++++++------- poetry.lock | 521 ++++++++++++++++++++------------------- 2 files changed, 379 insertions(+), 346 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index e95d341aa6e3..53bf7fce4935 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -162,9 +162,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arrow" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4caf25cdc4a985f91df42ed9e9308e1adbcd341a31a72605c697033fcef163e3" +checksum = "c91839b07e474b3995035fd8ac33ee54f9c9ccbbb1ea33d9909c71bffdf1259d" dependencies = [ "arrow-arith", "arrow-array", @@ -184,9 +184,9 @@ dependencies = [ [[package]] name = "arrow-arith" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91f2dfd1a7ec0aca967dfaa616096aec49779adc8eccec005e2f5e4111b1192a" +checksum = "855c57c4efd26722b044dcd3e348252560e3e0333087fb9f6479dc0bf744054f" dependencies = [ "arrow-array", "arrow-buffer", @@ -199,9 +199,9 @@ dependencies = [ [[package]] name = "arrow-array" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39387ca628be747394890a6e47f138ceac1aa912eab64f02519fed24b637af8" +checksum = "bd03279cea46569acf9295f6224fbc370c5df184b4d2ecfe97ccb131d5615a7f" dependencies = [ "ahash 0.8.11", "arrow-buffer", @@ -210,15 +210,15 @@ dependencies = [ "chrono", "chrono-tz", "half", - "hashbrown 0.14.5", + "hashbrown 0.15.1", "num", ] [[package]] name = "arrow-buffer" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e51e05228852ffe3eb391ce7178a0f97d2cf80cc6ef91d3c4a6b3cb688049ec" +checksum = "9e4a9b9b1d6d7117f6138e13bc4dd5daa7f94e671b70e8c9c4dc37b4f5ecfc16" dependencies = [ "bytes", "half", @@ -227,9 +227,9 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09aea56ec9fa267f3f3f6cdab67d8a9974cbba90b3aa38c8fe9d0bb071bd8c1" +checksum = "bc70e39916e60c5b7af7a8e2719e3ae589326039e1e863675a008bee5ffe90fd" dependencies = [ "arrow-array", "arrow-buffer", @@ -248,9 +248,9 @@ dependencies = [ [[package]] name = "arrow-csv" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07b5232be87d115fde73e32f2ca7f1b353bff1b44ac422d3c6fc6ae38f11f0d" +checksum = "789b2af43c1049b03a8d088ff6b2257cdcea1756cd76b174b1f2600356771b97" dependencies = [ "arrow-array", "arrow-buffer", @@ -267,9 +267,9 @@ dependencies = [ [[package]] name = "arrow-data" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98ae0af50890b494cebd7d6b04b35e896205c1d1df7b29a6272c5d0d0249ef5" +checksum = "e4e75edf21ffd53744a9b8e3ed11101f610e7ceb1a29860432824f1834a1f623" dependencies = [ "arrow-buffer", "arrow-schema", @@ -279,9 +279,9 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed91bdeaff5a1c00d28d8f73466bcb64d32bbd7093b5a30156b4b9f4dba3eee" +checksum = "d186a909dece9160bf8312f5124d797884f608ef5435a36d9d608e0b2a9bcbf8" dependencies = [ "arrow-array", "arrow-buffer", @@ -294,9 +294,9 @@ dependencies = [ [[package]] name = "arrow-json" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0471f51260a5309307e5d409c9dc70aede1cd9cf1d4ff0f0a1e8e1a2dd0e0d3c" +checksum = "b66ff2fedc1222942d0bd2fd391cb14a85baa3857be95c9373179bd616753b85" dependencies = [ "arrow-array", "arrow-buffer", @@ -314,9 +314,9 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2883d7035e0b600fb4c30ce1e50e66e53d8656aa729f2bfa4b51d359cf3ded52" +checksum = "ece7b5bc1180e6d82d1a60e1688c199829e8842e38497563c3ab6ea813e527fd" dependencies = [ "arrow-array", "arrow-buffer", @@ -329,9 +329,9 @@ dependencies = [ [[package]] name = "arrow-row" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552907e8e587a6fde4f8843fd7a27a576a260f65dab6c065741ea79f633fc5be" +checksum = "745c114c8f0e8ce211c83389270de6fbe96a9088a7b32c2a041258a443fe83ff" dependencies = [ "ahash 0.8.11", "arrow-array", @@ -343,18 +343,18 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "539ada65246b949bd99ffa0881a9a15a4a529448af1a07a9838dd78617dafab1" +checksum = "b95513080e728e4cec37f1ff5af4f12c9688d47795d17cda80b6ec2cf74d4678" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "arrow-select" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6259e566b752da6dceab91766ed8b2e67bf6270eb9ad8a6e07a33c1bede2b125" +checksum = "8e415279094ea70323c032c6e739c48ad8d80e78a09bef7117b8718ad5bf3722" dependencies = [ "ahash 0.8.11", "arrow-array", @@ -366,9 +366,9 @@ dependencies = [ [[package]] name = "arrow-string" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3179ccbd18ebf04277a095ba7321b93fd1f774f18816bd5f6b3ce2f594edb6c" +checksum = "11d956cae7002eb8d83a27dbd34daaea1cf5b75852f0b84deb4d93a276e92bbf" dependencies = [ "arrow-array", "arrow-buffer", @@ -418,7 +418,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -429,7 +429,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -590,7 +590,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.87", + "syn 2.0.89", "which", ] @@ -665,7 +665,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -779,7 +779,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.87", + "syn 2.0.89", "tempfile", "toml", ] @@ -921,7 +921,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1031,6 +1031,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1198,7 +1208,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1209,7 +1219,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1660,7 +1670,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1702,7 +1712,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1712,7 +1722,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1735,7 +1745,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -1989,7 +1999,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -2451,7 +2461,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -2861,7 +2871,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -3031,7 +3041,7 @@ dependencies = [ "ring", "rstest", "rustls", - "rustls-native-certs 0.8.0", + "rustls-native-certs 0.8.1", "tracing", ] @@ -3459,7 +3469,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -3536,7 +3546,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -3614,9 +3624,9 @@ dependencies = [ [[package]] name = "parquet" -version = "53.2.0" +version = "53.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea02606ba6f5e856561d8d507dba8bac060aefca2a6c0f1aa1d361fed91ff3e" +checksum = "2b449890367085eb65d7d3321540abc3d7babbd179ce31df0016e90719114191" dependencies = [ "ahash 0.8.11", "arrow-array", @@ -3633,7 +3643,7 @@ dependencies = [ "flate2", "futures", "half", - "hashbrown 0.14.5", + "hashbrown 0.15.1", "lz4_flex", "num", "num-bigint", @@ -3753,7 +3763,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -3851,7 +3861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -3889,9 +3899,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -4003,7 +4013,7 @@ checksum = "22c26fd8e9fc19f53f0c1e00bf61471de6789f7eb263056f7f944a9cceb5823e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -4035,7 +4045,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -4048,7 +4058,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -4411,7 +4421,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.87", + "syn 2.0.89", "unicode-ident", ] @@ -4501,20 +4511,19 @@ dependencies = [ "rustls-pemfile", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 2.11.1", ] [[package]] name = "rustls-native-certs" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.0.1", ] [[package]] @@ -4605,7 +4614,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1415a607e92bec364ea2cf9264646dcce0f91e6d65281bd6f2819cca3bf39c8" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -4650,7 +4672,7 @@ checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -4818,7 +4840,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -4884,7 +4906,7 @@ checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -4949,7 +4971,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -4972,7 +4994,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.87", + "syn 2.0.89", "tempfile", "tokio", "url", @@ -5129,7 +5151,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5151,9 +5173,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -5183,7 +5205,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5207,7 +5229,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5295,7 +5317,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5306,7 +5328,7 @@ checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5439,7 +5461,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5494,7 +5516,7 @@ dependencies = [ "futures-util", "log", "rustls", - "rustls-native-certs 0.8.0", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", "tokio-rustls", @@ -5596,7 +5618,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5656,7 +5678,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5712,7 +5734,7 @@ checksum = "560b82d656506509d43abe30e0ba64c56b1953ab3d4fe7ba5902747a7a3cedd5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -5939,7 +5961,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", "wasm-bindgen-shared", ] @@ -5973,7 +5995,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6009,9 +6031,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.6" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" dependencies = [ "rustls-pki-types", ] @@ -6108,7 +6130,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -6119,7 +6141,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -6368,7 +6390,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", "synstructure", ] @@ -6390,7 +6412,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] @@ -6410,7 +6432,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", "synstructure", ] @@ -6439,7 +6461,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.89", ] [[package]] diff --git a/poetry.lock b/poetry.lock index 6672e1ea5688..463df2cd490f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,87 +13,87 @@ files = [ [[package]] name = "aiohttp" -version = "3.11.6" +version = "3.11.7" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" files = [ - {file = "aiohttp-3.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7510b3ca2275691875ddf072a5b6cd129278d11fe09301add7d292fc8d3432de"}, - {file = "aiohttp-3.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfab0d2c3380c588fc925168533edb21d3448ad76c3eadc360ff963019161724"}, - {file = "aiohttp-3.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf02dba0f342f3a8228f43fae256aafc21c4bc85bffcf537ce4582e2b1565188"}, - {file = "aiohttp-3.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92daedf7221392e7a7984915ca1b0481a94c71457c2f82548414a41d65555e70"}, - {file = "aiohttp-3.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2274a7876e03429e3218589a6d3611a194bdce08c3f1e19962e23370b47c0313"}, - {file = "aiohttp-3.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a2e1eae2d2f62f3660a1591e16e543b2498358593a73b193006fb89ee37abc6"}, - {file = "aiohttp-3.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:978ec3fb0a42efcd98aae608f58c6cfcececaf0a50b4e86ee3ea0d0a574ab73b"}, - {file = "aiohttp-3.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51f87b27d9219ed4e202ed8d6f1bb96f829e5eeff18db0d52f592af6de6bdbf"}, - {file = "aiohttp-3.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:04d1a02a669d26e833c8099992c17f557e3b2fdb7960a0c455d7b1cbcb05121d"}, - {file = "aiohttp-3.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3679d5fcbc7f1ab518ab4993f12f80afb63933f6afb21b9b272793d398303b98"}, - {file = "aiohttp-3.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a4b24e03d04893b5c8ec9cd5f2f11dc9c8695c4e2416d2ac2ce6c782e4e5ffa5"}, - {file = "aiohttp-3.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d9abdfd35ecff1c95f270b7606819a0e2de9e06fa86b15d9080de26594cf4c23"}, - {file = "aiohttp-3.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8b5c3e7928a0ad80887a5eba1c1da1830512ddfe7394d805badda45c03db3109"}, - {file = "aiohttp-3.11.6-cp310-cp310-win32.whl", hash = "sha256:913dd9e9378f3c38aeb5c4fb2b8383d6490bc43f3b427ae79f2870651ae08f22"}, - {file = "aiohttp-3.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:4ac26d482c2000c3a59bf757a77adc972828c9d4177b4bd432a46ba682ca7271"}, - {file = "aiohttp-3.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:26ac4c960ea8debf557357a172b3ef201f2236a462aefa1bc17683a75483e518"}, - {file = "aiohttp-3.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b1f13ebc99fb98c7c13057b748f05224ccc36d17dee18136c695ef23faaf4ff"}, - {file = "aiohttp-3.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4679f1a47516189fab1774f7e45a6c7cac916224c91f5f94676f18d0b64ab134"}, - {file = "aiohttp-3.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74491fdb3d140ff561ea2128cb7af9ba0a360067ee91074af899c9614f88a18f"}, - {file = "aiohttp-3.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f51e1a90412d387e62aa2d243998c5eddb71373b199d811e6ed862a9f34f9758"}, - {file = "aiohttp-3.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72ab89510511c3bb703d0bb5504787b11e0ed8be928ed2a7cf1cda9280628430"}, - {file = "aiohttp-3.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6681c9e046d99646e8059266688374a063da85b2e4c0ebfa078cda414905d080"}, - {file = "aiohttp-3.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a17f8a6d3ab72cbbd137e494d1a23fbd3ea973db39587941f32901bb3c5c350"}, - {file = "aiohttp-3.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:867affc7612a314b95f74d93aac550ce0909bc6f0b6c658cc856890f4d326542"}, - {file = "aiohttp-3.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:00d894ebd609d5a423acef885bd61e7f6a972153f99c5b3ea45fc01fe909196c"}, - {file = "aiohttp-3.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:614c87be9d0d64477d1e4b663bdc5d1534fc0a7ebd23fb08347ab9fd5fe20fd7"}, - {file = "aiohttp-3.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:533ed46cf772f28f3bffae81c0573d916a64dee590b5dfaa3f3d11491da05b95"}, - {file = "aiohttp-3.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:589884cfbc09813afb1454816b45677e983442e146183143f988f7f5a040791a"}, - {file = "aiohttp-3.11.6-cp311-cp311-win32.whl", hash = "sha256:1da63633ba921669eec3d7e080459d4ceb663752b3dafb2f31f18edd248d2170"}, - {file = "aiohttp-3.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:d778ddda09622e7d83095cc8051698a0084c155a1474bfee9bac27d8613dbc31"}, - {file = "aiohttp-3.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:943a952df105a5305257984e7a1f5c2d0fd8564ff33647693c4d07eb2315446d"}, - {file = "aiohttp-3.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d24ec28b7658970a1f1d98608d67f88376c7e503d9d45ff2ba1949c09f2b358c"}, - {file = "aiohttp-3.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6720e809a660fdb9bec7c168c582e11cfedce339af0a5ca847a5d5b588dce826"}, - {file = "aiohttp-3.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4252d30da0ada6e6841b325869c7ef5104b488e8dd57ec439892abbb8d7b3615"}, - {file = "aiohttp-3.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f65f43ff01b238aa0b5c47962c83830a49577efe31bd37c1400c3d11d8a32835"}, - {file = "aiohttp-3.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc5933f6c9b26404444d36babb650664f984b8e5fa0694540e7b7315d11a4ff"}, - {file = "aiohttp-3.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bf546ba0c029dfffc718c4b67748687fd4f341b07b7c8f1719d6a3a46164798"}, - {file = "aiohttp-3.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c351d05bbeae30c088009c0bb3b17dda04fd854f91cc6196c448349cc98f71c3"}, - {file = "aiohttp-3.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:10499079b063576fad1597898de3f9c0a2ce617c19cc7cd6b62fdcff6b408bf7"}, - {file = "aiohttp-3.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:442ee82eda47dd59798d6866ce020fb8d02ea31ac9ac82b3d719ed349e6a9d52"}, - {file = "aiohttp-3.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:86fce9127bc317119b34786d9e9ae8af4508a103158828a535f56d201da6ab19"}, - {file = "aiohttp-3.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:973d26a5537ce5d050302eb3cd876457451745b1da0624cbb483217970e12567"}, - {file = "aiohttp-3.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:532b8f038a4e001137d3600cea5d3439d1881df41bdf44d0f9651264d562fdf0"}, - {file = "aiohttp-3.11.6-cp312-cp312-win32.whl", hash = "sha256:4863c59f748dbe147da82b389931f2a676aebc9d3419813ed5ca32d057c9cb32"}, - {file = "aiohttp-3.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:5d7f481f82c18ac1f7986e31ba6eea9be8b2e2c86f1ef035b6866179b6c5dd68"}, - {file = "aiohttp-3.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:40f502350496ba4c6820816d3164f8a0297b9aa4e95d910da31beb189866a9df"}, - {file = "aiohttp-3.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9072669b0bffb40f1f6977d0b5e8a296edc964f9cefca3a18e68649c214d0ce3"}, - {file = "aiohttp-3.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:518160ecf4e6ffd61715bc9173da0925fcce44ae6c7ca3d3f098fe42585370fb"}, - {file = "aiohttp-3.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f69cc1b45115ac44795b63529aa5caa9674be057f11271f65474127b24fc1ce6"}, - {file = "aiohttp-3.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6be90a6beced41653bda34afc891617c6d9e8276eef9c183f029f851f0a3c3d"}, - {file = "aiohttp-3.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00c22fe2486308770d22ef86242101d7b0f1e1093ce178f2358f860e5149a551"}, - {file = "aiohttp-3.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2607ebb783e3aeefa017ec8f34b506a727e6b6ab2c4b037d65f0bc7151f4430a"}, - {file = "aiohttp-3.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f761d6819870c2a8537f75f3e2fc610b163150cefa01f9f623945840f601b2c"}, - {file = "aiohttp-3.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e44d1bc6c88f5234115011842219ba27698a5f2deee245c963b180080572aaa2"}, - {file = "aiohttp-3.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e0cb6a1b1f499cb2aa0bab1c9f2169ad6913c735b7447e058e0c29c9e51c0b5"}, - {file = "aiohttp-3.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a76b4d4ca34254dca066acff2120811e2a8183997c135fcafa558280f2cc53f3"}, - {file = "aiohttp-3.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69051c1e45fb18c0ae4d39a075532ff0b015982e7997f19eb5932eb4a3e05c17"}, - {file = "aiohttp-3.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aff2ed18274c0bfe0c1d772781c87d5ca97ae50f439729007cec9644ee9b15fe"}, - {file = "aiohttp-3.11.6-cp313-cp313-win32.whl", hash = "sha256:2fbea25f2d44df809a46414a8baafa5f179d9dda7e60717f07bded56300589b3"}, - {file = "aiohttp-3.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:f77bc29a465c0f9f6573d1abe656d385fa673e34efe615bd4acc50899280ee47"}, - {file = "aiohttp-3.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:de6123b298d17bca9e53581f50a275b36e10d98e8137eb743ce69ee766dbdfe9"}, - {file = "aiohttp-3.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a10200f705f4fff00e148b7f41e5d1d929c7cd4ac523c659171a0ea8284cd6fb"}, - {file = "aiohttp-3.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7776ef6901b54dd557128d96c71e412eec0c39ebc07567e405ac98737995aad"}, - {file = "aiohttp-3.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e5c2a55583cd91936baf73d223807bb93ace6eb1fe54424782690f2707162ab"}, - {file = "aiohttp-3.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b032bd6cf7422583bf44f233f4a1489fee53c6d35920123a208adc54e2aba41e"}, - {file = "aiohttp-3.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fe2d99acbc5cf606f75d7347bf3a027c24c27bc052d470fb156f4cfcea5739"}, - {file = "aiohttp-3.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84a79c366375c2250934d1238abe5d5ea7754c823a1c7df0c52bf0a2bfded6a9"}, - {file = "aiohttp-3.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c33cbbe97dc94a34d1295a7bb68f82727bcbff2b284f73ae7e58ecc05903da97"}, - {file = "aiohttp-3.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:19e4fb9ac727834b003338dcdd27dcfe0de4fb44082b01b34ed0ab67c3469fc9"}, - {file = "aiohttp-3.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a97f6b2afbe1d27220c0c14ea978e09fb4868f462ef3d56d810d206bd2e057a2"}, - {file = "aiohttp-3.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c3f7afeea03a9bc49be6053dfd30809cd442cc12627d6ca08babd1c1f9e04ccf"}, - {file = "aiohttp-3.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0d10967600ce5bb69ddcb3e18d84b278efb5199d8b24c3c71a4959c2f08acfd0"}, - {file = "aiohttp-3.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:60f2f631b9fe7aa321fa0f0ff3f5d8b9f7f9b72afd4eecef61c33cf1cfea5d58"}, - {file = "aiohttp-3.11.6-cp39-cp39-win32.whl", hash = "sha256:4d2b75333deb5c5f61bac5a48bba3dbc142eebbd3947d98788b6ef9cc48628ae"}, - {file = "aiohttp-3.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:8908c235421972a2e02abcef87d16084aabfe825d14cc9a1debd609b3cfffbea"}, - {file = "aiohttp-3.11.6.tar.gz", hash = "sha256:fd9f55c1b51ae1c20a1afe7216a64a88d38afee063baa23c7fce03757023c999"}, + {file = "aiohttp-3.11.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8bedb1f6cb919af3b6353921c71281b1491f948ca64408871465d889b4ee1b66"}, + {file = "aiohttp-3.11.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f5022504adab881e2d801a88b748ea63f2a9d130e0b2c430824682a96f6534be"}, + {file = "aiohttp-3.11.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e22d1721c978a6494adc824e0916f9d187fa57baeda34b55140315fa2f740184"}, + {file = "aiohttp-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e993676c71288618eb07e20622572b1250d8713e7e00ab3aabae28cb70f3640d"}, + {file = "aiohttp-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e13a05db87d3b241c186d0936808d0e4e12decc267c617d54e9c643807e968b6"}, + {file = "aiohttp-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ba8d043fed7ffa117024d7ba66fdea011c0e7602327c6d73cacaea38abe4491"}, + {file = "aiohttp-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda3ed0a7869d2fa16aa41f9961ade73aa2c2e3b2fcb0a352524e7b744881889"}, + {file = "aiohttp-3.11.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43bfd25113c1e98aec6c70e26d5f4331efbf4aa9037ba9ad88f090853bf64d7f"}, + {file = "aiohttp-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3dd3e7e7c9ef3e7214f014f1ae260892286647b3cf7c7f1b644a568fd410f8ca"}, + {file = "aiohttp-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78c657ece7a73b976905ab9ec8be9ef2df12ed8984c24598a1791c58ce3b4ce4"}, + {file = "aiohttp-3.11.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:db70a47987e34494b451a334605bee57a126fe8d290511349e86810b4be53b01"}, + {file = "aiohttp-3.11.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9e67531370a3b07e49b280c1f8c2df67985c790ad2834d1b288a2f13cd341c5f"}, + {file = "aiohttp-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9202f184cc0582b1db15056f2225ab4c1e3dac4d9ade50dd0613ac3c46352ac2"}, + {file = "aiohttp-3.11.7-cp310-cp310-win32.whl", hash = "sha256:2257bdd5cf54a4039a4337162cd8048f05a724380a2283df34620f55d4e29341"}, + {file = "aiohttp-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:b7215bf2b53bc6cb35808149980c2ae80a4ae4e273890ac85459c014d5aa60ac"}, + {file = "aiohttp-3.11.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cea52d11e02123f125f9055dfe0ccf1c3857225fb879e4a944fae12989e2aef2"}, + {file = "aiohttp-3.11.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3ce18f703b7298e7f7633efd6a90138d99a3f9a656cb52c1201e76cb5d79cf08"}, + {file = "aiohttp-3.11.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:670847ee6aeb3a569cd7cdfbe0c3bec1d44828bbfbe78c5d305f7f804870ef9e"}, + {file = "aiohttp-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dda726f89bfa5c465ba45b76515135a3ece0088dfa2da49b8bb278f3bdeea12"}, + {file = "aiohttp-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25b74a811dba37c7ea6a14d99eb9402d89c8d739d50748a75f3cf994cf19c43"}, + {file = "aiohttp-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5522ee72f95661e79db691310290c4618b86dff2d9b90baedf343fd7a08bf79"}, + {file = "aiohttp-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fbf41a6bbc319a7816ae0f0177c265b62f2a59ad301a0e49b395746eb2a9884"}, + {file = "aiohttp-3.11.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59ee1925b5a5efdf6c4e7be51deee93984d0ac14a6897bd521b498b9916f1544"}, + {file = "aiohttp-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:24054fce8c6d6f33a3e35d1c603ef1b91bbcba73e3f04a22b4f2f27dac59b347"}, + {file = "aiohttp-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:351849aca2c6f814575c1a485c01c17a4240413f960df1bf9f5deb0003c61a53"}, + {file = "aiohttp-3.11.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:12724f3a211fa243570e601f65a8831372caf1a149d2f1859f68479f07efec3d"}, + {file = "aiohttp-3.11.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7ea4490360b605804bea8173d2d086b6c379d6bb22ac434de605a9cbce006e7d"}, + {file = "aiohttp-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e0bf378db07df0a713a1e32381a1b277e62ad106d0dbe17b5479e76ec706d720"}, + {file = "aiohttp-3.11.7-cp311-cp311-win32.whl", hash = "sha256:cd8d62cab363dfe713067027a5adb4907515861f1e4ce63e7be810b83668b847"}, + {file = "aiohttp-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:bf0e6cce113596377cadda4e3ac5fb89f095bd492226e46d91b4baef1dd16f60"}, + {file = "aiohttp-3.11.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4bb7493c3e3a36d3012b8564bd0e2783259ddd7ef3a81a74f0dbfa000fce48b7"}, + {file = "aiohttp-3.11.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e143b0ef9cb1a2b4f74f56d4fbe50caa7c2bb93390aff52f9398d21d89bc73ea"}, + {file = "aiohttp-3.11.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7c58a240260822dc07f6ae32a0293dd5bccd618bb2d0f36d51c5dbd526f89c0"}, + {file = "aiohttp-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d20cfe63a1c135d26bde8c1d0ea46fd1200884afbc523466d2f1cf517d1fe33"}, + {file = "aiohttp-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12e4d45847a174f77b2b9919719203769f220058f642b08504cf8b1cf185dacf"}, + {file = "aiohttp-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf4efa2d01f697a7dbd0509891a286a4af0d86902fc594e20e3b1712c28c0106"}, + {file = "aiohttp-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee6a4cdcbf54b8083dc9723cdf5f41f722c00db40ccf9ec2616e27869151129"}, + {file = "aiohttp-3.11.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6095aaf852c34f42e1bd0cf0dc32d1e4b48a90bfb5054abdbb9d64b36acadcb"}, + {file = "aiohttp-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1cf03d27885f8c5ebf3993a220cc84fc66375e1e6e812731f51aab2b2748f4a6"}, + {file = "aiohttp-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1a17f6a230f81eb53282503823f59d61dff14fb2a93847bf0399dc8e87817307"}, + {file = "aiohttp-3.11.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:481f10a1a45c5f4c4a578bbd74cff22eb64460a6549819242a87a80788461fba"}, + {file = "aiohttp-3.11.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:db37248535d1ae40735d15bdf26ad43be19e3d93ab3f3dad8507eb0f85bb8124"}, + {file = "aiohttp-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d18a8b44ec8502a7fde91446cd9c9b95ce7c49f1eacc1fb2358b8907d4369fd"}, + {file = "aiohttp-3.11.7-cp312-cp312-win32.whl", hash = "sha256:3d1c9c15d3999107cbb9b2d76ca6172e6710a12fda22434ee8bd3f432b7b17e8"}, + {file = "aiohttp-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:018f1b04883a12e77e7fc161934c0f298865d3a484aea536a6a2ca8d909f0ba0"}, + {file = "aiohttp-3.11.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:241a6ca732d2766836d62c58c49ca7a93d08251daef0c1e3c850df1d1ca0cbc4"}, + {file = "aiohttp-3.11.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aa3705a8d14de39898da0fbad920b2a37b7547c3afd2a18b9b81f0223b7d0f68"}, + {file = "aiohttp-3.11.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9acfc7f652b31853eed3b92095b0acf06fd5597eeea42e939bd23a17137679d5"}, + {file = "aiohttp-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcefcf2915a2dbdbce37e2fc1622129a1918abfe3d06721ce9f6cdac9b6d2eaa"}, + {file = "aiohttp-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c1f6490dd1862af5aae6cfcf2a274bffa9a5b32a8f5acb519a7ecf5a99a88866"}, + {file = "aiohttp-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac5462582d6561c1c1708853a9faf612ff4e5ea5e679e99be36143d6eabd8e"}, + {file = "aiohttp-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1a6309005acc4b2bcc577ba3b9169fea52638709ffacbd071f3503264620da"}, + {file = "aiohttp-3.11.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b973cce96793725ef63eb449adfb74f99c043c718acb76e0d2a447ae369962"}, + {file = "aiohttp-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ce91a24aac80de6be8512fb1c4838a9881aa713f44f4e91dd7bb3b34061b497d"}, + {file = "aiohttp-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:875f7100ce0e74af51d4139495eec4025affa1a605280f23990b6434b81df1bd"}, + {file = "aiohttp-3.11.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c171fc35d3174bbf4787381716564042a4cbc008824d8195eede3d9b938e29a8"}, + {file = "aiohttp-3.11.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ee9afa1b0d2293c46954f47f33e150798ad68b78925e3710044e0d67a9487791"}, + {file = "aiohttp-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8360c7cc620abb320e1b8d603c39095101391a82b1d0be05fb2225471c9c5c52"}, + {file = "aiohttp-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7a9318da4b4ada9a67c1dd84d1c0834123081e746bee311a16bb449f363d965e"}, + {file = "aiohttp-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:fc6da202068e0a268e298d7cd09b6e9f3997736cd9b060e2750963754552a0a9"}, + {file = "aiohttp-3.11.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:17829f37c0d31d89aa6b8b010475a10233774771f9b6dc2cc352ea4f8ce95d9a"}, + {file = "aiohttp-3.11.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d6177077a31b1aecfc3c9070bd2f11419dbb4a70f30f4c65b124714f525c2e48"}, + {file = "aiohttp-3.11.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:badda65ac99555791eed75e234afb94686ed2317670c68bff8a4498acdaee935"}, + {file = "aiohttp-3.11.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0de6466b9d742b4ee56fe1b2440706e225eb48c77c63152b1584864a236e7a50"}, + {file = "aiohttp-3.11.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04b0cc74d5a882c9dacaeeccc1444f0233212b6f5be8bc90833feef1e1ce14b9"}, + {file = "aiohttp-3.11.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c7af3e50e5903d21d7b935aceed901cc2475463bc16ddd5587653548661fdb"}, + {file = "aiohttp-3.11.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c63f898f683d1379b9be5afc3dd139e20b30b0b1e0bf69a3fc3681f364cf1629"}, + {file = "aiohttp-3.11.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdadc3f6a32d6eca45f9a900a254757fd7855dfb2d8f8dcf0e88f0fae3ff8eb1"}, + {file = "aiohttp-3.11.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d329300fb23e14ed1f8c6d688dfd867d1dcc3b1d7cd49b7f8c5b44e797ce0932"}, + {file = "aiohttp-3.11.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5578cf40440eafcb054cf859964bc120ab52ebe0e0562d2b898126d868749629"}, + {file = "aiohttp-3.11.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7b2f8107a3c329789f3c00b2daad0e35f548d0a55cda6291579136622099a46e"}, + {file = "aiohttp-3.11.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:43dd89a6194f6ab02a3fe36b09e42e2df19c211fc2050ce37374d96f39604997"}, + {file = "aiohttp-3.11.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d2fa6fc7cc865d26ff42480ac9b52b8c9b7da30a10a6442a9cdf429de840e949"}, + {file = "aiohttp-3.11.7-cp39-cp39-win32.whl", hash = "sha256:a7d9a606355655617fee25dd7e54d3af50804d002f1fd3118dd6312d26692d70"}, + {file = "aiohttp-3.11.7-cp39-cp39-win_amd64.whl", hash = "sha256:53c921b58fdc6485d6b2603e0132bb01cd59b8f0620ffc0907f525e0ba071687"}, + {file = "aiohttp-3.11.7.tar.gz", hash = "sha256:01a8aca4af3da85cea5c90141d23f4b0eee3cbecfd33b029a45a80f28c66c668"}, ] [package.dependencies] @@ -2933,19 +2933,19 @@ files = [ [[package]] name = "pydantic" -version = "2.9.2" +version = "2.10.0" description = "Data validation using Python type hints" optional = true python-versions = ">=3.8" files = [ - {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, - {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, + {file = "pydantic-2.10.0-py3-none-any.whl", hash = "sha256:5e7807ba9201bdf61b1b58aa6eb690916c40a47acfb114b1b4fef3e7fd5b30fc"}, + {file = "pydantic-2.10.0.tar.gz", hash = "sha256:0aca0f045ff6e2f097f1fe89521115335f15049eeb8a7bef3dafe4b19a74e289"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.23.4" -typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} +pydantic-core = "2.27.0" +typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -2953,100 +2953,111 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.23.4" +version = "2.27.0" description = "Core functionality for Pydantic validation and serialization" optional = true python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, - {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, - {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, - {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, - {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, - {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, - {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, - {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, - {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, - {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, - {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, - {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, - {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, - {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, - {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, - {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, - {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, - {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, - {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, - {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, - {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, - {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, - {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, - {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, - {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, - {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, - {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, - {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, - {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, - {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, - {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, - {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, - {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, - {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, - {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, - {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, - {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, - {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, + {file = "pydantic_core-2.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2ac6b919f7fed71b17fe0b4603c092a4c9b5bae414817c9c81d3c22d1e1bcc"}, + {file = "pydantic_core-2.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e015833384ca3e1a0565a79f5d953b0629d9138021c27ad37c92a9fa1af7623c"}, + {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db72e40628967f6dc572020d04b5f800d71264e0531c6da35097e73bdf38b003"}, + {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df45c4073bed486ea2f18757057953afed8dd77add7276ff01bccb79982cf46c"}, + {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:836a4bfe0cc6d36dc9a9cc1a7b391265bf6ce9d1eb1eac62ac5139f5d8d9a6fa"}, + {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bf1340ae507f6da6360b24179c2083857c8ca7644aab65807023cf35404ea8d"}, + {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ab325fc86fbc077284c8d7f996d904d30e97904a87d6fb303dce6b3de7ebba9"}, + {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1da0c98a85a6c6ed702d5556db3b09c91f9b0b78de37b7593e2de8d03238807a"}, + {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b0202ebf2268954090209a84f9897345719e46a57c5f2c9b7b250ca0a9d3e63"}, + {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:35380671c3c921fe8adf31ad349dc6f7588b7e928dbe44e1093789734f607399"}, + {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b4c19525c3538fbc0bbda6229f9682fb8199ce9ac37395880e6952798e00373"}, + {file = "pydantic_core-2.27.0-cp310-none-win32.whl", hash = "sha256:333c840a1303d1474f491e7be0b718226c730a39ead0f7dab2c7e6a2f3855555"}, + {file = "pydantic_core-2.27.0-cp310-none-win_amd64.whl", hash = "sha256:99b2863c1365f43f74199c980a3d40f18a218fbe683dd64e470199db426c4d6a"}, + {file = "pydantic_core-2.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4523c4009c3f39d948e01962223c9f5538602e7087a628479b723c939fab262d"}, + {file = "pydantic_core-2.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84af1cf7bfdcbc6fcf5a5f70cc9896205e0350306e4dd73d54b6a18894f79386"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e65466b31be1070b4a5b7dbfbd14b247884cb8e8b79c64fb0f36b472912dbaea"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5c022bb0d453192426221605efc865373dde43b17822a264671c53b068ac20c"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bb69bf3b6500f195c3deb69c1205ba8fc3cb21d1915f1f158a10d6b1ef29b6a"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aa4d1b2eba9a325897308b3124014a142cdccb9f3e016f31d3ebee6b5ea5e75"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e96ca781e0c01e32115912ebdf7b3fb0780ce748b80d7d28a0802fa9fbaf44e"}, + {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b872c86d8d71827235c7077461c502feb2db3f87d9d6d5a9daa64287d75e4fa0"}, + {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:82e1ad4ca170e8af4c928b67cff731b6296e6a0a0981b97b2eb7c275cc4e15bd"}, + {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:eb40f828bc2f73f777d1eb8fee2e86cd9692a4518b63b6b5aa8af915dfd3207b"}, + {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9a8fbf506fde1529a1e3698198fe64bfbe2e0c09557bc6a7dcf872e7c01fec40"}, + {file = "pydantic_core-2.27.0-cp311-none-win32.whl", hash = "sha256:24f984fc7762ed5f806d9e8c4c77ea69fdb2afd987b4fd319ef06c87595a8c55"}, + {file = "pydantic_core-2.27.0-cp311-none-win_amd64.whl", hash = "sha256:68950bc08f9735306322bfc16a18391fcaac99ded2509e1cc41d03ccb6013cfe"}, + {file = "pydantic_core-2.27.0-cp311-none-win_arm64.whl", hash = "sha256:3eb8849445c26b41c5a474061032c53e14fe92a11a5db969f722a2716cd12206"}, + {file = "pydantic_core-2.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8117839a9bdbba86e7f9df57018fe3b96cec934c3940b591b0fd3fbfb485864a"}, + {file = "pydantic_core-2.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a291d0b4243a259c8ea7e2b84eb9ccb76370e569298875a7c5e3e71baf49057a"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e35afd9e10b2698e6f2f32256678cb23ca6c1568d02628033a837638b3ed12"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58ab0d979c969983cdb97374698d847a4acffb217d543e172838864636ef10d9"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d06b667e53320332be2bf6f9461f4a9b78092a079b8ce8634c9afaa7e10cd9f"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78f841523729e43e3928a364ec46e2e3f80e6625a4f62aca5c345f3f626c6e8a"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:400bf470e4327e920883b51e255617dfe4496d4e80c3fea0b5a5d0bf2c404dd4"}, + {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:951e71da6c89d354572098bada5ba5b5dc3a9390c933af8a614e37755d3d1840"}, + {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a51ce96224eadd1845150b204389623c8e129fde5a67a84b972bd83a85c6c40"}, + {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:483c2213a609e7db2c592bbc015da58b6c75af7360ca3c981f178110d9787bcf"}, + {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:359e7951f04ad35111b5ddce184db3391442345d0ab073aa63a95eb8af25a5ef"}, + {file = "pydantic_core-2.27.0-cp312-none-win32.whl", hash = "sha256:ee7d9d5537daf6d5c74a83b38a638cc001b648096c1cae8ef695b0c919d9d379"}, + {file = "pydantic_core-2.27.0-cp312-none-win_amd64.whl", hash = "sha256:2be0ad541bb9f059954ccf8877a49ed73877f862529575ff3d54bf4223e4dd61"}, + {file = "pydantic_core-2.27.0-cp312-none-win_arm64.whl", hash = "sha256:6e19401742ed7b69e51d8e4df3c03ad5ec65a83b36244479fd70edde2828a5d9"}, + {file = "pydantic_core-2.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5f2b19b8d6fca432cb3acf48cf5243a7bf512988029b6e6fd27e9e8c0a204d85"}, + {file = "pydantic_core-2.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c86679f443e7085ea55a7376462553996c688395d18ef3f0d3dbad7838f857a2"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510b11e9c3b1a852876d1ccd8d5903684336d635214148637ceb27366c75a467"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb704155e73b833801c247f39d562229c0303f54770ca14fb1c053acb376cf10"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ce048deb1e033e7a865ca384770bccc11d44179cf09e5193a535c4c2f497bdc"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58560828ee0951bb125c6f2862fbc37f039996d19ceb6d8ff1905abf7da0bf3d"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb4785894936d7682635726613c44578c420a096729f1978cd061a7e72d5275"}, + {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2883b260f7a93235488699d39cbbd94fa7b175d3a8063fbfddd3e81ad9988cb2"}, + {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c6fcb3fa3855d583aa57b94cf146f7781d5d5bc06cb95cb3afece33d31aac39b"}, + {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e851a051f7260e6d688267eb039c81f05f23a19431bd7dfa4bf5e3cb34c108cd"}, + {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edb1bfd45227dec8d50bc7c7d86463cd8728bcc574f9b07de7369880de4626a3"}, + {file = "pydantic_core-2.27.0-cp313-none-win32.whl", hash = "sha256:678f66462058dd978702db17eb6a3633d634f7aa0deaea61e0a674152766d3fc"}, + {file = "pydantic_core-2.27.0-cp313-none-win_amd64.whl", hash = "sha256:d28ca7066d6cdd347a50d8b725dc10d9a1d6a1cce09836cf071ea6a2d4908be0"}, + {file = "pydantic_core-2.27.0-cp313-none-win_arm64.whl", hash = "sha256:6f4a53af9e81d757756508b57cae1cf28293f0f31b9fa2bfcb416cc7fb230f9d"}, + {file = "pydantic_core-2.27.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:e9f9feee7f334b72ceae46313333d002b56f325b5f04271b4ae2aadd9e993ae4"}, + {file = "pydantic_core-2.27.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:225bfff5d425c34e1fd562cef52d673579d59b967d9de06178850c4802af9039"}, + {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921ad596ff1a82f9c692b0758c944355abc9f0de97a4c13ca60ffc6d8dc15d4"}, + {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6354e18a9be37bfa124d6b288a87fb30c673745806c92956f1a25e3ae6e76b96"}, + {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ee4c2a75af9fe21269a4a0898c5425afb01af1f5d276063f57e2ae1bc64e191"}, + {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c91e3c04f5191fd3fb68764bddeaf02025492d5d9f23343b283870f6ace69708"}, + {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6ebfac28fd51890a61df36ef202adbd77d00ee5aca4a3dadb3d9ed49cfb929"}, + {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36aa167f69d8807ba7e341d67ea93e50fcaaf6bc433bb04939430fa3dab06f31"}, + {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e8d89c276234579cd3d095d5fa2a44eb10db9a218664a17b56363cddf226ff3"}, + {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:5cc822ab90a70ea3a91e6aed3afac570b276b1278c6909b1d384f745bd09c714"}, + {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e15315691fe2253eb447503153acef4d7223dfe7e7702f9ed66539fcd0c43801"}, + {file = "pydantic_core-2.27.0-cp38-none-win32.whl", hash = "sha256:dfa5f5c0a4c8fced1422dc2ca7eefd872d5d13eb33cf324361dbf1dbfba0a9fe"}, + {file = "pydantic_core-2.27.0-cp38-none-win_amd64.whl", hash = "sha256:513cb14c0cc31a4dfd849a4674b20c46d87b364f997bbcb02282306f5e187abf"}, + {file = "pydantic_core-2.27.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4148dc9184ab79e356dc00a4199dc0ee8647973332cb385fc29a7cced49b9f9c"}, + {file = "pydantic_core-2.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5fc72fbfebbf42c0856a824b8b0dc2b5cd2e4a896050281a21cfa6fed8879cb1"}, + {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:185ef205256cd8b38431205698531026979db89a79587725c1e55c59101d64e9"}, + {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:395e3e1148fa7809016231f8065f30bb0dc285a97b4dc4360cd86e17bab58af7"}, + {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33d14369739c5d07e2e7102cdb0081a1fa46ed03215e07f097b34e020b83b1ae"}, + {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7820bb0d65e3ce1e3e70b6708c2f66143f55912fa02f4b618d0f08b61575f12"}, + {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b61989068de9ce62296cde02beffabcadb65672207fc51e7af76dca75e6636"}, + {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15e350efb67b855cd014c218716feea4986a149ed1f42a539edd271ee074a196"}, + {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:433689845288f9a1ee5714444e65957be26d30915f7745091ede4a83cfb2d7bb"}, + {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:3fd8bc2690e7c39eecdf9071b6a889ce7b22b72073863940edc2a0a23750ca90"}, + {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:884f1806609c2c66564082540cffc96868c5571c7c3cf3a783f63f2fb49bd3cd"}, + {file = "pydantic_core-2.27.0-cp39-none-win32.whl", hash = "sha256:bf37b72834e7239cf84d4a0b2c050e7f9e48bced97bad9bdf98d26b8eb72e846"}, + {file = "pydantic_core-2.27.0-cp39-none-win_amd64.whl", hash = "sha256:31a2cae5f059329f9cfe3d8d266d3da1543b60b60130d186d9b6a3c20a346361"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4fb49cfdb53af5041aba909be00cccfb2c0d0a2e09281bf542371c5fd36ad04c"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:49633583eb7dc5cba61aaf7cdb2e9e662323ad394e543ee77af265736bcd3eaa"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153017e3d6cd3ce979de06d84343ca424bb6092727375eba1968c8b4693c6ecb"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff63a92f6e249514ef35bc795de10745be0226eaea06eb48b4bbeaa0c8850a4a"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5982048129f40b082c2654de10c0f37c67a14f5ff9d37cf35be028ae982f26df"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:91bc66f878557313c2a6bcf396e7befcffe5ab4354cfe4427318968af31143c3"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:68ef5377eb582fa4343c9d0b57a5b094046d447b4c73dd9fbd9ffb216f829e7d"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c5726eec789ee38f2c53b10b1821457b82274f81f4f746bb1e666d8741fcfadb"}, + {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c0c431e4be5c1a0c6654e0c31c661cd89e0ca956ef65305c3c3fd96f4e72ca39"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8e21d927469d04b39386255bf00d0feedead16f6253dcc85e9e10ddebc334084"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b51f964fcbb02949fc546022e56cdb16cda457af485e9a3e8b78ac2ecf5d77e"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a7fd4de38f7ff99a37e18fa0098c3140286451bc823d1746ba80cec5b433a1"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fda87808429c520a002a85d6e7cdadbf58231d60e96260976c5b8f9a12a8e13"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a150392102c402c538190730fda06f3bce654fc498865579a9f2c1d2b425833"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c9ed88b398ba7e3bad7bd64d66cc01dcde9cfcb7ec629a6fd78a82fa0b559d78"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:9fe94d9d2a2b4edd7a4b22adcd45814b1b59b03feb00e56deb2e89747aec7bfe"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d8b5ee4ae9170e2775d495b81f414cc20268041c42571530513496ba61e94ba3"}, + {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d29e235ce13c91902ef3efc3d883a677655b3908b1cbc73dee816e5e1f8f7739"}, + {file = "pydantic_core-2.27.0.tar.gz", hash = "sha256:f57783fbaf648205ac50ae7d646f27582fc706be3977e87c3c124e7a92407b10"}, ] [package.dependencies] @@ -4008,93 +4019,93 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "yarl" -version = "1.17.2" +version = "1.18.0" description = "Yet another URL library" optional = false python-versions = ">=3.9" files = [ - {file = "yarl-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:93771146ef048b34201bfa382c2bf74c524980870bb278e6df515efaf93699ff"}, - {file = "yarl-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8281db240a1616af2f9c5f71d355057e73a1409c4648c8949901396dc0a3c151"}, - {file = "yarl-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:170ed4971bf9058582b01a8338605f4d8c849bd88834061e60e83b52d0c76870"}, - {file = "yarl-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc61b005f6521fcc00ca0d1243559a5850b9dd1e1fe07b891410ee8fe192d0c0"}, - {file = "yarl-1.17.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:871e1b47eec7b6df76b23c642a81db5dd6536cbef26b7e80e7c56c2fd371382e"}, - {file = "yarl-1.17.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a58a2f2ca7aaf22b265388d40232f453f67a6def7355a840b98c2d547bd037f"}, - {file = "yarl-1.17.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:736bb076f7299c5c55dfef3eb9e96071a795cb08052822c2bb349b06f4cb2e0a"}, - {file = "yarl-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8fd51299e21da709eabcd5b2dd60e39090804431292daacbee8d3dabe39a6bc0"}, - {file = "yarl-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:358dc7ddf25e79e1cc8ee16d970c23faee84d532b873519c5036dbb858965795"}, - {file = "yarl-1.17.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:50d866f7b1a3f16f98603e095f24c0eeba25eb508c85a2c5939c8b3870ba2df8"}, - {file = "yarl-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8b9c4643e7d843a0dca9cd9d610a0876e90a1b2cbc4c5ba7930a0d90baf6903f"}, - {file = "yarl-1.17.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d63123bfd0dce5f91101e77c8a5427c3872501acece8c90df457b486bc1acd47"}, - {file = "yarl-1.17.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:4e76381be3d8ff96a4e6c77815653063e87555981329cf8f85e5be5abf449021"}, - {file = "yarl-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:734144cd2bd633a1516948e477ff6c835041c0536cef1d5b9a823ae29899665b"}, - {file = "yarl-1.17.2-cp310-cp310-win32.whl", hash = "sha256:26bfb6226e0c157af5da16d2d62258f1ac578d2899130a50433ffee4a5dfa673"}, - {file = "yarl-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:76499469dcc24759399accd85ec27f237d52dec300daaca46a5352fcbebb1071"}, - {file = "yarl-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:792155279dc093839e43f85ff7b9b6493a8eaa0af1f94f1f9c6e8f4de8c63500"}, - {file = "yarl-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38bc4ed5cae853409cb193c87c86cd0bc8d3a70fd2268a9807217b9176093ac6"}, - {file = "yarl-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4a8c83f6fcdc327783bdc737e8e45b2e909b7bd108c4da1892d3bc59c04a6d84"}, - {file = "yarl-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6d5fed96f0646bfdf698b0a1cebf32b8aae6892d1bec0c5d2d6e2df44e1e2d"}, - {file = "yarl-1.17.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:782ca9c58f5c491c7afa55518542b2b005caedaf4685ec814fadfcee51f02493"}, - {file = "yarl-1.17.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff6af03cac0d1a4c3c19e5dcc4c05252411bf44ccaa2485e20d0a7c77892ab6e"}, - {file = "yarl-1.17.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a3f47930fbbed0f6377639503848134c4aa25426b08778d641491131351c2c8"}, - {file = "yarl-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1fa68a3c921365c5745b4bd3af6221ae1f0ea1bf04b69e94eda60e57958907f"}, - {file = "yarl-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:187df91395c11e9f9dc69b38d12406df85aa5865f1766a47907b1cc9855b6303"}, - {file = "yarl-1.17.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:93d1c8cc5bf5df401015c5e2a3ce75a5254a9839e5039c881365d2a9dcfc6dc2"}, - {file = "yarl-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:11d86c6145ac5c706c53d484784cf504d7d10fa407cb73b9d20f09ff986059ef"}, - {file = "yarl-1.17.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c42774d1d1508ec48c3ed29e7b110e33f5e74a20957ea16197dbcce8be6b52ba"}, - {file = "yarl-1.17.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8e589379ef0407b10bed16cc26e7392ef8f86961a706ade0a22309a45414d7"}, - {file = "yarl-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1056cadd5e850a1c026f28e0704ab0a94daaa8f887ece8dfed30f88befb87bb0"}, - {file = "yarl-1.17.2-cp311-cp311-win32.whl", hash = "sha256:be4c7b1c49d9917c6e95258d3d07f43cfba2c69a6929816e77daf322aaba6628"}, - {file = "yarl-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:ac8eda86cc75859093e9ce390d423aba968f50cf0e481e6c7d7d63f90bae5c9c"}, - {file = "yarl-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dd90238d3a77a0e07d4d6ffdebc0c21a9787c5953a508a2231b5f191455f31e9"}, - {file = "yarl-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c74f0b0472ac40b04e6d28532f55cac8090e34c3e81f118d12843e6df14d0909"}, - {file = "yarl-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d486ddcaca8c68455aa01cf53d28d413fb41a35afc9f6594a730c9779545876"}, - {file = "yarl-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25b7e93f5414b9a983e1a6c1820142c13e1782cc9ed354c25e933aebe97fcf2"}, - {file = "yarl-1.17.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a0baff7827a632204060f48dca9e63fbd6a5a0b8790c1a2adfb25dc2c9c0d50"}, - {file = "yarl-1.17.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:460024cacfc3246cc4d9f47a7fc860e4fcea7d1dc651e1256510d8c3c9c7cde0"}, - {file = "yarl-1.17.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5870d620b23b956f72bafed6a0ba9a62edb5f2ef78a8849b7615bd9433384171"}, - {file = "yarl-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2941756754a10e799e5b87e2319bbec481ed0957421fba0e7b9fb1c11e40509f"}, - {file = "yarl-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9611b83810a74a46be88847e0ea616794c406dbcb4e25405e52bff8f4bee2d0a"}, - {file = "yarl-1.17.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cd7e35818d2328b679a13268d9ea505c85cd773572ebb7a0da7ccbca77b6a52e"}, - {file = "yarl-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6b981316fcd940f085f646b822c2ff2b8b813cbd61281acad229ea3cbaabeb6b"}, - {file = "yarl-1.17.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:688058e89f512fb7541cb85c2f149c292d3fa22f981d5a5453b40c5da49eb9e8"}, - {file = "yarl-1.17.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56afb44a12b0864d17b597210d63a5b88915d680f6484d8d202ed68ade38673d"}, - {file = "yarl-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:17931dfbb84ae18b287279c1f92b76a3abcd9a49cd69b92e946035cff06bcd20"}, - {file = "yarl-1.17.2-cp312-cp312-win32.whl", hash = "sha256:ff8d95e06546c3a8c188f68040e9d0360feb67ba8498baf018918f669f7bc39b"}, - {file = "yarl-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:4c840cc11163d3c01a9d8aad227683c48cd3e5be5a785921bcc2a8b4b758c4f3"}, - {file = "yarl-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3294f787a437cb5d81846de3a6697f0c35ecff37a932d73b1fe62490bef69211"}, - {file = "yarl-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1e7fedb09c059efee2533119666ca7e1a2610072076926fa028c2ba5dfeb78c"}, - {file = "yarl-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:da9d3061e61e5ae3f753654813bc1cd1c70e02fb72cf871bd6daf78443e9e2b1"}, - {file = "yarl-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91c012dceadc695ccf69301bfdccd1fc4472ad714fe2dd3c5ab4d2046afddf29"}, - {file = "yarl-1.17.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f11fd61d72d93ac23718d393d2a64469af40be2116b24da0a4ca6922df26807e"}, - {file = "yarl-1.17.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46c465ad06971abcf46dd532f77560181387b4eea59084434bdff97524444032"}, - {file = "yarl-1.17.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef6eee1a61638d29cd7c85f7fd3ac7b22b4c0fabc8fd00a712b727a3e73b0685"}, - {file = "yarl-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4434b739a8a101a837caeaa0137e0e38cb4ea561f39cb8960f3b1e7f4967a3fc"}, - {file = "yarl-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:752485cbbb50c1e20908450ff4f94217acba9358ebdce0d8106510859d6eb19a"}, - {file = "yarl-1.17.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:17791acaa0c0f89323c57da7b9a79f2174e26d5debbc8c02d84ebd80c2b7bff8"}, - {file = "yarl-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5c6ea72fe619fee5e6b5d4040a451d45d8175f560b11b3d3e044cd24b2720526"}, - {file = "yarl-1.17.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db5ac3871ed76340210fe028f535392f097fb31b875354bcb69162bba2632ef4"}, - {file = "yarl-1.17.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7a1606ba68e311576bcb1672b2a1543417e7e0aa4c85e9e718ba6466952476c0"}, - {file = "yarl-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9bc27dd5cfdbe3dc7f381b05e6260ca6da41931a6e582267d5ca540270afeeb2"}, - {file = "yarl-1.17.2-cp313-cp313-win32.whl", hash = "sha256:52492b87d5877ec405542f43cd3da80bdcb2d0c2fbc73236526e5f2c28e6db28"}, - {file = "yarl-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:8e1bf59e035534ba4077f5361d8d5d9194149f9ed4f823d1ee29ef3e8964ace3"}, - {file = "yarl-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c556fbc6820b6e2cda1ca675c5fa5589cf188f8da6b33e9fc05b002e603e44fa"}, - {file = "yarl-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f44a4247461965fed18b2573f3a9eb5e2c3cad225201ee858726cde610daca"}, - {file = "yarl-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3a3ede8c248f36b60227eb777eac1dbc2f1022dc4d741b177c4379ca8e75571a"}, - {file = "yarl-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2654caaf5584449d49c94a6b382b3cb4a246c090e72453493ea168b931206a4d"}, - {file = "yarl-1.17.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d41c684f286ce41fa05ab6af70f32d6da1b6f0457459a56cf9e393c1c0b2217"}, - {file = "yarl-1.17.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2270d590997445a0dc29afa92e5534bfea76ba3aea026289e811bf9ed4b65a7f"}, - {file = "yarl-1.17.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18662443c6c3707e2fc7fad184b4dc32dd428710bbe72e1bce7fe1988d4aa654"}, - {file = "yarl-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75ac158560dec3ed72f6d604c81090ec44529cfb8169b05ae6fcb3e986b325d9"}, - {file = "yarl-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1fee66b32e79264f428dc8da18396ad59cc48eef3c9c13844adec890cd339db5"}, - {file = "yarl-1.17.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:585ce7cd97be8f538345de47b279b879e091c8b86d9dbc6d98a96a7ad78876a3"}, - {file = "yarl-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c019abc2eca67dfa4d8fb72ba924871d764ec3c92b86d5b53b405ad3d6aa56b0"}, - {file = "yarl-1.17.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c6e659b9a24d145e271c2faf3fa6dd1fcb3e5d3f4e17273d9e0350b6ab0fe6e2"}, - {file = "yarl-1.17.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:d17832ba39374134c10e82d137e372b5f7478c4cceeb19d02ae3e3d1daed8721"}, - {file = "yarl-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bc3003710e335e3f842ae3fd78efa55f11a863a89a72e9a07da214db3bf7e1f8"}, - {file = "yarl-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f5ffc6b7ace5b22d9e73b2a4c7305740a339fbd55301d52735f73e21d9eb3130"}, - {file = "yarl-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:48e424347a45568413deec6f6ee2d720de2cc0385019bedf44cd93e8638aa0ed"}, - {file = "yarl-1.17.2-py3-none-any.whl", hash = "sha256:dd7abf4f717e33b7487121faf23560b3a50924f80e4bef62b22dab441ded8f3b"}, - {file = "yarl-1.17.2.tar.gz", hash = "sha256:753eaaa0c7195244c84b5cc159dc8204b7fd99f716f11198f999f2332a86b178"}, + {file = "yarl-1.18.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:074fee89caab89a97e18ef5f29060ef61ba3cae6cd77673acc54bfdd3214b7b7"}, + {file = "yarl-1.18.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b026cf2c32daf48d90c0c4e406815c3f8f4cfe0c6dfccb094a9add1ff6a0e41a"}, + {file = "yarl-1.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae38bd86eae3ba3d2ce5636cc9e23c80c9db2e9cb557e40b98153ed102b5a736"}, + {file = "yarl-1.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:685cc37f3f307c6a8e879986c6d85328f4c637f002e219f50e2ef66f7e062c1d"}, + {file = "yarl-1.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8254dbfce84ee5d1e81051ee7a0f1536c108ba294c0fdb5933476398df0654f3"}, + {file = "yarl-1.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20de4a8b04de70c49698dc2390b7fd2d18d424d3b876371f9b775e2b462d4b41"}, + {file = "yarl-1.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0a2074a37285570d54b55820687de3d2f2b9ecf1b714e482e48c9e7c0402038"}, + {file = "yarl-1.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f576ed278860df2721a5d57da3381040176ef1d07def9688a385c8330db61a1"}, + {file = "yarl-1.18.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3a3709450a574d61be6ac53d582496014342ea34876af8dc17cc16da32826c9a"}, + {file = "yarl-1.18.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bd80ed29761490c622edde5dd70537ca8c992c2952eb62ed46984f8eff66d6e8"}, + {file = "yarl-1.18.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:32141e13a1d5a48525e519c9197d3f4d9744d818d5c7d6547524cc9eccc8971e"}, + {file = "yarl-1.18.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8b8d3e4e014fb4274f1c5bf61511d2199e263909fb0b8bda2a7428b0894e8dc6"}, + {file = "yarl-1.18.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:701bb4a8f4de191c8c0cc9a1e6d5142f4df880e9d1210e333b829ca9425570ed"}, + {file = "yarl-1.18.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a45d94075ac0647621eaaf693c8751813a3eccac455d423f473ffed38c8ac5c9"}, + {file = "yarl-1.18.0-cp310-cp310-win32.whl", hash = "sha256:34176bfb082add67cb2a20abd85854165540891147f88b687a5ed0dc225750a0"}, + {file = "yarl-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:73553bbeea7d6ec88c08ad8027f4e992798f0abc459361bf06641c71972794dc"}, + {file = "yarl-1.18.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b8e8c516dc4e1a51d86ac975b0350735007e554c962281c432eaa5822aa9765c"}, + {file = "yarl-1.18.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e6b4466714a73f5251d84b471475850954f1fa6acce4d3f404da1d55d644c34"}, + {file = "yarl-1.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c893f8c1a6d48b25961e00922724732d00b39de8bb0b451307482dc87bddcd74"}, + {file = "yarl-1.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13aaf2bdbc8c86ddce48626b15f4987f22e80d898818d735b20bd58f17292ee8"}, + {file = "yarl-1.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd21c0128e301851de51bc607b0a6da50e82dc34e9601f4b508d08cc89ee7929"}, + {file = "yarl-1.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205de377bd23365cd85562c9c6c33844050a93661640fda38e0567d2826b50df"}, + {file = "yarl-1.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed69af4fe2a0949b1ea1d012bf065c77b4c7822bad4737f17807af2adb15a73c"}, + {file = "yarl-1.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e1c18890091aa3cc8a77967943476b729dc2016f4cfe11e45d89b12519d4a93"}, + {file = "yarl-1.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:91b8fb9427e33f83ca2ba9501221ffaac1ecf0407f758c4d2f283c523da185ee"}, + {file = "yarl-1.18.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:536a7a8a53b75b2e98ff96edb2dfb91a26b81c4fed82782035767db5a465be46"}, + {file = "yarl-1.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a64619a9c47c25582190af38e9eb382279ad42e1f06034f14d794670796016c0"}, + {file = "yarl-1.18.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c73a6bbc97ba1b5a0c3c992ae93d721c395bdbb120492759b94cc1ac71bc6350"}, + {file = "yarl-1.18.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a173401d7821a2a81c7b47d4e7d5c4021375a1441af0c58611c1957445055056"}, + {file = "yarl-1.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7520e799b1f84e095cce919bd6c23c9d49472deeef25fe1ef960b04cca51c3fc"}, + {file = "yarl-1.18.0-cp311-cp311-win32.whl", hash = "sha256:c4cb992d8090d5ae5f7afa6754d7211c578be0c45f54d3d94f7781c495d56716"}, + {file = "yarl-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:52c136f348605974c9b1c878addd6b7a60e3bf2245833e370862009b86fa4689"}, + {file = "yarl-1.18.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1ece25e2251c28bab737bdf0519c88189b3dd9492dc086a1d77336d940c28ced"}, + {file = "yarl-1.18.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:454902dc1830d935c90b5b53c863ba2a98dcde0fbaa31ca2ed1ad33b2a7171c6"}, + {file = "yarl-1.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01be8688fc211dc237e628fcc209dda412d35de7642453059a0553747018d075"}, + {file = "yarl-1.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d26f1fa9fa2167bb238f6f4b20218eb4e88dd3ef21bb8f97439fa6b5313e30d"}, + {file = "yarl-1.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b234a4a9248a9f000b7a5dfe84b8cb6210ee5120ae70eb72a4dcbdb4c528f72f"}, + {file = "yarl-1.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe94d1de77c4cd8caff1bd5480e22342dbd54c93929f5943495d9c1e8abe9f42"}, + {file = "yarl-1.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b4c90c5363c6b0a54188122b61edb919c2cd1119684999d08cd5e538813a28e"}, + {file = "yarl-1.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a98ecadc5a241c9ba06de08127ee4796e1009555efd791bac514207862b43d"}, + {file = "yarl-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9106025c7f261f9f5144f9aa7681d43867eed06349a7cfb297a1bc804de2f0d1"}, + {file = "yarl-1.18.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f275ede6199d0f1ed4ea5d55a7b7573ccd40d97aee7808559e1298fe6efc8dbd"}, + {file = "yarl-1.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f7edeb1dcc7f50a2c8e08b9dc13a413903b7817e72273f00878cb70e766bdb3b"}, + {file = "yarl-1.18.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c083f6dd6951b86e484ebfc9c3524b49bcaa9c420cb4b2a78ef9f7a512bfcc85"}, + {file = "yarl-1.18.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:80741ec5b471fbdfb997821b2842c59660a1c930ceb42f8a84ba8ca0f25a66aa"}, + {file = "yarl-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1a3297b9cad594e1ff0c040d2881d7d3a74124a3c73e00c3c71526a1234a9f7"}, + {file = "yarl-1.18.0-cp312-cp312-win32.whl", hash = "sha256:cd6ab7d6776c186f544f893b45ee0c883542b35e8a493db74665d2e594d3ca75"}, + {file = "yarl-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:039c299a0864d1f43c3e31570045635034ea7021db41bf4842693a72aca8df3a"}, + {file = "yarl-1.18.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6fb64dd45453225f57d82c4764818d7a205ee31ce193e9f0086e493916bd4f72"}, + {file = "yarl-1.18.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3adaaf9c6b1b4fc258584f4443f24d775a2086aee82d1387e48a8b4f3d6aecf6"}, + {file = "yarl-1.18.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:da206d1ec78438a563c5429ab808a2b23ad7bc025c8adbf08540dde202be37d5"}, + {file = "yarl-1.18.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:576d258b21c1db4c6449b1c572c75d03f16a482eb380be8003682bdbe7db2f28"}, + {file = "yarl-1.18.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e547c0a375c4bfcdd60eef82e7e0e8698bf84c239d715f5c1278a73050393"}, + {file = "yarl-1.18.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3818eabaefb90adeb5e0f62f047310079d426387991106d4fbf3519eec7d90a"}, + {file = "yarl-1.18.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f72421246c21af6a92fbc8c13b6d4c5427dfd949049b937c3b731f2f9076bd"}, + {file = "yarl-1.18.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fa7d37f2ada0f42e0723632993ed422f2a679af0e200874d9d861720a54f53e"}, + {file = "yarl-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:42ba84e2ac26a3f252715f8ec17e6fdc0cbf95b9617c5367579fafcd7fba50eb"}, + {file = "yarl-1.18.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6a49ad0102c0f0ba839628d0bf45973c86ce7b590cdedf7540d5b1833ddc6f00"}, + {file = "yarl-1.18.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:96404e8d5e1bbe36bdaa84ef89dc36f0e75939e060ca5cd45451aba01db02902"}, + {file = "yarl-1.18.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a0509475d714df8f6d498935b3f307cd122c4ca76f7d426c7e1bb791bcd87eda"}, + {file = "yarl-1.18.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1ff116f0285b5c8b3b9a2680aeca29a858b3b9e0402fc79fd850b32c2bcb9f8b"}, + {file = "yarl-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2580c1d7e66e6d29d6e11855e3b1c6381971e0edd9a5066e6c14d79bc8967af"}, + {file = "yarl-1.18.0-cp313-cp313-win32.whl", hash = "sha256:14408cc4d34e202caba7b5ac9cc84700e3421a9e2d1b157d744d101b061a4a88"}, + {file = "yarl-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1db1537e9cb846eb0ff206eac667f627794be8b71368c1ab3207ec7b6f8c5afc"}, + {file = "yarl-1.18.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fa2c9cb607e0f660d48c54a63de7a9b36fef62f6b8bd50ff592ce1137e73ac7d"}, + {file = "yarl-1.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c0f4808644baf0a434a3442df5e0bedf8d05208f0719cedcd499e168b23bfdc4"}, + {file = "yarl-1.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7db9584235895a1dffca17e1c634b13870852094f6389b68dcc6338086aa7b08"}, + {file = "yarl-1.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:309f8d27d6f93ceeeb80aa6980e883aa57895270f7f41842b92247e65d7aeddf"}, + {file = "yarl-1.18.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:609ffd44fed2ed88d9b4ef62ee860cf86446cf066333ad4ce4123505b819e581"}, + {file = "yarl-1.18.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f172b8b2c72a13a06ea49225a9c47079549036ad1b34afa12d5491b881f5b993"}, + {file = "yarl-1.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d89ae7de94631b60d468412c18290d358a9d805182373d804ec839978b120422"}, + {file = "yarl-1.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:466d31fd043ef9af822ee3f1df8fdff4e8c199a7f4012c2642006af240eade17"}, + {file = "yarl-1.18.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7609b8462351c4836b3edce4201acb6dd46187b207c589b30a87ffd1813b48dc"}, + {file = "yarl-1.18.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:d9d4f5e471e8dc49b593a80766c2328257e405f943c56a3dc985c125732bc4cf"}, + {file = "yarl-1.18.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:67b336c15e564d76869c9a21316f90edf546809a5796a083b8f57c845056bc01"}, + {file = "yarl-1.18.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b212452b80cae26cb767aa045b051740e464c5129b7bd739c58fbb7deb339e7b"}, + {file = "yarl-1.18.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:38b39b7b3e692b6c92b986b00137a3891eddb66311b229d1940dcbd4f025083c"}, + {file = "yarl-1.18.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a7ee6884a8848792d58b854946b685521f41d8871afa65e0d4a774954e9c9e89"}, + {file = "yarl-1.18.0-cp39-cp39-win32.whl", hash = "sha256:b4095c5019bb889aa866bf12ed4c85c0daea5aafcb7c20d1519f02a1e738f07f"}, + {file = "yarl-1.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:2d90f2e4d16a5b0915ee065218b435d2ef619dd228973b1b47d262a6f7cd8fa5"}, + {file = "yarl-1.18.0-py3-none-any.whl", hash = "sha256:dbf53db46f7cf176ee01d8d98c39381440776fcda13779d269a8ba664f69bec0"}, + {file = "yarl-1.18.0.tar.gz", hash = "sha256:20d95535e7d833889982bfe7cc321b7f63bf8879788fee982c76ae2b24cfb715"}, ] [package.dependencies] From bc3db2201fafae6de0ac4b00b1e9956fbb5d3040 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Nov 2024 07:08:16 +1100 Subject: [PATCH 34/78] Add send_text for WebSocketClient --- nautilus_core/network/src/websocket.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/nautilus_core/network/src/websocket.rs b/nautilus_core/network/src/websocket.rs index fe3fa1ce2549..59b11d89da1f 100644 --- a/nautilus_core/network/src/websocket.rs +++ b/nautilus_core/network/src/websocket.rs @@ -445,6 +445,12 @@ impl WebSocketClient { } } + pub async fn send_text(&self, data: String) -> Result<(), Error> { + tracing::trace!("Sending text: {data:?}"); + let mut guard = self.writer.lock().await; + guard.send(Message::Text(data)).await + } + pub async fn send_bytes(&self, data: Vec) -> Result<(), Error> { tracing::trace!("Sending bytes: {data:?}"); let mut guard = self.writer.lock().await; From a243de6928b145f5090642f820dc75b1663b061b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Nov 2024 18:22:11 +1100 Subject: [PATCH 35/78] Improve time range logging --- nautilus_trader/common/functions.py | 39 ++++++++++++++++++----------- nautilus_trader/core/datetime.pyx | 4 +-- nautilus_trader/live/data_client.py | 21 ++++++++-------- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/nautilus_trader/common/functions.py b/nautilus_trader/common/functions.py index 96a883cde4b4..6cdf1fb9e8aa 100644 --- a/nautilus_trader/common/functions.py +++ b/nautilus_trader/common/functions.py @@ -13,21 +13,32 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +import pandas as pd -def one(iterable): - it = iter(iterable) +from nautilus_trader.core.datetime import format_iso8601 - try: - first_value = next(it) - except StopIteration as e: - raise (ValueError("too few items in iterable (expected 1)")) from e - try: - second_value = next(it) - except StopIteration: - pass - else: - msg = f"Expected exactly one item in iterable, but got {first_value}, {second_value}, and perhaps more." - raise ValueError(msg) +def format_utc_timerange(start: pd.Timestamp | None, end: pd.Timestamp | None) -> str: + """ + Return a formatted time range string based on start and end timestamps (UTC). + + Parameters + ---------- + start : pd.Timestamp | None + The start timestamp (UTC). + end : pd.Timestamp | None + The end timestamp (UTC). - return first_value + Returns + ------- + str + + """ + if start and end: + return f" from {format_iso8601(start)} to {format_iso8601(end)}" + elif start: + return f" from {format_iso8601(start)}" + elif end: + return f" to {format_iso8601(end)}" + else: + return "" diff --git a/nautilus_trader/core/datetime.pyx b/nautilus_trader/core/datetime.pyx index 87aa19b80766..2ab19956e3df 100644 --- a/nautilus_trader/core/datetime.pyx +++ b/nautilus_trader/core/datetime.pyx @@ -316,7 +316,7 @@ def max_date_str(date1: str | int | None = None, date2: str | int | None = None) Returns ------- - str | None + str or ``None`` The maximum date as an ISO 8601 formatted string, or None if both input dates are None. """ @@ -345,7 +345,7 @@ def min_date_str(date1: str | int | None = None, date2: str | int | None = None) Returns ------- - str | None + str or ``None`` The minimum date as an ISO 8601 formatted string, or None if both input dates are None. """ diff --git a/nautilus_trader/live/data_client.py b/nautilus_trader/live/data_client.py index c9618f9d0c59..cc908acbdb53 100644 --- a/nautilus_trader/live/data_client.py +++ b/nautilus_trader/live/data_client.py @@ -35,6 +35,7 @@ from nautilus_trader.common.component import MessageBus from nautilus_trader.common.config import NautilusConfig from nautilus_trader.common.enums import LogColor +from nautilus_trader.common.functions import format_utc_timerange from nautilus_trader.common.providers import InstrumentProvider from nautilus_trader.core.correctness import PyCondition from nautilus_trader.core.uuid import UUID4 @@ -665,8 +666,8 @@ def request_instrument( start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, ) -> None: - time_range = f" {start} to {end}" if (start or end) else "" - self._log.info(f"Request {instrument_id} instrument{time_range}", LogColor.BLUE) + time_range_str = format_utc_timerange(start, end) + self._log.info(f"Request {instrument_id} instrument{time_range_str}", LogColor.BLUE) self.create_task( self._request_instrument( instrument_id=instrument_id, @@ -684,9 +685,9 @@ def request_instruments( start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, ) -> None: - time_range = f" {start} to {end}" if (start or end) else "" + time_range_str = format_utc_timerange(start, end) self._log.info( - f"Request {venue} instruments for{time_range}", + f"Request {venue} instruments for{time_range_str}", LogColor.BLUE, ) self.create_task( @@ -707,9 +708,9 @@ def request_quote_ticks( start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, ) -> None: - time_range = f" {start} to {end}" if (start or end) else "" + time_range_str = format_utc_timerange(start, end) limit_str = f" limit={limit}" if limit else "" - self._log.info(f"Request {instrument_id} quotes{time_range}{limit_str}", LogColor.BLUE) + self._log.info(f"Request {instrument_id} quotes{time_range_str}{limit_str}", LogColor.BLUE) self.create_task( self._request_quote_ticks( instrument_id=instrument_id, @@ -729,9 +730,9 @@ def request_trade_ticks( start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, ) -> None: - time_range = f" {start} to {end}" if (start or end) else "" + time_range_str = format_utc_timerange(start, end) limit_str = f" limit={limit}" if limit else "" - self._log.info(f"Request {instrument_id} trades{time_range}{limit_str}", LogColor.BLUE) + self._log.info(f"Request {instrument_id} trades{time_range_str}{limit_str}", LogColor.BLUE) self.create_task( self._request_trade_ticks( instrument_id=instrument_id, @@ -751,9 +752,9 @@ def request_bars( start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, ) -> None: - time_range = f" {start} to {end}" if (start or end) else "" + time_range_str = format_utc_timerange(start, end) limit_str = f" limit={limit}" if limit else "" - self._log.info(f"Request {bar_type} bars{time_range}{limit_str}", LogColor.BLUE) + self._log.info(f"Request {bar_type} bars{time_range_str}{limit_str}", LogColor.BLUE) self.create_task( self._request_bars( bar_type=bar_type, From 891b6eaa3df68679784780a2ac3c166e7226a892 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Nov 2024 18:23:26 +1100 Subject: [PATCH 36/78] Improve Tardis bar requests --- nautilus_trader/adapters/tardis/data.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/nautilus_trader/adapters/tardis/data.py b/nautilus_trader/adapters/tardis/data.py index 9240461c61d6..051d4f64467d 100644 --- a/nautilus_trader/adapters/tardis/data.py +++ b/nautilus_trader/adapters/tardis/data.py @@ -445,6 +445,17 @@ async def _request_bars( LogColor.MAGENTA, ) + if start and start.date() == self._clock.utc_now().date(): + self._log.error( + f"Cannot request bars: `start` cannot fall on the current UTC date, was {start.date()} (try an earlier `start`)", + ) + return + if start and end and start.date() == end.date(): + self._log.error( + f"Cannot request bars: `start` and `end` cannot fall on the same date, was {start.date()} (try an earlier `start`)", + ) + return + date_now_utc = self._clock.utc_now().date() replay_request = create_replay_normalized_request_options( @@ -478,8 +489,8 @@ async def _request_bars( bar for pycapsule in bar_capsules if (bar := capsule_to_data(pycapsule)) - and (start is None or bar.ts_event >= start) - and (end is None or bar.ts_event <= end) + and (start is None or bar.ts_event >= start.value) + and (end is None or bar.ts_event <= end.value) ] self._log.debug( From 94134e92ad2811540e416a6394aedc297e93c912 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 22 Nov 2024 19:34:47 +1100 Subject: [PATCH 37/78] Improve Tardis bar request performance --- RELEASES.md | 1 + .../adapters/src/tardis/python/machine.rs | 66 +++++++++++++++++++ nautilus_trader/adapters/tardis/data.py | 28 ++++---- nautilus_trader/core/nautilus_pyo3.pyi | 3 +- 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index a95f720cbd83..d23c7be37d51 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -10,6 +10,7 @@ Released on TBD (UTC). ### Internal Improvements - Improved live engines error logging (will now log all exceptions rather than just `RuntimeError`) - Improved symbol normalization for Tardis +- Improved historical bar request performance for Tardis - Refined `HttpClient` for use directly from Rust - Upgraded `datafusion` crate to v43.0.0 (#2056), thanks @twitu - Efficiently clean up expired timers in clocks (#2064), thanks @twitu diff --git a/nautilus_core/adapters/src/tardis/python/machine.rs b/nautilus_core/adapters/src/tardis/python/machine.rs index 3fd115890707..0f9410471fcc 100644 --- a/nautilus_core/adapters/src/tardis/python/machine.rs +++ b/nautilus_core/adapters/src/tardis/python/machine.rs @@ -17,8 +17,10 @@ use std::{collections::HashMap, path::Path, sync::Arc}; use futures_util::{pin_mut, Stream, StreamExt}; use nautilus_core::python::to_pyruntime_err; +use nautilus_model::data::{bar::Bar, Data}; use nautilus_model::python::data::data_to_pycapsule; use pyo3::prelude::*; +use pyo3::types::PyList; use crate::tardis::{ machine::{ @@ -119,6 +121,70 @@ impl TardisMachineClient { }) } + #[pyo3(name = "replay_bars")] + fn py_replay_bars<'py>( + &self, + instruments: Vec, + options: Vec, + py: Python<'py>, + ) -> PyResult> { + let map = if !instruments.is_empty() { + let mut instrument_map: HashMap> = + HashMap::new(); + for inst in instruments { + let key = inst.as_tardis_instrument_key(); + instrument_map.insert(key, Arc::new(inst.clone())); + } + instrument_map + } else { + self.instruments.clone() + }; + + let base_url = self.base_url.clone(); + let replay_signal = self.replay_signal.clone(); + + pyo3_async_runtimes::tokio::future_into_py(py, async move { + let stream = replay_normalized(&base_url, options, replay_signal) + .await + .map_err(to_pyruntime_err)?; + + // We use Box::pin to heap-allocate the stream and ensure it implements + // Unpin for safe async handling across lifetimes. + pin_mut!(stream); + + let mut bars: Vec = Vec::new(); + + while let Some(result) = stream.next().await { + match result { + Ok(msg) => { + let info = determine_instrument_info(&msg, &map); + + if let Some(info) = info { + if let Some(data) = parse_tardis_ws_message(msg, info) { + if let Data::Bar(bar) = data { + bars.push(bar) + } + } else { + continue; // Non-data message + } + } else { + continue; // No instrument info + } + } + Err(e) => { + tracing::error!("Error in WebSocket stream: {e:?}"); + break; + } + } + } + + Python::with_gil(|py| { + let pylist = PyList::new_bound(py, bars.into_iter().map(|bar| bar.into_py(py))); + Ok(pylist.into_py(py)) + }) + }) + } + #[pyo3(name = "stream")] fn py_stream<'py>( &self, diff --git a/nautilus_trader/adapters/tardis/data.py b/nautilus_trader/adapters/tardis/data.py index 051d4f64467d..285b1b4e0a39 100644 --- a/nautilus_trader/adapters/tardis/data.py +++ b/nautilus_trader/adapters/tardis/data.py @@ -466,33 +466,31 @@ async def _request_bars( data_types=[tardis_data_type], ) - bar_capsules: list[object] = [] - - await asyncio.ensure_future( - self._ws_client.replay( + pyo3_bars = await asyncio.ensure_future( + self._ws_client.replay_bars( instruments=[instrument_info], options=[replay_request], - callback=bar_capsules.append, ), ) self._log.debug( - f"Streamed {len(bar_capsules):,} {bar_type} bars from replay", + f"Streamed {len(pyo3_bars):,} {bar_type} bars from replay", LogColor.MAGENTA, ) if limit: - bar_capsules = bar_capsules[-limit:] - - # Convert capsules to bars and apply time filters in one pass - bars: list[Bar] = [ - bar - for pycapsule in bar_capsules - if (bar := capsule_to_data(pycapsule)) - and (start is None or bar.ts_event >= start.value) - and (end is None or bar.ts_event <= end.value) + pyo3_bars = pyo3_bars[-limit:] + + # Apply time filter + pyo3_bars = [ + pyo3_bar + for pyo3_bar in pyo3_bars + if (start is None or pyo3_bar.ts_event >= start.value) + and (end is None or pyo3_bar.ts_event <= end.value) ] + bars = Bar.from_pyo3_list(pyo3_bars) + self._log.debug( f"Sending response with {len(bars):,} bars after filtering", LogColor.MAGENTA, diff --git a/nautilus_trader/core/nautilus_pyo3.pyi b/nautilus_trader/core/nautilus_pyo3.pyi index 473ea6fac5ab..8877a4c5458e 100644 --- a/nautilus_trader/core/nautilus_pyo3.pyi +++ b/nautilus_trader/core/nautilus_pyo3.pyi @@ -4153,8 +4153,9 @@ class TardisMachineClient: def __init__(self, base_url: str | None = None, normalize_symbols: bool = True) -> None: ... def is_closed(self) -> bool: ... def close(self) -> None: ... - def replay(self, instruments: list[InstrumentMiniInfo], options: list[ReplayNormalizedRequestOptions], callback: Callable) -> Awaitable[None]: ... def stream(self, instruments: list[InstrumentMiniInfo], options: list[StreamNormalizedRequestOptions], callback: Callable,) -> Awaitable[None]: ... # noqa + def replay(self, instruments: list[InstrumentMiniInfo], options: list[ReplayNormalizedRequestOptions], callback: Callable) -> Awaitable[None]: ... + def replay_bars(self, instruments: list[InstrumentMiniInfo], options: list[ReplayNormalizedRequestOptions]) -> Awaitable[list[Bar]]: ... async def run_tardis_machine_replay(config_filepath: str, output_path: str | None = None) -> None: ... From 57cc3b756ed5a6195ebd4667ef052a80c2116c1f Mon Sep 17 00:00:00 2001 From: David Blom Date: Fri, 22 Nov 2024 20:21:39 +0100 Subject: [PATCH 38/78] Fix parsing best effort opened order status for dYdX (#2068) - Fix parsing best effort opened order status for dYdX - Only update the sequence if the grpc message is success - Format doc strings --- nautilus_trader/adapters/dydx/common/enums.py | 2 +- nautilus_trader/adapters/dydx/grpc/account.py | 94 +++++++++++-------- .../adapters/dydx/test_parsing.py | 2 +- .../adapters/dydx/test_websocket_schema.py | 2 +- 4 files changed, 60 insertions(+), 40 deletions(-) diff --git a/nautilus_trader/adapters/dydx/common/enums.py b/nautilus_trader/adapters/dydx/common/enums.py index 19303887951c..f7cda6cf60bc 100644 --- a/nautilus_trader/adapters/dydx/common/enums.py +++ b/nautilus_trader/adapters/dydx/common/enums.py @@ -245,7 +245,7 @@ def __init__(self) -> None: DYDXOrderStatus.FILLED: OrderStatus.FILLED, DYDXOrderStatus.CANCELED: OrderStatus.CANCELED, DYDXOrderStatus.BEST_EFFORT_CANCELED: OrderStatus.CANCELED, - DYDXOrderStatus.BEST_EFFORT_OPENED: OrderStatus.SUBMITTED, + DYDXOrderStatus.BEST_EFFORT_OPENED: OrderStatus.ACCEPTED, } self.dydx_to_nautilus_time_in_force = { diff --git a/nautilus_trader/adapters/dydx/grpc/account.py b/nautilus_trader/adapters/dydx/grpc/account.py index d61966e63c81..7cce3cd1a322 100644 --- a/nautilus_trader/adapters/dydx/grpc/account.py +++ b/nautilus_trader/adapters/dydx/grpc/account.py @@ -260,13 +260,15 @@ async def get_account(self, address: str) -> BaseAccount: """ Retrieve the account information for a given address. - Args: - ---- - address (str): The account address. + Parameters + ---------- + address : str + The account address. - Returns: + Returns ------- - BaseAccount: The base account information. + BaseAccount + The base account information. """ account = BaseAccount() @@ -282,13 +284,15 @@ async def get_account_balances(self, address: str) -> bank_query.QueryAllBalance """ Retrieve all account balances for a given address. - Args: - ---- - address (str): The account address. + Parameters + ---------- + address : str + The account address. - Returns: + Returns ------- - bank_query.QueryAllBalancesResponse: The response containing all account balances. + bank_query.QueryAllBalancesResponse + The response containing all account balances. """ stub = bank_query_grpc.QueryStub(self._channel) @@ -300,7 +304,8 @@ async def latest_block(self) -> tendermint_query.GetLatestBlockResponse: Returns ------- - tendermint_query.GetLatestBlockResponse: The response containing the latest block information. + tendermint_query.GetLatestBlockResponse + The response containing the latest block information. """ return await tendermint_query_grpc.ServiceStub(self._channel).GetLatestBlock( @@ -313,7 +318,8 @@ async def latest_block_height(self) -> int: Returns ------- - int: The height of the latest block. + int + The height of the latest block. """ block = await self.latest_block() @@ -325,7 +331,8 @@ async def get_fee_tiers(self) -> fee_tier_query.QueryPerpetualFeeParamsResponse: Returns ------- - fee_tier_query.QueryPerpetualFeeParamsResponse: The response containing the perpetual fee parameters. + fee_tier_query.QueryPerpetualFeeParamsResponse + The response containing the perpetual fee parameters. """ stub = fee_tier_query_grpc.QueryStub(self._channel) @@ -335,13 +342,15 @@ async def get_user_fee_tier(self, address: str) -> fee_tier_query.QueryUserFeeTi """ Retrieve the user fee tier for a given address. - Args: - ---- - address (str): The user address. + Parameters + ---------- + address : str + The user address. - Returns: + Returns ------- - fee_tier_query.QueryUserFeeTierResponse: The response containing the user fee tier. + fee_tier_query.QueryUserFeeTierResponse + The response containing the user fee tier. """ stub = fee_tier_query_grpc.QueryStub(self._channel) @@ -351,13 +360,16 @@ async def place_order(self, wallet: Wallet, order: Order) -> BroadcastTxResponse """ Places an order. - Args: - ---- - wallet (Wallet): The wallet to use for signing the transaction. - order (Order): The order to place. + Parameters + ---------- + wallet : Wallet + The wallet to use for signing the transaction. + order : Order + The order to place. - Returns: + Returns ------- + BroadcastTxResponse The response from the transaction broadcast. """ @@ -468,24 +480,29 @@ async def broadcast_message( """ Broadcast a message. - Args: - ---- - wallet (Wallet): The wallet to use for signing the transaction. - message (Message): The message to broadcast. - mode (BroadcastMode, optional): The broadcast mode. Defaults to BroadcastMode.BROADCAST_MODE_SYNC. + Parameters + ---------- + wallet : Wallet + The wallet to use for signing the transaction. + message : Message + The message to broadcast. + mode : BroadcastMode, optional + The broadcast mode. Defaults to BroadcastMode.BROADCAST_MODE_SYNC. - Returns: + Returns ------- The response from the broadcast. """ async with self._lock: response = await self.broadcast(self._transaction_builder.build(wallet, message), mode) - wallet.sequence += 1 + + if response.tx_response.code == 0: + wallet.sequence += 1 # The sequence number is not correct. Retrieve it from the gRPC channel. # The retry manager can retry the transaction. - if response.tx_response.code == ACCOUNT_SEQUENCE_MISMATCH_ERROR_CODE: + elif response.tx_response.code == ACCOUNT_SEQUENCE_MISMATCH_ERROR_CODE: account = await self.get_account(wallet.address) wallet.sequence = account.sequence @@ -499,14 +516,17 @@ async def broadcast( """ Broadcast a transaction. - Args: - ---- - transaction (Tx): The transaction to broadcast. - mode (BroadcastMode, optional): The broadcast mode. Defaults to BroadcastMode.BROADCAST_MODE_SYNC. + Parameters + ---------- + transaction : Tx + The transaction to broadcast. + mode : BroadcastMode, optional + The broadcast mode. Defaults to BroadcastMode.BROADCAST_MODE_SYNC. - Returns: + Returns ------- - BroadcastTxResponse: The response from the broadcast. + BroadcastTxResponse + The response from the broadcast. """ request = BroadcastTxRequest(tx_bytes=transaction.SerializeToString(), mode=mode) diff --git a/tests/integration_tests/adapters/dydx/test_parsing.py b/tests/integration_tests/adapters/dydx/test_parsing.py index f4f48d9098bc..f03b76fd2675 100644 --- a/tests/integration_tests/adapters/dydx/test_parsing.py +++ b/tests/integration_tests/adapters/dydx/test_parsing.py @@ -145,7 +145,7 @@ def test_parse_nautilus_order_side( (DYDXOrderStatus.FILLED, OrderStatus.FILLED), (DYDXOrderStatus.CANCELED, OrderStatus.CANCELED), (DYDXOrderStatus.BEST_EFFORT_CANCELED, OrderStatus.CANCELED), - (DYDXOrderStatus.BEST_EFFORT_OPENED, OrderStatus.SUBMITTED), + (DYDXOrderStatus.BEST_EFFORT_OPENED, OrderStatus.ACCEPTED), ], ) def test_parse_order_status( diff --git a/tests/integration_tests/adapters/dydx/test_websocket_schema.py b/tests/integration_tests/adapters/dydx/test_websocket_schema.py index 4515ffbe8a65..b2f8cbf1b3e1 100644 --- a/tests/integration_tests/adapters/dydx/test_websocket_schema.py +++ b/tests/integration_tests/adapters/dydx/test_websocket_schema.py @@ -511,7 +511,7 @@ def test_account_channel_data_new_order_opened() -> None: order_side=OrderSide.BUY, order_type=OrderType.LIMIT, time_in_force=TimeInForce.IOC, - order_status=OrderStatus.SUBMITTED, + order_status=OrderStatus.ACCEPTED, price=Price(2791.6, 4), quantity=Quantity(0.002, 5), filled_qty=Quantity(0, 5), From ec64ed272a8e4dc28fc041db3bb547f3a692c7e9 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 23 Nov 2024 06:25:12 +1100 Subject: [PATCH 39/78] Update dependencies and release notes --- RELEASES.md | 1 + nautilus_core/Cargo.lock | 8 +- nautilus_core/cryptography/Cargo.toml | 2 +- nautilus_core/network/Cargo.toml | 2 +- poetry.lock | 210 +++++++++++++------------- 5 files changed, 112 insertions(+), 111 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index d23c7be37d51..0111c1f37466 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -21,6 +21,7 @@ Released on TBD (UTC). ### Fixes - Fixed missing venue -> exchange mappings for Tardis integration - Fixed account balance and order status parsing for dYdX (#2067), thanks @davidsblom +- Fixed parsing best effort opened order status for dYdX (#2068), thanks @davidsblom --- diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 53bf7fce4935..2d0951ada1a8 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -1049,9 +1049,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -4487,9 +4487,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.17" +version = "0.23.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f1a745511c54ba6d4465e8d5dfbd81b45791756de28d4981af70d6dca128f1e" +checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" dependencies = [ "aws-lc-rs", "log", diff --git a/nautilus_core/cryptography/Cargo.toml b/nautilus_core/cryptography/Cargo.toml index 04606a77e201..dafc89f78c55 100644 --- a/nautilus_core/cryptography/Cargo.toml +++ b/nautilus_core/cryptography/Cargo.toml @@ -19,7 +19,7 @@ pyo3 = { workspace = true, optional = true } rand = { workspace = true } ring = { workspace = true } tracing = { workspace = true } -rustls = { version = "0.23.17", features = ["ring"] } +rustls = { version = "0.23.18", features = ["ring"] } rustls-native-certs = "0.8.0" pem = "3.0.4" diff --git a/nautilus_core/network/Cargo.toml b/nautilus_core/network/Cargo.toml index c8f911ea95b6..5d9eab313505 100644 --- a/nautilus_core/network/Cargo.toml +++ b/nautilus_core/network/Cargo.toml @@ -26,7 +26,7 @@ tokio-tungstenite = { workspace = true } dashmap = "6.1.0" http = "1.1.0" nonzero_ext = "0.3.0" -rustls = { version = "0.23.17", features = ["ring"] } +rustls = { version = "0.23.18", features = ["ring"] } tokio-rustls = "0.26.0" [dev-dependencies] diff --git a/poetry.lock b/poetry.lock index 463df2cd490f..601d9090aaa9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2933,18 +2933,18 @@ files = [ [[package]] name = "pydantic" -version = "2.10.0" +version = "2.10.1" description = "Data validation using Python type hints" optional = true python-versions = ">=3.8" files = [ - {file = "pydantic-2.10.0-py3-none-any.whl", hash = "sha256:5e7807ba9201bdf61b1b58aa6eb690916c40a47acfb114b1b4fef3e7fd5b30fc"}, - {file = "pydantic-2.10.0.tar.gz", hash = "sha256:0aca0f045ff6e2f097f1fe89521115335f15049eeb8a7bef3dafe4b19a74e289"}, + {file = "pydantic-2.10.1-py3-none-any.whl", hash = "sha256:a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e"}, + {file = "pydantic-2.10.1.tar.gz", hash = "sha256:a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.27.0" +pydantic-core = "2.27.1" typing-extensions = ">=4.12.2" [package.extras] @@ -2953,111 +2953,111 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.27.0" +version = "2.27.1" description = "Core functionality for Pydantic validation and serialization" optional = true python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2ac6b919f7fed71b17fe0b4603c092a4c9b5bae414817c9c81d3c22d1e1bcc"}, - {file = "pydantic_core-2.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e015833384ca3e1a0565a79f5d953b0629d9138021c27ad37c92a9fa1af7623c"}, - {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db72e40628967f6dc572020d04b5f800d71264e0531c6da35097e73bdf38b003"}, - {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df45c4073bed486ea2f18757057953afed8dd77add7276ff01bccb79982cf46c"}, - {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:836a4bfe0cc6d36dc9a9cc1a7b391265bf6ce9d1eb1eac62ac5139f5d8d9a6fa"}, - {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bf1340ae507f6da6360b24179c2083857c8ca7644aab65807023cf35404ea8d"}, - {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ab325fc86fbc077284c8d7f996d904d30e97904a87d6fb303dce6b3de7ebba9"}, - {file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1da0c98a85a6c6ed702d5556db3b09c91f9b0b78de37b7593e2de8d03238807a"}, - {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b0202ebf2268954090209a84f9897345719e46a57c5f2c9b7b250ca0a9d3e63"}, - {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:35380671c3c921fe8adf31ad349dc6f7588b7e928dbe44e1093789734f607399"}, - {file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b4c19525c3538fbc0bbda6229f9682fb8199ce9ac37395880e6952798e00373"}, - {file = "pydantic_core-2.27.0-cp310-none-win32.whl", hash = "sha256:333c840a1303d1474f491e7be0b718226c730a39ead0f7dab2c7e6a2f3855555"}, - {file = "pydantic_core-2.27.0-cp310-none-win_amd64.whl", hash = "sha256:99b2863c1365f43f74199c980a3d40f18a218fbe683dd64e470199db426c4d6a"}, - {file = "pydantic_core-2.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4523c4009c3f39d948e01962223c9f5538602e7087a628479b723c939fab262d"}, - {file = "pydantic_core-2.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84af1cf7bfdcbc6fcf5a5f70cc9896205e0350306e4dd73d54b6a18894f79386"}, - {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e65466b31be1070b4a5b7dbfbd14b247884cb8e8b79c64fb0f36b472912dbaea"}, - {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5c022bb0d453192426221605efc865373dde43b17822a264671c53b068ac20c"}, - {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bb69bf3b6500f195c3deb69c1205ba8fc3cb21d1915f1f158a10d6b1ef29b6a"}, - {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aa4d1b2eba9a325897308b3124014a142cdccb9f3e016f31d3ebee6b5ea5e75"}, - {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e96ca781e0c01e32115912ebdf7b3fb0780ce748b80d7d28a0802fa9fbaf44e"}, - {file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b872c86d8d71827235c7077461c502feb2db3f87d9d6d5a9daa64287d75e4fa0"}, - {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:82e1ad4ca170e8af4c928b67cff731b6296e6a0a0981b97b2eb7c275cc4e15bd"}, - {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:eb40f828bc2f73f777d1eb8fee2e86cd9692a4518b63b6b5aa8af915dfd3207b"}, - {file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9a8fbf506fde1529a1e3698198fe64bfbe2e0c09557bc6a7dcf872e7c01fec40"}, - {file = "pydantic_core-2.27.0-cp311-none-win32.whl", hash = "sha256:24f984fc7762ed5f806d9e8c4c77ea69fdb2afd987b4fd319ef06c87595a8c55"}, - {file = "pydantic_core-2.27.0-cp311-none-win_amd64.whl", hash = "sha256:68950bc08f9735306322bfc16a18391fcaac99ded2509e1cc41d03ccb6013cfe"}, - {file = "pydantic_core-2.27.0-cp311-none-win_arm64.whl", hash = "sha256:3eb8849445c26b41c5a474061032c53e14fe92a11a5db969f722a2716cd12206"}, - {file = "pydantic_core-2.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8117839a9bdbba86e7f9df57018fe3b96cec934c3940b591b0fd3fbfb485864a"}, - {file = "pydantic_core-2.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a291d0b4243a259c8ea7e2b84eb9ccb76370e569298875a7c5e3e71baf49057a"}, - {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e35afd9e10b2698e6f2f32256678cb23ca6c1568d02628033a837638b3ed12"}, - {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58ab0d979c969983cdb97374698d847a4acffb217d543e172838864636ef10d9"}, - {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d06b667e53320332be2bf6f9461f4a9b78092a079b8ce8634c9afaa7e10cd9f"}, - {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78f841523729e43e3928a364ec46e2e3f80e6625a4f62aca5c345f3f626c6e8a"}, - {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:400bf470e4327e920883b51e255617dfe4496d4e80c3fea0b5a5d0bf2c404dd4"}, - {file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:951e71da6c89d354572098bada5ba5b5dc3a9390c933af8a614e37755d3d1840"}, - {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a51ce96224eadd1845150b204389623c8e129fde5a67a84b972bd83a85c6c40"}, - {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:483c2213a609e7db2c592bbc015da58b6c75af7360ca3c981f178110d9787bcf"}, - {file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:359e7951f04ad35111b5ddce184db3391442345d0ab073aa63a95eb8af25a5ef"}, - {file = "pydantic_core-2.27.0-cp312-none-win32.whl", hash = "sha256:ee7d9d5537daf6d5c74a83b38a638cc001b648096c1cae8ef695b0c919d9d379"}, - {file = "pydantic_core-2.27.0-cp312-none-win_amd64.whl", hash = "sha256:2be0ad541bb9f059954ccf8877a49ed73877f862529575ff3d54bf4223e4dd61"}, - {file = "pydantic_core-2.27.0-cp312-none-win_arm64.whl", hash = "sha256:6e19401742ed7b69e51d8e4df3c03ad5ec65a83b36244479fd70edde2828a5d9"}, - {file = "pydantic_core-2.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5f2b19b8d6fca432cb3acf48cf5243a7bf512988029b6e6fd27e9e8c0a204d85"}, - {file = "pydantic_core-2.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c86679f443e7085ea55a7376462553996c688395d18ef3f0d3dbad7838f857a2"}, - {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510b11e9c3b1a852876d1ccd8d5903684336d635214148637ceb27366c75a467"}, - {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb704155e73b833801c247f39d562229c0303f54770ca14fb1c053acb376cf10"}, - {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ce048deb1e033e7a865ca384770bccc11d44179cf09e5193a535c4c2f497bdc"}, - {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58560828ee0951bb125c6f2862fbc37f039996d19ceb6d8ff1905abf7da0bf3d"}, - {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb4785894936d7682635726613c44578c420a096729f1978cd061a7e72d5275"}, - {file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2883b260f7a93235488699d39cbbd94fa7b175d3a8063fbfddd3e81ad9988cb2"}, - {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c6fcb3fa3855d583aa57b94cf146f7781d5d5bc06cb95cb3afece33d31aac39b"}, - {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e851a051f7260e6d688267eb039c81f05f23a19431bd7dfa4bf5e3cb34c108cd"}, - {file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edb1bfd45227dec8d50bc7c7d86463cd8728bcc574f9b07de7369880de4626a3"}, - {file = "pydantic_core-2.27.0-cp313-none-win32.whl", hash = "sha256:678f66462058dd978702db17eb6a3633d634f7aa0deaea61e0a674152766d3fc"}, - {file = "pydantic_core-2.27.0-cp313-none-win_amd64.whl", hash = "sha256:d28ca7066d6cdd347a50d8b725dc10d9a1d6a1cce09836cf071ea6a2d4908be0"}, - {file = "pydantic_core-2.27.0-cp313-none-win_arm64.whl", hash = "sha256:6f4a53af9e81d757756508b57cae1cf28293f0f31b9fa2bfcb416cc7fb230f9d"}, - {file = "pydantic_core-2.27.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:e9f9feee7f334b72ceae46313333d002b56f325b5f04271b4ae2aadd9e993ae4"}, - {file = "pydantic_core-2.27.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:225bfff5d425c34e1fd562cef52d673579d59b967d9de06178850c4802af9039"}, - {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921ad596ff1a82f9c692b0758c944355abc9f0de97a4c13ca60ffc6d8dc15d4"}, - {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6354e18a9be37bfa124d6b288a87fb30c673745806c92956f1a25e3ae6e76b96"}, - {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ee4c2a75af9fe21269a4a0898c5425afb01af1f5d276063f57e2ae1bc64e191"}, - {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c91e3c04f5191fd3fb68764bddeaf02025492d5d9f23343b283870f6ace69708"}, - {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6ebfac28fd51890a61df36ef202adbd77d00ee5aca4a3dadb3d9ed49cfb929"}, - {file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36aa167f69d8807ba7e341d67ea93e50fcaaf6bc433bb04939430fa3dab06f31"}, - {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e8d89c276234579cd3d095d5fa2a44eb10db9a218664a17b56363cddf226ff3"}, - {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:5cc822ab90a70ea3a91e6aed3afac570b276b1278c6909b1d384f745bd09c714"}, - {file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e15315691fe2253eb447503153acef4d7223dfe7e7702f9ed66539fcd0c43801"}, - {file = "pydantic_core-2.27.0-cp38-none-win32.whl", hash = "sha256:dfa5f5c0a4c8fced1422dc2ca7eefd872d5d13eb33cf324361dbf1dbfba0a9fe"}, - {file = "pydantic_core-2.27.0-cp38-none-win_amd64.whl", hash = "sha256:513cb14c0cc31a4dfd849a4674b20c46d87b364f997bbcb02282306f5e187abf"}, - {file = "pydantic_core-2.27.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4148dc9184ab79e356dc00a4199dc0ee8647973332cb385fc29a7cced49b9f9c"}, - {file = "pydantic_core-2.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5fc72fbfebbf42c0856a824b8b0dc2b5cd2e4a896050281a21cfa6fed8879cb1"}, - {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:185ef205256cd8b38431205698531026979db89a79587725c1e55c59101d64e9"}, - {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:395e3e1148fa7809016231f8065f30bb0dc285a97b4dc4360cd86e17bab58af7"}, - {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33d14369739c5d07e2e7102cdb0081a1fa46ed03215e07f097b34e020b83b1ae"}, - {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7820bb0d65e3ce1e3e70b6708c2f66143f55912fa02f4b618d0f08b61575f12"}, - {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b61989068de9ce62296cde02beffabcadb65672207fc51e7af76dca75e6636"}, - {file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15e350efb67b855cd014c218716feea4986a149ed1f42a539edd271ee074a196"}, - {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:433689845288f9a1ee5714444e65957be26d30915f7745091ede4a83cfb2d7bb"}, - {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:3fd8bc2690e7c39eecdf9071b6a889ce7b22b72073863940edc2a0a23750ca90"}, - {file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:884f1806609c2c66564082540cffc96868c5571c7c3cf3a783f63f2fb49bd3cd"}, - {file = "pydantic_core-2.27.0-cp39-none-win32.whl", hash = "sha256:bf37b72834e7239cf84d4a0b2c050e7f9e48bced97bad9bdf98d26b8eb72e846"}, - {file = "pydantic_core-2.27.0-cp39-none-win_amd64.whl", hash = "sha256:31a2cae5f059329f9cfe3d8d266d3da1543b60b60130d186d9b6a3c20a346361"}, - {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4fb49cfdb53af5041aba909be00cccfb2c0d0a2e09281bf542371c5fd36ad04c"}, - {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:49633583eb7dc5cba61aaf7cdb2e9e662323ad394e543ee77af265736bcd3eaa"}, - {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153017e3d6cd3ce979de06d84343ca424bb6092727375eba1968c8b4693c6ecb"}, - {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff63a92f6e249514ef35bc795de10745be0226eaea06eb48b4bbeaa0c8850a4a"}, - {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5982048129f40b082c2654de10c0f37c67a14f5ff9d37cf35be028ae982f26df"}, - {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:91bc66f878557313c2a6bcf396e7befcffe5ab4354cfe4427318968af31143c3"}, - {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:68ef5377eb582fa4343c9d0b57a5b094046d447b4c73dd9fbd9ffb216f829e7d"}, - {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c5726eec789ee38f2c53b10b1821457b82274f81f4f746bb1e666d8741fcfadb"}, - {file = "pydantic_core-2.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c0c431e4be5c1a0c6654e0c31c661cd89e0ca956ef65305c3c3fd96f4e72ca39"}, - {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8e21d927469d04b39386255bf00d0feedead16f6253dcc85e9e10ddebc334084"}, - {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b51f964fcbb02949fc546022e56cdb16cda457af485e9a3e8b78ac2ecf5d77e"}, - {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a7fd4de38f7ff99a37e18fa0098c3140286451bc823d1746ba80cec5b433a1"}, - {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fda87808429c520a002a85d6e7cdadbf58231d60e96260976c5b8f9a12a8e13"}, - {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a150392102c402c538190730fda06f3bce654fc498865579a9f2c1d2b425833"}, - {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c9ed88b398ba7e3bad7bd64d66cc01dcde9cfcb7ec629a6fd78a82fa0b559d78"}, - {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:9fe94d9d2a2b4edd7a4b22adcd45814b1b59b03feb00e56deb2e89747aec7bfe"}, - {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d8b5ee4ae9170e2775d495b81f414cc20268041c42571530513496ba61e94ba3"}, - {file = "pydantic_core-2.27.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d29e235ce13c91902ef3efc3d883a677655b3908b1cbc73dee816e5e1f8f7739"}, - {file = "pydantic_core-2.27.0.tar.gz", hash = "sha256:f57783fbaf648205ac50ae7d646f27582fc706be3977e87c3c124e7a92407b10"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, + {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, + {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, + {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, + {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, + {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, + {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, + {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, + {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, + {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, + {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, + {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, + {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, + {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, + {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, + {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, + {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, + {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, + {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, + {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, + {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, + {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, + {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, + {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, + {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, + {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, + {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, + {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, + {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, + {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, + {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, + {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, + {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, + {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, + {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, + {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, + {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, ] [package.dependencies] From fe29a41813e50005a86a72d16bb03e6d75af1b06 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 23 Nov 2024 06:38:32 +1100 Subject: [PATCH 40/78] Apply formatting --- nautilus_core/adapters/src/tardis/python/machine.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nautilus_core/adapters/src/tardis/python/machine.rs b/nautilus_core/adapters/src/tardis/python/machine.rs index 0f9410471fcc..d6ace6979be6 100644 --- a/nautilus_core/adapters/src/tardis/python/machine.rs +++ b/nautilus_core/adapters/src/tardis/python/machine.rs @@ -17,10 +17,11 @@ use std::{collections::HashMap, path::Path, sync::Arc}; use futures_util::{pin_mut, Stream, StreamExt}; use nautilus_core::python::to_pyruntime_err; -use nautilus_model::data::{bar::Bar, Data}; -use nautilus_model::python::data::data_to_pycapsule; -use pyo3::prelude::*; -use pyo3::types::PyList; +use nautilus_model::{ + data::{bar::Bar, Data}, + python::data::data_to_pycapsule, +}; +use pyo3::{prelude::*, types::PyList}; use crate::tardis::{ machine::{ From 616cfa0e5496e1f8f332fd771b3e857342cb9e7a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 23 Nov 2024 06:39:44 +1100 Subject: [PATCH 41/78] Refine WebSocketClient send rate limiting --- nautilus_core/network/src/python/websocket.rs | 26 +++++-------------- nautilus_core/network/src/websocket.rs | 19 ++++++++------ 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/nautilus_core/network/src/python/websocket.rs b/nautilus_core/network/src/python/websocket.rs index 57ad7e0d867f..9f391201aa1c 100644 --- a/nautilus_core/network/src/python/websocket.rs +++ b/nautilus_core/network/src/python/websocket.rs @@ -16,7 +16,6 @@ use std::sync::{atomic::Ordering, Arc}; use futures::SinkExt; -use futures_util::{stream, StreamExt}; use nautilus_core::python::to_pyvalue_err; use pyo3::{create_exception, exceptions::PyException, prelude::*}; use tokio_tungstenite::tungstenite::Message; @@ -136,18 +135,11 @@ impl WebSocketClient { py: Python<'py>, keys: Option>, ) -> PyResult> { - let keys = keys.unwrap_or_default(); let writer = slf.writer.clone(); let rate_limiter = slf.rate_limiter.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { - let tasks = keys.iter().map(|key| rate_limiter.until_key_ready(key)); - stream::iter(tasks) - .for_each(|key| async move { - key.await; - }) - .await; - - // Log after passing rate limit checks + rate_limiter.await_keys_ready(keys).await; tracing::trace!("Sending binary: {data:?}"); let mut guard = writer.lock().await; @@ -180,18 +172,11 @@ impl WebSocketClient { keys: Option>, ) -> PyResult> { let data = String::from_utf8(data).map_err(to_pyvalue_err)?; - let keys = keys.unwrap_or_default(); let writer = slf.writer.clone(); let rate_limiter = slf.rate_limiter.clone(); + pyo3_async_runtimes::tokio::future_into_py(py, async move { - let tasks = keys.iter().map(|key| rate_limiter.until_key_ready(key)); - stream::iter(tasks) - .for_each(|key| async move { - key.await; - }) - .await; - - // Log after passing rate limit checks + rate_limiter.await_keys_ready(keys).await; tracing::trace!("Sending text: {data}"); let mut guard = writer.lock().await; @@ -214,8 +199,9 @@ impl WebSocketClient { py: Python<'py>, ) -> PyResult> { let data_str = String::from_utf8(data.clone()).map_err(to_pyvalue_err)?; - tracing::trace!("Sending pong: {data_str}"); let writer = slf.writer.clone(); + tracing::trace!("Sending pong: {data_str}"); + pyo3_async_runtimes::tokio::future_into_py(py, async move { let mut guard = writer.lock().await; guard diff --git a/nautilus_core/network/src/websocket.rs b/nautilus_core/network/src/websocket.rs index 59b11d89da1f..c1a00bf8a4b8 100644 --- a/nautilus_core/network/src/websocket.rs +++ b/nautilus_core/network/src/websocket.rs @@ -316,9 +316,9 @@ impl Drop for WebSocketClientInner { pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.network") )] pub struct WebSocketClient { - pub(crate) rate_limiter: Arc>, pub(crate) writer: SharedMessageWriter, pub(crate) controller_task: task::JoinHandle<()>, + pub(crate) rate_limiter: Arc>, pub(crate) disconnect_mode: Arc, } @@ -337,9 +337,6 @@ impl WebSocketClient { let (writer, reader) = ws_stream.split(); let writer = Arc::new(Mutex::new(writer)); - let disconnect_mode = Arc::new(AtomicBool::new(false)); - let rate_limiter = Arc::new(RateLimiter::new_with_quota(default_quota, keyed_quotas)); - // Create config with minimal no-op Python handler so we incrementally // move towards a more Rust-native approach. let config = { @@ -355,6 +352,9 @@ impl WebSocketClient { } }; + let disconnect_mode = Arc::new(AtomicBool::new(false)); + let rate_limiter = Arc::new(RateLimiter::new_with_quota(default_quota, keyed_quotas)); + let inner = WebSocketClientInner::connect_url(config).await?; let controller_task = Self::spawn_controller_task( inner, @@ -367,9 +367,9 @@ impl WebSocketClient { Ok(( reader, Self { - rate_limiter, writer: writer.clone(), controller_task, + rate_limiter, disconnect_mode, }, )) @@ -409,9 +409,9 @@ impl WebSocketClient { }; Ok(Self { - rate_limiter, writer, controller_task, + rate_limiter, disconnect_mode, }) } @@ -473,9 +473,12 @@ impl WebSocketClient { max_reconnection_tries: Option, ) -> task::JoinHandle<()> { task::spawn(async move { + let check_interval = Duration::from_millis(100); + let retry_interval = Duration::from_millis(1000); let mut retry_counter: u64 = 0; + loop { - sleep(Duration::from_millis(100)).await; + sleep(check_interval).await; // Check if client needs to disconnect let disconnect = disconnect_mode.load(Ordering::SeqCst); @@ -501,7 +504,7 @@ impl WebSocketClient { if retry_counter < max_reconnection_tries { retry_counter += 1; tracing::warn!("Reconnect failed {e}. Retry {retry_counter}/{max_reconnection_tries}"); - sleep(Duration::from_millis(1000)).await; + sleep(retry_interval).await; } else { tracing::error!("Reconnect failed {e}"); break; From 8fc714fc4a6e21fbe0dc7f0517e2c592affb2eea Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 23 Nov 2024 06:47:52 +1100 Subject: [PATCH 42/78] Fix clippy lints --- nautilus_core/adapters/src/tardis/python/machine.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_core/adapters/src/tardis/python/machine.rs b/nautilus_core/adapters/src/tardis/python/machine.rs index d6ace6979be6..5d43f9232b97 100644 --- a/nautilus_core/adapters/src/tardis/python/machine.rs +++ b/nautilus_core/adapters/src/tardis/python/machine.rs @@ -163,7 +163,7 @@ impl TardisMachineClient { if let Some(info) = info { if let Some(data) = parse_tardis_ws_message(msg, info) { if let Data::Bar(bar) = data { - bars.push(bar) + bars.push(bar); } } else { continue; // Non-data message From b535febf8a2440717beb63c6788cafad585ace4f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 23 Nov 2024 06:57:59 +1100 Subject: [PATCH 43/78] Refine Tardis Machine streaming --- .../adapters/src/tardis/machine/client.rs | 14 +++---- .../adapters/src/tardis/python/machine.rs | 37 +++++-------------- 2 files changed, 15 insertions(+), 36 deletions(-) diff --git a/nautilus_core/adapters/src/tardis/machine/client.rs b/nautilus_core/adapters/src/tardis/machine/client.rs index f0efa01fb7c0..b7ebd22c09a9 100644 --- a/nautilus_core/adapters/src/tardis/machine/client.rs +++ b/nautilus_core/adapters/src/tardis/machine/client.rs @@ -137,20 +137,16 @@ where while let Some(result) = stream.next().await { match result { Ok(msg) => { - let info = if let Some(ref instrument) = instrument { - Some(instrument.clone()) - } else { - instrument_map.as_ref().and_then(|map| determine_instrument_info(&msg, map)) - }; + let info = instrument.clone().or_else(|| { + instrument_map + .as_ref() + .and_then(|map| determine_instrument_info(&msg, map)) + }); if let Some(info) = info { if let Some(data) = parse_tardis_ws_message(msg, info) { yield data; - } else { - continue; // Non-data message } - } else { - continue; // No instrument info } } Err(e) => { diff --git a/nautilus_core/adapters/src/tardis/python/machine.rs b/nautilus_core/adapters/src/tardis/python/machine.rs index 5d43f9232b97..06ab469507b7 100644 --- a/nautilus_core/adapters/src/tardis/python/machine.rs +++ b/nautilus_core/adapters/src/tardis/python/machine.rs @@ -130,13 +130,10 @@ impl TardisMachineClient { py: Python<'py>, ) -> PyResult> { let map = if !instruments.is_empty() { - let mut instrument_map: HashMap> = - HashMap::new(); - for inst in instruments { - let key = inst.as_tardis_instrument_key(); - instrument_map.insert(key, Arc::new(inst.clone())); - } - instrument_map + instruments + .into_iter() + .map(|inst| (inst.as_tardis_instrument_key(), Arc::new(inst))) + .collect() } else { self.instruments.clone() }; @@ -158,18 +155,10 @@ impl TardisMachineClient { while let Some(result) = stream.next().await { match result { Ok(msg) => { - let info = determine_instrument_info(&msg, &map); - - if let Some(info) = info { - if let Some(data) = parse_tardis_ws_message(msg, info) { - if let Data::Bar(bar) = data { - bars.push(bar); - } - } else { - continue; // Non-data message - } - } else { - continue; // No instrument info + if let Some(Data::Bar(bar)) = determine_instrument_info(&msg, &map) + .and_then(|info| parse_tardis_ws_message(msg, info)) + { + bars.push(bar); } } Err(e) => { @@ -250,13 +239,11 @@ async fn handle_python_stream( while let Some(result) = stream.next().await { match result { Ok(msg) => { - let info = if let Some(ref instrument) = instrument { - Some(instrument.clone()) - } else { + let info = instrument.clone().or_else(|| { instrument_map .as_ref() .and_then(|map| determine_instrument_info(&msg, map)) - }; + }); if let Some(info) = info { if let Some(data) = parse_tardis_ws_message(msg, info) { @@ -264,11 +251,7 @@ async fn handle_python_stream( let py_obj = data_to_pycapsule(py, data); let _ = call_python(py, &callback, py_obj); }); - } else { - continue; // Non-data message } - } else { - continue; // No instrument info } } Err(e) => { From adee3fcf8f2ec3bd2192edaa782526ff3b2b3d31 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 24 Nov 2024 13:49:34 +1100 Subject: [PATCH 44/78] Update dependencies and add smallvec for pyo3 --- nautilus_core/Cargo.lock | 48 +++++++-------- nautilus_core/Cargo.toml | 2 +- poetry.lock | 128 +++++++++++++++++++-------------------- pyproject.toml | 2 +- 4 files changed, 87 insertions(+), 93 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 2d0951ada1a8..45e032d755e1 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -383,9 +383,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" +checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" dependencies = [ "bzip2", "flate2", @@ -455,21 +455,20 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7c2840b66236045acd2607d5866e274380afd87ef99d6226e961e2cb47df45" +checksum = "f47bb8cc16b669d267eeccf585aea077d0882f4777b1c1f740217885d6e6e5a3" dependencies = [ "aws-lc-sys", - "mirai-annotations", "paste", "zeroize", ] [[package]] name = "aws-lc-sys" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad3a619a9de81e1d7de1f1186dcba4506ed661a0e483d84410fdef0ee87b2f96" +checksum = "a2101df3813227bbaaaa0b04cd61c534c7954b22bd68d399b440be937dc63ff7" dependencies = [ "bindgen", "cc", @@ -2736,9 +2735,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" @@ -2853,12 +2852,6 @@ 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" @@ -3835,9 +3828,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "powerfmt" @@ -3988,6 +3981,7 @@ dependencies = [ "pyo3-ffi", "pyo3-macros", "rust_decimal", + "smallvec", "unindent", ] @@ -5814,9 +5808,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -6372,9 +6366,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -6384,9 +6378,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", @@ -6417,18 +6411,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 06b09cd73f7e..e790bc446e67 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -48,7 +48,7 @@ itoa = "1.0.13" once_cell = "1.20.2" log = { version = "0.4.22", features = ["std", "kv_unstable", "serde", "release_max_level_debug"] } parquet = "53.2.0" # Keep in line with datafusion -pyo3 = { version = "0.22.6", features = ["rust_decimal", "indexmap"] } +pyo3 = { version = "0.22.6", features = ["rust_decimal", "indexmap", "smallvec"] } pyo3-async-runtimes = { version = "0.22.0", features = ["tokio-runtime", "tokio", "attributes"] } rand = "0.8.5" reqwest = { version = "0.12.9", features = ["blocking"] } diff --git a/poetry.lock b/poetry.lock index 601d9090aaa9..666b98abc381 100644 --- a/poetry.lock +++ b/poetry.lock @@ -913,73 +913,73 @@ files = [ [[package]] name = "coverage" -version = "7.6.7" +version = "7.6.8" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.6.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:108bb458827765d538abcbf8288599fee07d2743357bdd9b9dad456c287e121e"}, - {file = "coverage-7.6.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c973b2fe4dc445cb865ab369df7521df9c27bf40715c837a113edaa2aa9faf45"}, - {file = "coverage-7.6.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c6b24007c4bcd0b19fac25763a7cac5035c735ae017e9a349b927cfc88f31c1"}, - {file = "coverage-7.6.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acbb8af78f8f91b3b51f58f288c0994ba63c646bc1a8a22ad072e4e7e0a49f1c"}, - {file = "coverage-7.6.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad32a981bcdedb8d2ace03b05e4fd8dace8901eec64a532b00b15217d3677dd2"}, - {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:34d23e28ccb26236718a3a78ba72744212aa383141961dd6825f6595005c8b06"}, - {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e25bacb53a8c7325e34d45dddd2f2fbae0dbc230d0e2642e264a64e17322a777"}, - {file = "coverage-7.6.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af05bbba896c4472a29408455fe31b3797b4d8648ed0a2ccac03e074a77e2314"}, - {file = "coverage-7.6.7-cp310-cp310-win32.whl", hash = "sha256:796c9b107d11d2d69e1849b2dfe41730134b526a49d3acb98ca02f4985eeff7a"}, - {file = "coverage-7.6.7-cp310-cp310-win_amd64.whl", hash = "sha256:987a8e3da7da4eed10a20491cf790589a8e5e07656b6dc22d3814c4d88faf163"}, - {file = "coverage-7.6.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e61b0e77ff4dddebb35a0e8bb5a68bf0f8b872407d8d9f0c726b65dfabe2469"}, - {file = "coverage-7.6.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a5407a75ca4abc20d6252efeb238377a71ce7bda849c26c7a9bece8680a5d99"}, - {file = "coverage-7.6.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df002e59f2d29e889c37abd0b9ee0d0e6e38c24f5f55d71ff0e09e3412a340ec"}, - {file = "coverage-7.6.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:673184b3156cba06154825f25af33baa2671ddae6343f23175764e65a8c4c30b"}, - {file = "coverage-7.6.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e69ad502f1a2243f739f5bd60565d14a278be58be4c137d90799f2c263e7049a"}, - {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60dcf7605c50ea72a14490d0756daffef77a5be15ed1b9fea468b1c7bda1bc3b"}, - {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9c2eb378bebb2c8f65befcb5147877fc1c9fbc640fc0aad3add759b5df79d55d"}, - {file = "coverage-7.6.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c0317288f032221d35fa4cbc35d9f4923ff0dfd176c79c9b356e8ef8ef2dff4"}, - {file = "coverage-7.6.7-cp311-cp311-win32.whl", hash = "sha256:951aade8297358f3618a6e0660dc74f6b52233c42089d28525749fc8267dccd2"}, - {file = "coverage-7.6.7-cp311-cp311-win_amd64.whl", hash = "sha256:5e444b8e88339a2a67ce07d41faabb1d60d1004820cee5a2c2b54e2d8e429a0f"}, - {file = "coverage-7.6.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f07ff574986bc3edb80e2c36391678a271d555f91fd1d332a1e0f4b5ea4b6ea9"}, - {file = "coverage-7.6.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:49ed5ee4109258973630c1f9d099c7e72c5c36605029f3a91fe9982c6076c82b"}, - {file = "coverage-7.6.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3e8796434a8106b3ac025fd15417315d7a58ee3e600ad4dbcfddc3f4b14342c"}, - {file = "coverage-7.6.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3b925300484a3294d1c70f6b2b810d6526f2929de954e5b6be2bf8caa1f12c1"}, - {file = "coverage-7.6.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c42ec2c522e3ddd683dec5cdce8e62817afb648caedad9da725001fa530d354"}, - {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0266b62cbea568bd5e93a4da364d05de422110cbed5056d69339bd5af5685433"}, - {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e5f2a0f161d126ccc7038f1f3029184dbdf8f018230af17ef6fd6a707a5b881f"}, - {file = "coverage-7.6.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c132b5a22821f9b143f87446805e13580b67c670a548b96da945a8f6b4f2efbb"}, - {file = "coverage-7.6.7-cp312-cp312-win32.whl", hash = "sha256:7c07de0d2a110f02af30883cd7dddbe704887617d5c27cf373362667445a4c76"}, - {file = "coverage-7.6.7-cp312-cp312-win_amd64.whl", hash = "sha256:fd49c01e5057a451c30c9b892948976f5d38f2cbd04dc556a82743ba8e27ed8c"}, - {file = "coverage-7.6.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3"}, - {file = "coverage-7.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc"}, - {file = "coverage-7.6.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55"}, - {file = "coverage-7.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384"}, - {file = "coverage-7.6.7-cp313-cp313-win32.whl", hash = "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30"}, - {file = "coverage-7.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42"}, - {file = "coverage-7.6.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413"}, - {file = "coverage-7.6.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b"}, - {file = "coverage-7.6.7-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b"}, - {file = "coverage-7.6.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3"}, - {file = "coverage-7.6.7-cp313-cp313t-win32.whl", hash = "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8"}, - {file = "coverage-7.6.7-cp313-cp313t-win_amd64.whl", hash = "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56"}, - {file = "coverage-7.6.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37a15573f988b67f7348916077c6d8ad43adb75e478d0910957394df397d2874"}, - {file = "coverage-7.6.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b6cce5c76985f81da3769c52203ee94722cd5d5889731cd70d31fee939b74bf0"}, - {file = "coverage-7.6.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ab9763d291a17b527ac6fd11d1a9a9c358280adb320e9c2672a97af346ac2c"}, - {file = "coverage-7.6.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cf96ceaa275f071f1bea3067f8fd43bec184a25a962c754024c973af871e1b7"}, - {file = "coverage-7.6.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aee9cf6b0134d6f932d219ce253ef0e624f4fa588ee64830fcba193269e4daa3"}, - {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2bc3e45c16564cc72de09e37413262b9f99167803e5e48c6156bccdfb22c8327"}, - {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:623e6965dcf4e28a3debaa6fcf4b99ee06d27218f46d43befe4db1c70841551c"}, - {file = "coverage-7.6.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:850cfd2d6fc26f8346f422920ac204e1d28814e32e3a58c19c91980fa74d8289"}, - {file = "coverage-7.6.7-cp39-cp39-win32.whl", hash = "sha256:c296263093f099da4f51b3dff1eff5d4959b527d4f2f419e16508c5da9e15e8c"}, - {file = "coverage-7.6.7-cp39-cp39-win_amd64.whl", hash = "sha256:90746521206c88bdb305a4bf3342b1b7316ab80f804d40c536fc7d329301ee13"}, - {file = "coverage-7.6.7-pp39.pp310-none-any.whl", hash = "sha256:0ddcb70b3a3a57581b450571b31cb774f23eb9519c2aaa6176d3a84c9fc57671"}, - {file = "coverage-7.6.7.tar.gz", hash = "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24"}, + {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, + {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, + {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, + {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, + {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, + {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, + {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, + {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, + {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, + {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, + {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, + {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, + {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, + {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, + {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, + {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, ] [package.extras] @@ -4123,4 +4123,4 @@ polymarket = ["py-clob-client"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "08f857e064c1200b00004f423adc26ac67e4c1f82d1f2239f9edd85208ae08cd" +content-hash = "2884c6a44b0764501cfbc7d41c3b588681ee638d9f7cc13ec5e9620c97641edf" diff --git a/pyproject.toml b/pyproject.toml index 46390cf55646..79d6d58026b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,7 +101,7 @@ types-toml = "^0.10.2" optional = true [tool.poetry.group.test.dependencies] -coverage = "^7.6.7" +coverage = "^7.6.8" pytest = "^7.4.4" pytest-aiohttp = "^1.0.5" pytest-asyncio = "==0.21.1" # Pinned due Cython: cannot set '__pytest_asyncio_scoped_event_loop' attribute of immutable type From 8825a40f6ca5107f43e90465788ea3fa1d7c97f1 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 26 Nov 2024 17:00:25 +1100 Subject: [PATCH 45/78] Update dependencies with ruff autofixes --- .pre-commit-config.yaml | 2 +- nautilus_core/Cargo.lock | 26 ++-- nautilus_core/Cargo.toml | 2 +- nautilus_trader/adapters/bybit/execution.py | 3 +- .../adapters/databento/__init__.py | 6 +- .../interactive_brokers/client/client.py | 6 +- .../interactive_brokers/parsing/data.py | 7 +- .../adapters/interactive_brokers/providers.py | 12 +- .../adapters/polymarket/websocket/client.py | 7 +- nautilus_trader/common/events.py | 2 +- nautilus_trader/config/__init__.py | 28 ++-- nautilus_trader/model/enums.py | 62 ++++---- nautilus_trader/model/events/__init__.py | 2 +- nautilus_trader/model/instruments/__init__.py | 4 +- nautilus_trader/model/orders/__init__.py | 10 +- poetry.lock | 146 +++++++++--------- pyproject.toml | 6 +- 17 files changed, 160 insertions(+), 171 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 68277cedbb0e..8e7f68226982 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -83,7 +83,7 @@ repos: exclude: "docs/_pygments/monokai.py" - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.4 + rev: v0.8.0 hooks: - id: ruff args: ["--fix"] diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 45e032d755e1..00dc14aacac3 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -210,7 +210,7 @@ dependencies = [ "chrono", "chrono-tz", "half", - "hashbrown 0.15.1", + "hashbrown 0.15.2", "num", ] @@ -931,9 +931,9 @@ checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] name = "cmake" -version = "0.1.51" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" dependencies = [ "cc", ] @@ -2121,9 +2121,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hashlink" @@ -2503,7 +2503,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.1", + "hashbrown 0.15.2", "serde", ] @@ -2583,9 +2583,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jobserver" @@ -2696,9 +2696,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.164" +version = "0.2.165" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +checksum = "fcb4d3d38eab6c5239a362fa8bae48c03baf980a6e7079f063942d563ef3533e" [[package]] name = "libloading" @@ -3636,7 +3636,7 @@ dependencies = [ "flate2", "futures", "half", - "hashbrown 0.15.1", + "hashbrown 0.15.2", "lz4_flex", "num", "num-bigint", @@ -5617,9 +5617,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index e790bc446e67..40abffb68349 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -44,7 +44,7 @@ heck = "0.5.0" hex = "0.4.3" indexmap = { version = "2.6.0", features = ["serde"] } itertools = "0.13.0" -itoa = "1.0.13" +itoa = "1.0.14" once_cell = "1.20.2" log = { version = "0.4.22", features = ["std", "kv_unstable", "serde", "release_max_level_debug"] } parquet = "53.2.0" # Keep in line with datafusion diff --git a/nautilus_trader/adapters/bybit/execution.py b/nautilus_trader/adapters/bybit/execution.py index a9fb083ac83c..acf1b8acd6ea 100644 --- a/nautilus_trader/adapters/bybit/execution.py +++ b/nautilus_trader/adapters/bybit/execution.py @@ -1046,7 +1046,8 @@ def _handle_account_order_update(self, raw: bytes) -> None: # noqa: C901 (too c order, TrailingStopMarketOrder, ) - or bybit_order.orderType == BybitOrderType.LIMIT + ) or ( + bybit_order.orderType == BybitOrderType.LIMIT and not isinstance( order, TrailingStopLimitOrder, diff --git a/nautilus_trader/adapters/databento/__init__.py b/nautilus_trader/adapters/databento/__init__.py index eae20d4a70ea..aafcfb2ab724 100644 --- a/nautilus_trader/adapters/databento/__init__.py +++ b/nautilus_trader/adapters/databento/__init__.py @@ -24,12 +24,12 @@ __all__ = [ + "ALL_SYMBOLS", "DATABENTO", "DATABENTO_CLIENT_ID", - "ALL_SYMBOLS", - "DatabentoDataLoader", "DatabentoDataClientConfig", + "DatabentoDataLoader", + "DatabentoImbalance", "DatabentoLiveDataClientFactory", "DatabentoStatistics", - "DatabentoImbalance", ] diff --git a/nautilus_trader/adapters/interactive_brokers/client/client.py b/nautilus_trader/adapters/interactive_brokers/client/client.py index 67b77bcfb616..4731ebb8b884 100644 --- a/nautilus_trader/adapters/interactive_brokers/client/client.py +++ b/nautilus_trader/adapters/interactive_brokers/client/client.py @@ -581,10 +581,8 @@ async def _run_internal_msg_queue_processor(self) -> None: ) try: while ( - self._eclient.conn - and self._eclient.conn.isConnected() - or not self._internal_msg_queue.empty() - ): + self._eclient.conn and self._eclient.conn.isConnected() + ) or not self._internal_msg_queue.empty(): msg = await self._internal_msg_queue.get() if not await self._process_message(msg): break diff --git a/nautilus_trader/adapters/interactive_brokers/parsing/data.py b/nautilus_trader/adapters/interactive_brokers/parsing/data.py index 23886f5427e0..81eb4a361dce 100644 --- a/nautilus_trader/adapters/interactive_brokers/parsing/data.py +++ b/nautilus_trader/adapters/interactive_brokers/parsing/data.py @@ -68,11 +68,8 @@ def generate_trade_id(ts_event: int, price: float, size: Decimal) -> TradeId: def bar_spec_to_bar_size(bar_spec: BarSpecification) -> str: aggregation = bar_spec.aggregation step = bar_spec.step - if ( - aggregation == BarAggregation.SECOND - and step == 5 - or aggregation == BarAggregation.SECOND - and step in [10, 15, 30] + if (aggregation == BarAggregation.SECOND and step == 5) or ( + aggregation == BarAggregation.SECOND and step in [10, 15, 30] ): return f"{step} secs" elif aggregation == BarAggregation.MINUTE and step in [1, 2, 3, 5, 10, 15, 20, 30]: diff --git a/nautilus_trader/adapters/interactive_brokers/providers.py b/nautilus_trader/adapters/interactive_brokers/providers.py index 73320e3c32da..113d6407a999 100644 --- a/nautilus_trader/adapters/interactive_brokers/providers.py +++ b/nautilus_trader/adapters/interactive_brokers/providers.py @@ -133,10 +133,8 @@ async def get_contract_details( ) if ( - contract.secType in ["FUT", "CONTFUT"] - and contract.build_futures_chain - or self._build_futures_chain - ): + contract.secType in ["FUT", "CONTFUT"] and contract.build_futures_chain + ) or self._build_futures_chain: # Return Underlying contract details with Future Chains details = await self.get_future_chain_details( underlying=qualified.contract, @@ -156,10 +154,8 @@ async def get_contract_details( self._log.debug(f"Got {details=}") if ( - contract.secType in ["STK", "FUT", "IND"] - and contract.build_options_chain - or self._build_options_chain - ): + contract.secType in ["STK", "FUT", "IND"] and contract.build_options_chain + ) or self._build_options_chain: # Return Underlying contract details with Option Chains, including for the Future Chains if apply for detail in set(details): if contract.lastTradeDateOrContractMonth: diff --git a/nautilus_trader/adapters/polymarket/websocket/client.py b/nautilus_trader/adapters/polymarket/websocket/client.py index 07b96b3a4c08..d62a0c74d336 100644 --- a/nautilus_trader/adapters/polymarket/websocket/client.py +++ b/nautilus_trader/adapters/polymarket/websocket/client.py @@ -171,11 +171,8 @@ def reconnect(self) -> None: """ Reconnect the client to the server and resubscribe to all streams. """ - if ( - self._channel == PolymarketWebSocketChannel.USER - and not self._markets - or self._channel == PolymarketWebSocketChannel.MARKET - and not self._assets + if (self._channel == PolymarketWebSocketChannel.USER and not self._markets) or ( + self._channel == PolymarketWebSocketChannel.MARKET and not self._assets ): if not self._markets: self._log.error("Cannot reconnect: no streams for initial connection") diff --git a/nautilus_trader/common/events.py b/nautilus_trader/common/events.py index 2751e049c9cc..90d6ae37f368 100644 --- a/nautilus_trader/common/events.py +++ b/nautilus_trader/common/events.py @@ -20,8 +20,8 @@ __all__ = [ - "TimeEvent", "ComponentStateChanged", "RiskEvent", + "TimeEvent", "TradingStateChanged", ] diff --git a/nautilus_trader/config/__init__.py b/nautilus_trader/config/__init__.py index efb9cfdada4c..027216ca194b 100644 --- a/nautilus_trader/config/__init__.py +++ b/nautilus_trader/config/__init__.py @@ -82,46 +82,46 @@ "CacheConfig", "ControllerConfig", "ControllerFactory", - "DatabaseConfig", "DataCatalogConfig", "DataEngineConfig", + "DatabaseConfig", "ExecAlgorithmConfig", "ExecAlgorithmFactory", "ExecEngineConfig", "FXRolloverInterestConfig", "ImportableActorConfig", + "ImportableConfig", "ImportableControllerConfig", "ImportableExecAlgorithmConfig", "ImportableStrategyConfig", "InstrumentProviderConfig", "InvalidConfiguration", + "LiveDataClientConfig", + "LiveDataEngineConfig", + "LiveExecClientConfig", + "LiveExecEngineConfig", + "LiveRiskEngineConfig", "LoggingConfig", "MessageBusConfig", "NautilusConfig", "NautilusKernelConfig", - "NonNegativeInt", "NonNegativeFloat", + "NonNegativeInt", "OrderEmulatorConfig", - "PositiveInt", "PositiveFloat", + "PositiveInt", "RiskEngineConfig", + "RoutingConfig", + "SimulationModuleConfig", "StrategyConfig", "StrategyFactory", "StreamingConfig", - "SimulationModuleConfig", - "ImportableConfig", - "LiveDataClientConfig", - "LiveDataEngineConfig", - "LiveExecClientConfig", - "LiveExecEngineConfig", - "LiveRiskEngineConfig", - "RoutingConfig", "TradingNodeConfig", - "msgspec_encoding_hook", "msgspec_decoding_hook", - "register_config_encoding", + "msgspec_encoding_hook", "register_config_decoding", - "resolve_path", + "register_config_encoding", "resolve_config_path", + "resolve_path", "tokenize_config", ] diff --git a/nautilus_trader/model/enums.py b/nautilus_trader/model/enums.py index c843fabbc51e..49077eccd163 100644 --- a/nautilus_trader/model/enums.py +++ b/nautilus_trader/model/enums.py @@ -98,12 +98,12 @@ "AggregationSource", "AggressorSide", "AssetClass", - "InstrumentClass", "BarAggregation", "BookAction", "BookType", "ContingencyType", "CurrencyType", + "InstrumentClass", "InstrumentCloseType", "LiquiditySide", "MarketStatus", @@ -113,63 +113,63 @@ "OrderSide", "OrderStatus", "OrderType", - "RecordFlag", "PositionSide", "PriceType", + "RecordFlag", "TimeInForce", "TradingState", "TrailingOffsetType", "TriggerType", - "account_type_to_str", "account_type_from_str", - "aggregation_source_to_str", + "account_type_to_str", "aggregation_source_from_str", - "aggressor_side_to_str", + "aggregation_source_to_str", "aggressor_side_from_str", - "asset_class_to_str", + "aggressor_side_to_str", "asset_class_from_str", - "instrument_class_to_str", - "instrument_class_from_str", - "bar_aggregation_to_str", + "asset_class_to_str", "bar_aggregation_from_str", - "book_action_to_str", + "bar_aggregation_to_str", "book_action_from_str", - "book_type_to_str", + "book_action_to_str", "book_type_from_str", - "contingency_type_to_str", + "book_type_to_str", "contingency_type_from_str", - "currency_type_to_str", + "contingency_type_to_str", "currency_type_from_str", - "instrument_close_type_to_str", + "currency_type_to_str", + "instrument_class_from_str", + "instrument_class_to_str", "instrument_close_type_from_str", - "liquidity_side_to_str", + "instrument_close_type_to_str", "liquidity_side_from_str", - "market_status_to_str", - "market_status_from_str", - "market_status_action_to_str", + "liquidity_side_to_str", "market_status_action_from_str", - "oms_type_to_str", + "market_status_action_to_str", + "market_status_from_str", + "market_status_to_str", "oms_type_from_str", - "option_kind_to_str", + "oms_type_to_str", "option_kind_from_str", - "order_side_to_str", + "option_kind_to_str", "order_side_from_str", - "order_status_to_str", + "order_side_to_str", "order_status_from_str", - "order_type_to_str", + "order_status_to_str", "order_type_from_str", - "record_flag_to_str", - "record_flag_from_str", - "position_side_to_str", + "order_type_to_str", "position_side_from_str", - "price_type_to_str", + "position_side_to_str", "price_type_from_str", - "time_in_force_to_str", + "price_type_to_str", + "record_flag_from_str", + "record_flag_to_str", "time_in_force_from_str", - "trading_state_to_str", + "time_in_force_to_str", "trading_state_from_str", - "trailing_offset_type_to_str", + "trading_state_to_str", "trailing_offset_type_from_str", - "trigger_type_to_str", + "trailing_offset_type_to_str", "trigger_type_from_str", + "trigger_type_to_str", ] diff --git a/nautilus_trader/model/events/__init__.py b/nautilus_trader/model/events/__init__.py index 3df7485457fd..aa9824ebe0fa 100644 --- a/nautilus_trader/model/events/__init__.py +++ b/nautilus_trader/model/events/__init__.py @@ -43,8 +43,8 @@ __all__ = [ "AccountState", "OrderAccepted", - "OrderCanceled", "OrderCancelRejected", + "OrderCanceled", "OrderDenied", "OrderEmulated", "OrderEvent", diff --git a/nautilus_trader/model/instruments/__init__.py b/nautilus_trader/model/instruments/__init__.py index aa592820667a..f27e0a4f1230 100644 --- a/nautilus_trader/model/instruments/__init__.py +++ b/nautilus_trader/model/instruments/__init__.py @@ -36,8 +36,6 @@ __all__ = [ - "Instrument", - "instruments_from_pyo3", "BettingInstrument", "BinaryOption", "Cfd", @@ -49,7 +47,9 @@ "FuturesContract", "FuturesSpread", "IndexInstrument", + "Instrument", "OptionsContract", "OptionsSpread", "SyntheticInstrument", + "instruments_from_pyo3", ] diff --git a/nautilus_trader/model/orders/__init__.py b/nautilus_trader/model/orders/__init__.py index aa89895a2d14..8360abd7d04b 100644 --- a/nautilus_trader/model/orders/__init__.py +++ b/nautilus_trader/model/orders/__init__.py @@ -32,16 +32,16 @@ __all__ = [ - "Order", - "LimitOrder", "LimitIfTouchedOrder", - "OrderList", - "MarketOrder", + "LimitOrder", "MarketIfTouchedOrder", + "MarketOrder", "MarketToLimitOrder", + "Order", + "OrderList", + "OrderUnpacker", "StopLimitOrder", "StopMarketOrder", "TrailingStopLimitOrder", "TrailingStopMarketOrder", - "OrderUnpacker", ] diff --git a/poetry.lock b/poetry.lock index 666b98abc381..89a565c893ce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1750,13 +1750,13 @@ test = ["eth-utils (>=2.0.0)", "hypothesis (>=3.44.24,<=6.31.6)", "pytest (>=7.0 [[package]] name = "identify" -version = "2.6.2" +version = "2.6.3" description = "File identification library for Python" optional = false python-versions = ">=3.9" files = [ - {file = "identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3"}, - {file = "identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd"}, + {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, + {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, ] [package.extras] @@ -2827,53 +2827,53 @@ files = [ [[package]] name = "pyarrow" -version = "18.0.0" +version = "18.1.0" description = "Python library for Apache Arrow" optional = false python-versions = ">=3.9" files = [ - {file = "pyarrow-18.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:2333f93260674e185cfbf208d2da3007132572e56871f451ba1a556b45dae6e2"}, - {file = "pyarrow-18.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:4c381857754da44326f3a49b8b199f7f87a51c2faacd5114352fc78de30d3aba"}, - {file = "pyarrow-18.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:603cd8ad4976568954598ef0a6d4ed3dfb78aff3d57fa8d6271f470f0ce7d34f"}, - {file = "pyarrow-18.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58a62549a3e0bc9e03df32f350e10e1efb94ec6cf63e3920c3385b26663948ce"}, - {file = "pyarrow-18.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bc97316840a349485fbb137eb8d0f4d7057e1b2c1272b1a20eebbbe1848f5122"}, - {file = "pyarrow-18.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:2e549a748fa8b8715e734919923f69318c953e077e9c02140ada13e59d043310"}, - {file = "pyarrow-18.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:606e9a3dcb0f52307c5040698ea962685fb1c852d72379ee9412be7de9c5f9e2"}, - {file = "pyarrow-18.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d5795e37c0a33baa618c5e054cd61f586cf76850a251e2b21355e4085def6280"}, - {file = "pyarrow-18.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:5f0510608ccd6e7f02ca8596962afb8c6cc84c453e7be0da4d85f5f4f7b0328a"}, - {file = "pyarrow-18.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616ea2826c03c16e87f517c46296621a7c51e30400f6d0a61be645f203aa2b93"}, - {file = "pyarrow-18.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1824f5b029ddd289919f354bc285992cb4e32da518758c136271cf66046ef22"}, - {file = "pyarrow-18.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6dd1b52d0d58dd8f685ced9971eb49f697d753aa7912f0a8f50833c7a7426319"}, - {file = "pyarrow-18.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:320ae9bd45ad7ecc12ec858b3e8e462578de060832b98fc4d671dee9f10d9954"}, - {file = "pyarrow-18.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:2c992716cffb1088414f2b478f7af0175fd0a76fea80841b1706baa8fb0ebaad"}, - {file = "pyarrow-18.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:e7ab04f272f98ebffd2a0661e4e126036f6936391ba2889ed2d44c5006237802"}, - {file = "pyarrow-18.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:03f40b65a43be159d2f97fd64dc998f769d0995a50c00f07aab58b0b3da87e1f"}, - {file = "pyarrow-18.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be08af84808dff63a76860847c48ec0416928a7b3a17c2f49a072cac7c45efbd"}, - {file = "pyarrow-18.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70c1965cde991b711a98448ccda3486f2a336457cf4ec4dca257a926e149c9"}, - {file = "pyarrow-18.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:00178509f379415a3fcf855af020e3340254f990a8534294ec3cf674d6e255fd"}, - {file = "pyarrow-18.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a71ab0589a63a3e987beb2bc172e05f000a5c5be2636b4b263c44034e215b5d7"}, - {file = "pyarrow-18.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe92efcdbfa0bcf2fa602e466d7f2905500f33f09eb90bf0bcf2e6ca41b574c8"}, - {file = "pyarrow-18.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:907ee0aa8ca576f5e0cdc20b5aeb2ad4d3953a3b4769fc4b499e00ef0266f02f"}, - {file = "pyarrow-18.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:66dcc216ebae2eb4c37b223feaf82f15b69d502821dde2da138ec5a3716e7463"}, - {file = "pyarrow-18.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1daf7c425f58527900876354390ee41b0ae962a73ad0959b9d829def583bb1"}, - {file = "pyarrow-18.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871b292d4b696b09120ed5bde894f79ee2a5f109cb84470546471df264cae136"}, - {file = "pyarrow-18.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:082ba62bdcb939824ba1ce10b8acef5ab621da1f4c4805e07bfd153617ac19d4"}, - {file = "pyarrow-18.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:2c664ab88b9766413197733c1720d3dcd4190e8fa3bbdc3710384630a0a7207b"}, - {file = "pyarrow-18.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc892be34dbd058e8d189b47db1e33a227d965ea8805a235c8a7286f7fd17d3a"}, - {file = "pyarrow-18.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:28f9c39a56d2c78bf6b87dcc699d520ab850919d4a8c7418cd20eda49874a2ea"}, - {file = "pyarrow-18.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:f1a198a50c409ab2d009fbf20956ace84567d67f2c5701511d4dd561fae6f32e"}, - {file = "pyarrow-18.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5bd7fd32e3ace012d43925ea4fc8bd1b02cc6cc1e9813b518302950e89b5a22"}, - {file = "pyarrow-18.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:336addb8b6f5208be1b2398442c703a710b6b937b1a046065ee4db65e782ff5a"}, - {file = "pyarrow-18.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:45476490dd4adec5472c92b4d253e245258745d0ccaabe706f8d03288ed60a79"}, - {file = "pyarrow-18.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:b46591222c864e7da7faa3b19455196416cd8355ff6c2cc2e65726a760a3c420"}, - {file = "pyarrow-18.0.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:eb7e3abcda7e1e6b83c2dc2909c8d045881017270a119cc6ee7fdcfe71d02df8"}, - {file = "pyarrow-18.0.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:09f30690b99ce34e0da64d20dab372ee54431745e4efb78ac938234a282d15f9"}, - {file = "pyarrow-18.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d5ca5d707e158540312e09fd907f9f49bacbe779ab5236d9699ced14d2293b8"}, - {file = "pyarrow-18.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6331f280c6e4521c69b201a42dd978f60f7e129511a55da9e0bfe426b4ebb8d"}, - {file = "pyarrow-18.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3ac24b2be732e78a5a3ac0b3aa870d73766dd00beba6e015ea2ea7394f8b4e55"}, - {file = "pyarrow-18.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b30a927c6dff89ee702686596f27c25160dd6c99be5bcc1513a763ae5b1bfc03"}, - {file = "pyarrow-18.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:8f40ec677e942374e3d7f2fad6a67a4c2811a8b975e8703c6fd26d3b168a90e2"}, - {file = "pyarrow-18.0.0.tar.gz", hash = "sha256:a6aa027b1a9d2970cf328ccd6dbe4a996bc13c39fd427f502782f5bdb9ca20f5"}, + {file = "pyarrow-18.1.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e21488d5cfd3d8b500b3238a6c4b075efabc18f0f6d80b29239737ebd69caa6c"}, + {file = "pyarrow-18.1.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:b516dad76f258a702f7ca0250885fc93d1fa5ac13ad51258e39d402bd9e2e1e4"}, + {file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f443122c8e31f4c9199cb23dca29ab9427cef990f283f80fe15b8e124bcc49b"}, + {file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a03da7f2758645d17b7b4f83c8bffeae5bbb7f974523fe901f36288d2eab71"}, + {file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ba17845efe3aa358ec266cf9cc2800fa73038211fb27968bfa88acd09261a470"}, + {file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3c35813c11a059056a22a3bef520461310f2f7eea5c8a11ef9de7062a23f8d56"}, + {file = "pyarrow-18.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9736ba3c85129d72aefa21b4f3bd715bc4190fe4426715abfff90481e7d00812"}, + {file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:eaeabf638408de2772ce3d7793b2668d4bb93807deed1725413b70e3156a7854"}, + {file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:3b2e2239339c538f3464308fd345113f886ad031ef8266c6f004d49769bb074c"}, + {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f39a2e0ed32a0970e4e46c262753417a60c43a3246972cfc2d3eb85aedd01b21"}, + {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31e9417ba9c42627574bdbfeada7217ad8a4cbbe45b9d6bdd4b62abbca4c6f6"}, + {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:01c034b576ce0eef554f7c3d8c341714954be9b3f5d5bc7117006b85fcf302fe"}, + {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f266a2c0fc31995a06ebd30bcfdb7f615d7278035ec5b1cd71c48d56daaf30b0"}, + {file = "pyarrow-18.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d4f13eee18433f99adefaeb7e01d83b59f73360c231d4782d9ddfaf1c3fbde0a"}, + {file = "pyarrow-18.1.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9f3a76670b263dc41d0ae877f09124ab96ce10e4e48f3e3e4257273cee61ad0d"}, + {file = "pyarrow-18.1.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:da31fbca07c435be88a0c321402c4e31a2ba61593ec7473630769de8346b54ee"}, + {file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:543ad8459bc438efc46d29a759e1079436290bd583141384c6f7a1068ed6f992"}, + {file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0743e503c55be0fdb5c08e7d44853da27f19dc854531c0570f9f394ec9671d54"}, + {file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d4b3d2a34780645bed6414e22dda55a92e0fcd1b8a637fba86800ad737057e33"}, + {file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c52f81aa6f6575058d8e2c782bf79d4f9fdc89887f16825ec3a66607a5dd8e30"}, + {file = "pyarrow-18.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ad4892617e1a6c7a551cfc827e072a633eaff758fa09f21c4ee548c30bcaf99"}, + {file = "pyarrow-18.1.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:84e314d22231357d473eabec709d0ba285fa706a72377f9cc8e1cb3c8013813b"}, + {file = "pyarrow-18.1.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:f591704ac05dfd0477bb8f8e0bd4b5dc52c1cadf50503858dce3a15db6e46ff2"}, + {file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acb7564204d3c40babf93a05624fc6a8ec1ab1def295c363afc40b0c9e66c191"}, + {file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74de649d1d2ccb778f7c3afff6085bd5092aed4c23df9feeb45dd6b16f3811aa"}, + {file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f96bd502cb11abb08efea6dab09c003305161cb6c9eafd432e35e76e7fa9b90c"}, + {file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:36ac22d7782554754a3b50201b607d553a8d71b78cdf03b33c1125be4b52397c"}, + {file = "pyarrow-18.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:25dbacab8c5952df0ca6ca0af28f50d45bd31c1ff6fcf79e2d120b4a65ee7181"}, + {file = "pyarrow-18.1.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a276190309aba7bc9d5bd2933230458b3521a4317acfefe69a354f2fe59f2bc"}, + {file = "pyarrow-18.1.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ad514dbfcffe30124ce655d72771ae070f30bf850b48bc4d9d3b25993ee0e386"}, + {file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aebc13a11ed3032d8dd6e7171eb6e86d40d67a5639d96c35142bd568b9299324"}, + {file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6cf5c05f3cee251d80e98726b5c7cc9f21bab9e9783673bac58e6dfab57ecc8"}, + {file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:11b676cd410cf162d3f6a70b43fb9e1e40affbc542a1e9ed3681895f2962d3d9"}, + {file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:b76130d835261b38f14fc41fdfb39ad8d672afb84c447126b84d5472244cfaba"}, + {file = "pyarrow-18.1.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:0b331e477e40f07238adc7ba7469c36b908f07c89b95dd4bd3a0ec84a3d1e21e"}, + {file = "pyarrow-18.1.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:2c4dd0c9010a25ba03e198fe743b1cc03cd33c08190afff371749c52ccbbaf76"}, + {file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f97b31b4c4e21ff58c6f330235ff893cc81e23da081b1a4b1c982075e0ed4e9"}, + {file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a4813cb8ecf1809871fd2d64a8eff740a1bd3691bbe55f01a3cf6c5ec869754"}, + {file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:05a5636ec3eb5cc2a36c6edb534a38ef57b2ab127292a716d00eabb887835f1e"}, + {file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:73eeed32e724ea3568bb06161cad5fa7751e45bc2228e33dcb10c614044165c7"}, + {file = "pyarrow-18.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:a1880dd6772b685e803011a6b43a230c23b566859a6e0c9a276c1e0faf4f4052"}, + {file = "pyarrow-18.1.0.tar.gz", hash = "sha256:9386d3ca9c145b5539a1cfc75df07757dff870168c959b473a0bccbc3abc8c73"}, ] [package.extras] @@ -3552,29 +3552,29 @@ test = ["hypothesis (==5.19.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "ruff" -version = "0.7.4" +version = "0.8.0" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478"}, - {file = "ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63"}, - {file = "ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06"}, - {file = "ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc"}, - {file = "ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172"}, - {file = "ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a"}, - {file = "ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd"}, - {file = "ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a"}, - {file = "ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac"}, - {file = "ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6"}, - {file = "ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f"}, - {file = "ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2"}, + {file = "ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea"}, + {file = "ruff-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b"}, + {file = "ruff-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a"}, + {file = "ruff-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99"}, + {file = "ruff-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c"}, + {file = "ruff-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9"}, + {file = "ruff-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362"}, + {file = "ruff-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df"}, + {file = "ruff-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3"}, + {file = "ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c"}, + {file = "ruff-0.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2"}, + {file = "ruff-0.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70"}, + {file = "ruff-0.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd"}, + {file = "ruff-0.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426"}, + {file = "ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468"}, + {file = "ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f"}, + {file = "ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6"}, + {file = "ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44"}, ] [[package]] @@ -3812,20 +3812,20 @@ files = [ [[package]] name = "tqdm" -version = "4.67.0" +version = "4.67.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.67.0-py3-none-any.whl", hash = "sha256:0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be"}, - {file = "tqdm-4.67.0.tar.gz", hash = "sha256:fe5a6f95e6fe0b9755e9469b77b9c3cf850048224ecaa8293d7d2d31f97d869a"}, + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] discord = ["requests"] notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] @@ -3999,13 +3999,13 @@ protobuf = ">=4.23" [[package]] name = "virtualenv" -version = "20.27.1" +version = "20.28.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, - {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, + {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, + {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, ] [package.dependencies] @@ -4123,4 +4123,4 @@ polymarket = ["py-clob-client"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "2884c6a44b0764501cfbc7d41c3b588681ee638d9f7cc13ec5e9620c97641edf" +content-hash = "bd1f4cbf3b59b8de179b7ea0a2c5bcb699e88504e5d434176cf73c1269ae6215" diff --git a/pyproject.toml b/pyproject.toml index 79d6d58026b6..36c146a6d536 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,9 +57,9 @@ click = "^8.1.7" fsspec = "^2024.10.0" msgspec = "^0.18.6" pandas = "^2.2.3" -pyarrow = ">=18.0.0" +pyarrow = ">=18.1.0" pytz = ">=2024.2.0" -tqdm = "^4.67.0" +tqdm = "^4.67.1" uvloop = {version = "^0.21.0", markers = "sys_platform != 'win32'"} async-timeout = {version = "^4.0.3", optional = true} @@ -92,7 +92,7 @@ docformatter = "^1.7.5" mypy = "^1.13.0" pandas-stubs = "^2.2.2" pre-commit = "^4.0.1" -ruff = "^0.7.4" +ruff = "^0.8.0" types-pytz = "^2024.1" types-requests = "^2.32" types-toml = "^0.10.2" From dda454bda648e21036f7ecfc7890e49aefc8a7ca Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 26 Nov 2024 17:37:16 +1100 Subject: [PATCH 46/78] Fix build for macOS --- nautilus_core/Cargo.lock | 5 +++-- nautilus_core/Cargo.toml | 1 + nautilus_core/common/Cargo.toml | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 00dc14aacac3..73626b684038 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2696,9 +2696,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.165" +version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb4d3d38eab6c5239a362fa8bae48c03baf980a6e7079f063942d563ef3533e" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "libloading" @@ -2976,6 +2976,7 @@ dependencies = [ "futures", "indexmap", "itertools 0.13.0", + "libc", "log", "nautilus-core", "nautilus-model", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 40abffb68349..3b46f496ad5f 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -46,6 +46,7 @@ indexmap = { version = "2.6.0", features = ["serde"] } itertools = "0.13.0" itoa = "1.0.14" once_cell = "1.20.2" +libc = "=0.2.164" # Pin due sysinfo issue: https://github.com/GuillaumeGomez/sysinfo/issues/1392 log = { version = "0.4.22", features = ["std", "kv_unstable", "serde", "release_max_level_debug"] } parquet = "53.2.0" # Keep in line with datafusion pyo3 = { version = "0.22.6", features = ["rust_decimal", "indexmap", "smallvec"] } diff --git a/nautilus_core/common/Cargo.toml b/nautilus_core/common/Cargo.toml index 9a2ca48ebe92..6047b8a8c1aa 100644 --- a/nautilus_core/common/Cargo.toml +++ b/nautilus_core/common/Cargo.toml @@ -19,6 +19,7 @@ chrono = { workspace = true } futures = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } +libc = { workspace = true } log = { workspace = true } pyo3 = { workspace = true, optional = true } pyo3-async-runtimes = { workspace = true, optional = true } From 559e1d6d4f5522ee13900fc6128ac73ef65baac6 Mon Sep 17 00:00:00 2001 From: Pushkar Mishra Date: Wed, 27 Nov 2024 02:22:49 +0530 Subject: [PATCH 47/78] Port Portfolio to Rust (#2058) --- nautilus_core/Cargo.lock | 1 + .../src/python/statistics/long_ratio.rs | 2 +- .../analysis/src/statistics/long_ratio.rs | 20 +- nautilus_core/common/src/cache/mod.rs | 79 +- nautilus_core/common/src/xrate.rs | 484 ++- nautilus_core/model/src/accounts/any.rs | 41 +- nautilus_core/model/src/accounts/base.rs | 14 +- nautilus_core/model/src/accounts/cash.rs | 31 + nautilus_core/model/src/events/order/any.rs | 48 +- .../model/src/events/position/mod.rs | 24 +- nautilus_core/portfolio/Cargo.toml | 1 + nautilus_core/portfolio/src/manager.rs | 649 +++- nautilus_core/portfolio/src/portfolio.rs | 3247 ++++++++++++++++- nautilus_core/risk/src/engine/config.rs | 2 +- nautilus_core/risk/src/engine/mod.rs | 28 +- 15 files changed, 4469 insertions(+), 202 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 73626b684038..029ce5038f22 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -3230,6 +3230,7 @@ dependencies = [ "strum", "thiserror 2.0.3", "ustr", + "uuid", ] [[package]] diff --git a/nautilus_core/analysis/src/python/statistics/long_ratio.rs b/nautilus_core/analysis/src/python/statistics/long_ratio.rs index 7053d4776903..c76157bdd95e 100644 --- a/nautilus_core/analysis/src/python/statistics/long_ratio.rs +++ b/nautilus_core/analysis/src/python/statistics/long_ratio.rs @@ -31,7 +31,7 @@ impl LongRatio { } #[pyo3(name = "calculate_from_positions")] - fn py_calculate_from_positions(&mut self, positions: Vec) -> Option { + fn py_calculate_from_positions(&mut self, positions: Vec) -> Option { self.calculate_from_positions(&positions) } } diff --git a/nautilus_core/analysis/src/statistics/long_ratio.rs b/nautilus_core/analysis/src/statistics/long_ratio.rs index 2543a7fc44bf..12b8756b73da 100644 --- a/nautilus_core/analysis/src/statistics/long_ratio.rs +++ b/nautilus_core/analysis/src/statistics/long_ratio.rs @@ -38,7 +38,7 @@ impl LongRatio { } impl PortfolioStatistic for LongRatio { - type Item = String; + type Item = f64; fn name(&self) -> String { stringify!(LongRatio).to_string() @@ -55,7 +55,9 @@ impl PortfolioStatistic for LongRatio { .collect(); let value = longs.len() as f64 / positions.len() as f64; - Some(format!("{:.1$}", value, self.precision)) + + let scale = 10f64.powi(self.precision as i32); + Some((value * scale).round() / scale) } } @@ -131,7 +133,7 @@ mod tests { let result = long_ratio.calculate_from_positions(&positions); assert!(result.is_some()); - assert_eq!(result.unwrap(), "1.00"); + assert_eq!(result.unwrap(), 1.00); } #[test] @@ -145,7 +147,7 @@ mod tests { let result = long_ratio.calculate_from_positions(&positions); assert!(result.is_some()); - assert_eq!(result.unwrap(), "0.00"); + assert_eq!(result.unwrap(), 0.00); } #[test] @@ -160,7 +162,7 @@ mod tests { let result = long_ratio.calculate_from_positions(&positions); assert!(result.is_some()); - assert_eq!(result.unwrap(), "0.50"); + assert_eq!(result.unwrap(), 0.50); } #[test] @@ -174,7 +176,7 @@ mod tests { let result = long_ratio.calculate_from_positions(&positions); assert!(result.is_some()); - assert_eq!(result.unwrap(), "0.667"); + assert_eq!(result.unwrap(), 0.667); } #[test] @@ -184,7 +186,7 @@ mod tests { let result = long_ratio.calculate_from_positions(&positions); assert!(result.is_some()); - assert_eq!(result.unwrap(), "1.00"); + assert_eq!(result.unwrap(), 1.00); } #[test] @@ -194,7 +196,7 @@ mod tests { let result = long_ratio.calculate_from_positions(&positions); assert!(result.is_some()); - assert_eq!(result.unwrap(), "0.00"); + assert_eq!(result.unwrap(), 0.00); } #[test] @@ -208,7 +210,7 @@ mod tests { let result = long_ratio.calculate_from_positions(&positions); assert!(result.is_some()); - assert_eq!(result.unwrap(), "1"); + assert_eq!(result.unwrap(), 1.00); } #[test] diff --git a/nautilus_core/common/src/cache/mod.rs b/nautilus_core/common/src/cache/mod.rs index 5659127e3158..7cfd2c668578 100644 --- a/nautilus_core/common/src/cache/mod.rs +++ b/nautilus_core/common/src/cache/mod.rs @@ -44,7 +44,7 @@ use nautilus_model::{ enums::{AggregationSource, OmsType, OrderSide, PositionSide, PriceType, TriggerType}, identifiers::{ AccountId, ClientId, ClientOrderId, ComponentId, ExecAlgorithmId, InstrumentId, - OrderListId, PositionId, StrategyId, Venue, VenueOrderId, + OrderListId, PositionId, StrategyId, Symbol, Venue, VenueOrderId, }, instruments::{any::InstrumentAny, synthetic::SyntheticInstrument}, orderbook::book::OrderBook, @@ -52,10 +52,13 @@ use nautilus_model::{ position::Position, types::{currency::Currency, price::Price, quantity::Quantity}, }; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use ustr::Ustr; -use crate::{enums::SerializationEncoding, msgbus::database::DatabaseConfig}; +use crate::{ + enums::SerializationEncoding, msgbus::database::DatabaseConfig, xrate::get_exchange_rate, +}; /// Configuration for `Cache` instances. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -2474,6 +2477,78 @@ impl Cache { self.bar_count(bar_type) > 0 } + #[must_use] + pub fn get_xrate( + &self, + venue: Venue, + from_currency: Currency, + to_currency: Currency, + price_type: PriceType, + ) -> Decimal { + if from_currency == to_currency { + return Decimal::ONE; + } + + let (bid_quote, ask_quote) = self.build_quote_table(&venue); + + get_exchange_rate(from_currency, to_currency, price_type, bid_quote, ask_quote) + } + + fn build_quote_table( + &self, + venue: &Venue, + ) -> (HashMap, HashMap) { + let mut bid_quotes = HashMap::new(); + let mut ask_quotes = HashMap::new(); + + for instrument_id in self.instruments.keys() { + if instrument_id.venue != *venue { + continue; + } + + let (bid_price, ask_price) = if let Some(ticks) = self.quotes.get(instrument_id) { + if let Some(tick) = ticks.front() { + (tick.bid_price, tick.ask_price) + } else { + continue; // Empty ticks vector + } + } else { + let bid_bar = self + .bars + .iter() + .find(|(k, _)| { + k.instrument_id() == *instrument_id + && matches!(k.spec().price_type, PriceType::Bid) + }) + .map(|(_, v)| v); + + let ask_bar = self + .bars + .iter() + .find(|(k, _)| { + k.instrument_id() == *instrument_id + && matches!(k.spec().price_type, PriceType::Ask) + }) + .map(|(_, v)| v); + + match (bid_bar, ask_bar) { + (Some(bid), Some(ask)) => { + let bid_price = bid.front().unwrap().close; + let ask_price = ask.front().unwrap().close; + + (bid_price, ask_price) + } + _ => continue, + } + }; + + bid_quotes.insert(instrument_id.symbol, bid_price.as_decimal()); + ask_quotes.insert(instrument_id.symbol, ask_price.as_decimal()); + } + + (bid_quotes, ask_quotes) + } + // -- INSTRUMENT QUERIES ---------------------------------------------------------------------- /// Returns a reference to the instrument for the given `instrument_id` (if found). diff --git a/nautilus_core/common/src/xrate.rs b/nautilus_core/common/src/xrate.rs index 89b5ac99221d..960cfcbc0189 100644 --- a/nautilus_core/common/src/xrate.rs +++ b/nautilus_core/common/src/xrate.rs @@ -27,21 +27,19 @@ use itertools::Itertools; use nautilus_core::correctness::{check_equal_usize, check_map_not_empty, FAILED}; use nautilus_model::{enums::PriceType, identifiers::Symbol, types::currency::Currency}; use rust_decimal::Decimal; -use rust_decimal_macros::dec; use ustr::Ustr; -const DECIMAL_ONE: Decimal = dec!(1.0); -const DECIMAL_TWO: Decimal = dec!(2.0); - +// TODO: Improve efficiency: Check Top Comment /// Returns the calculated exchange rate for the given price type using the /// given dictionary of bid and ask quotes. +#[must_use] pub fn get_exchange_rate( from_currency: Currency, to_currency: Currency, price_type: PriceType, quotes_bid: HashMap, quotes_ask: HashMap, -) -> anyhow::Result { +) -> Decimal { check_map_not_empty("es_bid, stringify!(quotes_bid)).expect(FAILED); check_map_not_empty("es_ask, stringify!(quotes_ask)).expect(FAILED); check_equal_usize( @@ -53,91 +51,457 @@ pub fn get_exchange_rate( .expect(FAILED); if from_currency == to_currency { - return Ok(DECIMAL_ONE); // No conversion necessary + return Decimal::ONE; } - let calculation_quotes: HashMap = match price_type { + let calculation_quotes = match price_type { PriceType::Bid => quotes_bid, PriceType::Ask => quotes_ask, - PriceType::Mid => { - let mut calculation_quotes = HashMap::new(); - for (symbol, bid_quote) in "es_bid { - if let Some(ask_quote) = quotes_ask.get(symbol) { - calculation_quotes.insert(*symbol, (bid_quote + ask_quote) / DECIMAL_TWO); - } - } - calculation_quotes + PriceType::Mid => quotes_bid + .iter() + .map(|(k, v)| { + let ask = quotes_ask.get(k).unwrap_or(v); + (*k, (v + ask) / Decimal::TWO) + }) + .collect(), + _ => { + panic!("Cannot calculate exchange rate for PriceType: {price_type:?}"); } - _ => panic!("Cannot calculate exchange rate for PriceType {price_type:?}"), }; + let mut codes = HashSet::new(); let mut exchange_rates: HashMap> = HashMap::new(); // Build quote table for (symbol, quote) in &calculation_quotes { + // Split symbol into currency pairs let pieces: Vec<&str> = symbol.as_str().split('/').collect(); let code_lhs = Ustr::from(pieces[0]); let code_rhs = Ustr::from(pieces[1]); - let lhs_rates = exchange_rates.entry(code_lhs).or_default(); - lhs_rates.insert(code_lhs, Decimal::new(1, 0)); - lhs_rates.insert(code_rhs, *quote); + codes.insert(code_lhs); + codes.insert(code_rhs); - let rhs_rates = exchange_rates.entry(code_rhs).or_default(); - rhs_rates.insert(code_lhs, Decimal::new(1, 0)); - rhs_rates.insert(code_rhs, *quote); - } + // Initialize currency dictionaries if they don't exist + exchange_rates.entry(code_lhs).or_default(); + exchange_rates.entry(code_rhs).or_default(); - // Clone exchange_rates to avoid borrowing conflicts - let exchange_rates_cloned = exchange_rates.clone(); + // Add base rates + if let Some(rates_lhs) = exchange_rates.get_mut(&code_lhs) { + rates_lhs.insert(code_lhs, Decimal::ONE); + rates_lhs.insert(code_rhs, *quote); + } + if let Some(rates_rhs) = exchange_rates.get_mut(&code_rhs) { + rates_rhs.insert(code_rhs, Decimal::ONE); + } + } // Generate possible currency pairs from all symbols - let mut codes: HashSet<&Ustr> = HashSet::new(); - for (code_lhs, code_rhs) in exchange_rates_cloned.keys().flat_map(|k| { - exchange_rates_cloned - .keys() - .map(move |code_rhs| (k, code_rhs)) - }) { - codes.insert(code_lhs); - codes.insert(code_rhs); - } - let _code_perms: Vec<(&Ustr, &Ustr)> = codes + let code_perms: Vec<(Ustr, Ustr)> = codes .iter() .cartesian_product(codes.iter()) .filter(|(a, b)| a != b) .map(|(a, b)| (*a, *b)) .collect(); - // TODO: Unable to solve borrowing issues for now (see top comment) // Calculate currency inverses - // for (perm_0, perm_1) in code_perms.iter() { - // let exchange_rates_perm_0 = exchange_rates.entry(**perm_0).or_insert_with(HashMap::new); - // let exchange_rates_perm_1 = exchange_rates.entry(**perm_1).or_insert_with(HashMap::new); - // if !exchange_rates_perm_0.contains_key(perm_1) { - // if let Some(rate) = exchange_rates_perm_0.get(perm_1) { - // exchange_rates_perm_1 - // .entry(**perm_0) - // .or_insert_with(|| Decimal::new(1, 0) / rate); - // } - // } - // if !exchange_rates_perm_1.contains_key(perm_0) { - // if let Some(rate) = exchange_rates_perm_1.get(perm_0) { - // exchange_rates_perm_0 - // .entry(**perm_1) - // .or_insert_with(|| Decimal::new(1, 0) / rate); - // } - // } - // } + for (perm0, perm1) in &code_perms { + // First direction: perm0 -> perm1 + let rate_0_to_1 = exchange_rates + .get(perm0) + .and_then(|rates| rates.get(perm1)) + .copied(); + if let Some(rate) = rate_0_to_1 { + if let Some(xrate_perm1) = exchange_rates.get_mut(perm1) { + if !xrate_perm1.contains_key(perm0) { + xrate_perm1.insert(*perm0, Decimal::ONE / rate); + } + } + } + + // Second direction: perm1 -> perm0 + let rate_1_to_0 = exchange_rates + .get(perm1) + .and_then(|rates| rates.get(perm0)) + .copied(); + + if let Some(rate) = rate_1_to_0 { + if let Some(xrate_perm0) = exchange_rates.get_mut(perm0) { + if !xrate_perm0.contains_key(perm1) { + xrate_perm0.insert(*perm1, Decimal::ONE / rate); + } + } + } + } + + // Check if we already have the rate if let Some(quotes) = exchange_rates.get(&from_currency.code) { - if let Some(xrate) = quotes.get(&to_currency.code) { - return Ok(*xrate); + if let Some(&rate) = quotes.get(&to_currency.code) { + return rate; + } + } + + // Calculate remaining exchange rates through common currencies + for (perm0, perm1) in &code_perms { + // Skip if rate already exists + if exchange_rates + .get(perm1) + .map_or(false, |rates| rates.contains_key(perm0)) + { + continue; + } + + // Search for common currency + for code in &codes { + // First check: rates through common currency + let rates_through_common = { + let rates_perm0 = exchange_rates.get(perm0); + let rates_perm1 = exchange_rates.get(perm1); + + match (rates_perm0, rates_perm1) { + (Some(rates0), Some(rates1)) => { + if let (Some(&rate1), Some(&rate2)) = (rates0.get(code), rates1.get(code)) { + Some((rate1, rate2)) + } else { + None + } + } + _ => None, + } + }; + + // Second check: rates from code's perspective + let rates_from_code = if rates_through_common.is_none() { + if let Some(rates_code) = exchange_rates.get(code) { + if let (Some(&rate1), Some(&rate2)) = + (rates_code.get(perm0), rates_code.get(perm1)) + { + Some((rate1, rate2)) + } else { + None + } + } else { + None + } + } else { + None + }; + + // Apply the found rates if any + if let Some((common_rate1, common_rate2)) = rates_through_common.or(rates_from_code) { + // Insert forward rate + if let Some(rates_perm1) = exchange_rates.get_mut(perm1) { + rates_perm1.insert(*perm0, common_rate2 / common_rate1); + } + + // Insert inverse rate + if let Some(rates_perm0) = exchange_rates.get_mut(perm0) { + if !rates_perm0.contains_key(perm1) { + rates_perm0.insert(*perm1, common_rate1 / common_rate2); + } + } + } + } + } + + let xrate = exchange_rates + .get(&from_currency.code) + .and_then(|quotes| quotes.get(&to_currency.code)) + .copied() + .unwrap_or(Decimal::ZERO); + + xrate +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use rust_decimal::prelude::FromPrimitive; + use rust_decimal_macros::dec; + + use super::*; + + // Helper function to create test quotes + fn setup_test_quotes() -> (HashMap, HashMap) { + let mut quotes_bid = HashMap::new(); + let mut quotes_ask = HashMap::new(); + + // Direct pairs + quotes_bid.insert(Symbol::from_str_unchecked("EUR/USD"), dec!(1.1000)); + quotes_ask.insert(Symbol::from_str_unchecked("EUR/USD"), dec!(1.1002)); + + quotes_bid.insert(Symbol::from_str_unchecked("GBP/USD"), dec!(1.3000)); + quotes_ask.insert(Symbol::from_str_unchecked("GBP/USD"), dec!(1.3002)); + + quotes_bid.insert(Symbol::from_str_unchecked("USD/JPY"), dec!(110.00)); + quotes_ask.insert(Symbol::from_str_unchecked("USD/JPY"), dec!(110.02)); + + quotes_bid.insert(Symbol::from_str_unchecked("AUD/USD"), dec!(0.7500)); + quotes_ask.insert(Symbol::from_str_unchecked("AUD/USD"), dec!(0.7502)); + + (quotes_bid, quotes_ask) + } + + #[test] + /// Test same currency conversion + fn test_same_currency() { + let (quotes_bid, quotes_ask) = setup_test_quotes(); + let rate = get_exchange_rate( + Currency::from_str("USD").unwrap(), + Currency::from_str("USD").unwrap(), + PriceType::Mid, + quotes_bid, + quotes_ask, + ); + assert_eq!(rate, Decimal::ONE); + } + + #[test] + /// Test direct pair conversion + fn test_direct_pair() { + let (quotes_bid, quotes_ask) = setup_test_quotes(); + + // Test bid price + let rate_bid = get_exchange_rate( + Currency::from_str("EUR").unwrap(), + Currency::from_str("USD").unwrap(), + PriceType::Bid, + quotes_bid.clone(), + quotes_ask.clone(), + ); + assert_eq!(rate_bid, dec!(1.1000)); + + // Test ask price + let rate_ask = get_exchange_rate( + Currency::from_str("EUR").unwrap(), + Currency::from_str("USD").unwrap(), + PriceType::Ask, + quotes_bid.clone(), + quotes_ask.clone(), + ); + assert_eq!(rate_ask, dec!(1.1002)); + + // Test mid price + let rate_mid = get_exchange_rate( + Currency::from_str("EUR").unwrap(), + Currency::from_str("USD").unwrap(), + PriceType::Mid, + quotes_bid, + quotes_ask, + ); + assert_eq!(rate_mid, dec!(1.1001)); + } + + #[test] + /// Test inverse pair calculation + fn test_inverse_pair() { + let (quotes_bid, quotes_ask) = setup_test_quotes(); + + let rate = get_exchange_rate( + Currency::from_str("USD").unwrap(), + Currency::from_str("EUR").unwrap(), + PriceType::Mid, + quotes_bid, + quotes_ask, + ); + + // USD/EUR should be approximately 1/1.1001 + let expected = Decimal::ONE / dec!(1.1001); + assert!((rate - expected).abs() < dec!(0.0001)); + } + + #[test] + /// Test cross pair calculation through USD + fn test_cross_pair_through_usd() { + let (quotes_bid, quotes_ask) = setup_test_quotes(); + + let rate = get_exchange_rate( + Currency::from_str("EUR").unwrap(), + Currency::from_str("JPY").unwrap(), + PriceType::Mid, + quotes_bid, + quotes_ask, + ); + + // EUR/JPY should be approximately EUR/USD * USD/JPY + let expected = dec!(1.1001) * dec!(110.01); + assert!((rate - expected).abs() < dec!(0.01)); + } + + #[test] + /// Test cross pair calculation through multiple paths + fn test_multiple_path_cross_pair() { + let (quotes_bid, quotes_ask) = setup_test_quotes(); + + let rate = get_exchange_rate( + Currency::from_str("GBP").unwrap(), + Currency::from_str("AUD").unwrap(), + PriceType::Mid, + quotes_bid, + quotes_ask, + ); + + // GBP/AUD should be calculated through USD + // GBP/USD * (1/AUD/USD) + let expected = dec!(1.3001) / dec!(0.7501); + assert!((rate - expected).abs() < dec!(0.01)); + } + + #[test] + /// Test handling of missing pairs + fn test_missing_pairs() { + let mut quotes_bid = HashMap::new(); + let mut quotes_ask = HashMap::new(); + + // Only adding one pair + quotes_bid.insert(Symbol::from_str_unchecked("EUR/USD"), dec!(1.1000)); + quotes_ask.insert(Symbol::from_str_unchecked("EUR/USD"), dec!(1.1002)); + + let rate = get_exchange_rate( + Currency::from_str("EUR").unwrap(), + Currency::from_str("JPY").unwrap(), + PriceType::Mid, + quotes_bid, + quotes_ask, + ); + + assert_eq!(rate, Decimal::ZERO); // Should return 0 for impossible conversions + } + + #[test] + #[should_panic] + fn test_empty_quotes() { + let quotes_bid = HashMap::new(); + let quotes_ask = HashMap::new(); + + let out_xrate = get_exchange_rate( + Currency::from_str("EUR").unwrap(), + Currency::from_str("USD").unwrap(), + PriceType::Mid, + quotes_bid, + quotes_ask, + ); + + assert_eq!(out_xrate, Decimal::ZERO); + } + + #[test] + #[should_panic] + fn test_unequal_quotes_length() { + let mut quotes_bid = HashMap::new(); + let mut quotes_ask = HashMap::new(); + + quotes_bid.insert(Symbol::from_str_unchecked("EUR/USD"), dec!(1.1000)); + quotes_bid.insert(Symbol::from_str_unchecked("GBP/USD"), dec!(1.3000)); + quotes_ask.insert(Symbol::from_str_unchecked("EUR/USD"), dec!(1.1002)); + + let out_xrate = get_exchange_rate( + Currency::from_str("EUR").unwrap(), + Currency::from_str("USD").unwrap(), + PriceType::Mid, + quotes_bid, + quotes_ask, + ); + + assert_eq!(out_xrate, Decimal::ZERO); + } + + #[test] + #[should_panic] + /// Test invalid price type handling + fn test_invalid_price_type() { + let (quotes_bid, quotes_ask) = setup_test_quotes(); + + let out_xrate = get_exchange_rate( + Currency::from_str("EUR").unwrap(), + Currency::from_str("USD").unwrap(), + PriceType::Last, // Invalid price type + quotes_bid, + quotes_ask, + ); + + assert_eq!(out_xrate, Decimal::ZERO); + } + + #[test] + /// Test extensive cross pairs + fn test_extensive_cross_pairs() { + let mut quotes_bid = HashMap::new(); + let mut quotes_ask = HashMap::new(); + + // Create a complex network of currency pairs + let pairs = vec![ + ("EUR/USD", (1.1000, 1.1002)), + ("GBP/USD", (1.3000, 1.3002)), + ("USD/JPY", (110.00, 110.02)), + ("EUR/GBP", (0.8461, 0.8463)), + ("AUD/USD", (0.7500, 0.7502)), + ("NZD/USD", (0.7000, 0.7002)), + ("USD/CAD", (1.2500, 1.2502)), + ]; + + for (pair, (bid, ask)) in pairs { + quotes_bid.insert( + Symbol::from_str_unchecked(pair), + Decimal::from_f64(bid).unwrap(), + ); + quotes_ask.insert( + Symbol::from_str_unchecked(pair), + Decimal::from_f64(ask).unwrap(), + ); + } + + // Test various cross pairs + let test_pairs = vec![ + ("EUR", "JPY", 121.022), // EUR/USD * USD/JPY + ("GBP", "JPY", 143.024), // GBP/USD * USD/JPY + ("AUD", "JPY", 82.51), // AUD/USD * USD/JPY + ("EUR", "CAD", 1.375), // EUR/USD * USD/CAD + ("NZD", "CAD", 0.875), // NZD/USD * USD/CAD + ("AUD", "NZD", 1.071), // AUD/USD / NZD/USD + ]; + + for (from, to, expected) in test_pairs { + let rate = get_exchange_rate( + Currency::from_str(from).unwrap(), + Currency::from_str(to).unwrap(), + PriceType::Mid, + quotes_bid.clone(), + quotes_ask.clone(), + ); + + let expected_dec = Decimal::from_f64(expected).unwrap(); + assert!( + (rate - expected_dec).abs() < dec!(0.01), + "Failed for pair {from}/{to}: got {rate}, expected {expected_dec}" + ); } } - // TODO: Improve efficiency - let empty: HashMap = HashMap::new(); - let quotes = exchange_rates.get(&from_currency.code).unwrap_or(&empty); + #[test] + /// Test rate consistency + fn test_rate_consistency() { + let (quotes_bid, quotes_ask) = setup_test_quotes(); - Ok(quotes.get(&to_currency.code).copied().unwrap_or(dec!(0.0))) + let rate_eur_usd = get_exchange_rate( + Currency::from_str("EUR").unwrap(), + Currency::from_str("USD").unwrap(), + PriceType::Mid, + quotes_bid.clone(), + quotes_ask.clone(), + ); + + let rate_usd_eur = get_exchange_rate( + Currency::from_str("USD").unwrap(), + Currency::from_str("EUR").unwrap(), + PriceType::Mid, + quotes_bid, + quotes_ask, + ); + + // Check if one rate is the inverse of the other + assert!((rate_eur_usd * rate_usd_eur - Decimal::ONE).abs() < dec!(0.0001)); + } } diff --git a/nautilus_core/model/src/accounts/any.rs b/nautilus_core/model/src/accounts/any.rs index e839d8977170..c328fe643feb 100644 --- a/nautilus_core/model/src/accounts/any.rs +++ b/nautilus_core/model/src/accounts/any.rs @@ -13,15 +13,19 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; use crate::{ accounts::{base::Account, cash::CashAccount, margin::MarginAccount}, enums::AccountType, - events::account::state::AccountState, + events::{account::state::AccountState, order::OrderFilled}, identifiers::AccountId, + instruments::any::InstrumentAny, + position::Position, + types::{balance::AccountBalance, currency::Currency, money::Money}, }; - #[derive(Debug, Clone, Serialize, Deserialize)] pub enum AccountAny { Margin(MarginAccount), @@ -58,6 +62,27 @@ impl AccountAny { } } + pub fn balances(&self) -> HashMap { + match self { + AccountAny::Margin(margin) => margin.balances(), + AccountAny::Cash(cash) => cash.balances(), + } + } + + pub fn balances_locked(&self) -> HashMap { + match self { + AccountAny::Margin(margin) => margin.balances_locked(), + AccountAny::Cash(cash) => cash.balances_locked(), + } + } + + pub fn base_currency(&self) -> Option { + match self { + AccountAny::Margin(margin) => margin.base_currency(), + AccountAny::Cash(cash) => cash.base_currency(), + } + } + pub fn from_events(events: Vec) -> anyhow::Result { if events.is_empty() { anyhow::bail!("No order events provided to create `AccountAny`"); @@ -70,6 +95,18 @@ impl AccountAny { } Ok(account) } + + pub fn calculate_pnls( + &self, + instrument: InstrumentAny, + fill: OrderFilled, + position: Option, + ) -> anyhow::Result> { + match self { + AccountAny::Margin(margin) => margin.calculate_pnls(instrument, fill, position), + AccountAny::Cash(cash) => cash.calculate_pnls(instrument, fill, position), + } + } } impl From for AccountAny { diff --git a/nautilus_core/model/src/accounts/base.rs b/nautilus_core/model/src/accounts/base.rs index fbfe3bcdbbb2..bb8279212104 100644 --- a/nautilus_core/model/src/accounts/base.rs +++ b/nautilus_core/model/src/accounts/base.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; -use rust_decimal::prelude::ToPrimitive; +use rust_decimal::{prelude::ToPrimitive, Decimal}; use serde::{Deserialize, Serialize}; use crate::{ @@ -135,6 +135,18 @@ impl BaseAccount { } } + pub fn update_commissions(&mut self, commission: Money) { + if commission.as_decimal() == Decimal::ZERO { + return; + } + + let currency = commission.currency; + let total_commissions = self.commissions.get(¤cy).unwrap_or(&0.0); + + self.commissions + .insert(currency, total_commissions + commission.as_f64()); + } + pub fn base_apply(&mut self, event: AccountState) { self.update_balances(event.balances.clone()); self.events.push(event); diff --git a/nautilus_core/model/src/accounts/cash.rs b/nautilus_core/model/src/accounts/cash.rs index adee23b08b68..0b656d476eaf 100644 --- a/nautilus_core/model/src/accounts/cash.rs +++ b/nautilus_core/model/src/accounts/cash.rs @@ -19,6 +19,7 @@ use std::{ ops::{Deref, DerefMut}, }; +use rust_decimal::{prelude::ToPrimitive, Decimal}; use serde::{Deserialize, Serialize}; use crate::{ @@ -66,6 +67,36 @@ impl CashAccount { pub const fn is_unleveraged(&self) -> bool { false } + + pub fn recalculate_balance(&mut self, currency: Currency) { + let current_balance = match self.balances.get(¤cy) { + Some(balance) => *balance, + None => { + return; + } + }; + + let total_locked = self + .balances + .values() + .filter(|balance| balance.currency == currency) + .fold(Decimal::ZERO, |acc, balance| { + acc + balance.locked.as_decimal() + }); + + let new_balance = AccountBalance::new( + current_balance.total, + Money::new(total_locked.to_f64().unwrap(), currency), + Money::new( + (current_balance.total.as_decimal() - total_locked) + .to_f64() + .unwrap(), + currency, + ), + ); + + self.balances.insert(currency, new_balance); + } } impl Account for CashAccount { diff --git a/nautilus_core/model/src/events/order/any.rs b/nautilus_core/model/src/events/order/any.rs index 59e02cd07850..e763578d02c8 100644 --- a/nautilus_core/model/src/events/order/any.rs +++ b/nautilus_core/model/src/events/order/any.rs @@ -26,7 +26,7 @@ use crate::{ OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted, OrderTriggered, OrderUpdated, }, - identifiers::{ClientOrderId, StrategyId, TraderId}, + identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId}, }; /// Wraps an `OrderEvent` allowing polymorphism. @@ -144,6 +144,52 @@ impl OrderEventAny { } } + #[must_use] + pub fn account_id(&self) -> Option { + match self { + Self::Initialized(event) => event.account_id(), + Self::Denied(event) => event.account_id(), + Self::Emulated(event) => event.account_id(), + Self::Released(event) => event.account_id(), + Self::Submitted(event) => event.account_id(), + Self::Accepted(event) => event.account_id(), + Self::Rejected(event) => event.account_id(), + Self::Canceled(event) => event.account_id(), + Self::Expired(event) => event.account_id(), + Self::Triggered(event) => event.account_id(), + Self::PendingUpdate(event) => event.account_id(), + Self::PendingCancel(event) => event.account_id(), + Self::ModifyRejected(event) => event.account_id(), + Self::CancelRejected(event) => event.account_id(), + Self::Updated(event) => event.account_id(), + Self::PartiallyFilled(event) => event.account_id(), + Self::Filled(event) => event.account_id(), + } + } + + #[must_use] + pub fn instrument_id(&self) -> InstrumentId { + match self { + Self::Initialized(event) => event.instrument_id(), + Self::Denied(event) => event.instrument_id(), + Self::Emulated(event) => event.instrument_id(), + Self::Released(event) => event.instrument_id(), + Self::Submitted(event) => event.instrument_id(), + Self::Accepted(event) => event.instrument_id(), + Self::Rejected(event) => event.instrument_id(), + Self::Canceled(event) => event.instrument_id(), + Self::Expired(event) => event.instrument_id(), + Self::Triggered(event) => event.instrument_id(), + Self::PendingUpdate(event) => event.instrument_id(), + Self::PendingCancel(event) => event.instrument_id(), + Self::ModifyRejected(event) => event.instrument_id(), + Self::CancelRejected(event) => event.instrument_id(), + Self::Updated(event) => event.instrument_id(), + Self::PartiallyFilled(event) => event.instrument_id(), + Self::Filled(event) => event.instrument_id(), + } + } + #[must_use] pub fn strategy_id(&self) -> StrategyId { match self { diff --git a/nautilus_core/model/src/events/position/mod.rs b/nautilus_core/model/src/events/position/mod.rs index 5338a1d16293..a81dd4e0b46d 100644 --- a/nautilus_core/model/src/events/position/mod.rs +++ b/nautilus_core/model/src/events/position/mod.rs @@ -13,10 +13,10 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use crate::events::position::{ - changed::PositionChanged, closed::PositionClosed, opened::PositionOpened, +use crate::{ + events::position::{changed::PositionChanged, closed::PositionClosed, opened::PositionOpened}, + identifiers::{AccountId, InstrumentId}, }; - pub mod changed; pub mod closed; pub mod opened; @@ -27,3 +27,21 @@ pub enum PositionEvent { PositionChanged(PositionChanged), PositionClosed(PositionClosed), } + +impl PositionEvent { + pub fn instrument_id(&self) -> InstrumentId { + match self { + PositionEvent::PositionOpened(position) => position.instrument_id, + PositionEvent::PositionChanged(position) => position.instrument_id, + PositionEvent::PositionClosed(position) => position.instrument_id, + } + } + + pub fn account_id(&self) -> AccountId { + match self { + PositionEvent::PositionOpened(position) => position.account_id, + PositionEvent::PositionChanged(position) => position.account_id, + PositionEvent::PositionClosed(position) => position.account_id, + } + } +} diff --git a/nautilus_core/portfolio/Cargo.toml b/nautilus_core/portfolio/Cargo.toml index e1c9743e1521..74d16c2eada6 100644 --- a/nautilus_core/portfolio/Cargo.toml +++ b/nautilus_core/portfolio/Cargo.toml @@ -28,6 +28,7 @@ serde_json = { workspace = true } strum = { workspace = true } thiserror = { workspace = true } ustr = { workspace = true } +uuid = { workspace = true } [dev-dependencies] criterion = { workspace = true } diff --git a/nautilus_core/portfolio/src/manager.rs b/nautilus_core/portfolio/src/manager.rs index c1edae0f75bc..4e7d96b82896 100644 --- a/nautilus_core/portfolio/src/manager.rs +++ b/nautilus_core/portfolio/src/manager.rs @@ -15,31 +15,30 @@ //! Provides account management functionality. -// Under development -#![allow(dead_code)] -#![allow(unused_variables)] - use std::{cell::RefCell, rc::Rc}; use nautilus_common::{cache::Cache, clock::Clock}; -use nautilus_core::nanos::UnixNanos; +use nautilus_core::{ffi::uuid::uuid4_new, nanos::UnixNanos}; use nautilus_model::{ - accounts::{any::AccountAny, cash::CashAccount, margin::MarginAccount}, - enums::OrderSideSpecified, + accounts::{any::AccountAny, base::Account, cash::CashAccount, margin::MarginAccount}, + enums::{AccountType, OrderSide, OrderSideSpecified, PriceType}, events::{account::state::AccountState, order::OrderFilled}, instruments::any::InstrumentAny, orders::any::OrderAny, position::Position, - types::money::Money, + types::{balance::AccountBalance, money::Money}, }; -use rust_decimal::Decimal; - +use rust_decimal::{prelude::ToPrimitive, Decimal}; pub struct AccountsManager { clock: Rc>, cache: Rc>, } impl AccountsManager { + pub fn new(clock: Rc>, cache: Rc>) -> Self { + Self { clock, cache } + } + #[must_use] pub fn update_balances( &self, @@ -47,65 +46,638 @@ impl AccountsManager { instrument: InstrumentAny, fill: OrderFilled, ) -> AccountState { - todo!() + let cache = self.cache.borrow(); + let position_id = if let Some(position_id) = fill.position_id { + position_id + } else { + let positions_open = cache.positions_open(None, Some(&fill.instrument_id), None, None); + positions_open + .first() + .unwrap_or_else(|| panic!("List of Positions is empty")) + .id + }; + + let position = cache.position(&position_id); + + let pnls = account.calculate_pnls(instrument, fill, position.cloned()); + + // Calculate final PnL including commissions + match account.base_currency() { + Some(base_currency) => { + let pnl = pnls.map_or_else( + |_| Money::new(0.0, base_currency), + |pnl_list| { + pnl_list + .first() + .copied() + .unwrap_or_else(|| Money::new(0.0, base_currency)) + }, + ); + + self.update_balance_single_currency(account.clone(), &fill, pnl); + } + None => { + if let Ok(mut pnl_list) = pnls { + self.update_balance_multi_currency(account.clone(), fill, &mut pnl_list); + } + } + } + + // Generate and return account state + self.generate_account_state(account, fill.ts_event) } #[must_use] pub fn update_orders( &self, - account: AccountAny, + account: &AccountAny, instrument: InstrumentAny, - orders_open: &[OrderAny], + orders_open: Vec<&OrderAny>, ts_event: UnixNanos, - ) -> AccountState { - todo!() + ) -> Option<(AccountAny, AccountState)> { + match account.clone() { + AccountAny::Cash(cash_account) => self + .update_balance_locked(&cash_account, instrument, orders_open, ts_event) + .map(|(updated_cash_account, state)| { + (AccountAny::Cash(updated_cash_account), state) + }), + AccountAny::Margin(margin_account) => self + .update_margin_init(&margin_account, instrument, orders_open, ts_event) + .map(|(updated_margin_account, state)| { + (AccountAny::Margin(updated_margin_account), state) + }), + } } #[must_use] pub fn update_positions( &self, - account: MarginAccount, + account: &MarginAccount, instrument: InstrumentAny, - positions: &[Position], + positions: Vec<&Position>, ts_event: UnixNanos, - ) -> AccountState { - todo!() + ) -> Option<(MarginAccount, AccountState)> { + let mut total_margin_maint = Decimal::ZERO; + let mut base_xrate = Decimal::ZERO; + let mut currency = instrument.settlement_currency(); + let mut account = account.clone(); + + for position in positions { + assert_eq!( + position.instrument_id, + instrument.id(), + "Position not for instrument {}", + instrument.id() + ); + + if !position.is_open() { + continue; + } + + let margin_maint = match instrument { + InstrumentAny::Betting(i) => account.calculate_maintenance_margin( + i, + position.quantity, + instrument.make_price(position.avg_px_open), + None, + ), + InstrumentAny::BinaryOption(i) => account.calculate_maintenance_margin( + i, + position.quantity, + instrument.make_price(position.avg_px_open), + None, + ), + InstrumentAny::CryptoFuture(i) => account.calculate_maintenance_margin( + i, + position.quantity, + instrument.make_price(position.avg_px_open), + None, + ), + InstrumentAny::CryptoPerpetual(i) => account.calculate_maintenance_margin( + i, + position.quantity, + instrument.make_price(position.avg_px_open), + None, + ), + InstrumentAny::CurrencyPair(i) => account.calculate_maintenance_margin( + i, + position.quantity, + instrument.make_price(position.avg_px_open), + None, + ), + InstrumentAny::Equity(i) => account.calculate_maintenance_margin( + i, + position.quantity, + instrument.make_price(position.avg_px_open), + None, + ), + InstrumentAny::FuturesContract(i) => account.calculate_maintenance_margin( + i, + position.quantity, + instrument.make_price(position.avg_px_open), + None, + ), + InstrumentAny::FuturesSpread(i) => account.calculate_maintenance_margin( + i, + position.quantity, + instrument.make_price(position.avg_px_open), + None, + ), + InstrumentAny::OptionsContract(i) => account.calculate_maintenance_margin( + i, + position.quantity, + instrument.make_price(position.avg_px_open), + None, + ), + InstrumentAny::OptionsSpread(i) => account.calculate_maintenance_margin( + i, + position.quantity, + instrument.make_price(position.avg_px_open), + None, + ), + }; + + let mut margin_maint = margin_maint.as_decimal(); + + if let Some(base_currency) = account.base_currency { + if base_xrate.is_zero() { + currency = base_currency; + base_xrate = self.calculate_xrate_to_base( + AccountAny::Margin(account.clone()), + instrument.clone(), + position.entry.as_specified(), + ); + + if base_xrate == Decimal::ZERO { + log::debug!("Cannot calculate maintenance (position) margin: insufficient data for {}/{}", instrument.settlement_currency(), base_currency); + return None; + } + } + + margin_maint = (margin_maint * base_xrate).round_dp(currency.precision.into()); + } + + total_margin_maint += margin_maint; + } + + let margin_maint_money = Money::new(total_margin_maint.to_f64()?, currency); + account.update_maintenance_margin(instrument.id(), margin_maint_money); + + log::info!( + "{} margin_maint={}", + instrument.id(), + margin_maint_money.to_string() + ); + + // Generate and return account state + Some(( + account.clone(), + self.generate_account_state(AccountAny::Margin(account), ts_event), + )) } fn update_balance_locked( &self, - account: CashAccount, + account: &CashAccount, instrument: InstrumentAny, - fill: OrderFilled, - ) -> AccountState { - todo!() + orders_open: Vec<&OrderAny>, + ts_event: UnixNanos, + ) -> Option<(CashAccount, AccountState)> { + let mut account = account.clone(); + if orders_open.is_empty() { + let balance = account.balances.remove(&instrument.quote_currency()); + if let Some(balance) = balance { + account.recalculate_balance(balance.currency); + } + return Some(( + account.clone(), + self.generate_account_state(AccountAny::Cash(account), ts_event), + )); + } + + let mut total_locked = Decimal::ZERO; + let mut base_xrate = Decimal::ZERO; + + let mut currency = instrument.settlement_currency(); + + for order in orders_open { + assert_eq!( + order.instrument_id(), + instrument.id(), + "Order not for instrument {}", + instrument.id() + ); + assert!(order.is_open(), "Order is not open"); + + if order.price().is_none() && order.trigger_price().is_none() { + continue; + } + + let price = if order.price().is_some() { + order.price() + } else { + order.trigger_price() + }; + + let mut locked = account + .calculate_balance_locked( + instrument.clone(), + order.order_side(), + order.quantity(), + price?, + None, + ) + .unwrap() + .as_decimal(); + + if let Some(base_curr) = account.base_currency() { + if base_xrate.is_zero() { + currency = base_curr; + base_xrate = self.calculate_xrate_to_base( + AccountAny::Cash(account.clone()), + instrument.clone(), + order.order_side_specified(), + ); + } + + locked = (locked * base_xrate).round_dp(u32::from(currency.precision)); + } + + total_locked += locked; + } + + let locked_money = Money::new(total_locked.to_f64()?, currency); + + if let Some(balance) = account.balances.get_mut(&instrument.quote_currency()) { + balance.locked = locked_money; + let currency = balance.currency; + account.recalculate_balance(currency); + } + + log::info!( + "{} balance_locked={}", + instrument.id(), + locked_money.to_string() + ); + + Some(( + account.clone(), + self.generate_account_state(AccountAny::Cash(account), ts_event), + )) } fn update_margin_init( &self, - account: MarginAccount, + account: &MarginAccount, instrument: InstrumentAny, - orders_open: &[OrderAny], + orders_open: Vec<&OrderAny>, ts_event: UnixNanos, - ) -> AccountState { - todo!() + ) -> Option<(MarginAccount, AccountState)> { + let mut total_margin_init = Decimal::ZERO; + let mut base_xrate = Decimal::ZERO; + let mut currency = instrument.settlement_currency(); + let mut account = account.clone(); + + for order in orders_open { + assert_eq!( + order.instrument_id(), + instrument.id(), + "Order not for instrument {}", + instrument.id() + ); + + if !order.is_open() || (order.price().is_none() && order.trigger_price().is_none()) { + continue; + } + + let price = if order.price().is_some() { + order.price() + } else { + order.trigger_price() + }; + + let margin_init = match instrument { + InstrumentAny::Betting(i) => { + account.calculate_initial_margin(i, order.quantity(), price?, None) + } + InstrumentAny::BinaryOption(i) => { + account.calculate_initial_margin(i, order.quantity(), price?, None) + } + InstrumentAny::CryptoFuture(i) => { + account.calculate_initial_margin(i, order.quantity(), price?, None) + } + InstrumentAny::CryptoPerpetual(i) => { + account.calculate_initial_margin(i, order.quantity(), price?, None) + } + InstrumentAny::CurrencyPair(i) => { + account.calculate_initial_margin(i, order.quantity(), price?, None) + } + InstrumentAny::Equity(i) => { + account.calculate_initial_margin(i, order.quantity(), price?, None) + } + InstrumentAny::FuturesContract(i) => { + account.calculate_initial_margin(i, order.quantity(), price?, None) + } + InstrumentAny::FuturesSpread(i) => { + account.calculate_initial_margin(i, order.quantity(), price?, None) + } + InstrumentAny::OptionsContract(i) => { + account.calculate_initial_margin(i, order.quantity(), price?, None) + } + InstrumentAny::OptionsSpread(i) => { + account.calculate_initial_margin(i, order.quantity(), price?, None) + } + }; + + let mut margin_init = margin_init.as_decimal(); + + if let Some(base_currency) = account.base_currency { + if base_xrate.is_zero() { + currency = base_currency; + base_xrate = self.calculate_xrate_to_base( + AccountAny::Margin(account.clone()), + instrument.clone(), + order.order_side_specified(), + ); + + if base_xrate == Decimal::ZERO { + log::debug!( + "Cannot calculate initial margin: insufficient data for {}/{}", + instrument.settlement_currency(), + base_currency + ); + continue; + } + } + + margin_init = (margin_init * base_xrate).round_dp(currency.precision.into()); + } + + total_margin_init += margin_init; + } + + let money = Money::new(total_margin_init.to_f64().unwrap_or(0.0), currency); + let margin_init_money = { + account.update_initial_margin(instrument.id(), money); + money + }; + + log::info!( + "{} margin_init={}", + instrument.id(), + margin_init_money.to_string() + ); + + Some(( + account.clone(), + self.generate_account_state(AccountAny::Margin(account), ts_event), + )) } - fn update_balance_single_currency(&self, account: AccountAny, fill: OrderFilled, pnl: Money) { - todo!() + fn update_balance_single_currency( + &self, + account: AccountAny, + fill: &OrderFilled, + mut pnl: Money, + ) { + let base_currency = if let Some(currency) = account.base_currency() { + currency + } else { + log::error!("Account has no base currency set"); + return; + }; + + let mut balances = Vec::new(); + let mut commission = fill.commission; + + if let Some(ref mut comm) = commission { + if comm.currency != base_currency { + let xrate = self.cache.borrow().get_xrate( + fill.instrument_id.venue, + comm.currency, + base_currency, + if fill.order_side == OrderSide::Sell { + PriceType::Bid + } else { + PriceType::Ask + }, + ); + + if xrate.is_zero() { + log::error!( + "Cannot calculate account state: insufficient data for {}/{}", + comm.currency, + base_currency + ); + return; + } + + *comm = Money::new((comm.as_decimal() * xrate).to_f64().unwrap(), base_currency); + } + } + + if pnl.currency != base_currency { + let xrate = self.cache.borrow().get_xrate( + fill.instrument_id.venue, + pnl.currency, + base_currency, + if fill.order_side == OrderSide::Sell { + PriceType::Bid + } else { + PriceType::Ask + }, + ); + + if xrate.is_zero() { + log::error!( + "Cannot calculate account state: insufficient data for {}/{}", + pnl.currency, + base_currency + ); + return; + } + + pnl = Money::new((pnl.as_decimal() * xrate).to_f64().unwrap(), base_currency); + } + + if let Some(comm) = commission { + pnl -= comm; + } + + if pnl.is_zero() { + return; + } + + let existing_balances = account.balances(); + let balance = if let Some(b) = existing_balances.get(&pnl.currency) { + b + } else { + log::error!( + "Cannot complete transaction: no balance for {}", + pnl.currency + ); + return; + }; + + let new_balance = + AccountBalance::new(balance.total + pnl, balance.locked, balance.free + pnl); + balances.push(new_balance); + + match account { + AccountAny::Cash(mut cash) => { + cash.update_balances(balances); + if let Some(comm) = commission { + cash.update_commissions(comm); + } + } + AccountAny::Margin(mut margin) => { + margin.update_balances(balances); + if let Some(comm) = commission { + margin.update_commissions(comm); + } + } + } } fn update_balance_multi_currency( &self, account: AccountAny, fill: OrderFilled, - pnls: &[Money], + pnls: &mut [Money], ) { - todo!() + let mut new_balances = Vec::new(); + let commission = fill.commission; + let mut apply_commission = commission.map_or(false, |c| !c.is_zero()); + + for pnl in pnls.iter_mut() { + if apply_commission && pnl.currency == commission.unwrap().currency { + *pnl -= commission.unwrap(); + apply_commission = false; + } + + if pnl.is_zero() { + continue; // No Adjustment + } + + let currency = pnl.currency; + let balances = account.balances(); + + let new_balance = if let Some(balance) = balances.get(¤cy) { + let new_total = balance.total.as_f64() + pnl.as_f64(); + let new_free = balance.free.as_f64() + pnl.as_f64(); + let total = Money::new(new_total, currency); + let free = Money::new(new_free, currency); + + if new_total < 0.0 { + log::error!( + "AccountBalanceNegative: balance = {}, currency = {}", + total.as_decimal(), + currency + ); + return; + } + if new_free < 0.0 { + log::error!( + "AccountMarginExceeded: balance = {}, margin = {}, currency = {}", + total.as_decimal(), + balance.locked.as_decimal(), + currency + ); + return; + } + + AccountBalance::new(total, balance.locked, free) + } else { + if pnl.as_decimal() < Decimal::ZERO { + log::error!( + "Cannot complete transaction: no {} to deduct a {} realized PnL from", + currency, + pnl + ); + return; + } + AccountBalance::new(*pnl, Money::new(0.0, currency), *pnl) + }; + + new_balances.push(new_balance); + } + + if apply_commission { + let commission = commission.unwrap(); + let currency = commission.currency; + let balances = account.balances(); + + let commission_balance = if let Some(balance) = balances.get(¤cy) { + let new_total = balance.total.as_decimal() - commission.as_decimal(); + let new_free = balance.free.as_decimal() - commission.as_decimal(); + AccountBalance::new( + Money::new(new_total.to_f64().unwrap(), currency), + balance.locked, + Money::new(new_free.to_f64().unwrap(), currency), + ) + } else { + if commission.as_decimal() > Decimal::ZERO { + log::error!( + "Cannot complete transaction: no {} balance to deduct a {} commission from", + currency, + commission + ); + return; + } + AccountBalance::new( + Money::new(0.0, currency), + Money::new(0.0, currency), + Money::new(0.0, currency), + ) + }; + new_balances.push(commission_balance); + } + + if new_balances.is_empty() { + return; + } + + match account { + AccountAny::Cash(mut cash) => { + cash.update_balances(new_balances); + if let Some(commission) = commission { + cash.update_commissions(commission); + } + } + AccountAny::Margin(mut margin) => { + margin.update_balances(new_balances); + if let Some(commission) = commission { + margin.update_commissions(commission); + } + } + } } fn generate_account_state(&self, account: AccountAny, ts_event: UnixNanos) -> AccountState { - todo!() + match account { + AccountAny::Cash(cash_account) => AccountState::new( + cash_account.id, + AccountType::Cash, + cash_account.balances.clone().into_values().collect(), + vec![], + false, + uuid4_new(), + ts_event, + self.clock.borrow().timestamp_ns(), + cash_account.base_currency(), + ), + AccountAny::Margin(margin_account) => AccountState::new( + margin_account.id, + AccountType::Cash, + vec![], + margin_account.margins.clone().into_values().collect(), + false, + uuid4_new(), + ts_event, + self.clock.borrow().timestamp_ns(), + margin_account.base_currency(), + ), + } } fn calculate_xrate_to_base( @@ -114,6 +686,17 @@ impl AccountsManager { instrument: InstrumentAny, side: OrderSideSpecified, ) -> Decimal { - todo!() + match account.base_currency() { + None => Decimal::ONE, + Some(base_curr) => self.cache.borrow().get_xrate( + instrument.id().venue, + instrument.settlement_currency(), + base_curr, + match side { + OrderSideSpecified::Sell => PriceType::Bid, + OrderSideSpecified::Buy => PriceType::Ask, + }, + ), + } } } diff --git a/nautilus_core/portfolio/src/portfolio.rs b/nautilus_core/portfolio/src/portfolio.rs index 835e37e79596..05830620e2ea 100644 --- a/nautilus_core/portfolio/src/portfolio.rs +++ b/nautilus_core/portfolio/src/portfolio.rs @@ -14,38 +14,137 @@ // ------------------------------------------------------------------------------------------------- //! Provides a generic `Portfolio` for all environments. - -// Under development -#![allow(dead_code)] -#![allow(unused_variables)] - use std::{ + any::Any, cell::RefCell, collections::{HashMap, HashSet}, rc::Rc, + sync::Arc, }; -use nautilus_analysis::analyzer::PortfolioAnalyzer; -use nautilus_common::{cache::Cache, clock::Clock, msgbus::MessageBus}; +use nautilus_analysis::{ + analyzer::PortfolioAnalyzer, + statistics::{ + expectancy::Expectancy, long_ratio::LongRatio, loser_max::MaxLoser, loser_min::MinLoser, + profit_factor::ProfitFactor, returns_avg::ReturnsAverage, + returns_avg_loss::ReturnsAverageLoss, returns_avg_win::ReturnsAverageWin, + returns_volatility::ReturnsVolatility, risk_return_ratio::RiskReturnRatio, + sharpe_ratio::SharpeRatio, sortino_ratio::SortinoRatio, win_rate::WinRate, + winner_avg::AvgWinner, winner_max::MaxWinner, winner_min::MinWinner, + }, +}; +use nautilus_common::{ + cache::Cache, + clock::Clock, + messages::data::DataResponse, + msgbus::{ + handler::{MessageHandler, ShareableMessageHandler}, + MessageBus, + }, +}; use nautilus_model::{ accounts::any::AccountAny, - data::quote::QuoteTick, - enums::OrderSide, + data::{quote::QuoteTick, Data}, + enums::{OrderSide, OrderType, PositionSide, PriceType}, events::{account::state::AccountState, order::OrderEventAny, position::PositionEvent}, identifiers::{InstrumentId, Venue}, instruments::any::InstrumentAny, + orders::any::OrderAny, position::Position, - types::money::Money, + types::{currency::Currency, money::Money, price::Price}, }; -use rust_decimal::Decimal; +use rust_decimal::{ + prelude::{FromPrimitive, ToPrimitive}, + Decimal, +}; +use ustr::Ustr; +use uuid::Uuid; -pub struct Portfolio { - clock: Rc>, - cache: Rc>, - msgbus: Rc>, - accounts: HashMap, +use crate::manager::AccountsManager; + +struct UpdateQuoteTickHandler { + id: Ustr, + callback: Box, +} + +impl MessageHandler for UpdateQuoteTickHandler { + fn id(&self) -> Ustr { + self.id + } + + fn handle(&self, msg: &dyn Any) { + (self.callback)(msg.downcast_ref::<&QuoteTick>().unwrap()); + } + fn handle_response(&self, _resp: DataResponse) {} + fn handle_data(&self, _data: Data) {} + fn as_any(&self) -> &dyn Any { + self + } +} + +struct UpdateOrderHandler { + id: Ustr, + callback: Box, +} + +impl MessageHandler for UpdateOrderHandler { + fn id(&self) -> Ustr { + self.id + } + + fn handle(&self, msg: &dyn Any) { + (self.callback)(msg.downcast_ref::<&OrderEventAny>().unwrap()); + } + fn handle_response(&self, _resp: DataResponse) {} + fn handle_data(&self, _data: Data) {} + fn as_any(&self) -> &dyn Any { + self + } +} + +struct UpdatePositionHandler { + id: Ustr, + callback: Box, +} + +impl MessageHandler for UpdatePositionHandler { + fn id(&self) -> Ustr { + self.id + } + + fn handle(&self, msg: &dyn Any) { + (self.callback)(msg.downcast_ref::<&PositionEvent>().unwrap()); + } + fn handle_response(&self, _resp: DataResponse) {} + fn handle_data(&self, _data: Data) {} + fn as_any(&self) -> &dyn Any { + self + } +} + +struct UpdateAccountHandler { + id: Ustr, + callback: Box, +} + +impl MessageHandler for UpdateAccountHandler { + fn id(&self) -> Ustr { + self.id + } + + fn handle(&self, msg: &dyn Any) { + (self.callback)(msg.downcast_ref::<&AccountState>().unwrap()); + } + fn handle_response(&self, _resp: DataResponse) {} + fn handle_data(&self, _data: Data) {} + fn as_any(&self) -> &dyn Any { + self + } +} + +struct PortfolioState { + accounts: AccountsManager, analyzer: PortfolioAnalyzer, - // venue: Option, // Added for completeness but was meant to be a "temporary hack" unrealized_pnls: HashMap, realized_pnls: HashMap, net_positions: HashMap, @@ -53,156 +152,3154 @@ pub struct Portfolio { initialized: bool, } +impl PortfolioState { + fn new(clock: Rc>, cache: Rc>) -> Self { + let mut analyzer = PortfolioAnalyzer::new(); + analyzer.register_statistic(Arc::new(MaxWinner {})); + analyzer.register_statistic(Arc::new(AvgWinner {})); + analyzer.register_statistic(Arc::new(MinWinner {})); + analyzer.register_statistic(Arc::new(MinLoser {})); + analyzer.register_statistic(Arc::new(MaxLoser {})); + analyzer.register_statistic(Arc::new(Expectancy {})); + analyzer.register_statistic(Arc::new(WinRate {})); + analyzer.register_statistic(Arc::new(ReturnsVolatility::new(None))); + analyzer.register_statistic(Arc::new(ReturnsAverage {})); + analyzer.register_statistic(Arc::new(ReturnsAverageLoss {})); + analyzer.register_statistic(Arc::new(ReturnsAverageWin {})); + analyzer.register_statistic(Arc::new(SharpeRatio::new(None))); + analyzer.register_statistic(Arc::new(SortinoRatio::new(None))); + analyzer.register_statistic(Arc::new(ProfitFactor {})); + analyzer.register_statistic(Arc::new(RiskReturnRatio {})); + analyzer.register_statistic(Arc::new(LongRatio::new(None))); + + Self { + accounts: AccountsManager::new(clock, cache), + analyzer, + unrealized_pnls: HashMap::new(), + realized_pnls: HashMap::new(), + net_positions: HashMap::new(), + pending_calcs: HashSet::new(), + initialized: false, + } + } + + fn reset(&mut self) { + log::debug!("RESETTING"); + self.net_positions.clear(); + self.unrealized_pnls.clear(); + self.realized_pnls.clear(); + self.pending_calcs.clear(); + self.analyzer.reset(); + log::debug!("READY"); + } +} + +pub struct Portfolio { + clock: Rc>, + cache: Rc>, + msgbus: Rc>, + inner: Rc>, +} + impl Portfolio { - // pub fn set_specific_venue(&mut self, venue: Venue) { // Lets try not to use this? - // todo!() - // } + pub fn new( + msgbus: Rc>, + cache: Rc>, + clock: Rc>, + ) -> Self { + let inner = Rc::new(RefCell::new(PortfolioState::new( + clock.clone(), + cache.clone(), + ))); - // -- QUERIES --------------------------------------------------------------------------------- + Self::register_message_handlers( + msgbus.clone(), + cache.clone(), + clock.clone(), + inner.clone(), + ); - #[must_use] - pub const fn is_initialized(&self) -> bool { - self.initialized + Self { + clock, + cache, + msgbus, + inner, + } } - #[must_use] - pub const fn analyzer(&self) -> &PortfolioAnalyzer { - &self.analyzer + fn register_message_handlers( + msgbus: Rc>, + cache: Rc>, + clock: Rc>, + inner: Rc>, + ) { + let update_account_handler = { + let cache = cache.clone(); + ShareableMessageHandler(Rc::new(UpdateAccountHandler { + id: Ustr::from(&Uuid::new_v4().to_string()), + callback: Box::new(move |event: &AccountState| { + update_account(cache.clone(), event); + }), + })) + }; + + let update_position_handler = { + let cache = cache.clone(); + let msgbus = msgbus.clone(); + let clock = clock.clone(); + let inner = inner.clone(); + ShareableMessageHandler(Rc::new(UpdatePositionHandler { + id: Ustr::from(&Uuid::new_v4().to_string()), + callback: Box::new(move |event: &PositionEvent| { + update_position( + cache.clone(), + msgbus.clone(), + clock.clone(), + inner.clone(), + event, + ); + }), + })) + }; + + let update_quote_handler = { + let cache = cache.clone(); + let msgbus = msgbus.clone(); + let clock = clock.clone(); + let inner = inner.clone(); + ShareableMessageHandler(Rc::new(UpdateQuoteTickHandler { + id: Ustr::from(&Uuid::new_v4().to_string()), + callback: Box::new(move |quote: &QuoteTick| { + update_quote_tick( + cache.clone(), + msgbus.clone(), + clock.clone(), + inner.clone(), + quote, + ); + }), + })) + }; + + let update_order_handler = { + let cache = cache; + let msgbus = msgbus.clone(); + let clock = clock.clone(); + let inner = inner; + ShareableMessageHandler(Rc::new(UpdateOrderHandler { + id: Ustr::from(&Uuid::new_v4().to_string()), + callback: Box::new(move |event: &OrderEventAny| { + update_order( + cache.clone(), + msgbus.clone(), + clock.clone(), + inner.clone(), + event, + ); + }), + })) + }; + + let mut borrowed_msgbus = msgbus.borrow_mut(); + borrowed_msgbus.register("Portfolio.update_account", update_account_handler.clone()); + + borrowed_msgbus.subscribe("data.quotes.*", update_quote_handler, Some(10)); + borrowed_msgbus.subscribe("events.order.*", update_order_handler, Some(10)); + borrowed_msgbus.subscribe("events.position.*", update_position_handler, Some(10)); + borrowed_msgbus.subscribe("events.account.*", update_account_handler, Some(10)); + } + + pub fn reset(&mut self) { + log::debug!("RESETTING"); + self.inner.borrow_mut().reset(); + log::debug!("READY"); } + // -- QUERIES --------------------------------------------------------------------------------- + #[must_use] - pub fn account(&self, venue: &Venue) -> Option<&AccountAny> { - self.accounts.get(venue) + pub fn is_initialized(&self) -> bool { + self.inner.borrow().initialized } #[must_use] - pub fn balances_locked(&self, venue: &Venue) -> HashMap { - todo!() + pub fn balances_locked(&self, venue: &Venue) -> HashMap { + self.cache.borrow().account_for_venue(venue).map_or_else( + || { + log::error!( + "Cannot get balances locked: no account generated for {}", + venue + ); + HashMap::new() + }, + nautilus_model::accounts::any::AccountAny::balances_locked, + ) } #[must_use] - pub fn margins_init(&self, venue: &Venue) -> HashMap { - todo!() + pub fn margins_init(&self, venue: &Venue) -> HashMap { + self.cache.borrow().account_for_venue(venue).map_or_else( + || { + log::error!( + "Cannot get initial (order) margins: no account registered for {}", + venue + ); + HashMap::new() + }, + |account| match account { + AccountAny::Margin(margin_account) => margin_account.initial_margins(), + AccountAny::Cash(_) => { + log::warn!("Initial margins not applicable for cash account"); + HashMap::new() + } + }, + ) } #[must_use] - pub fn margins_maint(&self, venue: &Venue) -> HashMap { - todo!() + pub fn margins_maint(&self, venue: &Venue) -> HashMap { + self.cache.borrow().account_for_venue(venue).map_or_else( + || { + log::error!( + "Cannot get maintenance (position) margins: no account registered for {}", + venue + ); + HashMap::new() + }, + |account| match account { + AccountAny::Margin(margin_account) => margin_account.maintenance_margins(), + AccountAny::Cash(_) => { + log::warn!("Maintenance margins not applicable for cash account"); + HashMap::new() + } + }, + ) } #[must_use] - pub fn unrealized_pnls(&self, venue: &Venue) -> HashMap { - todo!() + pub fn unrealized_pnls(&mut self, venue: &Venue) -> HashMap { + let instrument_ids = { + let borrowed_cache = self.cache.borrow(); + let positions = borrowed_cache.positions(Some(venue), None, None, None); + + if positions.is_empty() { + return HashMap::new(); // Nothing to calculate + } + + let instrument_ids: HashSet = + positions.iter().map(|p| p.instrument_id).collect(); + + instrument_ids + }; + + let mut unrealized_pnls: HashMap = HashMap::new(); + + for instrument_id in instrument_ids { + if let Some(&pnl) = self.inner.borrow_mut().unrealized_pnls.get(&instrument_id) { + // PnL already calculated + *unrealized_pnls.entry(pnl.currency).or_insert(0.0) += pnl.as_f64(); + continue; + } + + // Calculate PnL + match self.calculate_unrealized_pnl(&instrument_id) { + Some(pnl) => *unrealized_pnls.entry(pnl.currency).or_insert(0.0) += pnl.as_f64(), + None => continue, + } + } + + unrealized_pnls + .into_iter() + .map(|(currency, amount)| (currency, Money::new(amount, currency))) + .collect() } #[must_use] - pub fn realized_pnls(&self, venue: &Venue) -> HashMap { - todo!() + pub fn realized_pnls(&mut self, venue: &Venue) -> HashMap { + let instrument_ids = { + let borrowed_cache = self.cache.borrow(); + let positions = borrowed_cache.positions(Some(venue), None, None, None); + + if positions.is_empty() { + return HashMap::new(); // Nothing to calculate + } + + let instrument_ids: HashSet = + positions.iter().map(|p| p.instrument_id).collect(); + + instrument_ids + }; + + let mut realized_pnls: HashMap = HashMap::new(); + + for instrument_id in instrument_ids { + if let Some(&pnl) = self.inner.borrow_mut().realized_pnls.get(&instrument_id) { + // PnL already calculated + *realized_pnls.entry(pnl.currency).or_insert(0.0) += pnl.as_f64(); + continue; + } + + // Calculate PnL + match self.calculate_realized_pnl(&instrument_id) { + Some(pnl) => *realized_pnls.entry(pnl.currency).or_insert(0.0) += pnl.as_f64(), + None => continue, + } + } + + realized_pnls + .into_iter() + .map(|(currency, amount)| (currency, Money::new(amount, currency))) + .collect() } #[must_use] - pub fn net_exposures(&self, venue: &Venue) -> HashMap { - todo!() + pub fn net_exposures(&self, venue: &Venue) -> Option> { + let borrowed_cache = self.cache.borrow(); + let account = if let Some(account) = borrowed_cache.account_for_venue(venue) { + account + } else { + log::error!( + "Cannot calculate net exposures: no account registered for {}", + venue + ); + return None; // Cannot calculate + }; + + let positions_open = borrowed_cache.positions_open(Some(venue), None, None, None); + if positions_open.is_empty() { + return Some(HashMap::new()); // Nothing to calculate + } + + let mut net_exposures: HashMap = HashMap::new(); + + for position in positions_open { + let instrument = + if let Some(instrument) = borrowed_cache.instrument(&position.instrument_id) { + instrument + } else { + log::error!( + "Cannot calculate net exposures: no instrument for {}", + position.instrument_id + ); + return None; // Cannot calculate + }; + + if position.side == PositionSide::Flat { + log::error!( + "Cannot calculate net exposures: position is flat for {}", + position.instrument_id + ); + continue; // Nothing to calculate + } + + let last = self.get_last_price(position)?; + let xrate = self.calculate_xrate_to_base(instrument, account, position.entry); + if xrate == 0.0 { + log::error!( + "Cannot calculate net exposures: insufficient data for {}/{:?}", + instrument.settlement_currency(), + account.base_currency() + ); + return None; // Cannot calculate + } + + let settlement_currency = account + .base_currency() + .unwrap_or_else(|| instrument.settlement_currency()); + + let net_exposure = instrument + .calculate_notional_value(position.quantity, last, None) + .as_f64() + * xrate; + + let net_exposure = (net_exposure * 10f64.powi(settlement_currency.precision.into())) + .round() + / 10f64.powi(settlement_currency.precision.into()); + + *net_exposures.entry(settlement_currency).or_insert(0.0) += net_exposure; + } + + Some( + net_exposures + .into_iter() + .map(|(currency, amount)| (currency, Money::new(amount, currency))) + .collect(), + ) } #[must_use] - pub fn unrealized_pnl(&self, instrument_id: &InstrumentId) -> Option { - todo!() + pub fn unrealized_pnl(&mut self, instrument_id: &InstrumentId) -> Option { + if let Some(pnl) = self + .inner + .borrow() + .unrealized_pnls + .get(instrument_id) + .copied() + { + return Some(pnl); + } + + let pnl = self.calculate_unrealized_pnl(instrument_id)?; + self.inner + .borrow_mut() + .unrealized_pnls + .insert(*instrument_id, pnl); + Some(pnl) } #[must_use] - pub fn realized_pnl(&self, instrument_id: &InstrumentId) -> Option { - todo!() + pub fn realized_pnl(&mut self, instrument_id: &InstrumentId) -> Option { + if let Some(pnl) = self + .inner + .borrow() + .realized_pnls + .get(instrument_id) + .copied() + { + return Some(pnl); + } + + let pnl = self.calculate_realized_pnl(instrument_id)?; + self.inner + .borrow_mut() + .realized_pnls + .insert(*instrument_id, pnl); + Some(pnl) } #[must_use] pub fn net_exposure(&self, instrument_id: &InstrumentId) -> Option { - todo!() + let borrowed_cache = self.cache.borrow(); + let account = if let Some(account) = borrowed_cache.account_for_venue(&instrument_id.venue) + { + account + } else { + log::error!( + "Cannot calculate net exposure: no account registered for {}", + instrument_id.venue + ); + return None; + }; + + let instrument = if let Some(instrument) = borrowed_cache.instrument(instrument_id) { + instrument + } else { + log::error!( + "Cannot calculate net exposure: no instrument for {}", + instrument_id + ); + return None; + }; + + let positions_open = borrowed_cache.positions_open( + None, // Faster query filtering + Some(instrument_id), + None, + None, + ); + + if positions_open.is_empty() { + return Some(Money::new(0.0, instrument.settlement_currency())); + } + + let mut net_exposure = 0.0; + + for position in positions_open { + let last = self.get_last_price(position)?; + let xrate = self.calculate_xrate_to_base(instrument, account, position.entry); + if xrate == 0.0 { + log::error!( + "Cannot calculate net exposure: insufficient data for {}/{:?}", + instrument.settlement_currency(), + account.base_currency() + ); + return None; + } + + let notional_value = instrument + .calculate_notional_value(position.quantity, last, None) + .as_f64(); + + net_exposure += notional_value * xrate; + } + + let settlement_currency = account + .base_currency() + .unwrap_or_else(|| instrument.settlement_currency()); + + Some(Money::new(net_exposure, settlement_currency)) } #[must_use] - pub fn net_position(&self, instrument_id: &InstrumentId) -> Option { - todo!() + pub fn net_position(&self, instrument_id: &InstrumentId) -> Decimal { + self.inner + .borrow() + .net_positions + .get(instrument_id) + .copied() + .unwrap_or(Decimal::ZERO) } #[must_use] pub fn is_net_long(&self, instrument_id: &InstrumentId) -> bool { - todo!() + self.inner + .borrow() + .net_positions + .get(instrument_id) + .copied() + .map_or_else(|| false, |net_position| net_position > Decimal::ZERO) } #[must_use] pub fn is_net_short(&self, instrument_id: &InstrumentId) -> bool { - todo!() + self.inner + .borrow() + .net_positions + .get(instrument_id) + .copied() + .map_or_else(|| false, |net_position| net_position < Decimal::ZERO) } #[must_use] pub fn is_flat(&self, instrument_id: &InstrumentId) -> bool { - todo!() + self.inner + .borrow() + .net_positions + .get(instrument_id) + .copied() + .map_or_else(|| true, |net_position| net_position == Decimal::ZERO) } #[must_use] pub fn is_completely_flat(&self) -> bool { - todo!() + for net_position in self.inner.borrow().net_positions.values() { + if *net_position != Decimal::ZERO { + return false; + } + } + true } // -- COMMANDS -------------------------------------------------------------------------------- pub fn initialize_orders(&mut self) { - todo!() + let mut initialized = true; + let orders_and_instruments = { + let borrowed_cache = self.cache.borrow(); + let all_orders_open = borrowed_cache.orders_open(None, None, None, None); + + let mut instruments_with_orders = Vec::new(); + let mut instruments = HashSet::new(); + + for order in &all_orders_open { + instruments.insert(order.instrument_id()); + } + + for instrument_id in instruments { + if let Some(instrument) = borrowed_cache.instrument(&instrument_id) { + let orders = borrowed_cache + .orders_open(None, Some(&instrument_id), None, None) + .into_iter() + .cloned() + .collect::>(); + instruments_with_orders.push((instrument.clone(), orders)); + } else { + log::error!( + "Cannot update initial (order) margin: no instrument found for {}", + instrument_id + ); + initialized = false; + break; + } + } + instruments_with_orders + }; + + for (instrument, orders_open) in &orders_and_instruments { + let mut borrowed_cache = self.cache.borrow_mut(); + let account = + if let Some(account) = borrowed_cache.account_for_venue(&instrument.id().venue) { + account + } else { + log::error!( + "Cannot update initial (order) margin: no account registered for {}", + instrument.id().venue + ); + initialized = false; + break; + }; + + let result = self.inner.borrow_mut().accounts.update_orders( + account, + instrument.clone(), + orders_open.iter().collect(), + self.clock.borrow().timestamp_ns(), + ); + + match result { + Some((updated_account, _)) => { + borrowed_cache.add_account(updated_account).unwrap(); // Temp Fix to update the mutated account + } + None => { + initialized = false; + } + } + } + + let total_orders = orders_and_instruments + .into_iter() + .map(|(_, orders)| orders.len()) + .sum::(); + + log::info!( + "Initialized {} open order{}", + total_orders, + if total_orders == 1 { "" } else { "s" } + ); + + self.inner.borrow_mut().initialized = initialized; } pub fn initialize_positions(&mut self) { - todo!() + self.inner.borrow_mut().unrealized_pnls.clear(); + self.inner.borrow_mut().realized_pnls.clear(); + let all_positions_open: Vec; + let mut instruments = HashSet::new(); + { + let borrowed_cache = self.cache.borrow(); + all_positions_open = borrowed_cache + .positions_open(None, None, None, None) + .into_iter() + .cloned() + .collect(); + for position in &all_positions_open { + instruments.insert(position.instrument_id); + } + } + + let mut initialized = true; + + for instrument_id in instruments { + let positions_open: Vec = { + let borrowed_cache = self.cache.borrow(); + borrowed_cache + .positions_open(None, Some(&instrument_id), None, None) + .into_iter() + .cloned() + .collect() + }; + + self.update_net_position(&instrument_id, positions_open); + + let calculated_unrealized_pnl = self + .calculate_unrealized_pnl(&instrument_id) + .expect("Failed to calculate unrealized PnL"); + let calculated_realized_pnl = self + .calculate_realized_pnl(&instrument_id) + .expect("Failed to calculate realized PnL"); + + self.inner + .borrow_mut() + .unrealized_pnls + .insert(instrument_id, calculated_unrealized_pnl); + self.inner + .borrow_mut() + .realized_pnls + .insert(instrument_id, calculated_realized_pnl); + + let borrowed_cache = self.cache.borrow(); + let account = + if let Some(account) = borrowed_cache.account_for_venue(&instrument_id.venue) { + account + } else { + log::error!( + "Cannot update maintenance (position) margin: no account registered for {}", + instrument_id.venue + ); + initialized = false; + break; + }; + + let account = match account { + AccountAny::Cash(_) => continue, + AccountAny::Margin(margin_account) => margin_account, + }; + + let mut borrowed_cache = self.cache.borrow_mut(); + let instrument = if let Some(instrument) = borrowed_cache.instrument(&instrument_id) { + instrument + } else { + log::error!( + "Cannot update maintenance (position) margin: no instrument found for {}", + instrument_id + ); + initialized = false; + break; + }; + + let result = self.inner.borrow_mut().accounts.update_positions( + account, + instrument.clone(), + self.cache + .borrow() + .positions_open(None, Some(&instrument_id), None, None), + self.clock.borrow().timestamp_ns(), + ); + + match result { + Some((updated_account, _)) => { + borrowed_cache + .add_account(AccountAny::Margin(updated_account)) // Temp Fix to update the mutated account + .unwrap(); + } + None => { + initialized = false; + } + } + } + + let open_count = all_positions_open.len(); + self.inner.borrow_mut().initialized = initialized; + log::info!( + "Initialized {} open position{}", + open_count, + if open_count == 1 { "" } else { "s" } + ); } pub fn update_quote_tick(&mut self, quote: &QuoteTick) { - todo!() + update_quote_tick( + self.cache.clone(), + self.msgbus.clone(), + self.clock.clone(), + self.inner.clone(), + quote, + ); } pub fn update_account(&mut self, event: &AccountState) { - todo!() + update_account(self.cache.clone(), event); } pub fn update_order(&mut self, event: &OrderEventAny) { - todo!() + update_order( + self.cache.clone(), + self.msgbus.clone(), + self.clock.clone(), + self.inner.clone(), + event, + ); } pub fn update_position(&mut self, event: &PositionEvent) { - todo!() + update_position( + self.cache.clone(), + self.msgbus.clone(), + self.clock.clone(), + self.inner.clone(), + event, + ); } // -- INTERNAL -------------------------------------------------------------------------------- - // fn net_position(&self, instrument_id: &InstrumentId) -> Decimal { // Same as above? - // todo!() - // } + fn update_net_position(&mut self, instrument_id: &InstrumentId, positions_open: Vec) { + let mut net_position = Decimal::ZERO; - fn update_net_position( - &self, - instrument_id: &InstrumentId, - positions_open: Vec<&Position>, - ) -> Decimal { - todo!() + for open_position in positions_open { + log::debug!("open_position: {}", open_position); + net_position += Decimal::from_f64(open_position.signed_qty).unwrap_or(Decimal::ZERO); + } + + let existing_position = self.net_position(instrument_id); + if existing_position != net_position { + self.inner + .borrow_mut() + .net_positions + .insert(*instrument_id, net_position); + log::info!("{} net_position={}", instrument_id, net_position); + } } - fn calculate_unrealized_pnl(&self, instrument_id: &InstrumentId) -> Money { - todo!() + fn calculate_unrealized_pnl(&mut self, instrument_id: &InstrumentId) -> Option { + let borrowed_cache = self.cache.borrow(); + + let account = if let Some(account) = borrowed_cache.account_for_venue(&instrument_id.venue) + { + account + } else { + log::error!( + "Cannot calculate unrealized PnL: no account registered for {}", + instrument_id.venue + ); + return None; + }; + + let instrument = if let Some(instrument) = borrowed_cache.instrument(instrument_id) { + instrument + } else { + log::error!( + "Cannot calculate unrealized PnL: no instrument for {}", + instrument_id + ); + return None; + }; + + let currency = account + .base_currency() + .unwrap_or_else(|| instrument.settlement_currency()); + + let positions_open = borrowed_cache.positions_open( + None, // Faster query filtering + Some(instrument_id), + None, + None, + ); + + if positions_open.is_empty() { + return Some(Money::new(0.0, currency)); + } + + let mut total_pnl = 0.0; + + for position in positions_open { + if position.instrument_id != *instrument_id { + continue; // Nothing to calculate + } + + if position.side == PositionSide::Flat { + continue; // Nothing to calculate + } + + let last = if let Some(price) = self.get_last_price(position) { + price + } else { + log::debug!( + "Cannot calculate unrealized PnL: no prices for {}", + instrument_id + ); + self.inner.borrow_mut().pending_calcs.insert(*instrument_id); + return None; // Cannot calculate + }; + + let mut pnl = position.unrealized_pnl(last).as_f64(); + + if let Some(base_currency) = account.base_currency() { + let xrate = self.calculate_xrate_to_base(instrument, account, position.entry); + + if xrate == 0.0 { + log::debug!( + "Cannot calculate unrealized PnL: insufficient data for {}/{}", + instrument.settlement_currency(), + base_currency + ); + self.inner.borrow_mut().pending_calcs.insert(*instrument_id); + return None; + } + + let scale = 10f64.powi(currency.precision.into()); + pnl = ((pnl * xrate) * scale).round() / scale; + } + + total_pnl += pnl; + } + + Some(Money::new(total_pnl, currency)) } - fn calculate_realized_pnl(&self, instrument_id: &InstrumentId) -> Money { - todo!() + fn calculate_realized_pnl(&mut self, instrument_id: &InstrumentId) -> Option { + let borrowed_cache = self.cache.borrow(); + + let account = if let Some(account) = borrowed_cache.account_for_venue(&instrument_id.venue) + { + account + } else { + log::error!( + "Cannot calculate realized PnL: no account registered for {}", + instrument_id.venue + ); + return None; + }; + + let instrument = if let Some(instrument) = borrowed_cache.instrument(instrument_id) { + instrument + } else { + log::error!( + "Cannot calculate realized PnL: no instrument for {}", + instrument_id + ); + return None; + }; + + let currency = account + .base_currency() + .unwrap_or_else(|| instrument.settlement_currency()); + + let positions = borrowed_cache.positions( + None, // Faster query filtering + Some(instrument_id), + None, + None, + ); + + if positions.is_empty() { + return Some(Money::new(0.0, currency)); + } + + let mut total_pnl = 0.0; + + for position in positions { + if position.instrument_id != *instrument_id { + continue; // Nothing to calculate + } + + if position.side == PositionSide::Flat { + continue; // Nothing to calculate + } + + let mut pnl = position.realized_pnl?.as_f64(); + + if let Some(base_currency) = account.base_currency() { + let xrate = self.calculate_xrate_to_base(instrument, account, position.entry); + + if xrate == 0.0 { + log::debug!( + "Cannot calculate realized PnL: insufficient data for {}/{}", + instrument.settlement_currency(), + base_currency + ); + self.inner.borrow_mut().pending_calcs.insert(*instrument_id); + return None; // Cannot calculate + } + + let scale = 10f64.powi(currency.precision.into()); + pnl = ((pnl * xrate) * scale).round() / scale; + } + + total_pnl += pnl; + } + + Some(Money::new(total_pnl, currency)) } - fn get_last_price(&self, position: &Position) -> Money { - todo!() + fn get_last_price(&self, position: &Position) -> Option { + let price_type = match position.side { + PositionSide::Long => PriceType::Bid, + PositionSide::Short => PriceType::Ask, + _ => panic!("invalid `PositionSide`, was {}", position.side), + }; + + let borrowed_cache = self.cache.borrow(); + + borrowed_cache + .price(&position.instrument_id, price_type) + .or_else(|| borrowed_cache.price(&position.instrument_id, PriceType::Last)) } fn calculate_xrate_to_base( &self, - account: &AccountAny, instrument: &InstrumentAny, + account: &AccountAny, side: OrderSide, ) -> f64 { - todo!() + match account.base_currency() { + Some(base_currency) => { + let price_type = if side == OrderSide::Buy { + PriceType::Bid + } else { + PriceType::Ask + }; + + self.cache + .borrow() + .get_xrate( + instrument.id().venue, + instrument.settlement_currency(), + base_currency, + price_type, + ) + .to_f64() + .unwrap_or_else(|| { + log::error!( + "Failed to get/convert xrate for instrument {} from {} to {}", + instrument.id(), + instrument.settlement_currency(), + base_currency + ); + 1.0 + }) + } + None => 1.0, // No conversion needed + } + } +} + +// Helper functions +fn update_quote_tick( + cache: Rc>, + msgbus: Rc>, + clock: Rc>, + inner: Rc>, + quote: &QuoteTick, +) { + inner + .borrow_mut() + .unrealized_pnls + .remove("e.instrument_id); + + if inner.borrow().initialized || !inner.borrow().pending_calcs.contains("e.instrument_id) { + return; + } + + let result_init; + let mut result_maint = None; + + let account = { + let borrowed_cache = cache.borrow(); + let account = + if let Some(account) = borrowed_cache.account_for_venue("e.instrument_id.venue) { + account + } else { + log::error!( + "Cannot update tick: no account registered for {}", + quote.instrument_id.venue + ); + return; + }; + + let mut borrowed_cache = cache.borrow_mut(); + let instrument = if let Some(instrument) = borrowed_cache.instrument("e.instrument_id) { + instrument.clone() + } else { + log::error!( + "Cannot update tick: no instrument found for {}", + quote.instrument_id + ); + return; + }; + + // Clone the orders and positions to own the data + let orders_open: Vec = borrowed_cache + .orders_open(None, Some("e.instrument_id), None, None) + .iter() + .map(|o| (*o).clone()) + .collect(); + + let positions_open: Vec = borrowed_cache + .positions_open(None, Some("e.instrument_id), None, None) + .iter() + .map(|p| (*p).clone()) + .collect(); + + result_init = inner.borrow().accounts.update_orders( + account, + instrument.clone(), + orders_open.iter().collect(), + clock.borrow().timestamp_ns(), + ); + + if let AccountAny::Margin(margin_account) = account { + result_maint = inner.borrow().accounts.update_positions( + margin_account, + instrument, + positions_open.iter().collect(), + clock.borrow().timestamp_ns(), + ); + } + + if let Some((ref updated_account, _)) = result_init { + borrowed_cache.add_account(updated_account.clone()).unwrap(); // Temp Fix to update the mutated account + } + account.clone() + }; + + let mut portfolio_clone = Portfolio { + clock: clock.clone(), + cache, + msgbus, + inner: inner.clone(), + }; + + let result_unrealized_pnl: Option = + portfolio_clone.calculate_unrealized_pnl("e.instrument_id); + + if result_init.is_some() + && (matches!(account, AccountAny::Cash(_)) + || (result_maint.is_some() && result_unrealized_pnl.is_some())) + { + inner + .borrow_mut() + .pending_calcs + .remove("e.instrument_id); + if inner.borrow().pending_calcs.is_empty() { + inner.borrow_mut().initialized = true; + } + } +} + +fn update_order( + cache: Rc>, + msgbus: Rc>, + clock: Rc>, + inner: Rc>, + event: &OrderEventAny, +) { + let borrowed_cache = cache.borrow(); + let account_id = match event.account_id() { + Some(account_id) => account_id, + None => { + return; // No Account Assigned + } + }; + + let account = if let Some(account) = borrowed_cache.account(&account_id) { + account + } else { + log::error!( + "Cannot update order: no account registered for {}", + account_id + ); + return; + }; + + match account { + AccountAny::Cash(cash_account) => { + if !cash_account.base.calculate_account_state { + return; + } + } + AccountAny::Margin(margin_account) => { + if !margin_account.base.calculate_account_state { + return; + } + } + } + + match event { + OrderEventAny::Accepted(_) + | OrderEventAny::Canceled(_) + | OrderEventAny::Rejected(_) + | OrderEventAny::Updated(_) + | OrderEventAny::Filled(_) => {} + _ => { + return; + } + } + + let borrowed_cache = cache.borrow(); + let order = if let Some(order) = borrowed_cache.order(&event.client_order_id()) { + order + } else { + log::error!( + "Cannot update order: {} not found in the cache", + event.client_order_id() + ); + return; // No Order Found + }; + + if matches!(event, OrderEventAny::Rejected(_)) && order.order_type() != OrderType::StopLimit { + return; // No change to account state + } + + let instrument = if let Some(instrument_id) = borrowed_cache.instrument(&event.instrument_id()) + { + instrument_id + } else { + log::error!( + "Cannot update order: no instrument found for {}", + event.instrument_id() + ); + return; + }; + + if let OrderEventAny::Filled(order_filled) = event { + let _ = inner.borrow().accounts.update_balances( + account.clone(), + instrument.clone(), + *order_filled, + ); + + let mut portfolio_clone = Portfolio { + clock: clock.clone(), + cache: cache.clone(), + msgbus: msgbus.clone(), + inner: inner.clone(), + }; + + match portfolio_clone.calculate_unrealized_pnl(&order_filled.instrument_id) { + Some(unrealized_pnl) => { + inner + .borrow_mut() + .unrealized_pnls + .insert(event.instrument_id(), unrealized_pnl); + } + None => { + log::error!( + "Failed to calculate unrealized PnL for instrument {}", + event.instrument_id() + ); + } + } + } + + let orders_open = borrowed_cache.orders_open(None, Some(&event.instrument_id()), None, None); + + let account_state = inner.borrow_mut().accounts.update_orders( + account, + instrument.clone(), + orders_open, + clock.borrow().timestamp_ns(), + ); + + let mut borrowed_cache = cache.borrow_mut(); + borrowed_cache.update_account(account.clone()).unwrap(); + + if let Some(account_state) = account_state { + msgbus.borrow().publish( + &Ustr::from(&format!("events.account.{}", account.id())), + &account_state, + ); + } else { + log::debug!("Added pending calculation for {}", instrument.id()); + inner.borrow_mut().pending_calcs.insert(instrument.id()); + } + + log::debug!("Updated {}", event); +} + +fn update_position( + cache: Rc>, + msgbus: Rc>, + clock: Rc>, + inner: Rc>, + event: &PositionEvent, +) { + let instrument_id = event.instrument_id(); + + let positions_open: Vec = { + let borrowed_cache = cache.borrow(); + + borrowed_cache + .positions_open(None, Some(&instrument_id), None, None) + .iter() + .map(|o| (*o).clone()) + .collect() + }; + + log::debug!("postion fresh from cache -> {:?}", positions_open); + + let mut portfolio_clone = Portfolio { + clock: clock.clone(), + cache: cache.clone(), + msgbus, + inner: inner.clone(), + }; + + portfolio_clone.update_net_position(&instrument_id, positions_open.clone()); + + let calculated_unrealized_pnl = portfolio_clone + .calculate_unrealized_pnl(&instrument_id) + .expect("Failed to calculate unrealized PnL"); + let calculated_realized_pnl = portfolio_clone + .calculate_realized_pnl(&instrument_id) + .expect("Failed to calculate realized PnL"); + + inner + .borrow_mut() + .unrealized_pnls + .insert(event.instrument_id(), calculated_unrealized_pnl); + inner + .borrow_mut() + .realized_pnls + .insert(event.instrument_id(), calculated_realized_pnl); + + let borrowed_cache = cache.borrow(); + let account = borrowed_cache.account(&event.account_id()); + + if let Some(AccountAny::Margin(margin_account)) = account { + if !margin_account.calculate_account_state { + return; // Nothing to calculate + }; + + let borrowed_cache = cache.borrow(); + let instrument = if let Some(instrument) = borrowed_cache.instrument(&instrument_id) { + instrument + } else { + log::error!( + "Cannot update position: no instrument found for {}", + instrument_id + ); + return; + }; + + let result = inner.borrow_mut().accounts.update_positions( + margin_account, + instrument.clone(), + positions_open.iter().collect(), + clock.borrow().timestamp_ns(), + ); + let mut borrowed_cache = cache.borrow_mut(); + if let Some((margin_account, _)) = result { + borrowed_cache + .add_account(AccountAny::Margin(margin_account)) // Temp Fix to update the mutated account + .unwrap(); + } + } else if account.is_none() { + log::error!( + "Cannot update position: no account registered for {}", + event.account_id() + ); + } +} + +pub fn update_account(cache: Rc>, event: &AccountState) { + let mut borrowed_cache = cache.borrow_mut(); + + if let Some(existing) = borrowed_cache.account(&event.account_id) { + let mut account = existing.clone(); + account.apply(event.clone()); + + if let Err(e) = borrowed_cache.update_account(account.clone()) { + log::error!("Failed to update account: {}", e); + return; + } + } else { + let account = match AccountAny::from_events(vec![event.clone()]) { + Ok(account) => account, + Err(e) => { + log::error!("Failed to create account: {}", e); + return; + } + }; + + if let Err(e) = borrowed_cache.add_account(account) { + log::error!("Failed to add account: {}", e); + return; + } + } + + log::info!("Updated {}", event); +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// +#[cfg(test)] +mod tests { + use std::{cell::RefCell, rc::Rc}; + + use nautilus_common::{cache::Cache, clock::TestClock, msgbus::MessageBus}; + use nautilus_core::nanos::UnixNanos; + use nautilus_model::{ + data::quote::QuoteTick, + enums::{AccountType, LiquiditySide, OmsType, OrderSide, OrderType}, + events::{ + account::{state::AccountState, stubs::cash_account_state}, + order::{ + stubs::{order_accepted, order_filled, order_submitted}, + OrderAccepted, OrderEventAny, OrderFilled, OrderSubmitted, + }, + position::{ + changed::PositionChanged, closed::PositionClosed, opened::PositionOpened, + PositionEvent, + }, + }, + identifiers::{ + stubs::{account_id, uuid4}, + AccountId, ClientOrderId, PositionId, StrategyId, Symbol, TradeId, VenueOrderId, + }, + instruments::{ + any::InstrumentAny, + crypto_perpetual::CryptoPerpetual, + currency_pair::CurrencyPair, + stubs::{audusd_sim, currency_pair_btcusdt, default_fx_ccy, ethusdt_bitmex}, + }, + orders::{any::OrderAny, builder::OrderTestBuilder}, + position::Position, + types::{ + balance::AccountBalance, currency::Currency, money::Money, price::Price, + quantity::Quantity, + }, + }; + use rstest::{fixture, rstest}; + use rust_decimal::{prelude::FromPrimitive, Decimal}; + + use super::Portfolio; + + #[fixture] + fn msgbus() -> MessageBus { + MessageBus::default() + } + + #[fixture] + fn simple_cache() -> Cache { + Cache::new(None, None) + } + + #[fixture] + fn clock() -> TestClock { + TestClock::new() + } + + #[fixture] + fn venue() -> Venue { + Venue::new("SIM") + } + + #[fixture] + fn instrument_audusd(audusd_sim: CurrencyPair) -> InstrumentAny { + InstrumentAny::CurrencyPair(audusd_sim) + } + + #[fixture] + fn instrument_gbpusd() -> InstrumentAny { + InstrumentAny::CurrencyPair(default_fx_ccy( + Symbol::from("GBP/USD"), + Some(Venue::from("SIM")), + )) + } + + #[fixture] + fn instrument_btcusdt(currency_pair_btcusdt: CurrencyPair) -> InstrumentAny { + InstrumentAny::CurrencyPair(currency_pair_btcusdt) + } + + #[fixture] + fn instrument_ethusdt(ethusdt_bitmex: CryptoPerpetual) -> InstrumentAny { + InstrumentAny::CryptoPerpetual(ethusdt_bitmex) + } + + #[fixture] + fn portfolio( + msgbus: MessageBus, + mut simple_cache: Cache, + clock: TestClock, + instrument_audusd: InstrumentAny, + instrument_gbpusd: InstrumentAny, + instrument_btcusdt: InstrumentAny, + instrument_ethusdt: InstrumentAny, + ) -> Portfolio { + simple_cache.add_instrument(instrument_audusd).unwrap(); + simple_cache.add_instrument(instrument_gbpusd).unwrap(); + simple_cache.add_instrument(instrument_btcusdt).unwrap(); + simple_cache.add_instrument(instrument_ethusdt).unwrap(); + + Portfolio::new( + Rc::new(RefCell::new(msgbus)), + Rc::new(RefCell::new(simple_cache)), + Rc::new(RefCell::new(clock)), + ) + } + + use std::collections::HashMap; + + use nautilus_model::identifiers::Venue; + + // Helpers + fn get_cash_account(accountid: Option<&str>) -> AccountState { + AccountState::new( + match accountid { + Some(account_id_str) => AccountId::new(account_id_str), + None => account_id(), + }, + AccountType::Cash, + vec![ + AccountBalance::new( + Money::new(10.00000000, Currency::BTC()), + Money::new(0.00000000, Currency::BTC()), + Money::new(10.00000000, Currency::BTC()), + ), + AccountBalance::new( + Money::new(10.000, Currency::USD()), + Money::new(0.000, Currency::USD()), + Money::new(10.000, Currency::USD()), + ), + AccountBalance::new( + Money::new(100000.000, Currency::USDT()), + Money::new(0.000, Currency::USDT()), + Money::new(100000.000, Currency::USDT()), + ), + AccountBalance::new( + Money::new(20.000, Currency::ETH()), + Money::new(0.000, Currency::ETH()), + Money::new(20.000, Currency::ETH()), + ), + ], + vec![], + true, + uuid4(), + 0.into(), + 0.into(), + None, + ) + } + + fn get_margin_account(accountid: Option<&str>) -> AccountState { + AccountState::new( + match accountid { + Some(account_id_str) => AccountId::new(account_id_str), + None => account_id(), + }, + AccountType::Margin, + vec![ + AccountBalance::new( + Money::new(10.000, Currency::BTC()), + Money::new(0.000, Currency::BTC()), + Money::new(10.000, Currency::BTC()), + ), + AccountBalance::new( + Money::new(20.000, Currency::ETH()), + Money::new(0.000, Currency::ETH()), + Money::new(20.000, Currency::ETH()), + ), + AccountBalance::new( + Money::new(100000.000, Currency::USDT()), + Money::new(0.000, Currency::USDT()), + Money::new(100000.000, Currency::USDT()), + ), + AccountBalance::new( + Money::new(10.000, Currency::USD()), + Money::new(0.000, Currency::USD()), + Money::new(10.000, Currency::USD()), + ), + AccountBalance::new( + Money::new(10.000, Currency::GBP()), + Money::new(0.000, Currency::GBP()), + Money::new(10.000, Currency::GBP()), + ), + ], + Vec::new(), + true, + uuid4(), + 0.into(), + 0.into(), + None, + ) + } + + fn get_quote_tick( + instrument: &InstrumentAny, + bid: f64, + ask: f64, + bid_size: f64, + ask_size: f64, + ) -> QuoteTick { + QuoteTick::new( + instrument.id(), + Price::new(bid, 0), + Price::new(ask, 0), + Quantity::new(bid_size, 0), + Quantity::new(ask_size, 0), + 0.into(), + 0.into(), + ) + } + + fn submit_order(order: &OrderAny) -> OrderSubmitted { + order_submitted( + order.trader_id(), + order.strategy_id(), + order.instrument_id(), + order.client_order_id(), + account_id(), + uuid4(), + ) + } + + fn accept_order(order: &OrderAny) -> OrderAccepted { + order_accepted( + order.trader_id(), + order.strategy_id(), + order.instrument_id(), + order.client_order_id(), + account_id(), + order.venue_order_id().unwrap_or(VenueOrderId::new("1")), + uuid4(), + ) + } + + fn fill_order(order: &OrderAny) -> OrderFilled { + order_filled( + order.trader_id(), + order.strategy_id(), + order.instrument_id(), + order.client_order_id(), + uuid4(), + ) + } + + fn get_open_position(position: &Position) -> PositionOpened { + PositionOpened { + trader_id: position.trader_id, + strategy_id: position.strategy_id, + instrument_id: position.instrument_id, + position_id: position.id, + account_id: position.account_id, + opening_order_id: position.opening_order_id, + entry: position.entry, + side: position.side, + signed_qty: position.signed_qty, + quantity: position.quantity, + last_qty: position.quantity, + last_px: Price::new(position.avg_px_open, 0), + currency: position.settlement_currency, + avg_px_open: position.avg_px_open, + ts_event: 0.into(), + ts_init: 0.into(), + } + } + + fn get_changed_position(position: &Position) -> PositionChanged { + PositionChanged { + trader_id: position.trader_id, + strategy_id: position.strategy_id, + instrument_id: position.instrument_id, + position_id: position.id, + account_id: position.account_id, + opening_order_id: position.opening_order_id, + entry: position.entry, + side: position.side, + signed_qty: position.signed_qty, + quantity: position.quantity, + last_qty: position.quantity, + last_px: Price::new(position.avg_px_open, 0), + currency: position.settlement_currency, + avg_px_open: position.avg_px_open, + ts_event: 0.into(), + ts_init: 0.into(), + peak_quantity: position.quantity, + avg_px_close: position.avg_px_open, + realized_return: position.avg_px_open, + realized_pnl: Money::new(10.0, Currency::USD()), + unrealized_pnl: Money::new(10.0, Currency::USD()), + ts_opened: 0.into(), + } + } + + fn get_close_position(position: &Position) -> PositionClosed { + PositionClosed { + trader_id: position.trader_id, + strategy_id: position.strategy_id, + instrument_id: position.instrument_id, + position_id: position.id, + account_id: position.account_id, + opening_order_id: position.opening_order_id, + entry: position.entry, + side: position.side, + signed_qty: position.signed_qty, + quantity: position.quantity, + last_qty: position.quantity, + last_px: Price::new(position.avg_px_open, 0), + currency: position.settlement_currency, + avg_px_open: position.avg_px_open, + ts_event: 0.into(), + ts_init: 0.into(), + peak_quantity: position.quantity, + avg_px_close: position.avg_px_open, + realized_return: position.avg_px_open, + realized_pnl: Money::new(10.0, Currency::USD()), + unrealized_pnl: Money::new(10.0, Currency::USD()), + ts_opened: 0.into(), + closing_order_id: ClientOrderId::new("SSD"), + duration: 0, + ts_closed: 0.into(), + } + } + + // Tests + #[rstest] + fn test_account_when_account_returns_the_account_facade(mut portfolio: Portfolio) { + let account_id = "BINANCE-1513111"; + let state = get_cash_account(Some(account_id)); + + portfolio.update_account(&state); + + let borrowed_cache = portfolio.cache.borrow_mut(); + let account = borrowed_cache.account(&AccountId::new(account_id)).unwrap(); + assert_eq!(account.id().get_issuer(), "BINANCE".into()); + assert_eq!(account.id().get_issuers_id(), "1513111"); + } + + #[rstest] + fn test_balances_locked_when_no_account_for_venue_returns_none( + portfolio: Portfolio, + venue: Venue, + ) { + let result = portfolio.balances_locked(&venue); + assert_eq!(result, HashMap::new()); + } + + #[rstest] + fn test_margins_init_when_no_account_for_venue_returns_none( + portfolio: Portfolio, + venue: Venue, + ) { + let result = portfolio.margins_init(&venue); + assert_eq!(result, HashMap::new()); + } + + #[rstest] + fn test_margins_maint_when_no_account_for_venue_returns_none( + portfolio: Portfolio, + venue: Venue, + ) { + let result = portfolio.margins_maint(&venue); + assert_eq!(result, HashMap::new()); + } + + #[rstest] + fn test_unrealized_pnl_for_instrument_when_no_instrument_returns_none( + mut portfolio: Portfolio, + instrument_audusd: InstrumentAny, + ) { + let result = portfolio.unrealized_pnl(&instrument_audusd.id()); + assert!(result.is_none()); + } + + #[rstest] + fn test_unrealized_pnl_for_venue_when_no_account_returns_empty_dict( + mut portfolio: Portfolio, + venue: Venue, + ) { + let result = portfolio.unrealized_pnls(&venue); + assert_eq!(result, HashMap::new()); + } + + #[rstest] + fn test_realized_pnl_for_instrument_when_no_instrument_returns_none( + mut portfolio: Portfolio, + instrument_audusd: InstrumentAny, + ) { + let result = portfolio.realized_pnl(&instrument_audusd.id()); + assert!(result.is_none()); + } + + #[rstest] + fn test_realized_pnl_for_venue_when_no_account_returns_empty_dict( + mut portfolio: Portfolio, + venue: Venue, + ) { + let result = portfolio.realized_pnls(&venue); + assert_eq!(result, HashMap::new()); + } + + #[rstest] + fn test_net_position_when_no_positions_returns_zero( + portfolio: Portfolio, + instrument_audusd: InstrumentAny, + ) { + let result = portfolio.net_position(&instrument_audusd.id()); + assert_eq!(result, Decimal::ZERO); + } + + #[rstest] + fn test_net_exposures_when_no_positions_returns_none(portfolio: Portfolio, venue: Venue) { + let result = portfolio.net_exposures(&venue); + assert!(result.is_none()); + } + + #[rstest] + fn test_is_net_long_when_no_positions_returns_false( + portfolio: Portfolio, + instrument_audusd: InstrumentAny, + ) { + let result = portfolio.is_net_long(&instrument_audusd.id()); + assert!(!result); + } + + #[rstest] + fn test_is_net_short_when_no_positions_returns_false( + portfolio: Portfolio, + instrument_audusd: InstrumentAny, + ) { + let result = portfolio.is_net_short(&instrument_audusd.id()); + assert!(!result); + } + + #[rstest] + fn test_is_flat_when_no_positions_returns_true( + portfolio: Portfolio, + instrument_audusd: InstrumentAny, + ) { + let result = portfolio.is_flat(&instrument_audusd.id()); + assert!(result); + } + + #[rstest] + fn test_is_completely_flat_when_no_positions_returns_true(portfolio: Portfolio) { + let result = portfolio.is_completely_flat(); + assert!(result); + } + + #[rstest] + fn test_open_value_when_no_account_returns_none(portfolio: Portfolio, venue: Venue) { + let result = portfolio.net_exposures(&venue); + assert!(result.is_none()); + } + + #[rstest] + fn test_update_tick(mut portfolio: Portfolio, instrument_audusd: InstrumentAny) { + let tick = get_quote_tick(&instrument_audusd, 1.25, 1.251, 1.0, 1.0); + portfolio.update_quote_tick(&tick); + assert!(portfolio.unrealized_pnl(&instrument_audusd.id()).is_none()); + } + + //TODO: FIX: It should return an error + #[rstest] + fn test_exceed_free_balance_single_currency_raises_account_balance_negative_exception( + mut portfolio: Portfolio, + cash_account_state: AccountState, + instrument_audusd: InstrumentAny, + ) { + portfolio.update_account(&cash_account_state); + + let mut order = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("1000000")) + .build(); + + portfolio + .cache + .borrow_mut() + .add_order(order.clone(), None, None, false) + .unwrap(); + + let order_submitted = submit_order(&order); + order + .apply(OrderEventAny::Submitted(order_submitted)) + .unwrap(); + + portfolio.update_order(&OrderEventAny::Submitted(order_submitted)); + + let order_filled = fill_order(&order); + order.apply(OrderEventAny::Filled(order_filled)).unwrap(); + portfolio.update_order(&OrderEventAny::Filled(order_filled)); + } + + // TODO: It should return an error + #[rstest] + fn test_exceed_free_balance_multi_currency_raises_account_balance_negative_exception( + mut portfolio: Portfolio, + cash_account_state: AccountState, + instrument_audusd: InstrumentAny, + ) { + portfolio.update_account(&cash_account_state); + + let account = portfolio + .cache + .borrow_mut() + .account_for_venue(&Venue::from("SIM")) + .unwrap() + .clone(); + + // Create Order + let mut order = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("3.0")) + .build(); + + portfolio + .cache + .borrow_mut() + .add_order(order.clone(), None, None, false) + .unwrap(); + + let order_submitted = submit_order(&order); + order + .apply(OrderEventAny::Submitted(order_submitted)) + .unwrap(); + portfolio.update_order(&OrderEventAny::Submitted(order_submitted)); + + // Assert + assert_eq!( + account.balances().iter().next().unwrap().1.total.as_f64(), + 1525000.00 + ); + } + + #[rstest] + fn test_update_orders_open_cash_account( + mut portfolio: Portfolio, + cash_account_state: AccountState, + instrument_audusd: InstrumentAny, + ) { + portfolio.update_account(&cash_account_state); + + // Create Order + let mut order = OrderTestBuilder::new(OrderType::Limit) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("1.0")) + .price(Price::new(50000.0, 0)) + .build(); + + portfolio + .cache + .borrow_mut() + .add_order(order.clone(), None, None, false) + .unwrap(); + + let order_submitted = submit_order(&order); + order + .apply(OrderEventAny::Submitted(order_submitted)) + .unwrap(); + portfolio.update_order(&OrderEventAny::Submitted(order_submitted)); + + // ACCEPTED + let order_accepted = accept_order(&order); + order + .apply(OrderEventAny::Accepted(order_accepted)) + .unwrap(); + portfolio.update_order(&OrderEventAny::Accepted(order_accepted)); + + assert_eq!( + portfolio + .balances_locked(&Venue::from("SIM")) + .get(&Currency::USD()) + .unwrap() + .as_f64(), + 25000.0 + ); + } + + #[rstest] + fn test_update_orders_open_margin_account( + mut portfolio: Portfolio, + instrument_btcusdt: InstrumentAny, + ) { + let account_state = get_margin_account(Some("BINANCE-01234")); + portfolio.update_account(&account_state); + + // Create Order + let mut order1 = OrderTestBuilder::new(OrderType::StopMarket) + .instrument_id(instrument_btcusdt.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("100.0")) + .price(Price::new(55.0, 1)) + .trigger_price(Price::new(35.0, 1)) + .build(); + + let order2 = OrderTestBuilder::new(OrderType::StopMarket) + .instrument_id(instrument_btcusdt.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("1000.0")) + .price(Price::new(45.0, 1)) + .trigger_price(Price::new(30.0, 1)) + .build(); + + portfolio + .cache + .borrow_mut() + .add_order(order1.clone(), None, None, true) + .unwrap(); + + portfolio + .cache + .borrow_mut() + .add_order(order2, None, None, true) + .unwrap(); + + let order_submitted = submit_order(&order1); + order1 + .apply(OrderEventAny::Submitted(order_submitted)) + .unwrap(); + portfolio.cache.borrow_mut().update_order(&order1).unwrap(); + + // Push status to Accepted + let order_accepted = accept_order(&order1); + order1 + .apply(OrderEventAny::Accepted(order_accepted)) + .unwrap(); + portfolio.cache.borrow_mut().update_order(&order1).unwrap(); + + // TODO: Replace with Execution Engine once implemented. + portfolio + .cache + .borrow_mut() + .add_order(order1.clone(), None, None, true) + .unwrap(); + + let order_filled1 = fill_order(&order1); + order1.apply(OrderEventAny::Filled(order_filled1)).unwrap(); + + // Act + let last = get_quote_tick(&instrument_btcusdt, 25001.0, 25002.0, 15.0, 12.0); + portfolio.update_quote_tick(&last); + portfolio.initialize_orders(); + + // Assert + assert_eq!( + portfolio + .margins_init(&Venue::from("BINANCE")) + .get(&instrument_btcusdt.id()) + .unwrap() + .as_f64(), + 10.5 + ); + } + + #[rstest] + fn test_order_accept_updates_margin_init( + mut portfolio: Portfolio, + instrument_btcusdt: InstrumentAny, + ) { + let account_state = get_margin_account(Some("BINANCE-01234")); + portfolio.update_account(&account_state); + + // Create Order + let mut order = OrderTestBuilder::new(OrderType::Limit) + .client_order_id(ClientOrderId::new("55")) + .instrument_id(instrument_btcusdt.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("100.0")) + .price(Price::new(5.0, 0)) + .build(); + + portfolio + .cache + .borrow_mut() + .add_order(order.clone(), None, None, true) + .unwrap(); + + let order_submitted = submit_order(&order); + order + .apply(OrderEventAny::Submitted(order_submitted)) + .unwrap(); + portfolio.cache.borrow_mut().update_order(&order).unwrap(); + + let order_accepted = accept_order(&order); + order + .apply(OrderEventAny::Accepted(order_accepted)) + .unwrap(); + portfolio.cache.borrow_mut().update_order(&order).unwrap(); + + // TODO: Replace with Execution Engine once implemented. + portfolio + .cache + .borrow_mut() + .add_order(order.clone(), None, None, true) + .unwrap(); + + // Act + portfolio.initialize_orders(); + + // Assert + assert_eq!( + portfolio + .margins_init(&Venue::from("BINANCE")) + .get(&instrument_btcusdt.id()) + .unwrap() + .as_f64(), + 1.5 + ); + } + + #[rstest] + fn test_update_positions(mut portfolio: Portfolio, instrument_audusd: InstrumentAny) { + let account_state = get_cash_account(None); + portfolio.update_account(&account_state); + + // Create Order + let mut order1 = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("10.50")) + .build(); + + let order2 = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Sell) + .quantity(Quantity::from("10.50")) + .build(); + + portfolio + .cache + .borrow_mut() + .add_order(order1.clone(), None, None, true) + .unwrap(); + portfolio + .cache + .borrow_mut() + .add_order(order2.clone(), None, None, true) + .unwrap(); + + let order1_submitted = submit_order(&order1); + order1 + .apply(OrderEventAny::Submitted(order1_submitted)) + .unwrap(); + portfolio.update_order(&OrderEventAny::Submitted(order1_submitted)); + + // ACCEPTED + let order1_accepted = accept_order(&order1); + order1 + .apply(OrderEventAny::Accepted(order1_accepted)) + .unwrap(); + portfolio.update_order(&OrderEventAny::Accepted(order1_accepted)); + + let mut fill1 = fill_order(&order1); + fill1.position_id = Some(PositionId::new("SSD")); + + let mut fill2 = fill_order(&order2); + fill2.trade_id = TradeId::new("2"); + + let mut position1 = Position::new(&instrument_audusd, fill1); + position1.apply(&fill2); + + let order3 = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Sell) + .quantity(Quantity::from("10.00")) + .build(); + + let mut fill3 = fill_order(&order3); + fill3.position_id = Some(PositionId::new("SSsD")); + + let position2 = Position::new(&instrument_audusd, fill3); + + // Update the last quote + let last = get_quote_tick(&instrument_audusd, 250001.0, 250002.0, 1.0, 1.0); + + // Act + portfolio + .cache + .borrow_mut() + .add_position(position1, OmsType::Hedging) + .unwrap(); + portfolio + .cache + .borrow_mut() + .add_position(position2, OmsType::Hedging) + .unwrap(); + portfolio.cache.borrow_mut().add_quote(last).unwrap(); + portfolio.initialize_positions(); + portfolio.update_quote_tick(&last); + + // Assert + assert!(portfolio.is_net_long(&instrument_audusd.id())); + } + + #[rstest] + fn test_opening_one_long_position_updates_portfolio( + mut portfolio: Portfolio, + instrument_audusd: InstrumentAny, + ) { + let account_state = get_margin_account(None); + portfolio.update_account(&account_state); + + // Create Order + let order = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("10.00")) + .build(); + + let mut fill = fill_order(&order); + fill.position_id = Some(PositionId::new("SSD")); + + // Update the last quote + let last = get_quote_tick(&instrument_audusd, 10510.0, 10511.0, 1.0, 1.0); + portfolio.cache.borrow_mut().add_quote(last).unwrap(); + portfolio.update_quote_tick(&last); + + let position = Position::new(&instrument_audusd, fill); + + // Act + portfolio + .cache + .borrow_mut() + .add_position(position.clone(), OmsType::Hedging) + .unwrap(); + + let position_opened = get_open_position(&position); + portfolio.update_position(&PositionEvent::PositionOpened(position_opened)); + + // Assert + assert_eq!( + portfolio + .net_exposures(&Venue::from("SIM")) + .unwrap() + .get(&Currency::USD()) + .unwrap() + .as_f64(), + 10510.0 + ); + assert_eq!( + portfolio + .unrealized_pnls(&Venue::from("SIM")) + .get(&Currency::USD()) + .unwrap() + .as_f64(), + -6445.89 + ); + assert_eq!( + portfolio + .realized_pnls(&Venue::from("SIM")) + .get(&Currency::USD()) + .unwrap() + .as_f64(), + 0.0 + ); + assert_eq!( + portfolio + .net_exposure(&instrument_audusd.id()) + .unwrap() + .as_f64(), + 10510.0 + ); + assert_eq!( + portfolio + .unrealized_pnl(&instrument_audusd.id()) + .unwrap() + .as_f64(), + -6445.89 + ); + assert_eq!( + portfolio + .realized_pnl(&instrument_audusd.id()) + .unwrap() + .as_f64(), + 0.0 + ); + assert_eq!( + portfolio.net_position(&instrument_audusd.id()), + Decimal::new(561, 3) + ); + assert!(portfolio.is_net_long(&instrument_audusd.id())); + assert!(!portfolio.is_net_short(&instrument_audusd.id())); + assert!(!portfolio.is_flat(&instrument_audusd.id())); + assert!(!portfolio.is_completely_flat()); + } + + #[rstest] + fn test_opening_one_short_position_updates_portfolio( + mut portfolio: Portfolio, + instrument_audusd: InstrumentAny, + ) { + let account_state = get_margin_account(None); + portfolio.update_account(&account_state); + + // Create Order + let order = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Sell) + .quantity(Quantity::from("2")) + .build(); + + let fill = OrderFilled::new( + order.trader_id(), + order.strategy_id(), + order.instrument_id(), + order.client_order_id(), + VenueOrderId::new("123456"), + AccountId::new("SIM-001"), + TradeId::new("1"), + order.order_side(), + order.order_type(), + order.quantity(), + Price::new(10.0, 0), + Currency::USD(), + LiquiditySide::Taker, + uuid4(), + UnixNanos::default(), + UnixNanos::default(), + false, + Some(PositionId::new("SSD")), + Some(Money::from("12.2 USD")), + ); + + // Update the last quote + let last = get_quote_tick(&instrument_audusd, 15510.15, 15510.25, 13.0, 4.0); + + portfolio.cache.borrow_mut().add_quote(last).unwrap(); + portfolio.update_quote_tick(&last); + + let position = Position::new(&instrument_audusd, fill); + + // Act + portfolio + .cache + .borrow_mut() + .add_position(position.clone(), OmsType::Hedging) + .unwrap(); + + let position_opened = get_open_position(&position); + portfolio.update_position(&PositionEvent::PositionOpened(position_opened)); + + // Assert + assert_eq!( + portfolio + .net_exposures(&Venue::from("SIM")) + .unwrap() + .get(&Currency::USD()) + .unwrap() + .as_f64(), + 31020.0 + ); + assert_eq!( + portfolio + .unrealized_pnls(&Venue::from("SIM")) + .get(&Currency::USD()) + .unwrap() + .as_f64(), + -31000.0 + ); + assert_eq!( + portfolio + .realized_pnls(&Venue::from("SIM")) + .get(&Currency::USD()) + .unwrap() + .as_f64(), + -12.2 + ); + assert_eq!( + portfolio + .net_exposure(&instrument_audusd.id()) + .unwrap() + .as_f64(), + 31020.0 + ); + assert_eq!( + portfolio + .unrealized_pnl(&instrument_audusd.id()) + .unwrap() + .as_f64(), + -31000.0 + ); + assert_eq!( + portfolio + .realized_pnl(&instrument_audusd.id()) + .unwrap() + .as_f64(), + -12.2 + ); + assert_eq!( + portfolio.net_position(&instrument_audusd.id()), + Decimal::new(-2, 0) + ); + + assert!(!portfolio.is_net_long(&instrument_audusd.id())); + assert!(portfolio.is_net_short(&instrument_audusd.id())); + assert!(!portfolio.is_flat(&instrument_audusd.id())); + assert!(!portfolio.is_completely_flat()); + } + + #[rstest] + fn test_opening_positions_with_multi_asset_account( + mut portfolio: Portfolio, + instrument_btcusdt: InstrumentAny, + instrument_ethusdt: InstrumentAny, + ) { + let account_state = get_margin_account(Some("BITMEX-01234")); + portfolio.update_account(&account_state); + + let last_ethusd = get_quote_tick(&instrument_ethusdt, 376.05, 377.10, 16.0, 25.0); + let last_btcusd = get_quote_tick(&instrument_btcusdt, 10500.05, 10501.51, 2.54, 0.91); + + portfolio.cache.borrow_mut().add_quote(last_ethusd).unwrap(); + portfolio.cache.borrow_mut().add_quote(last_btcusd).unwrap(); + portfolio.update_quote_tick(&last_ethusd); + portfolio.update_quote_tick(&last_btcusd); + + // Create Order + let order = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_ethusdt.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("10000")) + .build(); + + let fill = OrderFilled::new( + order.trader_id(), + order.strategy_id(), + order.instrument_id(), + order.client_order_id(), + VenueOrderId::new("123456"), + AccountId::new("SIM-001"), + TradeId::new("1"), + order.order_side(), + order.order_type(), + order.quantity(), + Price::new(376.0, 0), + Currency::USD(), + LiquiditySide::Taker, + uuid4(), + UnixNanos::default(), + UnixNanos::default(), + false, + Some(PositionId::new("SSD")), + Some(Money::from("12.2 USD")), + ); + + let position = Position::new(&instrument_ethusdt, fill); + + // Act + portfolio + .cache + .borrow_mut() + .add_position(position.clone(), OmsType::Hedging) + .unwrap(); + + let position_opened = get_open_position(&position); + portfolio.update_position(&PositionEvent::PositionOpened(position_opened)); + + // Assert + assert_eq!( + portfolio + .net_exposures(&Venue::from("BITMEX")) + .unwrap() + .get(&Currency::ETH()) + .unwrap() + .as_f64(), + 26.59574468 + ); + assert_eq!( + portfolio + .unrealized_pnls(&Venue::from("BITMEX")) + .get(&Currency::ETH()) + .unwrap() + .as_f64(), + 0.0 + ); + // TODO: fix + // assert_eq!( + // portfolio + // .margins_maint(&Venue::from("SIM")) + // .get(&instrument_audusd.id()) + // .unwrap() + // .as_f64(), + // 0.0 + // ); + assert_eq!( + portfolio + .net_exposure(&instrument_ethusdt.id()) + .unwrap() + .as_f64(), + 26.59574468 + ); + } + + #[rstest] + fn test_market_value_when_insufficient_data_for_xrate_returns_none( + mut portfolio: Portfolio, + instrument_btcusdt: InstrumentAny, + instrument_ethusdt: InstrumentAny, + ) { + let account_state = get_margin_account(Some("BITMEX-01234")); + portfolio.update_account(&account_state); + + // Create Order + let order = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_ethusdt.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("100")) + .build(); + + let fill = OrderFilled::new( + order.trader_id(), + order.strategy_id(), + order.instrument_id(), + order.client_order_id(), + VenueOrderId::new("123456"), + AccountId::new("SIM-001"), + TradeId::new("1"), + order.order_side(), + order.order_type(), + order.quantity(), + Price::new(376.05, 0), + Currency::USD(), + LiquiditySide::Taker, + uuid4(), + UnixNanos::default(), + UnixNanos::default(), + false, + Some(PositionId::new("SSD")), + Some(Money::from("12.2 USD")), + ); + + let last_ethusd = get_quote_tick(&instrument_ethusdt, 376.05, 377.10, 16.0, 25.0); + let last_xbtusd = get_quote_tick(&instrument_btcusdt, 50000.00, 50000.00, 1.0, 1.0); + + let position = Position::new(&instrument_ethusdt, fill); + let position_opened = get_open_position(&position); + + // Act + portfolio.update_position(&PositionEvent::PositionOpened(position_opened)); + portfolio + .cache + .borrow_mut() + .add_position(position, OmsType::Hedging) + .unwrap(); + portfolio.cache.borrow_mut().add_quote(last_ethusd).unwrap(); + portfolio.cache.borrow_mut().add_quote(last_xbtusd).unwrap(); + portfolio.update_quote_tick(&last_ethusd); + portfolio.update_quote_tick(&last_xbtusd); + + // Assert + assert_eq!( + portfolio + .net_exposures(&Venue::from("BITMEX")) + .unwrap() + .get(&Currency::ETH()) + .unwrap() + .as_f64(), + 0.26595745 + ); + } + + #[rstest] + fn test_opening_several_positions_updates_portfolio( + mut portfolio: Portfolio, + instrument_audusd: InstrumentAny, + instrument_gbpusd: InstrumentAny, + ) { + let account_state = get_margin_account(None); + portfolio.update_account(&account_state); + + let last_audusd = get_quote_tick(&instrument_audusd, 0.80501, 0.80505, 1.0, 1.0); + let last_gbpusd = get_quote_tick(&instrument_gbpusd, 1.30315, 1.30317, 1.0, 1.0); + + portfolio.cache.borrow_mut().add_quote(last_audusd).unwrap(); + portfolio.cache.borrow_mut().add_quote(last_gbpusd).unwrap(); + portfolio.update_quote_tick(&last_audusd); + portfolio.update_quote_tick(&last_gbpusd); + + // Create Order + let order1 = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("100000")) + .build(); + + let order2 = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_gbpusd.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("100000")) + .build(); + + portfolio + .cache + .borrow_mut() + .add_order(order1.clone(), None, None, true) + .unwrap(); + portfolio + .cache + .borrow_mut() + .add_order(order2.clone(), None, None, true) + .unwrap(); + + let fill1 = OrderFilled::new( + order1.trader_id(), + order1.strategy_id(), + order1.instrument_id(), + order1.client_order_id(), + VenueOrderId::new("123456"), + AccountId::new("SIM-001"), + TradeId::new("1"), + order1.order_side(), + order1.order_type(), + order1.quantity(), + Price::new(376.05, 0), + Currency::USD(), + LiquiditySide::Taker, + uuid4(), + UnixNanos::default(), + UnixNanos::default(), + false, + Some(PositionId::new("SSD")), + Some(Money::from("12.2 USD")), + ); + let fill2 = OrderFilled::new( + order2.trader_id(), + order2.strategy_id(), + order2.instrument_id(), + order2.client_order_id(), + VenueOrderId::new("123456"), + AccountId::new("SIM-001"), + TradeId::new("1"), + order2.order_side(), + order2.order_type(), + order2.quantity(), + Price::new(376.05, 0), + Currency::USD(), + LiquiditySide::Taker, + uuid4(), + UnixNanos::default(), + UnixNanos::default(), + false, + Some(PositionId::new("SSD")), + Some(Money::from("12.2 USD")), + ); + + portfolio.cache.borrow_mut().update_order(&order1).unwrap(); + portfolio.cache.borrow_mut().update_order(&order2).unwrap(); + + let position1 = Position::new(&instrument_audusd, fill1); + let position2 = Position::new(&instrument_gbpusd, fill2); + + let position_opened1 = get_open_position(&position1); + let position_opened2 = get_open_position(&position2); + + // Act + portfolio + .cache + .borrow_mut() + .add_position(position1, OmsType::Hedging) + .unwrap(); + portfolio + .cache + .borrow_mut() + .add_position(position2, OmsType::Hedging) + .unwrap(); + portfolio.update_position(&PositionEvent::PositionOpened(position_opened1)); + portfolio.update_position(&PositionEvent::PositionOpened(position_opened2)); + + // Assert + assert_eq!( + portfolio + .net_exposures(&Venue::from("SIM")) + .unwrap() + .get(&Currency::USD()) + .unwrap() + .as_f64(), + 100000.0 + ); + + assert_eq!( + portfolio + .unrealized_pnls(&Venue::from("SIM")) + .get(&Currency::USD()) + .unwrap() + .as_f64(), + -37500000.0 + ); + + assert_eq!( + portfolio + .realized_pnls(&Venue::from("SIM")) + .get(&Currency::USD()) + .unwrap() + .as_f64(), + -12.2 + ); + // FIX: TODO: should not be empty + assert_eq!(portfolio.margins_maint(&Venue::from("SIM")), HashMap::new()); + assert_eq!( + portfolio + .net_exposure(&instrument_audusd.id()) + .unwrap() + .as_f64(), + 100000.0 + ); + assert_eq!( + portfolio + .net_exposure(&instrument_gbpusd.id()) + .unwrap() + .as_f64(), + 100000.0 + ); + assert_eq!( + portfolio + .unrealized_pnl(&instrument_audusd.id()) + .unwrap() + .as_f64(), + 0.0 + ); + assert_eq!( + portfolio + .unrealized_pnl(&instrument_gbpusd.id()) + .unwrap() + .as_f64(), + -37500000.0 + ); + assert_eq!( + portfolio + .realized_pnl(&instrument_audusd.id()) + .unwrap() + .as_f64(), + 0.0 + ); + assert_eq!( + portfolio + .realized_pnl(&instrument_gbpusd.id()) + .unwrap() + .as_f64(), + -12.2 + ); + assert_eq!( + portfolio.net_position(&instrument_audusd.id()), + Decimal::from_f64(100000.0).unwrap() + ); + assert_eq!( + portfolio.net_position(&instrument_gbpusd.id()), + Decimal::from_f64(100000.0).unwrap() + ); + assert!(portfolio.is_net_long(&instrument_audusd.id())); + assert!(!portfolio.is_net_short(&instrument_audusd.id())); + assert!(!portfolio.is_flat(&instrument_audusd.id())); + assert!(!portfolio.is_completely_flat()); + } + + #[rstest] + fn test_modifying_position_updates_portfolio( + mut portfolio: Portfolio, + instrument_audusd: InstrumentAny, + ) { + let account_state = get_margin_account(None); + portfolio.update_account(&account_state); + + let last_audusd = get_quote_tick(&instrument_audusd, 0.80501, 0.80505, 1.0, 1.0); + portfolio.cache.borrow_mut().add_quote(last_audusd).unwrap(); + portfolio.update_quote_tick(&last_audusd); + + // Create Order + let order1 = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("100000")) + .build(); + + let fill1 = OrderFilled::new( + order1.trader_id(), + order1.strategy_id(), + order1.instrument_id(), + order1.client_order_id(), + VenueOrderId::new("123456"), + AccountId::new("SIM-001"), + TradeId::new("1"), + order1.order_side(), + order1.order_type(), + order1.quantity(), + Price::new(376.05, 0), + Currency::USD(), + LiquiditySide::Taker, + uuid4(), + UnixNanos::default(), + UnixNanos::default(), + false, + Some(PositionId::new("SSD")), + Some(Money::from("12.2 USD")), + ); + + let mut position1 = Position::new(&instrument_audusd, fill1); + portfolio + .cache + .borrow_mut() + .add_position(position1.clone(), OmsType::Hedging) + .unwrap(); + let position_opened1 = get_open_position(&position1); + portfolio.update_position(&PositionEvent::PositionOpened(position_opened1)); + + let order2 = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Sell) + .quantity(Quantity::from("50000")) + .build(); + + let fill2 = OrderFilled::new( + order2.trader_id(), + order2.strategy_id(), + order2.instrument_id(), + order2.client_order_id(), + VenueOrderId::new("123456"), + AccountId::new("SIM-001"), + TradeId::new("2"), + order2.order_side(), + order2.order_type(), + order2.quantity(), + Price::new(1.00, 0), + Currency::USD(), + LiquiditySide::Taker, + uuid4(), + UnixNanos::default(), + UnixNanos::default(), + false, + Some(PositionId::new("SSD")), + Some(Money::from("1.2 USD")), + ); + + position1.apply(&fill2); + let position1_changed = get_changed_position(&position1); + + // Act + portfolio.update_position(&PositionEvent::PositionChanged(position1_changed)); + + // Assert + assert_eq!( + portfolio + .net_exposures(&Venue::from("SIM")) + .unwrap() + .get(&Currency::USD()) + .unwrap() + .as_f64(), + 100000.0 + ); + + assert_eq!( + portfolio + .unrealized_pnls(&Venue::from("SIM")) + .get(&Currency::USD()) + .unwrap() + .as_f64(), + -37500000.0 + ); + + assert_eq!( + portfolio + .realized_pnls(&Venue::from("SIM")) + .get(&Currency::USD()) + .unwrap() + .as_f64(), + -12.2 + ); + // FIX: TODO: should not be empty + assert_eq!(portfolio.margins_maint(&Venue::from("SIM")), HashMap::new()); + assert_eq!( + portfolio + .net_exposure(&instrument_audusd.id()) + .unwrap() + .as_f64(), + 100000.0 + ); + assert_eq!( + portfolio + .unrealized_pnl(&instrument_audusd.id()) + .unwrap() + .as_f64(), + -37500000.0 + ); + assert_eq!( + portfolio + .realized_pnl(&instrument_audusd.id()) + .unwrap() + .as_f64(), + -12.2 + ); + assert_eq!( + portfolio.net_position(&instrument_audusd.id()), + Decimal::from_f64(100000.0).unwrap() + ); + assert!(portfolio.is_net_long(&instrument_audusd.id())); + assert!(!portfolio.is_net_short(&instrument_audusd.id())); + assert!(!portfolio.is_flat(&instrument_audusd.id())); + assert!(!portfolio.is_completely_flat()); + assert_eq!( + portfolio.unrealized_pnls(&Venue::from("BINANCE")), + HashMap::new() + ); + assert_eq!( + portfolio.realized_pnls(&Venue::from("BINANCE")), + HashMap::new() + ); + assert_eq!(portfolio.net_exposures(&Venue::from("BINANCE")), None); + } + + #[rstest] + fn test_closing_position_updates_portfolio( + mut portfolio: Portfolio, + instrument_audusd: InstrumentAny, + ) { + let account_state = get_margin_account(None); + portfolio.update_account(&account_state); + + let last_audusd = get_quote_tick(&instrument_audusd, 0.80501, 0.80505, 1.0, 1.0); + portfolio.cache.borrow_mut().add_quote(last_audusd).unwrap(); + portfolio.update_quote_tick(&last_audusd); + + // Create Order + let order1 = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("100000")) + .build(); + + let fill1 = OrderFilled::new( + order1.trader_id(), + order1.strategy_id(), + order1.instrument_id(), + order1.client_order_id(), + VenueOrderId::new("123456"), + AccountId::new("SIM-001"), + TradeId::new("1"), + order1.order_side(), + order1.order_type(), + order1.quantity(), + Price::new(376.05, 0), + Currency::USD(), + LiquiditySide::Taker, + uuid4(), + UnixNanos::default(), + UnixNanos::default(), + false, + Some(PositionId::new("SSD")), + Some(Money::from("12.2 USD")), + ); + + let mut position1 = Position::new(&instrument_audusd, fill1); + portfolio + .cache + .borrow_mut() + .add_position(position1.clone(), OmsType::Hedging) + .unwrap(); + let position_opened1 = get_open_position(&position1); + portfolio.update_position(&PositionEvent::PositionOpened(position_opened1)); + + let order2 = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Sell) + .quantity(Quantity::from("50000")) + .build(); + + let fill2 = OrderFilled::new( + order2.trader_id(), + order2.strategy_id(), + order2.instrument_id(), + order2.client_order_id(), + VenueOrderId::new("123456"), + AccountId::new("SIM-001"), + TradeId::new("2"), + order2.order_side(), + order2.order_type(), + order2.quantity(), + Price::new(1.00, 0), + Currency::USD(), + LiquiditySide::Taker, + uuid4(), + UnixNanos::default(), + UnixNanos::default(), + false, + Some(PositionId::new("SSD")), + Some(Money::from("1.2 USD")), + ); + + position1.apply(&fill2); + portfolio + .cache + .borrow_mut() + .update_position(&position1) + .unwrap(); + + // Act + let position1_closed = get_close_position(&position1); + portfolio.update_position(&PositionEvent::PositionClosed(position1_closed)); + + // Assert + assert_eq!( + portfolio + .net_exposures(&Venue::from("SIM")) + .unwrap() + .get(&Currency::USD()) + .unwrap() + .as_f64(), + 100000.00 + ); + assert_eq!( + portfolio + .unrealized_pnls(&Venue::from("SIM")) + .get(&Currency::USD()) + .unwrap() + .as_f64(), + -37500000.00 + ); + assert_eq!( + portfolio + .realized_pnls(&Venue::from("SIM")) + .get(&Currency::USD()) + .unwrap() + .as_f64(), + -12.2 + ); + assert_eq!(portfolio.margins_maint(&Venue::from("SIM")), HashMap::new()); + } + + #[rstest] + fn test_several_positions_with_different_instruments_updates_portfolio( + mut portfolio: Portfolio, + instrument_audusd: InstrumentAny, + instrument_gbpusd: InstrumentAny, + ) { + let account_state = get_margin_account(None); + portfolio.update_account(&account_state); + + // Create Order + let order1 = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("100000")) + .build(); + let order2 = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_audusd.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("100000")) + .build(); + let order3 = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_gbpusd.id()) + .side(OrderSide::Buy) + .quantity(Quantity::from("100000")) + .build(); + let order4 = OrderTestBuilder::new(OrderType::Market) + .instrument_id(instrument_gbpusd.id()) + .side(OrderSide::Sell) + .quantity(Quantity::from("100000")) + .build(); + + let fill1 = OrderFilled::new( + order1.trader_id(), + StrategyId::new("S-1"), + order1.instrument_id(), + order1.client_order_id(), + VenueOrderId::new("123456"), + AccountId::new("SIM-001"), + TradeId::new("1"), + order1.order_side(), + order1.order_type(), + order1.quantity(), + Price::new(1.0, 0), + Currency::USD(), + LiquiditySide::Taker, + uuid4(), + UnixNanos::default(), + UnixNanos::default(), + false, + Some(PositionId::new("P-1")), + None, + ); + let fill2 = OrderFilled::new( + order2.trader_id(), + StrategyId::new("S-1"), + order2.instrument_id(), + order2.client_order_id(), + VenueOrderId::new("123456"), + AccountId::new("SIM-001"), + TradeId::new("2"), + order2.order_side(), + order2.order_type(), + order2.quantity(), + Price::new(1.0, 0), + Currency::USD(), + LiquiditySide::Taker, + uuid4(), + UnixNanos::default(), + UnixNanos::default(), + false, + Some(PositionId::new("P-2")), + None, + ); + let fill3 = OrderFilled::new( + order3.trader_id(), + StrategyId::new("S-1"), + order3.instrument_id(), + order3.client_order_id(), + VenueOrderId::new("123456"), + AccountId::new("SIM-001"), + TradeId::new("3"), + order3.order_side(), + order3.order_type(), + order3.quantity(), + Price::new(1.0, 0), + Currency::USD(), + LiquiditySide::Taker, + uuid4(), + UnixNanos::default(), + UnixNanos::default(), + false, + Some(PositionId::new("P-3")), + None, + ); + let fill4 = OrderFilled::new( + order4.trader_id(), + StrategyId::new("S-1"), + order4.instrument_id(), + order4.client_order_id(), + VenueOrderId::new("123456"), + AccountId::new("SIM-001"), + TradeId::new("4"), + order4.order_side(), + order4.order_type(), + order4.quantity(), + Price::new(1.0, 0), + Currency::USD(), + LiquiditySide::Taker, + uuid4(), + UnixNanos::default(), + UnixNanos::default(), + false, + Some(PositionId::new("P-4")), + None, + ); + + let position1 = Position::new(&instrument_audusd, fill1); + let position2 = Position::new(&instrument_audusd, fill2); + let mut position3 = Position::new(&instrument_gbpusd, fill3); + + let last_audusd = get_quote_tick(&instrument_audusd, 0.80501, 0.80505, 1.0, 1.0); + let last_gbpusd = get_quote_tick(&instrument_gbpusd, 1.30315, 1.30317, 1.0, 1.0); + + portfolio.cache.borrow_mut().add_quote(last_audusd).unwrap(); + portfolio.cache.borrow_mut().add_quote(last_gbpusd).unwrap(); + portfolio.update_quote_tick(&last_audusd); + portfolio.update_quote_tick(&last_gbpusd); + + portfolio + .cache + .borrow_mut() + .add_position(position1.clone(), OmsType::Hedging) + .unwrap(); + portfolio + .cache + .borrow_mut() + .add_position(position2.clone(), OmsType::Hedging) + .unwrap(); + portfolio + .cache + .borrow_mut() + .add_position(position3.clone(), OmsType::Hedging) + .unwrap(); + + let position_opened1 = get_open_position(&position1); + let position_opened2 = get_open_position(&position2); + let position_opened3 = get_open_position(&position3); + + portfolio.update_position(&PositionEvent::PositionOpened(position_opened1)); + portfolio.update_position(&PositionEvent::PositionOpened(position_opened2)); + portfolio.update_position(&PositionEvent::PositionOpened(position_opened3)); + + let position_closed3 = get_close_position(&position3); + position3.apply(&fill4); + portfolio + .cache + .borrow_mut() + .add_position(position3.clone(), OmsType::Hedging) + .unwrap(); + portfolio.update_position(&PositionEvent::PositionClosed(position_closed3)); + + // Assert + assert_eq!( + portfolio + .net_exposures(&Venue::from("SIM")) + .unwrap() + .get(&Currency::USD()) + .unwrap() + .as_f64(), + 200000.00 + ); + assert_eq!( + portfolio + .unrealized_pnls(&Venue::from("SIM")) + .get(&Currency::USD()) + .unwrap() + .as_f64(), + 0.0 + ); + assert_eq!( + portfolio + .realized_pnls(&Venue::from("SIM")) + .get(&Currency::USD()) + .unwrap() + .as_f64(), + 0.0 + ); + // FIX: TODO: should not be empty + assert_eq!(portfolio.margins_maint(&Venue::from("SIM")), HashMap::new()); } } diff --git a/nautilus_core/risk/src/engine/config.rs b/nautilus_core/risk/src/engine/config.rs index 1ef521e487c4..51ee77e43df0 100644 --- a/nautilus_core/risk/src/engine/config.rs +++ b/nautilus_core/risk/src/engine/config.rs @@ -13,7 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Provides a configuration for `ExecutionEngine` instances. +//! Provides a configuration for `RiskEngine` instances. use std::collections::HashMap; diff --git a/nautilus_core/risk/src/engine/mod.rs b/nautilus_core/risk/src/engine/mod.rs index 5f009fc2787d..e3ade98e741c 100644 --- a/nautilus_core/risk/src/engine/mod.rs +++ b/nautilus_core/risk/src/engine/mod.rs @@ -193,9 +193,9 @@ impl RiskEngine { } fn handle_submit_order_cache(cache: &Rc>, submit_order: &SubmitOrder) { - let mut burrowed_cache = cache.borrow_mut(); - if !burrowed_cache.order_exists(&submit_order.client_order_id) { - burrowed_cache + let mut borrowed_cache = cache.borrow_mut(); + if !borrowed_cache.order_exists(&submit_order.client_order_id) { + borrowed_cache .add_order(submit_order.order.clone(), None, None, false) .map_err(|e| { log::error!("Cannot add order to cache: {e}"); @@ -205,8 +205,8 @@ impl RiskEngine { } fn get_existing_order(cache: &Rc>, order: &ModifyOrder) -> Option { - let burrowed_cache = cache.borrow(); - if let Some(order) = burrowed_cache.order(&order.client_order_id) { + let borrowed_cache = cache.borrow(); + if let Some(order) = borrowed_cache.order(&order.client_order_id) { Some(order.clone()) } else { log::error!( @@ -424,8 +424,8 @@ impl RiskEngine { // VALIDATE COMMAND //////////////////////////////////////////////////////////////////////////////// let order_exists = { - let burrowed_cache = self.cache.borrow(); - burrowed_cache.order(&command.client_order_id).cloned() + let borrowed_cache = self.cache.borrow(); + borrowed_cache.order(&command.client_order_id).cloned() }; let order = if let Some(order) = order_exists { @@ -633,16 +633,16 @@ impl RiskEngine { last_px = match order { OrderAny::Market(_) | OrderAny::MarketToLimit(_) => { if last_px.is_none() { - let burrowed_cache = self.cache.borrow(); - if let Some(last_quote) = burrowed_cache.quote(&instrument.id()) { + let borrowed_cache = self.cache.borrow(); + if let Some(last_quote) = borrowed_cache.quote(&instrument.id()) { match order.order_side() { OrderSide::Buy => Some(last_quote.ask_price), OrderSide::Sell => Some(last_quote.bid_price), _ => panic!("Invalid order side"), } } else { - let burrowed_cache = self.cache.borrow(); - let last_trade = burrowed_cache.trade(&instrument.id()); + let borrowed_cache = self.cache.borrow(); + let last_trade = borrowed_cache.trade(&instrument.id()); if let Some(last_trade) = last_trade { Some(last_trade.price) @@ -937,9 +937,9 @@ impl RiskEngine { return; } - let mut burrowed_cache = self.cache.borrow_mut(); - if !burrowed_cache.order_exists(&order.client_order_id()) { - burrowed_cache + let mut borrowed_cache = self.cache.borrow_mut(); + if !borrowed_cache.order_exists(&order.client_order_id()) { + borrowed_cache .add_order(order.clone(), None, None, false) .map_err(|e| { log::error!("Cannot add order to cache: {e}"); From c456632e438ca8b53b708111a74c0f1cac3f792b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 27 Nov 2024 08:02:12 +1100 Subject: [PATCH 48/78] Update release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index 0111c1f37466..d1d926faf43d 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -8,6 +8,7 @@ Released on TBD (UTC). - Added `max_reconnection_tries` to data client config for dYdX (#2066), thanks @davidsblom ### Internal Improvements +- Ported `Portfolio` and `AccountManager` to Rust (#2058), thanks @Pushkarm029 - Improved live engines error logging (will now log all exceptions rather than just `RuntimeError`) - Improved symbol normalization for Tardis - Improved historical bar request performance for Tardis From f8a37506f029371239d30a076c114f83fb273a3a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 27 Nov 2024 08:13:01 +1100 Subject: [PATCH 49/78] Use proper isMaker field for Bybit executions --- nautilus_trader/adapters/bybit/execution.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nautilus_trader/adapters/bybit/execution.py b/nautilus_trader/adapters/bybit/execution.py index acf1b8acd6ea..dc1d6c4234ac 100644 --- a/nautilus_trader/adapters/bybit/execution.py +++ b/nautilus_trader/adapters/bybit/execution.py @@ -1008,9 +1008,7 @@ def _process_execution(self, execution: BybitWsAccountExecution) -> None: last_px=Price(float(execution.execPrice), instrument.price_precision), quote_currency=instrument.quote_currency, commission=Money(Decimal(execution.execFee), instrument.quote_currency), - liquidity_side=( - LiquiditySide.MAKER if order_type == OrderType.LIMIT else LiquiditySide.TAKER - ), + liquidity_side=LiquiditySide.MAKER if execution.isMaker else LiquiditySide.TAKER, ts_event=millis_to_nanos(float(execution.execTime)), ) From 5e1807026d34f4050a51886d688692b74294c136 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 27 Nov 2024 08:14:23 +1100 Subject: [PATCH 50/78] Rename BybitWebSocketClient Standardize WebSocket naming convention. --- nautilus_trader/adapters/bybit/data.py | 6 +++--- nautilus_trader/adapters/bybit/execution.py | 4 ++-- nautilus_trader/adapters/bybit/websocket/client.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nautilus_trader/adapters/bybit/data.py b/nautilus_trader/adapters/bybit/data.py index c5ca86d1ee7a..a039517c7a89 100644 --- a/nautilus_trader/adapters/bybit/data.py +++ b/nautilus_trader/adapters/bybit/data.py @@ -42,7 +42,7 @@ from nautilus_trader.adapters.bybit.schemas.ws import decoder_ws_kline from nautilus_trader.adapters.bybit.schemas.ws import decoder_ws_orderbook from nautilus_trader.adapters.bybit.schemas.ws import decoder_ws_trade -from nautilus_trader.adapters.bybit.websocket.client import BybitWebsocketClient +from nautilus_trader.adapters.bybit.websocket.client import BybitWebSocketClient from nautilus_trader.cache.cache import Cache from nautilus_trader.common.component import LiveClock from nautilus_trader.common.component import MessageBus @@ -135,12 +135,12 @@ def __init__( ) # WebSocket API - self._ws_clients: dict[BybitProductType, BybitWebsocketClient] = {} + self._ws_clients: dict[BybitProductType, BybitWebSocketClient] = {} self._decoders: dict[str, dict[BybitProductType, msgspec.json.Decoder]] = defaultdict( dict, ) for product_type in set(product_types): - self._ws_clients[product_type] = BybitWebsocketClient( + self._ws_clients[product_type] = BybitWebSocketClient( clock=clock, handler=partial(self._handle_ws_message, product_type), handler_reconnect=None, diff --git a/nautilus_trader/adapters/bybit/execution.py b/nautilus_trader/adapters/bybit/execution.py index dc1d6c4234ac..3f010cef46d2 100644 --- a/nautilus_trader/adapters/bybit/execution.py +++ b/nautilus_trader/adapters/bybit/execution.py @@ -46,7 +46,7 @@ from nautilus_trader.adapters.bybit.schemas.ws import BybitWsAccountOrderMsg from nautilus_trader.adapters.bybit.schemas.ws import BybitWsAccountPositionMsg from nautilus_trader.adapters.bybit.schemas.ws import BybitWsMessageGeneral -from nautilus_trader.adapters.bybit.websocket.client import BybitWebsocketClient +from nautilus_trader.adapters.bybit.websocket.client import BybitWebSocketClient from nautilus_trader.cache.cache import Cache from nautilus_trader.common.component import LiveClock from nautilus_trader.common.component import MessageBus @@ -171,7 +171,7 @@ def __init__( self._set_account_id(account_id) # WebSocket API - self._ws_client = BybitWebsocketClient( + self._ws_client = BybitWebSocketClient( clock=clock, handler=self._handle_ws_message, handler_reconnect=None, diff --git a/nautilus_trader/adapters/bybit/websocket/client.py b/nautilus_trader/adapters/bybit/websocket/client.py index 7179d37c872b..e1b7533dd2ec 100644 --- a/nautilus_trader/adapters/bybit/websocket/client.py +++ b/nautilus_trader/adapters/bybit/websocket/client.py @@ -32,7 +32,7 @@ MAX_ARGS_PER_SUBSCRIPTION_REQUEST = 10 -class BybitWebsocketClient: +class BybitWebSocketClient: """ Provides a Bybit streaming WebSocket client. From a84aeab4ff5811eac17ccc410e1560fed20caec0 Mon Sep 17 00:00:00 2001 From: faysou Date: Wed, 27 Nov 2024 00:33:43 +0000 Subject: [PATCH 51/78] Implement mixed catalog data requests with catalog update (#2043) Also add `metadata` parameter to request functions. --- examples/backtest/databento_option_greeks.py | 11 +- .../databento/databento_historical_data.py | 335 +++++++++++++ nautilus_trader/adapters/_template/data.py | 5 + nautilus_trader/adapters/binance/data.py | 19 +- nautilus_trader/adapters/bybit/data.py | 32 +- nautilus_trader/adapters/databento/data.py | 29 +- .../adapters/databento/data_utils.py | 7 +- nautilus_trader/adapters/dydx/data.py | 3 +- .../adapters/interactive_brokers/data.py | 14 +- nautilus_trader/adapters/okx/data.py | 27 +- nautilus_trader/adapters/polymarket/data.py | 29 +- nautilus_trader/adapters/tardis/data.py | 30 +- nautilus_trader/backtest/data_client.pyx | 16 +- nautilus_trader/backtest/node.py | 12 +- nautilus_trader/common/actor.pxd | 7 + nautilus_trader/common/actor.pyx | 60 ++- nautilus_trader/core/datetime.pxd | 1 + nautilus_trader/core/datetime.pyx | 58 ++- nautilus_trader/data/client.pxd | 15 +- nautilus_trader/data/client.pyx | 56 ++- nautilus_trader/data/engine.pxd | 14 +- nautilus_trader/data/engine.pyx | 353 +++++++++++--- nautilus_trader/live/data_client.py | 15 + nautilus_trader/persistence/catalog/base.py | 12 + .../persistence/catalog/parquet.py | 99 +++- nautilus_trader/persistence/config.py | 1 + nautilus_trader/risk/greeks.py | 18 +- nautilus_trader/system/config.py | 1 + nautilus_trader/system/kernel.py | 18 +- nautilus_trader/test_kit/mocks/data.py | 107 +++++ .../options_catalog/usd_short_term_rate.xml | 420 ++++++++++++++++ tests/unit_tests/data/test_aggregation.py | 127 ++--- tests/unit_tests/data/test_client.py | 5 +- tests/unit_tests/data/test_engine.py | 448 ++++++++++++++++++ 34 files changed, 2084 insertions(+), 320 deletions(-) create mode 100644 examples/live/databento/databento_historical_data.py create mode 100644 tests/test_data/databento/options_catalog/usd_short_term_rate.xml diff --git a/examples/backtest/databento_option_greeks.py b/examples/backtest/databento_option_greeks.py index 93d8860a7554..fa63ac6c28cf 100644 --- a/examples/backtest/databento_option_greeks.py +++ b/examples/backtest/databento_option_greeks.py @@ -57,7 +57,7 @@ # %% # import nautilus_trader.adapters.databento.data_utils as db_data_utils # from nautilus_trader.adapters.databento.data_utils import init_databento_client -# from option_trader import DATA_PATH, DATABENTO_API_KEY # personal library, use your own values especially for DATABENTO_API_KEY +# from option_trader import DATA_PATH # personal library, use your own value here # db_data_utils.DATA_PATH = DATA_PATH catalog_folder = "options_catalog" @@ -69,9 +69,9 @@ start_time = "2024-05-09T10:00" end_time = "2024-05-09T10:05" -# a valid databento key can be entered here, the example below runs with already saved test data -# db_data_utils.DATABENTO_API_KEY = DATABENTO_API_KEY -# init_databento_client() +# a valid databento key can be entered here (or as an env variable of the same name) +# DATABENTO_API_KEY = None +# init_databento_client(DATABENTO_API_KEY) # https://databento.com/docs/schemas-and-data-formats/whats-a-schema futures_data = databento_data( @@ -322,3 +322,6 @@ def user_log(self, msg): # %% engine.trader.generate_account_report(Venue("GLBX")) + +# %% +node.dispose() diff --git a/examples/live/databento/databento_historical_data.py b/examples/live/databento/databento_historical_data.py new file mode 100644 index 000000000000..8589271bfba5 --- /dev/null +++ b/examples/live/databento/databento_historical_data.py @@ -0,0 +1,335 @@ +# --- +# jupyter: +# jupytext: +# formats: py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.16.4 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# %% + +# %% [markdown] +# ## imports + +# %% +# Note: Use the python extension jupytext to be able to open this python file in jupyter as a notebook + +# %% +# from nautilus_trader.backtest.node import BacktestNode +# from nautilus_trader.common.enums import LogColor +# from nautilus_trader.config import BacktestDataConfig +# from nautilus_trader.config import BacktestEngineConfig +# from nautilus_trader.config import BacktestRunConfig +# from nautilus_trader.config import BacktestVenueConfig +# from nautilus_trader.config import ImportableActorConfig +# from nautilus_trader.config import ImportableStrategyConfig +# from nautilus_trader.config import LoggingConfig +# from nautilus_trader.config import StrategyConfig +# from nautilus_trader.config import StreamingConfig +# from nautilus_trader.core.datetime import unix_nanos_to_str +# from nautilus_trader.model.data import Bar +# from nautilus_trader.model.data import BarType +# from nautilus_trader.model.data import QuoteTick +# from nautilus_trader.model.enums import OrderSide +# from nautilus_trader.model.greeks import GreeksData +# from nautilus_trader.model.identifiers import InstrumentId +# from nautilus_trader.model.identifiers import Venue +# from nautilus_trader.model.objects import Price +# from nautilus_trader.model.objects import Quantity +# from nautilus_trader.risk.greeks import GreeksCalculator +# from nautilus_trader.risk.greeks import GreeksCalculatorConfig +# from nautilus_trader.risk.greeks import InterestRateProvider +# from nautilus_trader.risk.greeks import InterestRateProviderConfig +# from nautilus_trader.trading.strategy import Strategy +from typing import Any + +from nautilus_trader.adapters.databento import DATABENTO +from nautilus_trader.adapters.databento import DATABENTO_CLIENT_ID +from nautilus_trader.adapters.databento import DatabentoDataClientConfig +from nautilus_trader.adapters.databento import DatabentoLiveDataClientFactory +from nautilus_trader.adapters.databento.data_utils import databento_data +from nautilus_trader.adapters.databento.data_utils import load_catalog +from nautilus_trader.common.enums import LogColor +from nautilus_trader.config import InstrumentProviderConfig +from nautilus_trader.config import LiveExecEngineConfig +from nautilus_trader.config import LoggingConfig +from nautilus_trader.config import StrategyConfig +from nautilus_trader.config import TradingNodeConfig +from nautilus_trader.live.node import TradingNode +from nautilus_trader.model.book import OrderBook +from nautilus_trader.model.data import OrderBookDeltas +from nautilus_trader.model.data import QuoteTick +from nautilus_trader.model.data import TradeTick +from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.identifiers import TraderId +from nautilus_trader.trading.strategy import Strategy + + +# %% [markdown] +# ## parameters + +# %% +# import nautilus_trader.adapters.databento.data_utils as db_data_utils +# from nautilus_trader.adapters.databento.data_utils import init_databento_client +# from option_trader import DATA_PATH, DATABENTO_API_KEY # personal library, use your own values especially for DATABENTO_API_KEY +# db_data_utils.DATA_PATH = DATA_PATH + +catalog_folder = "options_catalog" +catalog = load_catalog(catalog_folder) + +future_symbols = ["ESM4"] +option_symbols = ["ESM4 P5230", "ESM4 P5250"] + +start_time = "2024-05-09T10:00" +end_time = "2024-05-09T10:05" + +# a valid databento key can be entered here, the example below runs with already saved test data +# db_data_utils.DATABENTO_API_KEY = DATABENTO_API_KEY +# init_databento_client() + +# https://databento.com/docs/schemas-and-data-formats/whats-a-schema +futures_data = databento_data( + future_symbols, + start_time, + end_time, + "ohlcv-1m", + "futures", + catalog_folder, +) +options_data = databento_data( + option_symbols, + start_time, + end_time, + "mbp-1", + "options", + catalog_folder, +) + + +# %% [markdown] +# ## strategy + + +# %% +class DataSubscriberConfig(StrategyConfig, frozen=True): + """ + Configuration for ``DataSubscriber`` instances. + + Parameters + ---------- + instrument_ids : list[InstrumentId] + The instrument IDs to subscribe to. + + """ + + instrument_ids: list[InstrumentId] + + +class DataSubscriber(Strategy): + """ + An example strategy which subscribes to live data. + + Parameters + ---------- + config : DataSubscriberConfig + The configuration for the instance. + + """ + + def __init__(self, config: DataSubscriberConfig) -> None: + super().__init__(config) + + # Configuration + self.instrument_ids = config.instrument_ids + + def on_start(self) -> None: + """ + Actions to be performed when the strategy is started. + + Here we specify the 'DATABENTO' client_id for subscriptions. + + """ + for instrument_id in self.instrument_ids: + # from nautilus_trader.model.enums import BookType + + # self.subscribe_order_book_deltas( + # instrument_id=instrument_id, + # book_type=BookType.L3_MBO, + # client_id=DATABENTO_CLIENT_ID, + # ) + # self.subscribe_order_book_at_interval( + # instrument_id=instrument_id, + # book_type=BookType.L2_MBP, + # depth=10, + # client_id=DATABENTO_CLIENT_ID, + # interval_ms=1000, + # ) + + self.subscribe_quote_ticks(instrument_id, client_id=DATABENTO_CLIENT_ID) + self.subscribe_trade_ticks(instrument_id, client_id=DATABENTO_CLIENT_ID) + # self.subscribe_instrument_status(instrument_id, client_id=DATABENTO_CLIENT_ID) + # self.request_quote_ticks(instrument_id) + # self.request_trade_ticks(instrument_id) + + # from nautilus_trader.model.data import DataType + # from nautilus_trader.model.data import InstrumentStatus + # + # status_data_type = DataType( + # type=InstrumentStatus, + # metadata={"instrument_id": instrument_id}, + # ) + # self.request_data(status_data_type, client_id=DATABENTO_CLIENT_ID) + + # from nautilus_trader.model.data import BarType + # self.request_bars(BarType.from_str(f"{instrument_id}-1-MINUTE-LAST-EXTERNAL")) + + # # Imbalance + # from nautilus_trader.adapters.databento import DatabentoImbalance + # + # metadata = {"instrument_id": instrument_id} + # self.request_data( + # data_type=DataType(type=DatabentoImbalance, metadata=metadata), + # client_id=DATABENTO_CLIENT_ID, + # ) + + # # Statistics + # from nautilus_trader.adapters.databento import DatabentoStatistics + # + # metadata = {"instrument_id": instrument_id} + # self.subscribe_data( + # data_type=DataType(type=DatabentoStatistics, metadata=metadata), + # client_id=DATABENTO_CLIENT_ID, + # ) + # self.request_data( + # data_type=DataType(type=DatabentoStatistics, metadata=metadata), + # client_id=DATABENTO_CLIENT_ID, + # ) + + # self.request_instruments(venue=Venue("GLBX"), client_id=DATABENTO_CLIENT_ID) + # self.request_instruments(venue=Venue("XCHI"), client_id=DATABENTO_CLIENT_ID) + # self.request_instruments(venue=Venue("XNAS"), client_id=DATABENTO_CLIENT_ID) + + def on_stop(self) -> None: + """ + Actions to be performed when the strategy is stopped. + """ + # Databento does not support live data unsubscribing + + def on_historical_data(self, data: Any) -> None: + self.log.info(repr(data), LogColor.CYAN) + + def on_order_book_deltas(self, deltas: OrderBookDeltas) -> None: + """ + Actions to be performed when the strategy is running and receives order book + deltas. + + Parameters + ---------- + deltas : OrderBookDeltas + The order book deltas received. + + """ + self.log.info(repr(deltas), LogColor.CYAN) + + def on_order_book(self, order_book: OrderBook) -> None: + """ + Actions to be performed when an order book update is received. + """ + self.log.info(f"\n{order_book.instrument_id}\n{order_book.pprint(10)}", LogColor.CYAN) + + def on_quote_tick(self, tick: QuoteTick) -> None: + """ + Actions to be performed when the strategy is running and receives a quote tick. + + Parameters + ---------- + tick : QuoteTick + The tick received. + + """ + self.log.info(repr(tick), LogColor.CYAN) + + def on_trade_tick(self, tick: TradeTick) -> None: + """ + Actions to be performed when the strategy is running and receives a trade tick. + + Parameters + ---------- + tick : TradeTick + The tick received. + + """ + self.log.info(repr(tick), LogColor.CYAN) + + +# %% [markdown] +# ## backtest node + +# %% +# For correct subscription operation, you must specify all instruments to be immediately +# subscribed for as part of the data client configuration +instrument_ids = [ + InstrumentId.from_str("ES.c.0.GLBX"), + # InstrumentId.from_str("ES.FUT.GLBX"), + # InstrumentId.from_str("CL.FUT.GLBX"), + # InstrumentId.from_str("LO.OPT.GLBX"), + # InstrumentId.from_str("AAPL.XNAS"), +] + +# %% +# Configure the trading node +config_node = TradingNodeConfig( + trader_id=TraderId("TESTER-001"), + logging=LoggingConfig(log_level="INFO", use_pyo3=True), + exec_engine=LiveExecEngineConfig( + reconciliation=False, # Not applicable + inflight_check_interval_ms=0, # Not applicable + # snapshot_orders=True, + # snapshot_positions=True, + # snapshot_positions_interval_secs=5.0, + ), + data_clients={ + DATABENTO: DatabentoDataClientConfig( + api_key=None, # 'DATABENTO_API_KEY' env var + http_gateway=None, + instrument_provider=InstrumentProviderConfig(load_all=True), + instrument_ids=instrument_ids, + parent_symbols={"GLBX.MDP3": {"ES.FUT"}}, + mbo_subscriptions_delay=10.0, + ), + }, + timeout_connection=20.0, + timeout_reconciliation=10.0, # Not applicable + timeout_portfolio=10.0, + timeout_disconnection=10.0, + timeout_post_stop=0.0, # Not required as no order state +) + +# %% +# Instantiate the node with a configuration +node = TradingNode(config=config_node) + +strat_config = DataSubscriberConfig(instrument_ids=instrument_ids) +strategy = DataSubscriber(config=strat_config) + +# Add your strategies and modules +node.trader.add_strategy(strategy) + +# Register your client factories with the node (can take user-defined factories) +node.add_data_client_factory(DATABENTO, DatabentoLiveDataClientFactory) +node.build() + +# %% +node.run() + +# %% +node.dispose() + +# %% diff --git a/nautilus_trader/adapters/_template/data.py b/nautilus_trader/adapters/_template/data.py index de2b624c538d..cb43a26d20f3 100644 --- a/nautilus_trader/adapters/_template/data.py +++ b/nautilus_trader/adapters/_template/data.py @@ -296,6 +296,7 @@ async def _request_instrument( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: raise NotImplementedError( "method `_request_instrument` must be implemented in the subclass", @@ -307,6 +308,7 @@ async def _request_instruments( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: raise NotImplementedError( "method `_request_instruments` must be implemented in the subclass", @@ -329,6 +331,7 @@ async def _request_quote_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: raise NotImplementedError( "method `_request_quote_tick` must be implemented in the subclass", @@ -341,6 +344,7 @@ async def _request_trade_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: raise NotImplementedError( "method `_request_trade_ticks` must be implemented in the subclass", @@ -353,6 +357,7 @@ async def _request_bars( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: raise NotImplementedError( "method `_request_bars` must be implemented in the subclass", diff --git a/nautilus_trader/adapters/binance/data.py b/nautilus_trader/adapters/binance/data.py index 12b5f0790f79..cc3e50748934 100644 --- a/nautilus_trader/adapters/binance/data.py +++ b/nautilus_trader/adapters/binance/data.py @@ -538,6 +538,7 @@ async def _request_instrument( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if start is not None: self._log.warning( @@ -554,16 +555,7 @@ async def _request_instrument( self._log.error(f"Cannot find instrument for {instrument_id}") return - data_type = DataType( - type=Instrument, - metadata={"instrument_id": instrument_id}, - ) - - self._handle_data_response( - data_type=data_type, - data=[instrument], # Data engine handles lists of instruments - correlation_id=correlation_id, - ) + self._handle_instrument(instrument, correlation_id, metadata) async def _request_quote_ticks( self, @@ -572,6 +564,7 @@ async def _request_quote_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: self._log.error( "Cannot request historical quotes: not published by Binance", @@ -584,6 +577,7 @@ async def _request_trade_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if limit == 0 or limit > 1000: limit = 1000 @@ -616,7 +610,7 @@ async def _request_trade_ticks( ts_init=self._clock.timestamp_ns(), ) - self._handle_trade_ticks(instrument_id, ticks, correlation_id) + self._handle_trade_ticks(instrument_id, ticks, correlation_id, metadata) async def _request_bars( # (too complex) self, @@ -625,6 +619,7 @@ async def _request_bars( # (too complex) correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if bar_type.spec.price_type != PriceType.LAST: self._log.error( @@ -693,7 +688,7 @@ async def _request_bars( # (too complex) ) partial: Bar = bars.pop() - self._handle_bars(bar_type, bars, partial, correlation_id) + self._handle_bars(bar_type, bars, partial, correlation_id, metadata) async def _request_order_book_snapshot( self, diff --git a/nautilus_trader/adapters/bybit/data.py b/nautilus_trader/adapters/bybit/data.py index a039517c7a89..44719549d5be 100644 --- a/nautilus_trader/adapters/bybit/data.py +++ b/nautilus_trader/adapters/bybit/data.py @@ -394,6 +394,7 @@ async def _request_instrument( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if start is not None: self._log.warning( @@ -409,15 +410,8 @@ async def _request_instrument( if instrument is None: self._log.error(f"Cannot find instrument for {instrument_id}") return - data_type = DataType( - type=Instrument, - metadata={"instrument_id": instrument_id}, - ) - self._handle_data_response( - data_type=data_type, - data=instrument, - correlation_id=correlation_id, - ) + + self._handle_instrument(instrument, correlation_id, metadata) async def _request_instruments( self, @@ -425,6 +419,7 @@ async def _request_instruments( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if start is not None: self._log.warning( @@ -441,15 +436,8 @@ async def _request_instruments( for instrument in all_instruments.values(): if instrument.venue == venue: target_instruments.append(instrument) - data_type = DataType( - type=Instrument, - metadata={"venue": venue}, - ) - self._handle_data_response( - data_type=data_type, - data=target_instruments, - correlation_id=correlation_id, - ) + + self._handle_instruments(venue, target_instruments, correlation_id, metadata) async def _request_quote_ticks( self, @@ -458,6 +446,7 @@ async def _request_quote_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: self._log.error( "Cannot request historical quotes: not published by Bybit", @@ -470,6 +459,7 @@ async def _request_trade_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if limit == 0 or limit > 1000: limit = 1000 @@ -488,7 +478,8 @@ async def _request_trade_ticks( limit=limit, ts_init=self._clock.timestamp_ns(), ) - self._handle_trade_ticks(instrument_id, trades, correlation_id) + + self._handle_trade_ticks(instrument_id, trades, correlation_id, metadata) async def _request_bars( self, @@ -497,6 +488,7 @@ async def _request_bars( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if limit == 0 or limit > 1000: limit = 1000 @@ -538,7 +530,7 @@ async def _request_bars( ts_init=self._clock.timestamp_ns(), ) partial: Bar = bars.pop() - self._handle_bars(bar_type, bars, partial, correlation_id) + self._handle_bars(bar_type, bars, partial, correlation_id, metadata) async def _handle_ticker_data_request(self, symbol: Symbol, correlation_id: UUID4) -> None: bybit_symbol = BybitSymbol(symbol.value) diff --git a/nautilus_trader/adapters/databento/data.py b/nautilus_trader/adapters/databento/data.py index 47316e122b30..aed21be26a75 100644 --- a/nautilus_trader/adapters/databento/data.py +++ b/nautilus_trader/adapters/databento/data.py @@ -811,6 +811,7 @@ async def _request_instrument( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: dataset: Dataset = self._loader.get_dataset_for_venue(instrument_id.venue) _, available_end = await self._get_dataset_range(dataset) @@ -839,10 +840,7 @@ async def _request_instrument( ) return - self._handle_instrument( - instrument=instruments[0], - correlation_id=correlation_id, - ) + self._handle_instrument(instruments[0], correlation_id, metadata) async def _request_instruments( self, @@ -850,6 +848,7 @@ async def _request_instruments( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: dataset: Dataset = self._loader.get_dataset_for_venue(venue) _, available_end = await self._get_dataset_range(dataset) @@ -872,11 +871,7 @@ async def _request_instruments( instruments = instruments_from_pyo3(pyo3_instruments) - self._handle_instruments( - instruments=instruments, - venue=venue, - correlation_id=correlation_id, - ) + self._handle_instruments(instruments, venue, correlation_id, metadata) async def _request_quote_ticks( self, @@ -885,6 +880,7 @@ async def _request_quote_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: dataset: Dataset = self._loader.get_dataset_for_venue(instrument_id.venue) _, available_end = await self._get_dataset_range(dataset) @@ -911,11 +907,7 @@ async def _request_quote_ticks( quotes = QuoteTick.from_pyo3_list(pyo3_quotes) - self._handle_quote_ticks( - instrument_id=instrument_id, - ticks=quotes, - correlation_id=correlation_id, - ) + self._handle_quote_ticks(instrument_id, quotes, correlation_id, metadata) async def _request_trade_ticks( self, @@ -924,6 +916,7 @@ async def _request_trade_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: dataset: Dataset = self._loader.get_dataset_for_venue(instrument_id.venue) _, available_end = await self._get_dataset_range(dataset) @@ -950,11 +943,7 @@ async def _request_trade_ticks( trades = TradeTick.from_pyo3_list(pyo3_trades) - self._handle_trade_ticks( - instrument_id=instrument_id, - ticks=trades, - correlation_id=correlation_id, - ) + self._handle_trade_ticks(instrument_id, trades, correlation_id, metadata) async def _request_bars( self, @@ -963,6 +952,7 @@ async def _request_bars( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: dataset: Dataset = self._loader.get_dataset_for_venue(bar_type.instrument_id.venue) _, available_end = await self._get_dataset_range(dataset) @@ -998,6 +988,7 @@ async def _request_bars( bars=bars, partial=None, # No partials correlation_id=correlation_id, + metadata=metadata, ) def _handle_msg_pyo3( diff --git a/nautilus_trader/adapters/databento/data_utils.py b/nautilus_trader/adapters/databento/data_utils.py index ee0002d2b31e..45a355ff145f 100644 --- a/nautilus_trader/adapters/databento/data_utils.py +++ b/nautilus_trader/adapters/databento/data_utils.py @@ -10,14 +10,11 @@ # where you store all your databento data DATA_PATH = PACKAGE_ROOT / "tests" / "test_data" / "databento" -# this variable can be modified with a valid key if downloading data is needed -DATABENTO_API_KEY = "db-XXXXX" - - client = None -def init_databento_client(): +# if DATABENTO_API_KEY is None, an environment variable with the same name can be used +def init_databento_client(DATABENTO_API_KEY=None): import databento as db global client diff --git a/nautilus_trader/adapters/dydx/data.py b/nautilus_trader/adapters/dydx/data.py index 815097081515..a205f09c2bab 100644 --- a/nautilus_trader/adapters/dydx/data.py +++ b/nautilus_trader/adapters/dydx/data.py @@ -579,6 +579,7 @@ async def _request_bars( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: max_bars = 100 @@ -634,4 +635,4 @@ async def _request_bars( ] partial: Bar = bars.pop() - self._handle_bars(bar_type, bars, partial, correlation_id) + self._handle_bars(bar_type, bars, partial, correlation_id, metadata) diff --git a/nautilus_trader/adapters/interactive_brokers/data.py b/nautilus_trader/adapters/interactive_brokers/data.py index a59057c5ef01..513cb3c7ab21 100644 --- a/nautilus_trader/adapters/interactive_brokers/data.py +++ b/nautilus_trader/adapters/interactive_brokers/data.py @@ -278,6 +278,7 @@ async def _request_instrument( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if start is not None: self._log.warning( @@ -295,7 +296,8 @@ async def _request_instrument( else: self._log.warning(f"Instrument for {instrument_id} not available") return - self._handle_instrument(instrument, correlation_id) + + self._handle_instrument(instrument, correlation_id, metadata) async def _request_instruments( self, @@ -303,6 +305,7 @@ async def _request_instruments( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: raise NotImplementedError( # pragma: no cover "implement the `_request_instruments` coroutine", # pragma: no cover @@ -315,6 +318,7 @@ async def _request_quote_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if not (instrument := self._cache.instrument(instrument_id)): self._log.error( @@ -333,7 +337,7 @@ async def _request_quote_ticks( self._log.warning(f"No quote tick data received for {instrument_id}") return - self._handle_quote_ticks(instrument_id, ticks, correlation_id) + self._handle_quote_ticks(instrument_id, ticks, correlation_id, metadata) async def _request_trade_ticks( self, @@ -342,6 +346,7 @@ async def _request_trade_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if not (instrument := self._cache.instrument(instrument_id)): self._log.error( @@ -366,7 +371,7 @@ async def _request_trade_ticks( self._log.warning(f"No trades received for {instrument_id}") return - self._handle_trade_ticks(instrument_id, ticks, correlation_id) + self._handle_trade_ticks(instrument_id, ticks, correlation_id, metadata) async def _handle_ticks_request( self, @@ -407,6 +412,7 @@ async def _request_bars( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if not (instrument := self._cache.instrument(bar_type.instrument_id)): self._log.error(f"Cannot request {bar_type} bars: instrument not found") @@ -448,7 +454,7 @@ async def _request_bars( if bars: bars = list(set(bars)) bars.sort(key=lambda x: x.ts_init) - self._handle_bars(bar_type, bars, bars[0], correlation_id) + self._handle_bars(bar_type, bars, bars[0], correlation_id, metadata) status_msg = {"id": correlation_id, "status": "Success"} else: self._log.warning(f"No bar data received for {bar_type}") diff --git a/nautilus_trader/adapters/okx/data.py b/nautilus_trader/adapters/okx/data.py index 2a77960fde4e..7796dd0315a7 100644 --- a/nautilus_trader/adapters/okx/data.py +++ b/nautilus_trader/adapters/okx/data.py @@ -391,6 +391,7 @@ async def _request_instrument( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if start is not None: self._log.warning( @@ -406,15 +407,8 @@ async def _request_instrument( if instrument is None: self._log.error(f"Cannot find instrument for {instrument_id}") return - data_type = DataType( - type=Instrument, - metadata={"instrument_id": instrument_id}, - ) - self._handle_data_response( - data_type=data_type, - data=instrument, - correlation_id=correlation_id, - ) + + self._handle_instrument(instrument, correlation_id, metadata) async def _request_instruments( self, @@ -422,6 +416,7 @@ async def _request_instruments( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if start is not None: self._log.warning( @@ -438,15 +433,8 @@ async def _request_instruments( for instrument in all_instruments.values(): if instrument.venue == venue: target_instruments.append(instrument) - data_type = DataType( - type=Instrument, - metadata={"venue": venue}, - ) - self._handle_data_response( - data_type=data_type, - data=target_instruments, - correlation_id=correlation_id, - ) + + self._handle_instruments(target_instruments, venue, correlation_id, metadata) async def _request_quote_ticks( self, @@ -455,6 +443,7 @@ async def _request_quote_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: self._log.error( "Cannot request historical quotes: not published by OKX. Subscribe to " @@ -469,6 +458,7 @@ async def _request_trade_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: self._log.error("Cannot request historical trades: not yet implemented for OKX") return @@ -480,6 +470,7 @@ async def _request_bars( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: self._log.error("Cannot request historical bars: not yet implemented for OKX") return diff --git a/nautilus_trader/adapters/polymarket/data.py b/nautilus_trader/adapters/polymarket/data.py index 149c92b6deae..574574444655 100644 --- a/nautilus_trader/adapters/polymarket/data.py +++ b/nautilus_trader/adapters/polymarket/data.py @@ -41,7 +41,6 @@ from nautilus_trader.live.data_client import LiveMarketDataClient from nautilus_trader.model.book import OrderBook from nautilus_trader.model.data import BarType -from nautilus_trader.model.data import DataType from nautilus_trader.model.data import OrderBookDeltas from nautilus_trader.model.data import QuoteTick from nautilus_trader.model.enums import BookType @@ -49,7 +48,6 @@ from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.instruments import BinaryOption -from nautilus_trader.model.instruments import Instrument class PolymarketDataClient(LiveMarketDataClient): @@ -284,6 +282,7 @@ async def _request_instrument( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if start is not None: self._log.warning( @@ -299,15 +298,8 @@ async def _request_instrument( if instrument is None: self._log.error(f"Cannot find instrument for {instrument_id}") return - data_type = DataType( - type=Instrument, - metadata={"instrument_id": instrument_id}, - ) - self._handle_data_response( - data_type=data_type, - data=instrument, - correlation_id=correlation_id, - ) + + self._handle_instrument(instrument, correlation_id, metadata) async def _request_instruments( self, @@ -315,6 +307,7 @@ async def _request_instruments( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if start is not None: self._log.warning( @@ -331,15 +324,8 @@ async def _request_instruments( for instrument in all_instruments.values(): if instrument.venue == venue: target_instruments.append(instrument) - data_type = DataType( - type=Instrument, - metadata={"venue": venue}, - ) - self._handle_data_response( - data_type=data_type, - data=target_instruments, - correlation_id=correlation_id, - ) + + self._handle_instruments(target_instruments, venue, correlation_id, metadata) async def _request_quote_ticks( self, @@ -348,6 +334,7 @@ async def _request_quote_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: self._log.error("Cannot request historical quotes: not published by Polymarket") @@ -358,6 +345,7 @@ async def _request_trade_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: self._log.error("Cannot request historical trades: not published by Polymarket") @@ -368,6 +356,7 @@ async def _request_bars( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: self._log.error("Cannot request historical bars: not published by Polymarket") diff --git a/nautilus_trader/adapters/tardis/data.py b/nautilus_trader/adapters/tardis/data.py index 285b1b4e0a39..cee8a5ca3727 100644 --- a/nautilus_trader/adapters/tardis/data.py +++ b/nautilus_trader/adapters/tardis/data.py @@ -35,7 +35,6 @@ from nautilus_trader.live.data_client import LiveMarketDataClient from nautilus_trader.model.data import Bar from nautilus_trader.model.data import BarType -from nautilus_trader.model.data import DataType from nautilus_trader.model.data import OrderBookDelta from nautilus_trader.model.data import OrderBookDepth10 from nautilus_trader.model.data import QuoteTick @@ -327,6 +326,7 @@ async def _request_instrument( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if start is not None: self._log.warning( @@ -342,15 +342,8 @@ async def _request_instrument( if instrument is None: self._log.error(f"Cannot find instrument for {instrument_id}") return - data_type = DataType( - type=Instrument, - metadata={"instrument_id": instrument_id}, - ) - self._handle_data_response( - data_type=data_type, - data=instrument, - correlation_id=correlation_id, - ) + + self._handle_instrument(instrument, correlation_id, metadata) async def _request_instruments( self, @@ -358,6 +351,7 @@ async def _request_instruments( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if start is not None: self._log.warning( @@ -374,15 +368,8 @@ async def _request_instruments( for instrument in all_instruments.values(): if instrument.venue == venue: target_instruments.append(instrument) - data_type = DataType( - type=Instrument, - metadata={"venue": venue}, - ) - self._handle_data_response( - data_type=data_type, - data=target_instruments, - correlation_id=correlation_id, - ) + + self._handle_instruments(target_instruments, venue, correlation_id, metadata) async def _request_quote_ticks( self, @@ -391,6 +378,7 @@ async def _request_quote_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: self._log.error( f"Cannot request historical quotes for {instrument_id}: not supported in this version", @@ -403,6 +391,7 @@ async def _request_trade_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: self._log.error( f"Cannot request historical trades for {instrument_id}: not supported in this version", @@ -415,6 +404,7 @@ async def _request_bars( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: if bar_type.is_internally_aggregated(): self._log.error( @@ -496,7 +486,7 @@ async def _request_bars( LogColor.MAGENTA, ) - self._handle_bars(bar_type, bars, None, correlation_id) + self._handle_bars(bar_type, bars, None, correlation_id, metadata) def _handle_msg( self, diff --git a/nautilus_trader/backtest/data_client.pyx b/nautilus_trader/backtest/data_client.pyx index d34a429d2abe..44260d630706 100644 --- a/nautilus_trader/backtest/data_client.pyx +++ b/nautilus_trader/backtest/data_client.pyx @@ -317,6 +317,7 @@ cdef class BacktestMarketDataClient(MarketDataClient): UUID4 correlation_id, datetime start: datetime | None = None, datetime end: datetime | None = None, + dict metadata = None, ): Condition.not_none(instrument_id, "instrument_id") Condition.not_none(correlation_id, "correlation_id") @@ -331,10 +332,7 @@ cdef class BacktestMarketDataClient(MarketDataClient): metadata={"instrument_id": instrument_id}, ) - self._handle_instrument( - instrument=instrument, - correlation_id=correlation_id, - ) + self._handle_instrument(instrument, correlation_id, metadata) cpdef void request_instruments( self, @@ -342,6 +340,7 @@ cdef class BacktestMarketDataClient(MarketDataClient): UUID4 correlation_id, datetime start: datetime | None = None, datetime end: datetime | None = None, + dict metadata = None, ): Condition.not_none(correlation_id, "correlation_id") @@ -350,11 +349,7 @@ cdef class BacktestMarketDataClient(MarketDataClient): self._log.error(f"Cannot find instruments") return - self._handle_instruments( - venue=venue, - instruments=instruments, - correlation_id=correlation_id, - ) + self._handle_instruments(venue, instruments, correlation_id, metadata) cpdef void request_order_book_snapshot( self, @@ -374,6 +369,7 @@ cdef class BacktestMarketDataClient(MarketDataClient): UUID4 correlation_id, datetime start: datetime | None = None, datetime end: datetime | None = None, + dict metadata = None, ): Condition.not_none(instrument_id, "instrument_id") Condition.not_none(correlation_id, "correlation_id") @@ -387,6 +383,7 @@ cdef class BacktestMarketDataClient(MarketDataClient): UUID4 correlation_id, datetime start: datetime | None = None, datetime end: datetime | None = None, + dict metadata = None, ): Condition.not_none(instrument_id, "instrument_id") Condition.not_negative_int(limit, "limit") @@ -401,6 +398,7 @@ cdef class BacktestMarketDataClient(MarketDataClient): UUID4 correlation_id, datetime start: datetime | None = None, datetime end: datetime | None = None, + dict metadata = None, ): Condition.not_none(bar_type, "bar_type") Condition.not_negative_int(limit, "limit") diff --git a/nautilus_trader/backtest/node.py b/nautilus_trader/backtest/node.py index fa17705534aa..8a70db765624 100644 --- a/nautilus_trader/backtest/node.py +++ b/nautilus_trader/backtest/node.py @@ -32,8 +32,8 @@ from nautilus_trader.core import nautilus_pyo3 from nautilus_trader.core.correctness import PyCondition from nautilus_trader.core.datetime import dt_to_unix_nanos -from nautilus_trader.core.datetime import max_date_str -from nautilus_trader.core.datetime import min_date_str +from nautilus_trader.core.datetime import max_date +from nautilus_trader.core.datetime import min_date from nautilus_trader.core.inspect import is_nautilus_class from nautilus_trader.core.nautilus_pyo3 import DataBackendSession from nautilus_trader.model import BOOK_DATA_TYPES @@ -370,11 +370,11 @@ def _run_streaming( used_start = config.start_time if used_start is not None or start is not None: - used_start = max_date_str(used_start, start) + used_start = max_date(used_start, start).isoformat() used_end = config.end_time if used_end is not None or end is not None: - used_end = min_date_str(used_end, end) + used_end = min_date(used_end, end).isoformat() session = catalog.backend_session( data_cls=config.data_type, @@ -466,10 +466,10 @@ def load_data_config( config_query = config.query if config_query["start"] is not None or start is not None: - config_query["start"] = max_date_str(config_query["start"], start) + config_query["start"] = max_date(config_query["start"], start).isoformat() if config_query["end"] is not None or end is not None: - config_query["end"] = min_date_str(config_query["end"], end) + config_query["end"] = min_date(config_query["end"], end).isoformat() return CatalogDataResult( data_cls=config.data_type, diff --git a/nautilus_trader/common/actor.pxd b/nautilus_trader/common/actor.pxd index 3db707e7680f..31c37b84471b 100644 --- a/nautilus_trader/common/actor.pxd +++ b/nautilus_trader/common/actor.pxd @@ -191,6 +191,7 @@ cdef class Actor(Component): datetime end=*, ClientId client_id=*, callback=*, + bint update_catalog=*, ) cpdef UUID4 request_instruments( self, @@ -199,6 +200,7 @@ cdef class Actor(Component): datetime end=*, ClientId client_id=*, callback=*, + bint update_catalog=*, ) cpdef UUID4 request_order_book_snapshot( self, @@ -214,6 +216,8 @@ cdef class Actor(Component): datetime end=*, ClientId client_id=*, callback=*, + bint update_catalog=*, + str quote_type=*, ) cpdef UUID4 request_trade_ticks( self, @@ -222,6 +226,7 @@ cdef class Actor(Component): datetime end=*, ClientId client_id=*, callback=*, + bint update_catalog=*, ) cpdef UUID4 request_bars( self, @@ -230,6 +235,7 @@ cdef class Actor(Component): datetime end=*, ClientId client_id=*, callback=*, + bint update_catalog=*, ) cpdef UUID4 request_aggregated_bars( self, @@ -240,6 +246,7 @@ cdef class Actor(Component): bint include_external_data=*, ClientId client_id=*, callback=*, + bint update_catalog=*, ) cpdef bint is_pending_request(self, UUID4 request_id) cpdef bint has_pending_requests(self) diff --git a/nautilus_trader/common/actor.pyx b/nautilus_trader/common/actor.pyx index 187eeb14ffea..1bf3fe68a7af 100644 --- a/nautilus_trader/common/actor.pyx +++ b/nautilus_trader/common/actor.pyx @@ -1951,6 +1951,7 @@ cdef class Actor(Component): datetime end = None, ClientId client_id = None, callback: Callable[[UUID4], None] | None = None, + bint update_catalog = False, ): """ Request `Instrument` data for the given instrument ID. @@ -1972,6 +1973,8 @@ cdef class Actor(Component): callback : Callable[[UUID4], None], optional The registered callback, to be called with the request ID when the response has completed processing. + update_catalog : bool, default False + If True then updates the catalog with new data received from a client. Returns ------- @@ -1988,6 +1991,11 @@ cdef class Actor(Component): """ Condition.is_true(self.trader_id is not None, "The actor has not been registered") Condition.not_none(instrument_id, "instrument_id") + now = self.clock.utc_now() + if start is not None: + Condition.is_true(start <= now, "start was > now") + if end is not None: + Condition.is_true(end <= now, "end was > now") if start is not None and end is not None: Condition.is_true(start < end, "start was >= end") Condition.callable_or_none(callback, "callback") @@ -2000,6 +2008,7 @@ cdef class Actor(Component): "instrument_id": instrument_id, "start": start, "end": end, + "update_catalog": update_catalog, }), callback=self._handle_instrument_response, request_id=request_id, @@ -2010,7 +2019,6 @@ cdef class Actor(Component): self._send_data_req(request) return request_id - cpdef UUID4 request_instruments( self, Venue venue, @@ -2018,6 +2026,7 @@ cdef class Actor(Component): datetime end = None, ClientId client_id = None, callback: Callable[[UUID4], None] | None = None, + bint update_catalog = False, ): """ Request all `Instrument` data for the given venue. @@ -2039,6 +2048,8 @@ cdef class Actor(Component): callback : Callable[[UUID4], None], optional The registered callback, to be called with the request ID when the response has completed processing. + update_catalog : bool, default False + If True then updates the catalog with new data received from a client. Returns ------- @@ -2055,6 +2066,11 @@ cdef class Actor(Component): """ Condition.is_true(self.trader_id is not None, "The actor has not been registered") Condition.not_none(venue, "venue") + now = self.clock.utc_now() + if start is not None: + Condition.is_true(start <= now, "start was > now") + if end is not None: + Condition.is_true(end <= now, "start was > now") if start is not None and end is not None: Condition.is_true(start < end, "start was >= end") Condition.callable_or_none(callback, "callback") @@ -2067,6 +2083,7 @@ cdef class Actor(Component): "venue": venue, "start": start, "end": end, + "update_catalog": update_catalog, }), callback=self._handle_instruments_response, request_id=request_id, @@ -2099,6 +2116,8 @@ cdef class Actor(Component): If None, it will be inferred from the venue in the instrument ID. callback : Callable[[UUID4], None], optional The registered callback, to be called with the request ID when the response has completed processing. + update_catalog : bool, default False + If True then updates the catalog with new data received from a client. Returns ------- @@ -2142,6 +2161,8 @@ cdef class Actor(Component): datetime end = None, ClientId client_id = None, callback: Callable[[UUID4], None] | None = None, + bint update_catalog = False, + str quote_type = "", ): """ Request historical `QuoteTick` data. @@ -2163,6 +2184,8 @@ cdef class Actor(Component): callback : Callable[[UUID4], None], optional The registered callback, to be called with the request ID when the response has completed processing. + update_catalog : bool, default False + If True then updates the catalog with new data received from a client. Returns ------- @@ -2179,6 +2202,11 @@ cdef class Actor(Component): """ Condition.is_true(self.trader_id is not None, "The actor has not been registered") Condition.not_none(instrument_id, "instrument_id") + now = self.clock.utc_now() + if start is not None: + Condition.is_true(start <= now, "start was > now") + if end is not None: + Condition.is_true(end <= now, "start was > now") if start is not None and end is not None: Condition.is_true(start < end, "start was >= end") Condition.callable_or_none(callback, "callback") @@ -2191,6 +2219,8 @@ cdef class Actor(Component): "instrument_id": instrument_id, "start": start, "end": end, + "update_catalog": update_catalog, + "quote_type": quote_type, }), callback=self._handle_quote_ticks_response, request_id=request_id, @@ -2209,6 +2239,7 @@ cdef class Actor(Component): datetime end = None, ClientId client_id = None, callback: Callable[[UUID4], None] | None = None, + bint update_catalog = False, ): """ Request historical `TradeTick` data. @@ -2230,6 +2261,8 @@ cdef class Actor(Component): callback : Callable[[UUID4], None], optional The registered callback, to be called with the request ID when the response has completed processing. + update_catalog : bool, default False + If True then updates the catalog with new data received from a client. Returns ------- @@ -2246,6 +2279,11 @@ cdef class Actor(Component): """ Condition.is_true(self.trader_id is not None, "The actor has not been registered") Condition.not_none(instrument_id, "instrument_id") + now = self.clock.utc_now() + if start is not None: + Condition.is_true(start <= now, "start was > now") + if end is not None: + Condition.is_true(end <= now, "start was > now") if start is not None and end is not None: Condition.is_true(start < end, "start was >= end") Condition.callable_or_none(callback, "callback") @@ -2258,6 +2296,7 @@ cdef class Actor(Component): "instrument_id": instrument_id, "start": start, "end": end, + "update_catalog": update_catalog, }), callback=self._handle_trade_ticks_response, request_id=request_id, @@ -2276,6 +2315,7 @@ cdef class Actor(Component): datetime end = None, ClientId client_id = None, callback: Callable[[UUID4], None] | None = None, + bint update_catalog = False, ): """ Request historical `Bar` data. @@ -2297,6 +2337,8 @@ cdef class Actor(Component): callback : Callable[[UUID4], None], optional The registered callback, to be called with the request ID when the response has completed processing. + update_catalog : bool, default False + If True then updates the catalog with new data received from a client. Returns ------- @@ -2313,6 +2355,11 @@ cdef class Actor(Component): """ Condition.is_true(self.trader_id is not None, "The actor has not been registered") Condition.not_none(bar_type, "bar_type") + now = self.clock.utc_now() + if start is not None: + Condition.is_true(start <= now, "start was > now") + if end is not None: + Condition.is_true(end <= now, "start was > now") if start is not None and end is not None: Condition.is_true(start < end, "start was >= end") Condition.callable_or_none(callback, "callback") @@ -2325,6 +2372,7 @@ cdef class Actor(Component): "bar_type": bar_type, "start": start, "end": end, + "update_catalog": update_catalog, }), callback=self._handle_bars_response, request_id=request_id, @@ -2345,6 +2393,7 @@ cdef class Actor(Component): bint include_external_data = False, ClientId client_id = None, callback: Callable[[UUID4], None] | None = None, + bint update_catalog = False, ): """ Request historical aggregated `Bar` data for multiple bar types. @@ -2375,6 +2424,8 @@ cdef class Actor(Component): callback : Callable[[UUID4], None], optional The registered callback, to be called with the request ID when the response has completed processing. + update_catalog : bool, default False + If True then updates the catalog with new data received from a client. Returns ------- @@ -2394,6 +2445,11 @@ cdef class Actor(Component): Condition.is_true(self.trader_id is not None, "The actor has not been registered") Condition.not_empty(bar_types, "bar_types") Condition.list_type(bar_types, BarType, "bar_types") + now = self.clock.utc_now() + if start is not None: + Condition.is_true(start <= now, "start was > now") + if end is not None: + Condition.is_true(end <= now, "start was > now") if start is not None and end is not None: Condition.is_true(start < end, "start was >= end") Condition.callable_or_none(callback, "callback") @@ -2426,6 +2482,7 @@ cdef class Actor(Component): "end": end, "update_existing_subscriptions": update_existing_subscriptions, "include_external_data": include_external_data, + "update_catalog": update_catalog, }), callback=self._handle_aggregated_bars_response, request_id=request_id, @@ -2436,6 +2493,7 @@ cdef class Actor(Component): self._send_data_req(request) return request_id + cpdef bint is_pending_request(self, UUID4 request_id): """ Return whether the request for the given identifier is pending processing. diff --git a/nautilus_trader/core/datetime.pxd b/nautilus_trader/core/datetime.pxd index 39ae2c97f031..e7fb6561ef7f 100644 --- a/nautilus_trader/core/datetime.pxd +++ b/nautilus_trader/core/datetime.pxd @@ -29,4 +29,5 @@ cpdef bint is_tz_aware(time_object) cpdef bint is_tz_naive(time_object) cpdef datetime as_utc_timestamp(datetime dt) cpdef object as_utc_index(time_object) +cpdef time_object_to_dt(time_object) cpdef str format_iso8601(datetime dt) diff --git a/nautilus_trader/core/datetime.pyx b/nautilus_trader/core/datetime.pyx index 2ab19956e3df..8d5a992eeba3 100644 --- a/nautilus_trader/core/datetime.pyx +++ b/nautilus_trader/core/datetime.pyx @@ -276,6 +276,32 @@ cpdef object as_utc_index(data: pd.DataFrame): return data +cpdef time_object_to_dt(time_object): + """ + Return the datetime (UTC) from the given UNIX timestamp as integer (nanoseconds), string or pd.Timestamp. + Returns None if the input is None. + + Parameters + ---------- + time_object : pd.Timestamp | str | int | None + The time object to convert. + + Returns + ------- + pd.Timestamp + + """ + if time_object is None: + return None + + if isinstance(time_object, pd.Timestamp): + used_date = time_object + else: + used_date = pd.Timestamp(time_object) + + return as_utc_timestamp(used_date) + + cpdef str format_iso8601(datetime dt): """ Format the given datetime to a millisecond accurate ISO 8601 specification string. @@ -303,20 +329,20 @@ cpdef str format_iso8601(datetime dt): return f"{dt_partitioned[0]}.{dt_partitioned[2][:3]}Z" -def max_date_str(date1: str | int | None = None, date2: str | int | None = None) -> str | None: +def max_date(date1: pd.Timestamp | str | int | None = None, date2: str | int | None = None) -> pd.Timestamp | None: """ - Returns the maximum date as an ISO 8601 formatted string. + Returns the maximum date as a datetime (UTC). Parameters ---------- - date1 : str | int | None, optional + date1 : pd.Timestamp | str | int | None, optional The first date to compare. Can be a string, integer (timestamp), or None. Default is None. - date2 : str | int | None, optional + date2 : pd.Timestamp | str | int | None, optional The second date to compare. Can be a string, integer (timestamp), or None. Default is None. Returns ------- - str or ``None`` + pd.Timestamp | None The maximum date as an ISO 8601 formatted string, or None if both input dates are None. """ @@ -324,28 +350,28 @@ def max_date_str(date1: str | int | None = None, date2: str | int | None = None) return None if date1 is None: - return pd.Timestamp(date2).isoformat() + return time_object_to_dt(date2) if date2 is None: - return pd.Timestamp(date1).isoformat() + return time_object_to_dt(date1) - return max(pd.Timestamp(date1), pd.Timestamp(date2)).isoformat() + return max(time_object_to_dt(date1), time_object_to_dt(date2)) -def min_date_str(date1: str | int | None = None, date2: str | int | None = None) -> str | None: +def min_date(date1: pd.Timestamp | str | int | None = None, date2: str | int | None = None) -> pd.Timestamp | None: """ - Returns the minimum date as an ISO 8601 formatted string. + Returns the minimum date as a datetime (UTC). Parameters ---------- - date1 : str | int | None, optional + date1 : pd.Timestamp | str | int | None, optional The first date to compare. Can be a string, integer (timestamp), or None. Default is None. - date2 : str | int | None, optional + date2 : pd.Timestamp | str | int | None, optional The second date to compare. Can be a string, integer (timestamp), or None. Default is None. Returns ------- - str or ``None`` + pd.Timestamp | None The minimum date as an ISO 8601 formatted string, or None if both input dates are None. """ @@ -353,9 +379,9 @@ def min_date_str(date1: str | int | None = None, date2: str | int | None = None) return None if date1 is None: - return pd.Timestamp(date2).isoformat() + return time_object_to_dt(date2) if date2 is None: - return pd.Timestamp(date1).isoformat() + return time_object_to_dt(date1) - return min(pd.Timestamp(date1), pd.Timestamp(date2)).isoformat() + return min(time_object_to_dt(date1), time_object_to_dt(date2)) diff --git a/nautilus_trader/data/client.pxd b/nautilus_trader/data/client.pxd index c03ef2272543..7a615f62d649 100644 --- a/nautilus_trader/data/client.pxd +++ b/nautilus_trader/data/client.pxd @@ -126,6 +126,7 @@ cdef class MarketDataClient(DataClient): UUID4 correlation_id, datetime start=*, datetime end=*, + dict metadata=*, ) cpdef void request_instruments( self, @@ -133,6 +134,7 @@ cdef class MarketDataClient(DataClient): UUID4 correlation_id, datetime start=*, datetime end=*, + dict metadata=*, ) cpdef void request_order_book_snapshot( self, @@ -147,6 +149,7 @@ cdef class MarketDataClient(DataClient): UUID4 correlation_id, datetime start=*, datetime end=*, + dict metadata=*, ) cpdef void request_trade_ticks( self, @@ -155,6 +158,7 @@ cdef class MarketDataClient(DataClient): UUID4 correlation_id, datetime start=*, datetime end=*, + dict metadata=*, ) cpdef void request_bars( self, @@ -163,12 +167,13 @@ cdef class MarketDataClient(DataClient): UUID4 correlation_id, datetime start=*, datetime end=*, + dict metadata=*, ) # -- DATA HANDLERS -------------------------------------------------------------------------------- - cpdef void _handle_instrument(self, Instrument instrument, UUID4 correlation_id) - cpdef void _handle_instruments(self, Venue venue, list instruments, UUID4 correlation_id) - cpdef void _handle_quote_ticks(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id) - cpdef void _handle_trade_ticks(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id) - cpdef void _handle_bars(self, BarType bar_type, list bars, Bar partial, UUID4 correlation_id) + cpdef void _handle_instrument(self, Instrument instrument, UUID4 correlation_id, dict metadata) + cpdef void _handle_instruments(self, Venue venue, list instruments, UUID4 correlation_id, dict metadata) + cpdef void _handle_quote_ticks(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id, dict metadata) + cpdef void _handle_trade_ticks(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id, dict metadata) + cpdef void _handle_bars(self, BarType bar_type, list bars, Bar partial, UUID4 correlation_id, dict metadata) diff --git a/nautilus_trader/data/client.pyx b/nautilus_trader/data/client.pyx index d2ecfcfaacf9..97206c5de0f4 100644 --- a/nautilus_trader/data/client.pyx +++ b/nautilus_trader/data/client.pyx @@ -766,6 +766,7 @@ cdef class MarketDataClient(DataClient): UUID4 correlation_id, datetime start = None, datetime end = None, + dict metadata = None, ): """ Request `Instrument` data for the given instrument ID. @@ -781,19 +782,21 @@ cdef class MarketDataClient(DataClient): end : datetime, optional The end datetime (UTC) of request time range. The inclusiveness depends on individual data client implementation. + metadata : dict, optional + Additional metadata to be sent with the request. """ self._log.error( # pragma: no cover f"Cannot request `Instrument` data for {instrument_id}: not implemented. " # pragma: no cover f"You can implement by overriding the `request_instrument` method for this client", # pragma: no cover # noqa ) - cpdef void request_instruments( self, Venue venue, UUID4 correlation_id, datetime start = None, datetime end = None, + dict metadata = None, ): """ Request all `Instrument` data for the given venue. @@ -809,6 +812,8 @@ cdef class MarketDataClient(DataClient): end : datetime, optional The end datetime (UTC) of request time range. The inclusiveness depends on individual data client implementation. + metadata : dict, optional + Additional metadata to be sent with the request. """ self._log.error( # pragma: no cover @@ -847,6 +852,7 @@ cdef class MarketDataClient(DataClient): UUID4 correlation_id, datetime start = None, datetime end = None, + dict metadata = None, ): """ Request historical `QuoteTick` data. @@ -864,6 +870,8 @@ cdef class MarketDataClient(DataClient): end : datetime, optional The end datetime (UTC) of request time range. The inclusiveness depends on individual data client implementation. + metadata : dict, optional + Additional metadata to be sent with the request. """ self._log.error( # pragma: no cover @@ -878,6 +886,7 @@ cdef class MarketDataClient(DataClient): UUID4 correlation_id, datetime start = None, datetime end = None, + dict metadata = None, ): """ Request historical `TradeTick` data. @@ -895,6 +904,8 @@ cdef class MarketDataClient(DataClient): end : datetime, optional The end datetime (UTC) of request time range. The inclusiveness depends on individual data client implementation. + metadata : dict, optional + Additional metadata to be sent with the request. """ self._log.error( # pragma: no cover @@ -909,6 +920,7 @@ cdef class MarketDataClient(DataClient): UUID4 correlation_id, datetime start = None, datetime end = None, + dict metadata = None, ): """ Request historical `Bar` data. @@ -926,6 +938,8 @@ cdef class MarketDataClient(DataClient): end : datetime, optional The end datetime (UTC) of request time range. The inclusiveness depends on individual data client implementation. + metadata : dict, optional + Additional metadata to be sent with the request. """ self._log.error( # pragma: no cover @@ -942,20 +956,20 @@ cdef class MarketDataClient(DataClient): def _handle_data_py(self, Data data): self._handle_data(data) - def _handle_instrument_py(self, Instrument instrument, UUID4 correlation_id): - self._handle_instrument(instrument, correlation_id) + def _handle_instrument_py(self, Instrument instrument, UUID4 correlation_id, dict metadata): + self._handle_instrument(instrument, correlation_id, metadata) - def _handle_instruments_py(self, Venue venue, list instruments, UUID4 correlation_id): - self._handle_instruments(venue, instruments, correlation_id) + def _handle_instruments_py(self, Venue venue, list instruments, UUID4 correlation_id, dict metadata): + self._handle_instruments(venue, instruments, correlation_id, metadata) - def _handle_quote_ticks_py(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id): - self._handle_quote_ticks(instrument_id, ticks, correlation_id) + def _handle_quote_ticks_py(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id, dict metadata): + self._handle_quote_ticks(instrument_id, ticks, correlation_id, metadata) - def _handle_trade_ticks_py(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id): - self._handle_trade_ticks(instrument_id, ticks, correlation_id) + def _handle_trade_ticks_py(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id, dict metadata): + self._handle_trade_ticks(instrument_id, ticks, correlation_id, metadata) - def _handle_bars_py(self, BarType bar_type, list bars, Bar partial, UUID4 correlation_id): - self._handle_bars(bar_type, bars, partial, correlation_id) + def _handle_bars_py(self, BarType bar_type, list bars, Bar partial, UUID4 correlation_id, dict metadata): + self._handle_bars(bar_type, bars, partial, correlation_id, metadata) def _handle_data_response_py(self, DataType data_type, data, UUID4 correlation_id): self._handle_data_response(data_type, data, correlation_id) @@ -965,11 +979,11 @@ cdef class MarketDataClient(DataClient): cpdef void _handle_data(self, Data data): self._msgbus.send(endpoint="DataEngine.process", msg=data) - cpdef void _handle_instrument(self, Instrument instrument, UUID4 correlation_id): + cpdef void _handle_instrument(self, Instrument instrument, UUID4 correlation_id, dict metadata): cdef DataResponse response = DataResponse( client_id=self.id, venue=instrument.venue, - data_type=DataType(Instrument, metadata={"instrument_id": instrument.id}), + data_type=DataType(Instrument, metadata=({"instrument_id": instrument.id} | (metadata if metadata else {}))), data=instrument, correlation_id=correlation_id, response_id=UUID4(), @@ -978,11 +992,11 @@ cdef class MarketDataClient(DataClient): self._msgbus.send(endpoint="DataEngine.response", msg=response) - cpdef void _handle_instruments(self, Venue venue, list instruments, UUID4 correlation_id): + cpdef void _handle_instruments(self, Venue venue, list instruments, UUID4 correlation_id, dict metadata): cdef DataResponse response = DataResponse( client_id=self.id, venue=venue, - data_type=DataType(Instrument, metadata={"venue": venue}), + data_type=DataType(Instrument, metadata=({"venue": venue} | (metadata if metadata else {}))), data=instruments, correlation_id=correlation_id, response_id=UUID4(), @@ -991,11 +1005,11 @@ cdef class MarketDataClient(DataClient): self._msgbus.send(endpoint="DataEngine.response", msg=response) - cpdef void _handle_quote_ticks(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id): + cpdef void _handle_quote_ticks(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id, dict metadata): cdef DataResponse response = DataResponse( client_id=self.id, venue=instrument_id.venue, - data_type=DataType(QuoteTick, metadata={"instrument_id": instrument_id}), + data_type=DataType(QuoteTick, metadata=({"instrument_id": instrument_id} | (metadata if metadata else {}))), data=ticks, correlation_id=correlation_id, response_id=UUID4(), @@ -1004,11 +1018,11 @@ cdef class MarketDataClient(DataClient): self._msgbus.send(endpoint="DataEngine.response", msg=response) - cpdef void _handle_trade_ticks(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id): + cpdef void _handle_trade_ticks(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id, dict metadata): cdef DataResponse response = DataResponse( client_id=self.id, venue=instrument_id.venue, - data_type=DataType(TradeTick, metadata={"instrument_id": instrument_id}), + data_type=DataType(TradeTick, metadata=({"instrument_id": instrument_id} | (metadata if metadata else {}))), data=ticks, correlation_id=correlation_id, response_id=UUID4(), @@ -1017,11 +1031,11 @@ cdef class MarketDataClient(DataClient): self._msgbus.send(endpoint="DataEngine.response", msg=response) - cpdef void _handle_bars(self, BarType bar_type, list bars, Bar partial, UUID4 correlation_id): + cpdef void _handle_bars(self, BarType bar_type, list bars, Bar partial, UUID4 correlation_id, dict metadata): cdef DataResponse response = DataResponse( client_id=self.id, venue=bar_type.instrument_id.venue, - data_type=DataType(Bar, metadata={"bar_type": bar_type, "Partial": partial}), + data_type=DataType(Bar, metadata=(({"bar_type": bar_type, "Partial": partial} | (metadata if metadata else {})))), data=bars, correlation_id=correlation_id, response_id=UUID4(), diff --git a/nautilus_trader/data/engine.pxd b/nautilus_trader/data/engine.pxd index e609c5214074..59bfc385861c 100644 --- a/nautilus_trader/data/engine.pxd +++ b/nautilus_trader/data/engine.pxd @@ -21,6 +21,7 @@ from nautilus_trader.common.component cimport Component from nautilus_trader.common.component cimport TimeEvent from nautilus_trader.core.data cimport Data from nautilus_trader.core.rust.model cimport BookType +from nautilus_trader.core.uuid cimport UUID4 from nautilus_trader.data.aggregation cimport BarAggregator from nautilus_trader.data.client cimport DataClient from nautilus_trader.data.client cimport MarketDataClient @@ -52,7 +53,7 @@ cdef class DataEngine(Component): cdef readonly Cache _cache cdef readonly DataClient _default_client cdef readonly set[ClientId] _external_clients - cdef readonly list[ParquetDataCatalog] _catalogs + cdef readonly dict[str, ParquetDataCatalog] _catalogs cdef readonly dict[ClientId, DataClient] _clients cdef readonly dict[Venue, DataClient] _routing_map @@ -64,6 +65,9 @@ cdef class DataEngine(Component): cdef readonly list[InstrumentId] _subscribed_synthetic_trades cdef readonly dict[InstrumentId, list[OrderBookDelta]] _buffered_deltas_map cdef readonly dict[str, SnapshotInfo] _snapshot_info + cdef readonly dict[UUID4, int] _query_group_n_components + cdef readonly dict[UUID4, list] _query_group_components + cdef readonly bint _time_bars_build_with_no_updates cdef readonly bint _time_bars_timestamp_on_close cdef readonly str _time_bars_interval_type @@ -150,7 +154,7 @@ cdef class DataEngine(Component): # -- DATA HANDLERS -------------------------------------------------------------------------------- cpdef void _handle_data(self, Data data) - cpdef void _handle_instrument(self, Instrument instrument) + cpdef void _handle_instrument(self, Instrument instrument, bint update_catalog=*) cpdef void _handle_order_book_delta(self, OrderBookDelta delta) cpdef void _handle_order_book_deltas(self, OrderBookDeltas deltas) cpdef void _handle_order_book_depth(self, OrderBookDepth10 depth) @@ -164,7 +168,11 @@ cdef class DataEngine(Component): # -- RESPONSE HANDLERS ---------------------------------------------------------------------------- cpdef void _handle_response(self, DataResponse response) - cpdef void _handle_instruments(self, list instruments) + cpdef void _handle_instruments(self, list instruments, bint update_catalog=*) + cpdef void _update_catalog(self, list ticks, bint is_instrument=*) + cpdef void _new_query_group(self, UUID4 correlation_id, int n_components) + cpdef object _handle_query_group(self, UUID4 correlation_id, list ticks) + cdef object _handle_query_group_aux(self, UUID4 correlation_id, list ticks) cpdef void _handle_quote_ticks(self, list ticks) cpdef void _handle_trade_ticks(self, list ticks) cpdef void _handle_bars(self, list bars, Bar partial) diff --git a/nautilus_trader/data/engine.pyx b/nautilus_trader/data/engine.pyx index 1646679d16fe..6562eb777900 100644 --- a/nautilus_trader/data/engine.pyx +++ b/nautilus_trader/data/engine.pyx @@ -32,12 +32,13 @@ just need to override the `execute`, `process`, `send` and `receive` methods. from typing import Callable from nautilus_trader.common.enums import LogColor +from nautilus_trader.core.datetime import max_date +from nautilus_trader.core.datetime import time_object_to_dt from nautilus_trader.data.config import DataEngineConfig from nautilus_trader.model.enums import RecordFlag from nautilus_trader.persistence.catalog import ParquetDataCatalog from cpython.datetime cimport datetime -from cpython.datetime cimport timedelta from libc.stdint cimport uint64_t from nautilus_trader.common.component cimport CMD @@ -46,7 +47,6 @@ from nautilus_trader.common.component cimport REQ from nautilus_trader.common.component cimport RES from nautilus_trader.common.component cimport Clock from nautilus_trader.common.component cimport Component -from nautilus_trader.common.component cimport Logger from nautilus_trader.common.component cimport MessageBus from nautilus_trader.common.component cimport TestClock from nautilus_trader.core.correctness cimport Condition @@ -133,7 +133,7 @@ cdef class DataEngine(Component): self._routing_map: dict[Venue, DataClient] = {} self._default_client: DataClient | None = None self._external_clients: set[ClientId] = set() - self._catalogs: list[ParquetDataCatalog] = [] + self._catalogs: dict[str, ParquetDataCatalog] = {} self._order_book_intervals: dict[tuple[InstrumentId, int], list[Callable[[OrderBook], None]]] = {} self._bar_aggregators: dict[BarType, BarAggregator] = {} self._synthetic_quote_feeds: dict[InstrumentId, list[SyntheticInstrument]] = {} @@ -142,6 +142,8 @@ cdef class DataEngine(Component): self._subscribed_synthetic_trades: list[InstrumentId] = [] self._buffered_deltas_map: dict[InstrumentId, list[OrderBookDelta]] = {} self._snapshot_info: dict[str, SnapshotInfo] = {} + self._query_group_n_components: dict[UUID4, int] = {} + self._query_group_components: dict[UUID4, list] = {} # Settings self.debug = config.debug @@ -239,7 +241,7 @@ cdef class DataEngine(Component): # --REGISTRATION ---------------------------------------------------------------------------------- - def register_catalog(self, catalog: ParquetDataCatalog) -> None: + def register_catalog(self, catalog: ParquetDataCatalog, name: str = "catalog_0") -> None: """ Register the given data catalog with the engine. @@ -251,7 +253,7 @@ cdef class DataEngine(Component): """ Condition.not_none(catalog, "catalog") - self._catalogs.append(catalog) + self._catalogs[name] = catalog cpdef void register_client(self, DataClient client): """ @@ -1262,85 +1264,224 @@ cdef class DataEngine(Component): # -- REQUEST HANDLERS ----------------------------------------------------------------------------- + def _catalogs_last_timestamp( + self, + data_cls: type, + instrument_id: str | None = None, + bar_type: str | None = None, + ts_column: str = "ts_init", + ): + last_timestamp = None + last_timestamp_catalog = None + + for catalog in self._catalogs.values(): + prev_last_timestamp = last_timestamp + last_timestamp = max_date( + last_timestamp, + catalog.query_last_timestamp(data_cls, instrument_id, bar_type, ts_column) + ) + + if last_timestamp is not None and (prev_last_timestamp is None or last_timestamp > prev_last_timestamp): + last_timestamp_catalog = catalog + + return last_timestamp, last_timestamp_catalog + cpdef void _handle_request(self, DataRequest request): if self.debug: self._log.debug(f"{RECV}{REQ} {request}", LogColor.MAGENTA) - self.request_count += 1 - # Query data catalog - if self._catalogs: - # For now we'll just query the catalog if its present (as very likely this is a backtest) - self._query_catalog(request) - return + self.request_count += 1 # Query data client cdef DataClient client = self._clients.get(request.client_id) + if client is None: client = self._routing_map.get( request.venue, self._default_client, ) - if client is None: - self._log.error( - f"Cannot handle request: " - f"no client registered for '{request.client_id}', {request}") - return # No client to handle request - # Field defined when using actor.request_aggregated_bars - market_data_type = request.data_type.metadata.get("market_data_type") + if client is not None: + Condition.is_true(isinstance(client, MarketDataClient), "client was not a MarketDataClient") + + metadata = request.data_type.metadata + aggregated_bars_market_data_type = metadata.get("market_data_type", "") + update_catalog = metadata.get("update_catalog", False) + + now = self._clock.utc_now() + start = time_object_to_dt(metadata.get("start")) + end = time_object_to_dt(metadata.get("end")) if request.data_type.type == Instrument: - Condition.is_true(isinstance(client, MarketDataClient), "client was not a MarketDataClient") instrument_id = request.data_type.metadata.get("instrument_id") + if instrument_id is None: + if self._catalogs and not update_catalog: + self._query_catalog(request) + return + + if client is None: + self._log.error( + f"Cannot handle request: " + f"no client registered for '{request.client_id}', {request}") + return # No client to handle request + client.request_instruments( request.data_type.metadata.get("venue"), request.id, - request.data_type.metadata.get("start"), - request.data_type.metadata.get("end"), + start, + end, + metadata, ) else: + last_timestamp, _ = self._catalogs_last_timestamp( + Instrument, + instrument_id, + ) + + if last_timestamp: + self._query_catalog(request) + return + + if client is None: + self._log.error( + f"Cannot handle request: " + f"no client registered for '{request.client_id}', {request}") + return # No client to handle request + client.request_instrument( instrument_id, request.id, - request.data_type.metadata.get("start"), - request.data_type.metadata.get("end"), + start, + end, + metadata, ) elif request.data_type.type == OrderBookDeltas: - Condition.is_true(isinstance(client, MarketDataClient), "client was not a MarketDataClient") + instrument_id = request.data_type.metadata.get("instrument_id") + + if client is None: + self._log.error( + f"Cannot handle request: " + f"no client registered for '{request.client_id}', {request}") + return # No client to handle request + client.request_order_book_snapshot( - request.data_type.metadata.get("instrument_id"), + instrument_id, request.data_type.metadata.get("limit", 0), request.id ) - elif request.data_type.type == QuoteTick or (market_data_type and market_data_type == "quote_ticks"): - Condition.is_true(isinstance(client, MarketDataClient), "client was not a MarketDataClient") + elif request.data_type.type == QuoteTick or aggregated_bars_market_data_type == "quote_ticks": + instrument_id = request.data_type.metadata.get("instrument_id") + + last_timestamp, _ = self._catalogs_last_timestamp( + QuoteTick, + instrument_id, + ) + + if last_timestamp and now <= last_timestamp or end and end <= last_timestamp: + self._query_catalog(request) + return + + if client is None: + self._log.error( + f"Cannot handle request: " + f"no client registered for '{request.client_id}', {request}") + return # No client to handle request + + if last_timestamp and start and start <= last_timestamp: + self._new_query_group(request.id, 2) + self._query_catalog(request) + + client_start = max_date(start, last_timestamp) client.request_quote_ticks( - request.data_type.metadata.get("instrument_id"), + instrument_id, request.data_type.metadata.get("limit", 0), request.id, - request.data_type.metadata.get("start"), - request.data_type.metadata.get("end"), + client_start, + end, + metadata, ) - elif request.data_type.type == TradeTick or (market_data_type and market_data_type == "trade_ticks"): - Condition.is_true(isinstance(client, MarketDataClient), "client was not a MarketDataClient") + elif request.data_type.type == TradeTick or aggregated_bars_market_data_type == "trade_ticks": + instrument_id = request.data_type.metadata.get("instrument_id") + + last_timestamp, _ = self._catalogs_last_timestamp( + TradeTick, + instrument_id, + ) + + if last_timestamp and now <= last_timestamp or end and end <= last_timestamp: + self._query_catalog(request) + return + + if client is None: + self._log.error( + f"Cannot handle request: " + f"no client registered for '{request.client_id}', {request}") + return # No client to handle request + + if last_timestamp and start and start <= last_timestamp: + self._new_query_group(request.id, 2) + self._query_catalog(request) + + client_start = max_date(start, last_timestamp) client.request_trade_ticks( - request.data_type.metadata.get("instrument_id"), + instrument_id, request.data_type.metadata.get("limit", 0), request.id, - request.data_type.metadata.get("start"), - request.data_type.metadata.get("end"), + client_start, + end, + metadata, ) - elif request.data_type.type == Bar or (market_data_type and market_data_type == "bars"): - Condition.is_true(isinstance(client, MarketDataClient), "client was not a MarketDataClient") + elif request.data_type.type == Bar or aggregated_bars_market_data_type == "bars": + bar_type = request.data_type.metadata.get("bar_type") + + last_timestamp, _ = self._catalogs_last_timestamp( + Bar, + bar_type=bar_type, + ) + + if last_timestamp and now <= last_timestamp or end and end <= last_timestamp: + self._query_catalog(request) + return + + if client is None: + self._log.error( + f"Cannot handle request: " + f"no client registered for '{request.client_id}', {request}") + return # No client to handle request + + if last_timestamp and start and start <= last_timestamp: + self._new_query_group(request.id, 2) + self._query_catalog(request) + + client_start = max_date(start, last_timestamp) client.request_bars( - request.data_type.metadata.get("bar_type"), + bar_type, request.data_type.metadata.get("limit", 0), request.id, - request.data_type.metadata.get("start"), - request.data_type.metadata.get("end"), + client_start, + end, + metadata, ) else: + last_timestamp, _ = self._catalogs_last_timestamp( + request.data_type.type, + ) + + if last_timestamp and now <= last_timestamp or end and end <= last_timestamp: + self._query_catalog(request) + return + + if client is None: + self._log.error( + f"Cannot handle request: " + f"no client registered for '{request.client_id}', {request}") + return # No client to handle request + + if last_timestamp and start and start <= last_timestamp: + self._new_query_group(request.id, 2) + self._query_catalog(request) + try: client.request(request.data_type, request.id) except NotImplementedError: @@ -1371,20 +1512,20 @@ cdef class DataEngine(Component): if request.data_type.type == Instrument: instrument_id = request.data_type.metadata.get("instrument_id") if instrument_id is None: - for catalog in self._catalogs: + for catalog in self._catalogs.values(): data += catalog.instruments() else: - for catalog in self._catalogs: + for catalog in self._catalogs.values(): data += catalog.instruments(instrument_ids=[str(instrument_id)]) elif request.data_type.type == QuoteTick or (market_data_type and market_data_type == "quote_ticks"): - for catalog in self._catalogs: + for catalog in self._catalogs.values(): data += catalog.quote_ticks( instrument_ids=[str(request.data_type.metadata.get("instrument_id"))], start=ts_start, end=ts_end, ) elif request.data_type.type == TradeTick or (market_data_type and market_data_type == "trade_ticks"): - for catalog in self._catalogs: + for catalog in self._catalogs.values(): data += catalog.trade_ticks( instrument_ids=[str(request.data_type.metadata.get("instrument_id"))], start=ts_start, @@ -1396,7 +1537,7 @@ cdef class DataEngine(Component): self._log.error("No bar type provided for bars request") return - for catalog in self._catalogs: + for catalog in self._catalogs.values(): data += catalog.bars( instrument_ids=[str(bar_type.instrument_id)], bar_type=str(bar_type), @@ -1404,14 +1545,14 @@ cdef class DataEngine(Component): end=ts_end, ) elif request.data_type.type == InstrumentClose: - for catalog in self._catalogs: + for catalog in self._catalogs.values(): data += catalog.instrument_closes( instrument_ids=[str(request.data_type.metadata.get("instrument_id"))], start=ts_start, end=ts_end, ) else: - for catalog in self._catalogs: + for catalog in self._catalogs.values(): data += catalog.custom_data( cls=request.data_type.type, metadata=request.data_type.metadata, @@ -1426,10 +1567,14 @@ cdef class DataEngine(Component): f"data[-1].ts_init={data[-1].ts_init}, {ts_now=}", ) + metadata = request.data_type.metadata.copy() + metadata["update_catalog"] = False + data_type = DataType(request.data_type.type, metadata) + response = DataResponse( client_id=request.client_id, venue=request.venue, - data_type=request.data_type, + data_type=data_type, data=data, correlation_id=request.id, response_id=UUID4(), @@ -1465,8 +1610,12 @@ cdef class DataEngine(Component): else: self._log.error(f"Cannot handle data: unrecognized type {type(data)} {data}") - cpdef void _handle_instrument(self, Instrument instrument): + cpdef void _handle_instrument(self, Instrument instrument, bint update_catalog = False): self._cache.add_instrument(instrument) + + if update_catalog: + self._update_catalog([instrument], is_instrument=True) + self._msgbus.publish_c( topic=f"data.instrument" f".{instrument.id.venue}" @@ -1640,13 +1789,38 @@ cdef class DataEngine(Component): cpdef void _handle_response(self, DataResponse response): if self.debug: self._log.debug(f"{RECV}{RES} {response}", LogColor.MAGENTA) + self.response_count += 1 + correlation_id = response.correlation_id + update_catalog = False + + if response.data_type.metadata is not None: + update_catalog = response.data_type.metadata.get("update_catalog", False) + + if type(response.data) is list: + response_data = response.data + else: + #for request_instrument case + response_data = [response.data] + + if update_catalog and response.data_type.type != Instrument: + # for instruments we want to handle each instrument individually + self._update_catalog(response_data) + + response_data = self._handle_query_group(correlation_id, response_data) + + if response_data is None: + return + + if response.data_type.type != Instrument: + response.data = response_data + if response.data_type.type == Instrument: if isinstance(response.data, list): - self._handle_instruments(response.data) + self._handle_instruments(response.data, update_catalog) else: - self._handle_instrument(response.data) + self._handle_instrument(response.data, update_catalog) elif response.data_type.type == QuoteTick: self._handle_quote_ticks(response.data) elif response.data_type.type == TradeTick: @@ -1659,10 +1833,82 @@ cdef class DataEngine(Component): self._msgbus.response(response) - cpdef void _handle_instruments(self, list instruments): + cpdef void _update_catalog(self, list ticks, bint is_instrument = False): + if len(ticks) == 0: + return + + if type(ticks[0]) is Bar: + last_timestamp, last_timestamp_catalog = self._catalogs_last_timestamp(Bar, bar_type=ticks[0].bar_type) + else: + last_timestamp, last_timestamp_catalog = self._catalogs_last_timestamp(type(ticks[0]), ticks[0].instrument_id) + + # We don't want to write in the catalog several times the same instrument + if last_timestamp_catalog and is_instrument: + return + + if last_timestamp_catalog is None and len(self._catalogs) > 0: + last_timestamp_catalog = self._catalogs[0] + + if last_timestamp_catalog is not None: + last_timestamp_catalog.write_data(ticks, mode="append") + else: + self._log.warning("No catalog available for appending data.") + + cpdef void _new_query_group(self, UUID4 correlation_id, int n_components): + self._query_group_n_components[correlation_id] = n_components + + cpdef object _handle_query_group(self, UUID4 correlation_id, list ticks): + # closure is not allowed in cpdef functions so we call a cdef function + return self._handle_query_group_aux(correlation_id, ticks) + + cdef object _handle_query_group_aux(self, UUID4 correlation_id, list ticks): + # return None or a list of ticks + if correlation_id not in self._query_group_n_components: + return ticks + + if self._query_group_n_components[correlation_id] == 1: + del self._query_group_n_components[correlation_id] + return ticks + + if correlation_id not in self._query_group_components: + self._query_group_components[correlation_id] = [] + + self._query_group_components[correlation_id].append(ticks) + + if len(self._query_group_components[correlation_id]) != self._query_group_n_components[correlation_id]: + return None + + components = [] + + for component in self._query_group_components[correlation_id]: + if len(component) > 0: + components.append(component) + + components = sorted(components, key=lambda l: l[0].ts_init) + result = components[0] + last_timestamp = result[-1].ts_init + + if len(components) > 1: + for component in components[1:]: + first_index = 0 + + for i in range(len(component)): + if component[i].ts_init > last_timestamp: + first_index = i + last_timestamp = component[-1].ts_init + break + + result += component[first_index:] + + del self._query_group_n_components[correlation_id] + del self._query_group_components[correlation_id] + + return result + + cpdef void _handle_instruments(self, list instruments, bint update_catalog = False): cdef Instrument instrument for instrument in instruments: - self._handle_instrument(instrument) + self._handle_instrument(instrument, update_catalog) cpdef void _handle_quote_ticks(self, list ticks): self._cache.add_quote_ticks(ticks) @@ -1801,6 +2047,7 @@ cdef class DataEngine(Component): cpdef void _internal_update_instruments(self, list instruments: [Instrument]): # Handle all instruments individually cdef Instrument instrument + for instrument in instruments: self._handle_instrument(instrument) diff --git a/nautilus_trader/live/data_client.py b/nautilus_trader/live/data_client.py index cc908acbdb53..bd9c3ef99438 100644 --- a/nautilus_trader/live/data_client.py +++ b/nautilus_trader/live/data_client.py @@ -665,6 +665,7 @@ def request_instrument( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: time_range_str = format_utc_timerange(start, end) self._log.info(f"Request {instrument_id} instrument{time_range_str}", LogColor.BLUE) @@ -674,6 +675,7 @@ def request_instrument( correlation_id=correlation_id, start=start, end=end, + metadata=metadata, ), log_msg=f"request: instrument {instrument_id}", ) @@ -684,6 +686,7 @@ def request_instruments( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: time_range_str = format_utc_timerange(start, end) self._log.info( @@ -696,6 +699,7 @@ def request_instruments( correlation_id=correlation_id, start=start, end=end, + metadata=metadata, ), log_msg=f"request: instruments for {venue}", ) @@ -707,6 +711,7 @@ def request_quote_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: time_range_str = format_utc_timerange(start, end) limit_str = f" limit={limit}" if limit else "" @@ -718,6 +723,7 @@ def request_quote_ticks( correlation_id=correlation_id, start=start, end=end, + metadata=metadata, ), log_msg=f"request: quotes {instrument_id}", ) @@ -729,6 +735,7 @@ def request_trade_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: time_range_str = format_utc_timerange(start, end) limit_str = f" limit={limit}" if limit else "" @@ -740,6 +747,7 @@ def request_trade_ticks( correlation_id=correlation_id, start=start, end=end, + metadata=metadata, ), log_msg=f"request: trades {instrument_id}", ) @@ -751,6 +759,7 @@ def request_bars( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: time_range_str = format_utc_timerange(start, end) limit_str = f" limit={limit}" if limit else "" @@ -762,6 +771,7 @@ def request_bars( correlation_id=correlation_id, start=start, end=end, + metadata=metadata, ), log_msg=f"request: bars {bar_type}", ) @@ -919,6 +929,7 @@ async def _request_instrument( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: raise NotImplementedError( # pragma: no cover "implement the `_request_instrument` coroutine", # pragma: no cover @@ -930,6 +941,7 @@ async def _request_instruments( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: raise NotImplementedError( # pragma: no cover "implement the `_request_instruments` coroutine", # pragma: no cover @@ -942,6 +954,7 @@ async def _request_quote_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: raise NotImplementedError( # pragma: no cover "implement the `_request_quote_ticks` coroutine", # pragma: no cover @@ -954,6 +967,7 @@ async def _request_trade_ticks( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: raise NotImplementedError( # pragma: no cover "implement the `_request_trade_ticks` coroutine", # pragma: no cover @@ -966,6 +980,7 @@ async def _request_bars( correlation_id: UUID4, start: pd.Timestamp | None = None, end: pd.Timestamp | None = None, + metadata: dict | None = None, ) -> None: raise NotImplementedError( # pragma: no cover "implement the `_request_bars` coroutine", # pragma: no cover diff --git a/nautilus_trader/persistence/catalog/base.py b/nautilus_trader/persistence/catalog/base.py index 3e73a84d1009..ea28c2d6e981 100644 --- a/nautilus_trader/persistence/catalog/base.py +++ b/nautilus_trader/persistence/catalog/base.py @@ -20,6 +20,8 @@ from abc import abstractmethod from typing import Any +import pandas as pd + from nautilus_trader.core.data import Data from nautilus_trader.model.data import Bar from nautilus_trader.model.data import CustomData @@ -67,6 +69,16 @@ def query( ) -> list[Data]: raise NotImplementedError + @abstractmethod + def query_last_timestamp( + self, + data_cls: type, + instrument_id: str | None = None, + bar_type: str | None = None, + ts_column: str = "ts_init", + ) -> pd.Timestamp | None: + raise NotImplementedError + def _query_subclasses( self, base_cls: type, diff --git a/nautilus_trader/persistence/catalog/parquet.py b/nautilus_trader/persistence/catalog/parquet.py index 76765d764855..4990e5573374 100644 --- a/nautilus_trader/persistence/catalog/parquet.py +++ b/nautilus_trader/persistence/catalog/parquet.py @@ -39,6 +39,7 @@ from nautilus_trader.core.correctness import PyCondition from nautilus_trader.core.data import Data from nautilus_trader.core.datetime import dt_to_unix_nanos +from nautilus_trader.core.datetime import time_object_to_dt from nautilus_trader.core.inspect import is_nautilus_class from nautilus_trader.core.message import Event from nautilus_trader.core.nautilus_pyo3 import DataBackendSession @@ -577,6 +578,30 @@ def _load_pyarrow_table( start: TimestampLike | None = None, end: TimestampLike | None = None, ts_column: str = "ts_init", + ) -> pds.Dataset | None: + # Original dataset + dataset = self._load_dataset( + path=path, + instrument_ids=instrument_ids, + bar_types=bar_types, + ) + + if dataset is None: + return None + + return self._filter_dataset( + dataset=dataset, + filter_expr=filter_expr, + start=start, + end=end, + ts_column=ts_column, + ) + + def _load_dataset( + self, + path: str, + instrument_ids: list[str] | str | None = None, + bar_types: list[str] | str | None = None, ) -> pds.Dataset | None: # Original dataset dataset = pds.dataset(path, filesystem=self.fs) @@ -585,6 +610,7 @@ def _load_pyarrow_table( if instrument_ids is not None: if not isinstance(instrument_ids, list): instrument_ids = [instrument_ids] + valid_files = [ fn for fn in dataset.files @@ -595,12 +621,27 @@ def _load_pyarrow_table( if bar_types is not None: if not isinstance(bar_types, list): bar_types = [bar_types] + valid_files = [ - fn for fn in dataset.files if any(x.replace("/", "") in fn for x in bar_types) + fn for fn in dataset.files if any(str(x).replace("/", "") in fn for x in bar_types) ] dataset = pds.dataset(valid_files, filesystem=self.fs) + return dataset + + def _filter_dataset( + self, + dataset: pds.Dataset, + filter_expr: str | None = None, + start: TimestampLike | None = None, + end: TimestampLike | None = None, + ts_column: str = "ts_init", + ) -> pds.Dataset | None: + if dataset is None: + return None + filters: list[pds.Expression] = [filter_expr] if filter_expr is not None else [] + if start is not None: filters.append(pds.field(ts_column) >= pd.Timestamp(start).value) if end is not None: @@ -609,8 +650,64 @@ def _load_pyarrow_table( filter_ = combine_filters(*filters) else: filter_ = None + return dataset.to_table(filter=filter_) + def query_last_timestamp( + self, + data_cls: type, + instrument_id: str | None = None, + bar_type: str | None = None, + ts_column: str = "ts_init", + ) -> pd.Timestamp | None: + if data_cls == Instrument: + for instrument_type in Instrument.__subclasses__(): + last_timestamp = self._query_last_timestamp( + data_cls=instrument_type, + instrument_id=instrument_id, + bar_type=bar_type, + ts_column=ts_column, + ) + + if last_timestamp is not None: + return last_timestamp + + return None + + return self._query_last_timestamp( + data_cls=data_cls, + instrument_id=instrument_id, + bar_type=bar_type, + ts_column=ts_column, + ) + + def _query_last_timestamp( + self, + data_cls: type, + instrument_id: str | None = None, + bar_type: str | None = None, + ts_column: str = "ts_init", + ) -> pd.Timestamp | None: + file_prefix = class_to_filename(data_cls) + dataset_path = f"{self.path}/data/{file_prefix}" + + if not self.fs.exists(dataset_path): + return None + + # Original dataset + dataset = self._load_dataset( + path=dataset_path, + instrument_ids=instrument_id, + bar_types=bar_type, + ) + + if dataset is None: + return None + + return time_object_to_dt( + dataset.sort_by([(ts_column, "descending")]).head(1)[ts_column].to_pylist()[0], + ) + def _build_query( self, table: str, diff --git a/nautilus_trader/persistence/config.py b/nautilus_trader/persistence/config.py index e708b7b2804f..e6c51e353e31 100644 --- a/nautilus_trader/persistence/config.py +++ b/nautilus_trader/persistence/config.py @@ -100,3 +100,4 @@ class DataCatalogConfig(NautilusConfig, frozen=True): path: str fs_protocol: str | None = None fs_storage_options: dict | None = None + name: str | None = None diff --git a/nautilus_trader/risk/greeks.py b/nautilus_trader/risk/greeks.py index 0f7ccf26d750..04b4559d5d9b 100644 --- a/nautilus_trader/risk/greeks.py +++ b/nautilus_trader/risk/greeks.py @@ -270,7 +270,7 @@ def update_interest_rate(self, alert=None): # get the interest rate for the current month utc_now_ns = alert.ts_init if alert is not None else self.clock.timestamp_ns() utc_now = unix_nanos_to_dt(utc_now_ns) - month_string = f"{utc_now.year}-{str(utc_now.month).zfill(2)}" + month_string = f"{utc_now.year}-{str(utc_now.month).zfill(2)}" # 2024-01 interest_rate_value = float(self.interest_rates_df.loc[month_string, "interest_rate"]) interest_rate = InterestRateData( @@ -303,8 +303,6 @@ def update_interest_rate(self, alert=None): def import_interest_rates(xml_interest_rate_file): import xmltodict - data_dict = None - with open(xml_interest_rate_file) as xml_file: data_dict = xmltodict.parse(xml_file.read()) @@ -314,21 +312,17 @@ def import_interest_rates(xml_interest_rate_file): "generic:Obs" ] ] - interest_rates.sort(key=lambda x: x[0]) return ( - pd.DataFrame(interest_rates, columns=["month", "interest_rate"]).set_index("month") / 100.0 + pd.DataFrame(interest_rates, columns=["month", "interest_rate"]) + .set_index("month") + .sort_index() + / 100.0 ) def next_month_start_from_timestamp(timestamp): - return (timestamp + pd.offsets.MonthBegin(1)).replace( - hour=0, - minute=0, - second=0, - microsecond=0, - nanosecond=0, - ) + return (timestamp + pd.offsets.MonthBegin(1)).floor(freq="d") def date_to_string(date, string_format="%Y%m%d"): diff --git a/nautilus_trader/system/config.py b/nautilus_trader/system/config.py index 3e18db91d2b9..4414b5956996 100644 --- a/nautilus_trader/system/config.py +++ b/nautilus_trader/system/config.py @@ -61,6 +61,7 @@ class NautilusKernelConfig(NautilusConfig, frozen=True): The configuration for streaming to feather files. catalogs : list[DataCatalogConfig], optional The list of data catalog configs. + We assume that catalogs have no duplicate data. actors : list[ImportableActorConfig] The actor configurations for the kernel. strategies : list[ImportableStrategyConfig] diff --git a/nautilus_trader/system/kernel.py b/nautilus_trader/system/kernel.py index 11a95a4d3af4..0063bca40198 100644 --- a/nautilus_trader/system/kernel.py +++ b/nautilus_trader/system/kernel.py @@ -474,16 +474,24 @@ def __init__( # noqa (too complex) self._setup_streaming(config=config.streaming) # Set up data catalog - self._catalogs: list[ParquetDataCatalog] = [] + self._catalogs: dict[str, ParquetDataCatalog] = {} if config.catalogs: + catalog_name_index = 0 for catalog_config in config.catalogs: catalog = ParquetDataCatalog( path=catalog_config.path, fs_protocol=catalog_config.fs_protocol, fs_storage_options=catalog_config.fs_storage_options, ) - self._catalogs.append(catalog) - self._data_engine.register_catalog(catalog=catalog) + + used_catalog_name = catalog_config.name + + if used_catalog_name is None: + used_catalog_name = f"catalog_{catalog_name_index}" + catalog_name_index += 1 + + self._catalogs[used_catalog_name] = catalog + self._data_engine.register_catalog(catalog, used_catalog_name) # Create importable actors for actor_config in config.actors: @@ -843,13 +851,13 @@ def writer(self) -> StreamingFeatherWriter | None: return self._writer @property - def catalogs(self) -> list[ParquetDataCatalog]: + def catalogs(self) -> dict[str, ParquetDataCatalog]: """ Return the kernel's list of data catalogs. Returns ------- - list[ParquetDataCatalog] + dict[str, ParquetDataCatalog] """ return self._catalogs diff --git a/nautilus_trader/test_kit/mocks/data.py b/nautilus_trader/test_kit/mocks/data.py index acea01392d10..3e1deff4059e 100644 --- a/nautilus_trader/test_kit/mocks/data.py +++ b/nautilus_trader/test_kit/mocks/data.py @@ -13,9 +13,23 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from datetime import datetime from pathlib import Path from typing import Literal +from nautilus_trader.cache.cache import Cache +from nautilus_trader.common.component import Clock +from nautilus_trader.common.component import MessageBus +from nautilus_trader.core.uuid import UUID4 +from nautilus_trader.data.client import MarketDataClient +from nautilus_trader.model.data import Bar +from nautilus_trader.model.data import BarType +from nautilus_trader.model.data import QuoteTick +from nautilus_trader.model.data import TradeTick +from nautilus_trader.model.identifiers import ClientId +from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.identifiers import Venue +from nautilus_trader.model.instruments.base import Instrument from nautilus_trader.persistence.catalog.parquet import ParquetDataCatalog from nautilus_trader.persistence.catalog.singleton import clear_singleton_instances from nautilus_trader.persistence.wranglers import QuoteTickDataWrangler @@ -25,6 +39,99 @@ from nautilus_trader.trading.filters import NewsEvent +class MockMarketDataClient(MarketDataClient): + """ + Provides an implementation of `MarketDataClient` for testing. + + Parameters + ---------- + client_id : ClientId + The data client ID. + msgbus : MessageBus + The message bus for the client. + cache : Cache + The cache for the client. + clock : Clock + The clock for the client. + + """ + + def __init__( + self, + client_id: ClientId, + msgbus: MessageBus, + cache: Cache, + clock: Clock, + ): + super().__init__( + client_id=client_id, + venue=Venue(str(client_id)), + msgbus=msgbus, + cache=cache, + clock=clock, + ) + self._set_connected() + + self.instrument: Instrument | None = None + self.instruments: list[Instrument] = [] + self.quote_ticks: list[QuoteTick] = [] + self.trade_ticks: list[TradeTick] = [] + self.bars: list[Bar] = [] + + def request_instrument( + self, + instrument_id: InstrumentId, + correlation_id: UUID4, + start: datetime | None = None, + end: datetime | None = None, + metadata: dict | None = None, + ) -> None: + self._handle_instrument(self.instrument, correlation_id, metadata) + + def request_instruments( + self, + venue: Venue, + correlation_id: UUID4, + start: datetime | None = None, + end: datetime | None = None, + metadata: dict | None = None, + ) -> None: + self._handle_instruments(venue, self.instruments, correlation_id, metadata) + + def request_quote_ticks( + self, + instrument_id: InstrumentId, + limit: int, + correlation_id: UUID4, + start: datetime | None = None, + end: datetime | None = None, + metadata: dict | None = None, + ) -> None: + self._handle_quote_ticks(instrument_id, self.quote_ticks, correlation_id, metadata) + + def request_trade_ticks( + self, + instrument_id: InstrumentId, + limit: int, + correlation_id: UUID4, + start: datetime | None = None, + end: datetime | None = None, + metadata: dict | None = None, + ) -> None: + self._handle_trade_ticks(instrument_id, self.trade_ticks, correlation_id, metadata) + + def request_bars( + self, + bar_type: BarType, + limit: int, + correlation_id: UUID4, + start: datetime | None = None, + end: datetime | None = None, + metadata: dict | None = None, + ) -> None: + self._handle_bars(bar_type, self.bars, None, correlation_id, metadata) + + _AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy("AUD/USD") _ETHUSDT_BINANCE = TestInstrumentProvider.ethusdt_binance() diff --git a/tests/test_data/databento/options_catalog/usd_short_term_rate.xml b/tests/test_data/databento/options_catalog/usd_short_term_rate.xml new file mode 100644 index 000000000000..d958287f5332 --- /dev/null +++ b/tests/test_data/databento/options_catalog/usd_short_term_rate.xml @@ -0,0 +1,420 @@ + + + + IREF029152 + false + 2024-09-27T15:20:10 + + + + + + + Information + DSD_STES + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/unit_tests/data/test_aggregation.py b/tests/unit_tests/data/test_aggregation.py index ef19e55f95f2..1171eac8b9e3 100644 --- a/tests/unit_tests/data/test_aggregation.py +++ b/tests/unit_tests/data/test_aggregation.py @@ -50,6 +50,7 @@ from nautilus_trader.test_kit.stubs.identifiers import TestIdStubs +NANOSECONDS_IN_SECOND = 1_000_000_000 AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy("AUD/USD") BTCUSDT_BINANCE = TestInstrumentProvider.btcusdt_binance() ETHUSDT_BITMEX = TestInstrumentProvider.ethusd_bitmex() @@ -93,8 +94,8 @@ def test_set_partial_updates_bar_to_expected_properties(self): low=Price.from_str("1.00000"), close=Price.from_str("1.00002"), volume=Quantity.from_str("1"), - ts_event=1_000_000_000, - ts_init=1_000_000_000, + ts_event=NANOSECONDS_IN_SECOND, + ts_init=NANOSECONDS_IN_SECOND, ) # Act @@ -108,8 +109,8 @@ def test_set_partial_updates_bar_to_expected_properties(self): assert bar.low == Price.from_str("1.00000") assert bar.close == Price.from_str("1.00002") assert bar.volume == Quantity.from_str("1") - assert bar.ts_init == 1_000_000_000 - assert builder.ts_last == 1_000_000_000 + assert bar.ts_init == NANOSECONDS_IN_SECOND + assert builder.ts_last == NANOSECONDS_IN_SECOND def test_set_partial_when_already_set_does_not_update(self): # Arrange @@ -123,8 +124,8 @@ def test_set_partial_when_already_set_does_not_update(self): low=Price.from_str("1.00000"), close=Price.from_str("1.00002"), volume=Quantity.from_str("1"), - ts_event=1_000_000_000, - ts_init=1_000_000_000, + ts_event=NANOSECONDS_IN_SECOND, + ts_init=NANOSECONDS_IN_SECOND, ) partial_bar2 = Bar( @@ -134,7 +135,7 @@ def test_set_partial_when_already_set_does_not_update(self): low=Price.from_str("2.00000"), close=Price.from_str("2.00002"), volume=Quantity.from_str("2"), - ts_event=1_000_000_000, + ts_event=NANOSECONDS_IN_SECOND, ts_init=3_000_000_000, ) @@ -151,7 +152,7 @@ def test_set_partial_when_already_set_does_not_update(self): assert bar.close == Price.from_str("1.00002") assert bar.volume == Quantity.from_str("1") assert bar.ts_init == 4_000_000_000 - assert builder.ts_last == 1_000_000_000 + assert builder.ts_last == NANOSECONDS_IN_SECOND def test_single_update_results_in_expected_properties(self): # Arrange @@ -178,8 +179,8 @@ def test_single_bar_update_results_in_expected_properties(self): low=Price.from_str("1.00000"), close=Price.from_str("1.00005"), volume=Quantity.from_str("1.5"), - ts_event=1_000_000_000, - ts_init=1_000_000_000, + ts_event=NANOSECONDS_IN_SECOND, + ts_init=NANOSECONDS_IN_SECOND, ) # Act @@ -187,7 +188,7 @@ def test_single_bar_update_results_in_expected_properties(self): # Assert assert builder.initialized - assert builder.ts_last == 1_000_000_000 + assert builder.ts_last == NANOSECONDS_IN_SECOND assert builder.count == 1 built_bar = builder.build_now() @@ -304,7 +305,7 @@ def test_build_when_received_updates_returns_expected_bar(self): builder.update( Price.from_str("1.00000"), Quantity.from_str("1.5"), - 1_000_000_000, + NANOSECONDS_IN_SECOND, ) # Act @@ -316,8 +317,8 @@ def test_build_when_received_updates_returns_expected_bar(self): assert bar.low == Price.from_str("1.00000") assert bar.close == Price.from_str("1.00000") assert bar.volume == Quantity.from_str("4.0") - assert bar.ts_init == 1_000_000_000 - assert builder.ts_last == 1_000_000_000 + assert bar.ts_init == NANOSECONDS_IN_SECOND + assert builder.ts_last == NANOSECONDS_IN_SECOND assert builder.count == 0 def test_build_when_received_bar_updates_returns_expected_bar(self): @@ -356,8 +357,8 @@ def test_build_when_received_bar_updates_returns_expected_bar(self): low=Price.from_str("1.00000"), close=Price.from_str("1.00000"), volume=Quantity.from_str("1.5"), - ts_event=1_000_000_000, - ts_init=1_000_000_000, + ts_event=NANOSECONDS_IN_SECOND, + ts_init=NANOSECONDS_IN_SECOND, ) builder.update_bar(bar3, bar3.volume, bar3.ts_init) @@ -370,8 +371,8 @@ def test_build_when_received_bar_updates_returns_expected_bar(self): assert bar.low == Price.from_str("1.00000") assert bar.close == Price.from_str("1.00000") assert bar.volume == Quantity.from_str("4.0") - assert bar.ts_init == 1_000_000_000 - assert builder.ts_last == 1_000_000_000 + assert bar.ts_init == NANOSECONDS_IN_SECOND + assert builder.ts_last == NANOSECONDS_IN_SECOND assert builder.count == 0 def test_build_with_previous_close(self): @@ -1803,8 +1804,8 @@ def test_update_timer_with_test_clock_sends_single_bar_to_handler(self): ask_price=Price.from_str("1.00003"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), - ts_event=1 * 60 * 1_000_000_000, # 1 minute in nanoseconds - ts_init=1 * 60 * 1_000_000_000, # 1 minute in nanoseconds + ts_event=1 * 60 * NANOSECONDS_IN_SECOND, + ts_init=1 * 60 * NANOSECONDS_IN_SECOND, ) # Act @@ -1827,7 +1828,7 @@ def test_update_timer_with_test_clock_sends_single_bar_to_handler(self): def test_batch_update_sends_single_bar_to_handler(self): # Arrange clock = TestClock() - clock.set_time(3 * 60 * 1_000_000_000) + clock.set_time(3 * 60 * NANOSECONDS_IN_SECOND) handler = [] instrument_id = TestIdStubs.audusd_id() bar_spec = BarSpecification(3, BarAggregation.MINUTE, PriceType.MID) @@ -1845,8 +1846,8 @@ def test_batch_update_sends_single_bar_to_handler(self): ask_price=Price.from_str("1.00004"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), - ts_event=1 * 60 * 1_000_000_000, - ts_init=1 * 60 * 1_000_000_000, + ts_event=1 * 60 * NANOSECONDS_IN_SECOND, + ts_init=1 * 60 * NANOSECONDS_IN_SECOND, ) tick2 = QuoteTick( @@ -1855,8 +1856,8 @@ def test_batch_update_sends_single_bar_to_handler(self): ask_price=Price.from_str("1.00005"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), - ts_event=2 * 60 * 1_000_000_000, - ts_init=2 * 60 * 1_000_000_000, + ts_event=2 * 60 * NANOSECONDS_IN_SECOND, + ts_init=2 * 60 * NANOSECONDS_IN_SECOND, ) tick3 = QuoteTick( @@ -1865,8 +1866,8 @@ def test_batch_update_sends_single_bar_to_handler(self): ask_price=Price.from_str("1.00003"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), - ts_event=3 * 60 * 1_000_000_000, # 1 minute in nanoseconds - ts_init=3 * 60 * 1_000_000_000, # 1 minute in nanoseconds + ts_event=3 * 60 * NANOSECONDS_IN_SECOND, + ts_init=3 * 60 * NANOSECONDS_IN_SECOND, ) # Act @@ -1916,8 +1917,8 @@ def test_update_timer_with_test_clock_sends_single_bar_to_handler_with_bars(self low=Price.from_str("1.00004"), close=Price.from_str("1.00007"), volume=Quantity.from_int(1), - ts_event=1 * 60 * 1_000_000_000, # 1 minute in nanoseconds - ts_init=1 * 60 * 1_000_000_000, # 1 minute in nanoseconds + ts_event=1 * 60 * NANOSECONDS_IN_SECOND, + ts_init=1 * 60 * NANOSECONDS_IN_SECOND, ) bar2 = Bar( @@ -1927,8 +1928,8 @@ def test_update_timer_with_test_clock_sends_single_bar_to_handler_with_bars(self low=Price.from_str("1.00003"), close=Price.from_str("1.00015"), volume=Quantity.from_int(1), - ts_event=2 * 60 * 1_000_000_000, # 1 minute in nanoseconds - ts_init=2 * 60 * 1_000_000_000, # 1 minute in nanoseconds + ts_event=2 * 60 * NANOSECONDS_IN_SECOND, + ts_init=2 * 60 * NANOSECONDS_IN_SECOND, ) bar3 = Bar( @@ -1938,8 +1939,8 @@ def test_update_timer_with_test_clock_sends_single_bar_to_handler_with_bars(self low=Price.from_str("1.00007"), close=Price.from_str("1.00008"), volume=Quantity.from_int(1), - ts_event=3 * 60 * 1_000_000_000, # 1 minute in nanoseconds - ts_init=3 * 60 * 1_000_000_000, # 1 minute in nanoseconds + ts_event=3 * 60 * NANOSECONDS_IN_SECOND, + ts_init=3 * 60 * NANOSECONDS_IN_SECOND, ) # Act @@ -1958,14 +1959,14 @@ def test_update_timer_with_test_clock_sends_single_bar_to_handler_with_bars(self assert bar.low == Price.from_str("1.00003") assert bar.close == Price.from_str("1.00008") assert bar.volume == Quantity.from_int(3) - assert bar.ts_init == 3 * 60 * 1_000_000_000 + assert bar.ts_init == 3 * 60 * NANOSECONDS_IN_SECOND def test_update_timer_with_test_clock_sends_single_bar_to_handler_with_bars_and_time_origin( self, ): # Arrange clock = TestClock() - clock.set_time(30 * 60 * 1_000_000_000) + clock.set_time(30 * 60 * NANOSECONDS_IN_SECOND) handler = [] instrument_id = TestIdStubs.audusd_id() bar_spec3 = BarSpecification(3, BarAggregation.MINUTE, PriceType.LAST) @@ -1994,8 +1995,8 @@ def test_update_timer_with_test_clock_sends_single_bar_to_handler_with_bars_and_ low=Price.from_str("1.00004"), close=Price.from_str("1.00007"), volume=Quantity.from_int(1), - ts_event=31 * 60 * 1_000_000_000, # 1 minute in nanoseconds - ts_init=1 * 60 * 1_000_000_000, # 1 minute in nanoseconds + ts_event=31 * 60 * NANOSECONDS_IN_SECOND, + ts_init=31 * 60 * NANOSECONDS_IN_SECOND, ) bar2 = Bar( @@ -2005,8 +2006,8 @@ def test_update_timer_with_test_clock_sends_single_bar_to_handler_with_bars_and_ low=Price.from_str("1.00003"), close=Price.from_str("1.00015"), volume=Quantity.from_int(1), - ts_event=32 * 60 * 1_000_000_000, # 1 minute in nanoseconds - ts_init=2 * 60 * 1_000_000_000, # 1 minute in nanoseconds + ts_event=32 * 60 * NANOSECONDS_IN_SECOND, + ts_init=32 * 60 * NANOSECONDS_IN_SECOND, ) bar3 = Bar( @@ -2016,8 +2017,8 @@ def test_update_timer_with_test_clock_sends_single_bar_to_handler_with_bars_and_ low=Price.from_str("1.00007"), close=Price.from_str("1.00008"), volume=Quantity.from_int(1), - ts_event=33 * 60 * 1_000_000_000, # 1 minute in nanoseconds - ts_init=33 * 60 * 1_000_000_000, # 1 minute in nanoseconds + ts_event=33 * 60 * NANOSECONDS_IN_SECOND, + ts_init=33 * 60 * NANOSECONDS_IN_SECOND, ) # Act @@ -2036,7 +2037,7 @@ def test_update_timer_with_test_clock_sends_single_bar_to_handler_with_bars_and_ assert bar.low == Price.from_str("1.00003") assert bar.close == Price.from_str("1.00008") assert bar.volume == Quantity.from_int(3) - assert bar.ts_init == 33 * 60 * 1_000_000_000 + assert bar.ts_init == 33 * 60 * NANOSECONDS_IN_SECOND def test_update_timer_with_test_clock_sends_single_monthly_bar_to_handler_with_bars(self): # Arrange @@ -2069,8 +2070,8 @@ def test_update_timer_with_test_clock_sends_single_monthly_bar_to_handler_with_b low=Price.from_str("1.00004"), close=Price.from_str("1.00007"), volume=Quantity.from_int(1), - ts_event=pd.Timestamp("2024-3-24").value, # 1 minute in nanoseconds - ts_init=pd.Timestamp("2024-3-24").value, # 1 minute in nanoseconds + ts_event=pd.Timestamp("2024-3-24").value, # time in nanoseconds + ts_init=pd.Timestamp("2024-3-24").value, ) bar2 = Bar( @@ -2080,8 +2081,8 @@ def test_update_timer_with_test_clock_sends_single_monthly_bar_to_handler_with_b low=Price.from_str("1.00003"), close=Price.from_str("1.00015"), volume=Quantity.from_int(1), - ts_event=pd.Timestamp("2024-3-25").value, # 1 minute in nanoseconds - ts_init=pd.Timestamp("2024-3-25").value, # 1 minute in nanoseconds + ts_event=pd.Timestamp("2024-3-25").value, + ts_init=pd.Timestamp("2024-3-25").value, ) bar3 = Bar( @@ -2091,8 +2092,8 @@ def test_update_timer_with_test_clock_sends_single_monthly_bar_to_handler_with_b low=Price.from_str("1.00007"), close=Price.from_str("1.00008"), volume=Quantity.from_int(1), - ts_event=pd.Timestamp("2024-3-26").value, # 1 minute in nanoseconds - ts_init=pd.Timestamp("2024-3-26").value, # 1 minute in nanoseconds + ts_event=pd.Timestamp("2024-3-26").value, + ts_init=pd.Timestamp("2024-3-26").value, ) # Act @@ -2144,8 +2145,8 @@ def test_update_timer_with_test_clock_sends_single_weekly_bar_to_handler_with_ba low=Price.from_str("1.00004"), close=Price.from_str("1.00007"), volume=Quantity.from_int(1), - ts_event=pd.Timestamp("2024-3-20").value, # 1 minute in nanoseconds - ts_init=pd.Timestamp("2024-3-20").value, # 1 minute in nanoseconds + ts_event=pd.Timestamp("2024-3-20").value, # time in nanoseconds + ts_init=pd.Timestamp("2024-3-20").value, ) bar2 = Bar( @@ -2155,8 +2156,8 @@ def test_update_timer_with_test_clock_sends_single_weekly_bar_to_handler_with_ba low=Price.from_str("1.00003"), close=Price.from_str("1.00015"), volume=Quantity.from_int(1), - ts_event=pd.Timestamp("2024-3-21").value, # 1 minute in nanoseconds - ts_init=pd.Timestamp("2024-3-21").value, # 1 minute in nanoseconds + ts_event=pd.Timestamp("2024-3-21").value, + ts_init=pd.Timestamp("2024-3-21").value, ) bar3 = Bar( @@ -2166,8 +2167,8 @@ def test_update_timer_with_test_clock_sends_single_weekly_bar_to_handler_with_ba low=Price.from_str("1.00007"), close=Price.from_str("1.00008"), volume=Quantity.from_int(1), - ts_event=pd.Timestamp("2024-3-22").value, # 1 minute in nanoseconds - ts_init=pd.Timestamp("2024-3-22").value, # 1 minute in nanoseconds + ts_event=pd.Timestamp("2024-3-22").value, + ts_init=pd.Timestamp("2024-3-22").value, ) # Act @@ -2191,7 +2192,7 @@ def test_update_timer_with_test_clock_sends_single_weekly_bar_to_handler_with_ba def test_batch_update_sends_single_bar_to_handler_with_bars(self): # Arrange clock = TestClock() - clock.set_time(3 * 60 * 1_000_000_000) + clock.set_time(3 * 60 * NANOSECONDS_IN_SECOND) handler = [] instrument_id = TestIdStubs.audusd_id() bar_spec3 = BarSpecification(3, BarAggregation.MINUTE, PriceType.LAST) @@ -2219,8 +2220,8 @@ def test_batch_update_sends_single_bar_to_handler_with_bars(self): low=Price.from_str("1.00004"), close=Price.from_str("1.00007"), volume=Quantity.from_int(1), - ts_event=1 * 60 * 1_000_000_000, # 1 minute in nanoseconds - ts_init=1 * 60 * 1_000_000_000, # 1 minute in nanoseconds + ts_event=1 * 60 * NANOSECONDS_IN_SECOND, + ts_init=1 * 60 * NANOSECONDS_IN_SECOND, ) bar2 = Bar( @@ -2230,8 +2231,8 @@ def test_batch_update_sends_single_bar_to_handler_with_bars(self): low=Price.from_str("1.00003"), close=Price.from_str("1.00015"), volume=Quantity.from_int(1), - ts_event=2 * 60 * 1_000_000_000, # 1 minute in nanoseconds - ts_init=2 * 60 * 1_000_000_000, # 1 minute in nanoseconds + ts_event=2 * 60 * NANOSECONDS_IN_SECOND, + ts_init=2 * 60 * NANOSECONDS_IN_SECOND, ) bar3 = Bar( @@ -2241,8 +2242,8 @@ def test_batch_update_sends_single_bar_to_handler_with_bars(self): low=Price.from_str("1.00007"), close=Price.from_str("1.00008"), volume=Quantity.from_int(1), - ts_event=3 * 60 * 1_000_000_000, # 1 minute in nanoseconds - ts_init=3 * 60 * 1_000_000_000, # 1 minute in nanoseconds + ts_event=3 * 60 * NANOSECONDS_IN_SECOND, + ts_init=3 * 60 * NANOSECONDS_IN_SECOND, ) # Act @@ -2261,7 +2262,7 @@ def test_batch_update_sends_single_bar_to_handler_with_bars(self): assert bar.low == Price.from_str("1.00003") assert bar.close == Price.from_str("1.00008") assert bar.volume == Quantity.from_int(3) - assert bar.ts_init == 3 * 60 * 1_000_000_000 + assert bar.ts_init == 3 * 60 * NANOSECONDS_IN_SECOND @pytest.mark.parametrize( ("step", "aggregation"), @@ -2305,7 +2306,7 @@ def test_aggregation_for_same_sec_and_minute_intervals(self, step, aggregation): # Assert assert clock.timestamp_ns() == 1610064046674000000 - assert aggregator.interval_ns == 1_000_000_000 + assert aggregator.interval_ns == NANOSECONDS_IN_SECOND assert aggregator.next_close_ns == 1610064047000000000 assert handler[0].open == Price.from_str("39432.99") assert handler[0].high == Price.from_str("39435.66") diff --git a/tests/unit_tests/data/test_client.py b/tests/unit_tests/data/test_client.py index a8fae7c23cb4..c0ccbd35c4ed 100644 --- a/tests/unit_tests/data/test_client.py +++ b/tests/unit_tests/data/test_client.py @@ -234,14 +234,14 @@ def test_handle_bar_sends_to_data_engine(self): def test_handle_quote_ticks_sends_to_data_engine(self): # Arrange, Act - self.client._handle_quote_ticks_py(AUDUSD_SIM.id, [], UUID4()) + self.client._handle_quote_ticks_py(AUDUSD_SIM.id, [], UUID4(), None) # Assert assert self.data_engine.response_count == 1 def test_handle_trade_ticks_sends_to_data_engine(self): # Arrange, Act - self.client._handle_trade_ticks_py(AUDUSD_SIM.id, [], UUID4()) + self.client._handle_trade_ticks_py(AUDUSD_SIM.id, [], UUID4(), None) # Assert assert self.data_engine.response_count == 1 @@ -253,6 +253,7 @@ def test_handle_bars_sends_to_data_engine(self): [], None, UUID4(), + None, ) # Assert diff --git a/tests/unit_tests/data/test_engine.py b/tests/unit_tests/data/test_engine.py index 2c8a385c7790..e29e7f65010f 100644 --- a/tests/unit_tests/data/test_engine.py +++ b/tests/unit_tests/data/test_engine.py @@ -15,12 +15,14 @@ import sys +import pandas as pd import pytest from nautilus_trader.backtest.data_client import BacktestMarketDataClient from nautilus_trader.common.component import MessageBus from nautilus_trader.common.component import TestClock from nautilus_trader.core.data import Data +from nautilus_trader.core.datetime import time_object_to_dt from nautilus_trader.core.uuid import UUID4 from nautilus_trader.data.engine import DataEngine from nautilus_trader.data.engine import DataEngineConfig @@ -38,6 +40,7 @@ from nautilus_trader.model.data import OrderBookDeltas from nautilus_trader.model.data import QuoteTick from nautilus_trader.model.data import TradeTick +from nautilus_trader.model.enums import AggressorSide from nautilus_trader.model.enums import BarAggregation from nautilus_trader.model.enums import BookType from nautilus_trader.model.enums import PriceType @@ -45,11 +48,13 @@ from nautilus_trader.model.identifiers import ClientId from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.identifiers import Symbol +from nautilus_trader.model.identifiers import TradeId from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.instruments import Instrument from nautilus_trader.model.objects import Price from nautilus_trader.model.objects import Quantity from nautilus_trader.portfolio.portfolio import Portfolio +from nautilus_trader.test_kit.mocks.data import MockMarketDataClient from nautilus_trader.test_kit.mocks.data import setup_catalog from nautilus_trader.test_kit.providers import TestInstrumentProvider from nautilus_trader.test_kit.stubs.component import TestComponentStubs @@ -127,6 +132,13 @@ def setup(self): clock=self.clock, ) + self.mock_market_data_client = MockMarketDataClient( + client_id=ClientId(BINANCE.value), + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + ) + self.data_engine.process(BTCUSDT_BINANCE) self.data_engine.process(ETHUSDT_BINANCE) self.data_engine.process(XBTUSD_BITMEX) @@ -2240,6 +2252,442 @@ def test_request_order_book_snapshot_reaches_client(self): assert self.data_engine.request_count == 1 assert len(handler) == 0 + def test_request_bars_reaches_client(self): + # Arrange + self.data_engine.register_client(self.mock_market_data_client) + bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID) + bar_type = BarType(ETHUSDT_BINANCE.id, bar_spec) + bar = Bar( + bar_type, + Price.from_str("1051.00000"), + Price.from_str("1055.00000"), + Price.from_str("1050.00000"), + Price.from_str("1052.00000"), + Quantity.from_int(100), + 0, + 0, + ) + self.mock_market_data_client.bars = [bar] + + handler = [] + request = DataRequest( + client_id=None, + venue=bar_type.instrument_id.venue, + data_type=DataType( + Bar, + metadata={ + "bar_type": bar_type, + "start": None, + "end": None, + "update_catalog": False, + }, + ), + callback=handler.append, + request_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + # Act + self.msgbus.request(endpoint="DataEngine.request", request=request) + + # Assert + assert self.data_engine.request_count == 1 + assert len(handler) == 1 + assert handler[0].data == [bar] + + def test_request_bars_when_catalog_registered(self): + # Arrange + catalog = setup_catalog(protocol="file") + bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID) + bar_type = BarType(ETHUSDT_BINANCE.id, bar_spec) + bar = Bar( + bar_type, + Price.from_str("1051.00000"), + Price.from_str("1055.00000"), + Price.from_str("1050.00000"), + Price.from_str("1052.00000"), + Quantity.from_int(100), + 0, + 0, + ) + catalog.write_data([bar]) + self.data_engine.register_catalog(catalog) + + handler = [] + request = DataRequest( + client_id=None, + venue=bar_type.instrument_id.venue, + data_type=DataType( + Bar, + metadata={ + "bar_type": bar_type, + "start": None, + "end": None, + "update_catalog": False, + }, + ), + callback=handler.append, + request_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + # Act + self.msgbus.request(endpoint="DataEngine.request", request=request) + + # Assert + assert self.data_engine.request_count == 1 + assert len(handler) == 1 + assert handler[0].data == [bar] + + def test_request_bars_when_catalog_and_client_registered(self): + # Arrange + catalog = setup_catalog(protocol="file") + bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID) + bar_type = BarType(ETHUSDT_BINANCE.id, bar_spec) + bar = Bar( + bar_type, + Price.from_str("1051.00000"), + Price.from_str("1055.00000"), + Price.from_str("1050.00000"), + Price.from_str("1052.00000"), + Quantity.from_int(100), + pd.Timestamp("2024-3-24").value, + pd.Timestamp("2024-3-24").value, + ) + catalog.write_data([bar]) + self.data_engine.register_catalog(catalog) + + self.data_engine.register_client(self.mock_market_data_client) + bar2 = Bar( + bar_type, + Price.from_str("1051.00000"), + Price.from_str("1055.00000"), + Price.from_str("1050.00000"), + Price.from_str("1052.00000"), + Quantity.from_int(100), + pd.Timestamp("2024-3-25").value, + pd.Timestamp("2024-3-25").value, + ) + self.mock_market_data_client.bars = [bar2] + + handler = [] + request = DataRequest( + client_id=None, + venue=bar_type.instrument_id.venue, + data_type=DataType( + Bar, + metadata={ + "bar_type": bar_type, + "start": pd.Timestamp("2024-3-24"), + "end": pd.Timestamp("2024-3-25"), + "update_catalog": True, + }, + ), + callback=handler.append, + request_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + self.clock.advance_time(pd.Timestamp("2024-3-25").value) + + # Act + self.msgbus.request(endpoint="DataEngine.request", request=request) + + # Assert + assert self.data_engine.request_count == 1 + assert len(handler) == 1 + print(handler[0].data) + assert handler[0].data == [bar, bar2] + assert catalog.query_last_timestamp(Bar, bar_type=bar_type) == time_object_to_dt( + pd.Timestamp("2024-3-25"), + ) + + def test_request_quote_ticks_reaches_client(self): + # Arrange + self.data_engine.register_client(self.mock_market_data_client) + quote_tick = QuoteTick( + ETHUSDT_BINANCE.id, + Price.from_str("1051.00000"), + Price.from_str("1052.00000"), + Quantity.from_int(100), + Quantity.from_int(100), + 0, + 0, + ) + self.mock_market_data_client.quote_ticks = [quote_tick] + + handler = [] + request = DataRequest( + client_id=None, + venue=ETHUSDT_BINANCE.venue, + data_type=DataType( + QuoteTick, + metadata={ + "instrument_id": ETHUSDT_BINANCE.id, + "start": None, + "end": None, + "update_catalog": False, + }, + ), + callback=handler.append, + request_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + # Act + self.msgbus.request(endpoint="DataEngine.request", request=request) + + # Assert + assert self.data_engine.request_count == 1 + assert len(handler) == 1 + assert handler[0].data == [quote_tick] + + def test_request_quote_ticks_when_catalog_registered(self): + # Arrange + catalog = setup_catalog(protocol="file") + quote_tick = QuoteTick( + ETHUSDT_BINANCE.id, + Price.from_str("1051.00000"), + Price.from_str("1052.00000"), + Quantity.from_int(100), + Quantity.from_int(100), + 0, + 0, + ) + catalog.write_data([quote_tick]) + self.data_engine.register_catalog(catalog) + + handler = [] + request = DataRequest( + client_id=None, + venue=ETHUSDT_BINANCE.venue, + data_type=DataType( + QuoteTick, + metadata={ + "instrument_id": ETHUSDT_BINANCE.id, + "start": None, + "end": None, + "update_catalog": False, + }, + ), + callback=handler.append, + request_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + # Act + self.msgbus.request(endpoint="DataEngine.request", request=request) + + # Assert + assert self.data_engine.request_count == 1 + assert len(handler) == 1 + # assert handler[0].data == [quote_tick] + + def test_request_quote_ticks_when_catalog_and_client_registered(self): + # Arrange + catalog = setup_catalog(protocol="file") + quote_tick = QuoteTick( + ETHUSDT_BINANCE.id, + Price.from_str("1051.00000"), + Price.from_str("1052.00000"), + Quantity.from_int(100), + Quantity.from_int(100), + pd.Timestamp("2024-3-24").value, + pd.Timestamp("2024-3-24").value, + ) + catalog.write_data([quote_tick]) + self.data_engine.register_catalog(catalog) + + self.data_engine.register_client(self.mock_market_data_client) + quote_tick2 = QuoteTick( + ETHUSDT_BINANCE.id, + Price.from_str("1051.00000"), + Price.from_str("1052.00000"), + Quantity.from_int(100), + Quantity.from_int(100), + pd.Timestamp("2024-3-25").value, + pd.Timestamp("2024-3-25").value, + ) + self.mock_market_data_client.quote_ticks = [quote_tick2] + + handler = [] + request = DataRequest( + client_id=None, + venue=ETHUSDT_BINANCE.venue, + data_type=DataType( + QuoteTick, + metadata={ + "instrument_id": ETHUSDT_BINANCE.id, + "start": pd.Timestamp("2024-3-24"), + "end": pd.Timestamp("2024-3-25"), + "update_catalog": True, + }, + ), + callback=handler.append, + request_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + self.clock.advance_time(pd.Timestamp("2024-3-25").value) + + # Act + self.msgbus.request(endpoint="DataEngine.request", request=request) + + # Assert + assert self.data_engine.request_count == 1 + assert len(handler) == 1 + # assert handler[0].data == [quote_tick, quote_tick2] + assert catalog.query_last_timestamp( + QuoteTick, + instrument_id=ETHUSDT_BINANCE.id, + ) == time_object_to_dt( + pd.Timestamp("2024-3-25"), + ) + + def test_request_trade_ticks_reaches_client(self): + # Arrange + self.data_engine.register_client(self.mock_market_data_client) + trade_tick = TradeTick( + instrument_id=ETHUSDT_BINANCE.id, + price=Price.from_str("1051.00000"), + size=Quantity.from_int(100), + aggressor_side=AggressorSide.BUYER, + trade_id=TradeId("123456"), + ts_event=0, + ts_init=0, + ) + self.mock_market_data_client.trade_ticks = [trade_tick] + + handler = [] + request = DataRequest( + client_id=None, + venue=ETHUSDT_BINANCE.venue, + data_type=DataType( + TradeTick, + metadata={ + "instrument_id": ETHUSDT_BINANCE.id, + "start": None, + "end": None, + "update_catalog": False, + }, + ), + callback=handler.append, + request_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + # Act + self.msgbus.request(endpoint="DataEngine.request", request=request) + + # Assert + assert self.data_engine.request_count == 1 + assert len(handler) == 1 + assert handler[0].data == [trade_tick] + + def test_request_trade_ticks_when_catalog_registered(self): + # Arrange + catalog = setup_catalog(protocol="file") + trade_tick = TradeTick( + instrument_id=ETHUSDT_BINANCE.id, + price=Price.from_str("1051.00000"), + size=Quantity.from_int(100), + aggressor_side=AggressorSide.BUYER, + trade_id=TradeId("123456"), + ts_event=0, + ts_init=0, + ) + catalog.write_data([trade_tick]) + self.data_engine.register_catalog(catalog) + + assert trade_tick == trade_tick + + handler = [] + request = DataRequest( + client_id=None, + venue=ETHUSDT_BINANCE.venue, + data_type=DataType( + TradeTick, + metadata={ + "instrument_id": ETHUSDT_BINANCE.id, + "start": None, + "end": None, + "update_catalog": False, + }, + ), + callback=handler.append, + request_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + # Act + self.msgbus.request(endpoint="DataEngine.request", request=request) + + # Assert + assert self.data_engine.request_count == 1 + assert len(handler) == 1 + # assert handler[0].data == [trade_tick] + + def test_request_trade_ticks_when_catalog_and_client_registered(self): + # Arrange + catalog = setup_catalog(protocol="file") + trade_tick = TradeTick( + instrument_id=ETHUSDT_BINANCE.id, + price=Price.from_str("1051.00000"), + size=Quantity.from_int(100), + aggressor_side=AggressorSide.BUYER, + trade_id=TradeId("123456"), + ts_event=pd.Timestamp("2024-3-24").value, + ts_init=pd.Timestamp("2024-3-24").value, + ) + catalog.write_data([trade_tick]) + self.data_engine.register_catalog(catalog) + + self.data_engine.register_client(self.mock_market_data_client) + trade_tick2 = TradeTick( + instrument_id=ETHUSDT_BINANCE.id, + price=Price.from_str("1051.00000"), + size=Quantity.from_int(100), + aggressor_side=AggressorSide.BUYER, + trade_id=TradeId("123456"), + ts_event=pd.Timestamp("2024-3-25").value, + ts_init=pd.Timestamp("2024-3-25").value, + ) + self.mock_market_data_client.trade_ticks = [trade_tick2] + + handler = [] + request = DataRequest( + client_id=None, + venue=ETHUSDT_BINANCE.venue, + data_type=DataType( + TradeTick, + metadata={ + "instrument_id": ETHUSDT_BINANCE.id, + "start": pd.Timestamp("2024-3-24"), + "end": pd.Timestamp("2024-3-25"), + "update_catalog": True, + }, + ), + callback=handler.append, + request_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + self.clock.advance_time(pd.Timestamp("2024-3-25").value) + + # Act + self.msgbus.request(endpoint="DataEngine.request", request=request) + + # Assert + assert self.data_engine.request_count == 1 + assert len(handler) == 1 + # assert handler[0].data == [trade_tick, trade_tick2] + assert catalog.query_last_timestamp( + TradeTick, + instrument_id=ETHUSDT_BINANCE.id, + ) == time_object_to_dt( + pd.Timestamp("2024-3-25"), + ) + # TODO: Implement with new Rust datafusion backend" # def test_request_quote_ticks_when_catalog_registered_using_rust(self) -> None: # # Arrange From 162276624f79d1d098910e9c479e179fd18740fb Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 27 Nov 2024 17:04:25 +1100 Subject: [PATCH 52/78] Update release notes --- RELEASES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASES.md b/RELEASES.md index d1d926faf43d..b99397799530 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -3,7 +3,9 @@ Released on TBD (UTC). ### Enhancements +- Implemented mixed catalog data requests with catalog update (#2043), thanks @faysou - Improved `Cache` behavior when adding more recent quotes, trades, or bars (now adds to cache) +- Added `metadata` parameter for data requests (#2043), thanks @faysou - Added `STOP_MARKET` and `STOP_LIMIT` order support for dYdX (#2066), thanks @davidsblom - Added `max_reconnection_tries` to data client config for dYdX (#2066), thanks @davidsblom From 37858fd28dd5730abef9a8cde58223d241e645c2 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 27 Nov 2024 17:27:39 +1100 Subject: [PATCH 53/78] Update dependencies and unpin libc crate --- nautilus_core/Cargo.lock | 59 ++++++++++++++------------------- nautilus_core/Cargo.toml | 1 - nautilus_core/common/Cargo.toml | 3 +- nautilus_core/model/Cargo.toml | 2 +- poetry.lock | 12 +++---- 5 files changed, 33 insertions(+), 44 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 029ce5038f22..2731e20e2c3e 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -976,7 +976,7 @@ checksum = "24f165e7b643266ea80cb858aed492ad9280e3e05ce24d4a99d7d7b889b6a4d9" dependencies = [ "strum", "strum_macros", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] @@ -2696,9 +2696,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.164" +version = "0.2.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" [[package]] name = "libloading" @@ -2976,7 +2976,6 @@ dependencies = [ "futures", "indexmap", "itertools 0.13.0", - "libc", "log", "nautilus-core", "nautilus-model", @@ -3579,13 +3578,13 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "papergrid" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7419ad52a7de9b60d33e11085a0fe3df1fbd5926aa3f93d3dd53afbc9e86725" +checksum = "d2b0f8def1f117e13c895f3eda65a7b5650688da29d6ad04635f61bc7b92eebd" dependencies = [ "bytecount", "fnv", - "unicode-width 0.1.11", + "unicode-width", ] [[package]] @@ -3869,27 +3868,25 @@ dependencies = [ ] [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ - "proc-macro-error-attr", "proc-macro2", "quote", - "syn 1.0.109", - "version_check", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ + "proc-macro-error-attr2", "proc-macro2", "quote", - "version_check", + "syn 2.0.89", ] [[package]] @@ -4373,9 +4370,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" dependencies = [ "const-oid", "digest", @@ -5206,9 +5203,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.32.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ae3f4f7d64646c46c4cae4e3f01d1c5d255c7406fdd7c7f999a94e488791" +checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" dependencies = [ "core-foundation-sys", "libc", @@ -5241,9 +5238,9 @@ dependencies = [ [[package]] name = "tabled" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c9303ee60b9bedf722012ea29ae3711ba13a67c9b9ae28993838b63057cb1b" +checksum = "c6709222f3973137427ce50559cd564dc187a95b9cfe01613d2f4e93610e510a" dependencies = [ "papergrid", "tabled_derive", @@ -5251,12 +5248,12 @@ dependencies = [ [[package]] name = "tabled_derive" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0fb8bfdc709786c154e24a66777493fb63ae97e3036d914c8666774c477069" +checksum = "931be476627d4c54070a1f3a9739ccbfec9b36b39815106a20cce2243bbcefe1" dependencies = [ "heck 0.4.1", - "proc-macro-error", + "proc-macro-error2", "proc-macro2", "quote", "syn 1.0.109", @@ -5608,9 +5605,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", @@ -5778,12 +5775,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unicode-width" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" - [[package]] name = "unicode-width" version = "0.2.0" diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 3b46f496ad5f..40abffb68349 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -46,7 +46,6 @@ indexmap = { version = "2.6.0", features = ["serde"] } itertools = "0.13.0" itoa = "1.0.14" once_cell = "1.20.2" -libc = "=0.2.164" # Pin due sysinfo issue: https://github.com/GuillaumeGomez/sysinfo/issues/1392 log = { version = "0.4.22", features = ["std", "kv_unstable", "serde", "release_max_level_debug"] } parquet = "53.2.0" # Keep in line with datafusion pyo3 = { version = "0.22.6", features = ["rust_decimal", "indexmap", "smallvec"] } diff --git a/nautilus_core/common/Cargo.toml b/nautilus_core/common/Cargo.toml index 6047b8a8c1aa..3ff9797067db 100644 --- a/nautilus_core/common/Cargo.toml +++ b/nautilus_core/common/Cargo.toml @@ -19,7 +19,6 @@ chrono = { workspace = true } futures = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } -libc = { workspace = true } log = { workspace = true } pyo3 = { workspace = true, optional = true } pyo3-async-runtimes = { workspace = true, optional = true } @@ -34,7 +33,7 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } ustr = { workspace = true } uuid = { workspace = true } -sysinfo = "0.32.0" +sysinfo = "0.32.1" [dev-dependencies] proptest = { workspace = true } diff --git a/nautilus_core/model/Cargo.toml b/nautilus_core/model/Cargo.toml index 9453f9ec81f3..852a9e887a0b 100644 --- a/nautilus_core/model/Cargo.toml +++ b/nautilus_core/model/Cargo.toml @@ -29,7 +29,7 @@ thousands = { workspace = true } ustr = { workspace = true } evalexpr = "12.0.1" implied-vol = { version = "1.0.0", features = ["normal-distribution"] } -tabled = "0.16.0" +tabled = "0.17.0" [dev-dependencies] nautilus-adapters = { path = "../adapters" } diff --git a/poetry.lock b/poetry.lock index 89a565c893ce..6d9a185e0bd1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2405,13 +2405,13 @@ xml = ["lxml (>=4.9.2)"] [[package]] name = "pandas-stubs" -version = "2.2.3.241009" +version = "2.2.3.241126" description = "Type annotations for pandas" optional = false python-versions = ">=3.10" files = [ - {file = "pandas_stubs-2.2.3.241009-py3-none-any.whl", hash = "sha256:3a6f8f142105a42550be677ba741ba532621f4e0acad2155c0e7b2450f114cfa"}, - {file = "pandas_stubs-2.2.3.241009.tar.gz", hash = "sha256:d4ab618253f0acf78a5d0d2bfd6dffdd92d91a56a69bdc8144e5a5c6d25be3b5"}, + {file = "pandas_stubs-2.2.3.241126-py3-none-any.whl", hash = "sha256:74aa79c167af374fe97068acc90776c0ebec5266a6e5c69fe11e9c2cf51f2267"}, + {file = "pandas_stubs-2.2.3.241126.tar.gz", hash = "sha256:cf819383c6d9ae7d4dabf34cd47e1e45525bb2f312e6ad2939c2c204cb708acd"}, ] [package.dependencies] @@ -2933,13 +2933,13 @@ files = [ [[package]] name = "pydantic" -version = "2.10.1" +version = "2.10.2" description = "Data validation using Python type hints" optional = true python-versions = ">=3.8" files = [ - {file = "pydantic-2.10.1-py3-none-any.whl", hash = "sha256:a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e"}, - {file = "pydantic-2.10.1.tar.gz", hash = "sha256:a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560"}, + {file = "pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e"}, + {file = "pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa"}, ] [package.dependencies] From 4bd9964d30e5eeb6cf987c99a4a63ce7b2cb83e7 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 27 Nov 2024 17:51:59 +1100 Subject: [PATCH 54/78] Refine actor params and docstrings --- nautilus_trader/common/actor.pxd | 6 +-- nautilus_trader/common/actor.pyx | 86 ++++++++++++++++++++++---------- 2 files changed, 63 insertions(+), 29 deletions(-) diff --git a/nautilus_trader/common/actor.pxd b/nautilus_trader/common/actor.pxd index 31c37b84471b..a2b08e936bde 100644 --- a/nautilus_trader/common/actor.pxd +++ b/nautilus_trader/common/actor.pxd @@ -216,8 +216,8 @@ cdef class Actor(Component): datetime end=*, ClientId client_id=*, callback=*, - bint update_catalog=*, str quote_type=*, + bint update_catalog=*, ) cpdef UUID4 request_trade_ticks( self, @@ -242,10 +242,10 @@ cdef class Actor(Component): list bar_types, datetime start=*, datetime end=*, - bint update_existing_subscriptions=*, - bint include_external_data=*, ClientId client_id=*, callback=*, + bint include_external_data=*, + bint update_existing_subscriptions=*, bint update_catalog=*, ) cpdef bint is_pending_request(self, UUID4 request_id) diff --git a/nautilus_trader/common/actor.pyx b/nautilus_trader/common/actor.pyx index 1bf3fe68a7af..f02890854455 100644 --- a/nautilus_trader/common/actor.pyx +++ b/nautilus_trader/common/actor.pyx @@ -1983,6 +1983,10 @@ cdef class Actor(Component): Raises ------ + ValueError + If `start` is not `None` and > current timestamp (now). + ValueError + If `end` is not `None` and > current timestamp (now). ValueError If `start` and `end` are not `None` and `start` is >= `end`. TypeError @@ -1991,7 +1995,8 @@ cdef class Actor(Component): """ Condition.is_true(self.trader_id is not None, "The actor has not been registered") Condition.not_none(instrument_id, "instrument_id") - now = self.clock.utc_now() + + cdef datetime now = self.clock.utc_now() if start is not None: Condition.is_true(start <= now, "start was > now") if end is not None: @@ -2058,6 +2063,10 @@ cdef class Actor(Component): Raises ------ + ValueError + If `start` is not `None` and > current timestamp (now). + ValueError + If `end` is not `None` and > current timestamp (now). ValueError If `start` and `end` are not `None` and `start` is >= `end`. TypeError @@ -2066,11 +2075,12 @@ cdef class Actor(Component): """ Condition.is_true(self.trader_id is not None, "The actor has not been registered") Condition.not_none(venue, "venue") - now = self.clock.utc_now() + + cdef datetime now = self.clock.utc_now() if start is not None: Condition.is_true(start <= now, "start was > now") if end is not None: - Condition.is_true(end <= now, "start was > now") + Condition.is_true(end <= now, "end was > now") if start is not None and end is not None: Condition.is_true(start < end, "start was >= end") Condition.callable_or_none(callback, "callback") @@ -2161,8 +2171,8 @@ cdef class Actor(Component): datetime end = None, ClientId client_id = None, callback: Callable[[UUID4], None] | None = None, - bint update_catalog = False, str quote_type = "", + bint update_catalog = False, ): """ Request historical `QuoteTick` data. @@ -2184,6 +2194,8 @@ cdef class Actor(Component): callback : Callable[[UUID4], None], optional The registered callback, to be called with the request ID when the response has completed processing. + quote_type : str, default '' + The specified quote type applicable to certain client implementations. update_catalog : bool, default False If True then updates the catalog with new data received from a client. @@ -2194,6 +2206,10 @@ cdef class Actor(Component): Raises ------ + ValueError + If `start` is not `None` and > current timestamp (now). + ValueError + If `end` is not `None` and > current timestamp (now). ValueError If `start` and `end` are not `None` and `start` is >= `end`. TypeError @@ -2202,11 +2218,12 @@ cdef class Actor(Component): """ Condition.is_true(self.trader_id is not None, "The actor has not been registered") Condition.not_none(instrument_id, "instrument_id") - now = self.clock.utc_now() + + cdef datetime now = self.clock.utc_now() if start is not None: Condition.is_true(start <= now, "start was > now") if end is not None: - Condition.is_true(end <= now, "start was > now") + Condition.is_true(end <= now, "end was > now") if start is not None and end is not None: Condition.is_true(start < end, "start was >= end") Condition.callable_or_none(callback, "callback") @@ -2271,6 +2288,10 @@ cdef class Actor(Component): Raises ------ + ValueError + If `start` is not `None` and > current timestamp (now). + ValueError + If `end` is not `None` and > current timestamp (now). ValueError If `start` and `end` are not `None` and `start` is >= `end`. TypeError @@ -2279,11 +2300,12 @@ cdef class Actor(Component): """ Condition.is_true(self.trader_id is not None, "The actor has not been registered") Condition.not_none(instrument_id, "instrument_id") - now = self.clock.utc_now() + + cdef datetime now = self.clock.utc_now() if start is not None: Condition.is_true(start <= now, "start was > now") if end is not None: - Condition.is_true(end <= now, "start was > now") + Condition.is_true(end <= now, "end was > now") if start is not None and end is not None: Condition.is_true(start < end, "start was >= end") Condition.callable_or_none(callback, "callback") @@ -2347,6 +2369,10 @@ cdef class Actor(Component): Raises ------ + ValueError + If `start` is not `None` and > current timestamp (now). + ValueError + If `end` is not `None` and > current timestamp (now). ValueError If `start` and `end` are not `None` and `start` is >= `end`. TypeError @@ -2355,11 +2381,12 @@ cdef class Actor(Component): """ Condition.is_true(self.trader_id is not None, "The actor has not been registered") Condition.not_none(bar_type, "bar_type") - now = self.clock.utc_now() + + cdef datetime now = self.clock.utc_now() if start is not None: Condition.is_true(start <= now, "start was > now") if end is not None: - Condition.is_true(end <= now, "start was > now") + Condition.is_true(end <= now, "end was > now") if start is not None and end is not None: Condition.is_true(start < end, "start was >= end") Condition.callable_or_none(callback, "callback") @@ -2385,15 +2412,15 @@ cdef class Actor(Component): return request_id cpdef UUID4 request_aggregated_bars( - self, - list bar_types, - datetime start = None, - datetime end = None, - bint update_existing_subscriptions = False, - bint include_external_data = False, - ClientId client_id = None, - callback: Callable[[UUID4], None] | None = None, - bint update_catalog = False, + self, + list bar_types, + datetime start = None, + datetime end = None, + ClientId client_id = None, + callback: Callable[[UUID4], None] | None = None, + bint include_external_data = False, + bint update_existing_subscriptions = False, + bint update_catalog = False, ): """ Request historical aggregated `Bar` data for multiple bar types. @@ -2414,16 +2441,16 @@ cdef class Actor(Component): end : datetime, optional The end datetime (UTC) of request time range. The inclusiveness depends on individual data client implementation. - update_existing_subscriptions : bool, default False - If True, updates the aggregators of any existing subscription with the queried external data. - include_external_data : bool, default False - If True, includes the queried external data in the response. client_id : ClientId, optional The specific client ID for the command. If ``None`` then will be inferred from the venue in the instrument ID. callback : Callable[[UUID4], None], optional The registered callback, to be called with the request ID when the response has completed processing. + include_external_data : bool, default False + If True, includes the queried external data in the response. + update_existing_subscriptions : bool, default False + If True, updates the aggregators of any existing subscription with the queried external data. update_catalog : bool, default False If True then updates the catalog with new data received from a client. @@ -2434,22 +2461,29 @@ cdef class Actor(Component): Raises ------ + ValueError + If `start` is not `None` and > current timestamp (now). + ValueError + If `end` is not `None` and > current timestamp (now). ValueError If `start` and `end` are not `None` and `start` is >= `end`. + ValueError If `bar_types` is empty. TypeError If `callback` is not `None` and not of type `Callable`. + TypeError If `bar_types` is empty or contains elements not of type `BarType`. """ Condition.is_true(self.trader_id is not None, "The actor has not been registered") Condition.not_empty(bar_types, "bar_types") Condition.list_type(bar_types, BarType, "bar_types") - now = self.clock.utc_now() + + cdef datetime now = self.clock.utc_now() if start is not None: Condition.is_true(start <= now, "start was > now") if end is not None: - Condition.is_true(end <= now, "start was > now") + Condition.is_true(end <= now, "end was > now") if start is not None and end is not None: Condition.is_true(start < end, "start was >= end") Condition.callable_or_none(callback, "callback") @@ -2480,8 +2514,8 @@ cdef class Actor(Component): "bar_type": first.composite(), "start": start, "end": end, - "update_existing_subscriptions": update_existing_subscriptions, "include_external_data": include_external_data, + "update_existing_subscriptions": update_existing_subscriptions, "update_catalog": update_catalog, }), callback=self._handle_aggregated_bars_response, From 7ac3b4472ad03fbe0b792daefdcf3cd070aa66e6 Mon Sep 17 00:00:00 2001 From: sunlei Date: Thu, 28 Nov 2024 03:26:57 +0800 Subject: [PATCH 55/78] Add wallet subscription for Bybit (#2076) --- nautilus_trader/adapters/bybit/data.py | 2 +- nautilus_trader/adapters/bybit/execution.py | 26 ++++++++--- .../adapters/bybit/schemas/common.py | 4 +- nautilus_trader/adapters/bybit/schemas/ws.py | 45 ++++++++++++++++++- .../adapters/bybit/websocket/client.py | 9 ++++ .../ws_messages/private/ws_wallet.json | 11 +++-- .../adapters/bybit/test_ws_decoders.py | 3 ++ 7 files changed, 85 insertions(+), 15 deletions(-) diff --git a/nautilus_trader/adapters/bybit/data.py b/nautilus_trader/adapters/bybit/data.py index 44719549d5be..8bff21c6184e 100644 --- a/nautilus_trader/adapters/bybit/data.py +++ b/nautilus_trader/adapters/bybit/data.py @@ -35,8 +35,8 @@ from nautilus_trader.adapters.bybit.http.client import BybitHttpClient from nautilus_trader.adapters.bybit.http.market import BybitMarketHttpAPI from nautilus_trader.adapters.bybit.providers import BybitInstrumentProvider +from nautilus_trader.adapters.bybit.schemas.common import BYBIT_PONG from nautilus_trader.adapters.bybit.schemas.market.ticker import BybitTickerData -from nautilus_trader.adapters.bybit.schemas.ws import BYBIT_PONG from nautilus_trader.adapters.bybit.schemas.ws import BybitWsMessageGeneral from nautilus_trader.adapters.bybit.schemas.ws import BybitWsTickerLinearMsg from nautilus_trader.adapters.bybit.schemas.ws import decoder_ws_kline diff --git a/nautilus_trader/adapters/bybit/execution.py b/nautilus_trader/adapters/bybit/execution.py index 3f010cef46d2..be95fd1b15b3 100644 --- a/nautilus_trader/adapters/bybit/execution.py +++ b/nautilus_trader/adapters/bybit/execution.py @@ -39,12 +39,13 @@ from nautilus_trader.adapters.bybit.http.errors import BybitError from nautilus_trader.adapters.bybit.http.errors import should_retry from nautilus_trader.adapters.bybit.providers import BybitInstrumentProvider +from nautilus_trader.adapters.bybit.schemas.common import BYBIT_PONG from nautilus_trader.adapters.bybit.schemas.common import BybitWsSubscriptionMsg -from nautilus_trader.adapters.bybit.schemas.ws import BYBIT_PONG from nautilus_trader.adapters.bybit.schemas.ws import BybitWsAccountExecution from nautilus_trader.adapters.bybit.schemas.ws import BybitWsAccountExecutionMsg from nautilus_trader.adapters.bybit.schemas.ws import BybitWsAccountOrderMsg from nautilus_trader.adapters.bybit.schemas.ws import BybitWsAccountPositionMsg +from nautilus_trader.adapters.bybit.schemas.ws import BybitWsAccountWalletMsg from nautilus_trader.adapters.bybit.schemas.ws import BybitWsMessageGeneral from nautilus_trader.adapters.bybit.websocket.client import BybitWebSocketClient from nautilus_trader.cache.cache import Cache @@ -202,13 +203,11 @@ def __init__( # Decoders self._decoder_ws_msg_general = msgspec.json.Decoder(BybitWsMessageGeneral) self._decoder_ws_subscription = msgspec.json.Decoder(BybitWsSubscriptionMsg) + self._decoder_ws_account_order_update = msgspec.json.Decoder(BybitWsAccountOrderMsg) - self._decoder_ws_account_execution_update = msgspec.json.Decoder( - BybitWsAccountExecutionMsg, - ) - self._decoder_ws_account_position_update = msgspec.json.Decoder( - BybitWsAccountPositionMsg, - ) + self._decoder_ws_account_execution_update = msgspec.json.Decoder(BybitWsAccountExecutionMsg) + self._decoder_ws_account_position_update = msgspec.json.Decoder(BybitWsAccountPositionMsg) + self._decoder_ws_account_wallet_update = msgspec.json.Decoder(BybitWsAccountWalletMsg) # Hot caches self._instrument_ids: dict[str, InstrumentId] = {} @@ -230,6 +229,7 @@ async def _connect(self) -> None: await self._ws_client.connect() await self._ws_client.subscribe_executions_update() await self._ws_client.subscribe_orders_update() + await self._ws_client.subscribe_wallet_update() async def _disconnect(self) -> None: await self._ws_client.disconnect() @@ -941,6 +941,8 @@ def _handle_ws_message(self, raw: bytes) -> None: self._handle_account_order_update(raw) elif "execution" in ws_message.topic: self._handle_account_execution_update(raw) + elif "wallet" == ws_message.topic: + self._handle_account_wallet_update(raw) else: self._log.error(f"Unknown websocket message topic: {ws_message.topic}") except Exception as e: @@ -1158,6 +1160,16 @@ def _handle_account_order_update(self, raw: bytes) -> None: # noqa: C901 (too c except Exception as e: self._log.error(repr(e)) + def _handle_account_wallet_update(self, raw: bytes) -> None: + try: + self._process_wallet_update(raw) + except Exception as e: + self._log.exception(f"Failed to handle account wallet update: {e}", e) + + def _process_wallet_update(self, raw: bytes) -> None: + msg: BybitWsAccountWalletMsg = self._decoder_ws_account_wallet_update.decode(raw) + msg.handle_account_wallet_update(self) + def _create_market_batch_order( self, order: MarketOrder, diff --git a/nautilus_trader/adapters/bybit/schemas/common.py b/nautilus_trader/adapters/bybit/schemas/common.py index b32064d651a5..eb8c423136e8 100644 --- a/nautilus_trader/adapters/bybit/schemas/common.py +++ b/nautilus_trader/adapters/bybit/schemas/common.py @@ -13,13 +13,15 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from typing import Any, Generic, TypeVar +from typing import Any, Final, Generic, TypeVar import msgspec T = TypeVar("T") +BYBIT_PONG: Final[str] = "pong" + class BybitListResult(Generic[T], msgspec.Struct): list: list[T] diff --git a/nautilus_trader/adapters/bybit/schemas/ws.py b/nautilus_trader/adapters/bybit/schemas/ws.py index 082883d2c193..ff687d10c3ee 100644 --- a/nautilus_trader/adapters/bybit/schemas/ws.py +++ b/nautilus_trader/adapters/bybit/schemas/ws.py @@ -13,8 +13,10 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from __future__ import annotations + from decimal import Decimal -from typing import Final +from typing import TYPE_CHECKING import msgspec @@ -50,11 +52,16 @@ from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.identifiers import TradeId from nautilus_trader.model.identifiers import VenueOrderId +from nautilus_trader.model.objects import AccountBalance +from nautilus_trader.model.objects import Currency +from nautilus_trader.model.objects import MarginBalance +from nautilus_trader.model.objects import Money from nautilus_trader.model.objects import Price from nautilus_trader.model.objects import Quantity -BYBIT_PONG: Final[str] = "pong" +if TYPE_CHECKING: + from nautilus_trader.adapters.bybit.execution import BybitExecutionClient class BybitWsMessageGeneral(msgspec.Struct): @@ -694,6 +701,25 @@ class BybitWsAccountWalletCoin(msgspec.Struct): collateralSwitch: bool marginCollateral: bool locked: str + spotHedgingQty: str + + def parse_to_account_balance(self) -> AccountBalance: + currency = Currency.from_str(self.coin) + total = Decimal(self.walletBalance) + locked = Decimal(self.locked) # TODO: Locked only valid for Spot + free = total - locked + return AccountBalance( + total=Money(total, currency), + locked=Money(locked, currency), + free=Money(free, currency), + ) + + def parse_to_margin_balance(self) -> MarginBalance: + currency: Currency = Currency.from_str(self.coin) + return MarginBalance( + initial=Money(Decimal(self.totalPositionIM), currency), + maintenance=Money(Decimal(self.totalPositionMM), currency), + ) class BybitWsAccountWallet(msgspec.Struct): @@ -710,9 +736,24 @@ class BybitWsAccountWallet(msgspec.Struct): accountLTV: str accountType: str + def parse_to_account_balance(self) -> list[AccountBalance]: + return [coin.parse_to_account_balance() for coin in self.coin] + + def parse_to_margin_balance(self) -> list[MarginBalance]: + return [coin.parse_to_margin_balance() for coin in self.coin] + class BybitWsAccountWalletMsg(msgspec.Struct): topic: str id: str creationTime: int data: list[BybitWsAccountWallet] + + def handle_account_wallet_update(self, exec_client: BybitExecutionClient): + for wallet in self.data: + exec_client.generate_account_state( + balances=wallet.parse_to_account_balance(), + margins=wallet.parse_to_margin_balance(), + reported=True, + ts_event=millis_to_nanos(self.creationTime), + ) diff --git a/nautilus_trader/adapters/bybit/websocket/client.py b/nautilus_trader/adapters/bybit/websocket/client.py index e1b7533dd2ec..dad5725e002f 100644 --- a/nautilus_trader/adapters/bybit/websocket/client.py +++ b/nautilus_trader/adapters/bybit/websocket/client.py @@ -253,6 +253,15 @@ async def subscribe_executions_update(self) -> None: msg = {"op": "subscribe", "args": [subscription]} await self._send(msg) + async def subscribe_wallet_update(self) -> None: + subscription = "wallet" + if subscription in self._subscriptions: + return + + self._subscriptions.append(subscription) + msg = {"op": "subscribe", "args": [subscription]} + await self._send(msg) + def _get_signature(self): expires = self._clock.timestamp_ms() + 5_000 sign = f"GET/realtime{expires}" diff --git a/tests/integration_tests/adapters/bybit/resources/ws_messages/private/ws_wallet.json b/tests/integration_tests/adapters/bybit/resources/ws_messages/private/ws_wallet.json index 37b067d05b3e..884978326db4 100644 --- a/tests/integration_tests/adapters/bybit/resources/ws_messages/private/ws_wallet.json +++ b/tests/integration_tests/adapters/bybit/resources/ws_messages/private/ws_wallet.json @@ -31,7 +31,8 @@ "bonus": "0", "collateralSwitch": true, "marginCollateral": true, - "locked": "0" + "locked": "0", + "spotHedgingQty": "0" }, { "coin": "BTC", @@ -50,7 +51,8 @@ "bonus": "0", "collateralSwitch": false, "marginCollateral": true, - "locked": "0" + "locked": "0", + "spotHedgingQty": "0" }, { "coin": "USDT", @@ -69,11 +71,12 @@ "bonus": "0", "collateralSwitch": true, "marginCollateral": true, - "locked": "0" + "locked": "0", + "spotHedgingQty": "0" } ], "accountLTV": "0", "accountType": "UNIFIED" } ] -} \ No newline at end of file +} diff --git a/tests/integration_tests/adapters/bybit/test_ws_decoders.py b/tests/integration_tests/adapters/bybit/test_ws_decoders.py index 690fa8d7a4d5..019ea3917f14 100644 --- a/tests/integration_tests/adapters/bybit/test_ws_decoders.py +++ b/tests/integration_tests/adapters/bybit/test_ws_decoders.py @@ -618,6 +618,7 @@ def test_ws_private_wallet(self): collateralSwitch=True, marginCollateral=True, locked="0", + spotHedgingQty="0", ) coin_btc = BybitWsAccountWalletCoin( coin="BTC", @@ -637,6 +638,7 @@ def test_ws_private_wallet(self): collateralSwitch=False, marginCollateral=True, locked="0", + spotHedgingQty="0", ) coin_usdt = BybitWsAccountWalletCoin( coin="USDT", @@ -656,6 +658,7 @@ def test_ws_private_wallet(self): collateralSwitch=True, marginCollateral=True, locked="0", + spotHedgingQty="0", ) wallet_data = BybitWsAccountWallet( accountIMRate="0.4782", From 1a5c7113c46102383a6da554b0cbbc30030e233f Mon Sep 17 00:00:00 2001 From: Javaid <105105941+rsmb7z@users.noreply.github.com> Date: Wed, 27 Nov 2024 22:45:43 +0300 Subject: [PATCH 56/78] Support for Databento symbology (#2073) --- docs/integrations/ib.md | 29 +++++-- .../interactive_brokers/connect_with_tws.py | 2 + .../adapters/interactive_brokers/config.py | 27 ++++-- .../adapters/interactive_brokers/execution.py | 8 +- .../parsing/instruments.py | 87 ++++++++++++++++--- .../adapters/interactive_brokers/providers.py | 50 ++++++++--- .../interactive_brokers/test_parsing.py | 73 ++++++++++++++-- 7 files changed, 229 insertions(+), 47 deletions(-) diff --git a/docs/integrations/ib.md b/docs/integrations/ib.md index 9e21bb4b7d2e..3c6571d91dab 100644 --- a/docs/integrations/ib.md +++ b/docs/integrations/ib.md @@ -101,11 +101,11 @@ To troubleshoot TWS API incoming message issues, consider starting at the `Inter ## Symbology -The InteractiveBrokersInstrumentProvider supports two methods for constructing InstrumentId instances, which can be configured via the strict_symbology flag in InteractiveBrokersInstrumentProviderConfig. +The InteractiveBrokersInstrumentProvider supports three methods for constructing InstrumentId instances, which can be configured via the `symbology_method` enum in `InteractiveBrokersInstrumentProviderConfig`. ### Simplified Symbology -When strict_symbology is set to False (the default setting), the system utilizes the following parsing rules for symbology: +When symbology_method is set to `IB_SIMPLIFIED` (the default setting), the system utilizes the following parsing rules for symbology: - Forex: The format is `{symbol}/{currency}.{exchange}`, where the currency pair is constructed as `EUR/USD.IDEALPRO`. - Stocks: The format is `{localSymbol}.{primaryExchange}`. Any spaces in localSymbol are replaced with -, e.g., `BF-B.NYSE`. @@ -113,15 +113,22 @@ When strict_symbology is set to False (the default setting), the system utilizes - Options: The format is `{localSymbol}.{exchange}`, with all spaces removed from localSymbol, e.g., `AAPL230217P00155000.SMART`. - Index: The format is `^{localSymbol}.{exchange}`, e.g., `^SPX.CBOE`. -### Strict Symbology +### Raw Symbology -Setting strict_symbology to True enforces stricter parsing rules that align directly with the fields defined in the ibapi. The format for each security type is as follows: +Setting symbology_method to `IB_RAW` enforces stricter parsing rules that align directly with the fields defined in the ibapi. The format for each security type is as follows: - CFDs: `{localSymbol}={secType}.IBCFD` - Commodities: `{localSymbol}={secType}.IBCMDTY` - Default for Other Types: `{localSymbol}={secType}.{exchange}` This configuration ensures that the symbology is explicitly defined and matched with the Interactive Brokers API requirements, providing clear and consistent instrument identification. +While this format may lack visual clarity, it is robust and supports instruments from any region, +especially those with non-standard symbology where simplified parsing may fail. + +### Databento Symbology + +Setting symbology_method to `DATABENTO`, the system utilized the symbology rules defined by `DatabentoInstrumentProvider`. +Note that this symbology is only compatible with venues supported by Databento and there is not automatic fall-back to other symbology methods to avoid any conflicts. ## Instruments & Contracts @@ -129,7 +136,7 @@ In IB, a NautilusTrader `Instrument` is equivalent to a [Contract](https://ibkrc To search for contract information, use the [IB Contract Information Center](https://pennies.interactivebrokers.com/cstools/contract_info/). -It's typically suggested to utilize `strict_symbology=False` (which is the default setting). This provides a cleaner and more intuitive use of `InstrumentId` by employing `load_ids` in the `InteractiveBrokersInstrumentProviderConfig`, following the guidelines established in the Simplified Symbology section. +It's typically suggested to utilize `symbology_method=SymbologyMethod.IB_SIMPLIFIED` (which is the default setting). This provides a cleaner and more intuitive use of `InstrumentId` by employing `load_ids` in the `InteractiveBrokersInstrumentProviderConfig`, following the guidelines established in the Simplified Symbology section. In order to load multiple Instruments, such as Options Instrument without having to specify each strike explicitly, you would need to utilize `load_contracts` with provided instances of `IBContract`. ```python @@ -232,9 +239,11 @@ Additionally, this provider offers specialized methods to build and retrieve the ```python from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersInstrumentProviderConfig +from nautilus_trader.adapters.interactive_brokers.config import SymbologyMethod instrument_provider_config = InteractiveBrokersInstrumentProviderConfig( + symbology_method=SymbologyMethod.IB_SIMPLIFIED, build_futures_chain=False, # Set to True if fetching futures build_options_chain=False, # Set to True if fetching options min_expiry_days=10, # Relevant for futures/options with expiration @@ -259,6 +268,16 @@ instrument_provider_config = InteractiveBrokersInstrumentProviderConfig( ) ``` +### Integration with Databento Data Client +To integrate with `DatabentoDataClient`, set the `symbology_method` in `InteractiveBrokersInstrumentProviderConfig` +to `SymbologyMethod.DATABENTO`. This ensures seamless compatibility with Databento symbology, eliminating the need +for manual translations or mappings within your strategy. + +When using this configuration: +- `InteractiveBrokersInstrumentProvider` will not publish instruments to the cache to prevent conflicts. +- Instruments Cache management must be handled exclusively by `DatabentoDataClient`. + + ### Data Client `InteractiveBrokersDataClient` interfaces with IB for streaming and retrieving market data. Upon diff --git a/examples/live/interactive_brokers/connect_with_tws.py b/examples/live/interactive_brokers/connect_with_tws.py index bbce83fc5473..22cd05a26ff1 100644 --- a/examples/live/interactive_brokers/connect_with_tws.py +++ b/examples/live/interactive_brokers/connect_with_tws.py @@ -20,6 +20,7 @@ from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersDataClientConfig from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersExecClientConfig from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersInstrumentProviderConfig +from nautilus_trader.adapters.interactive_brokers.config import SymbologyMethod from nautilus_trader.adapters.interactive_brokers.factories import InteractiveBrokersLiveDataClientFactory from nautilus_trader.adapters.interactive_brokers.factories import InteractiveBrokersLiveExecClientFactory from nautilus_trader.config import LiveDataEngineConfig @@ -42,6 +43,7 @@ instrument_provider = InteractiveBrokersInstrumentProviderConfig( + symbology_method=SymbologyMethod.IB_SIMPLIFIED, load_ids=frozenset( [ "EUR/USD.IDEALPRO", diff --git a/nautilus_trader/adapters/interactive_brokers/config.py b/nautilus_trader/adapters/interactive_brokers/config.py index 9d5c35222983..b7b01f83f555 100644 --- a/nautilus_trader/adapters/interactive_brokers/config.py +++ b/nautilus_trader/adapters/interactive_brokers/config.py @@ -15,6 +15,7 @@ from __future__ import annotations +from enum import Enum from typing import Literal from ibapi.common import MarketDataTypeEnum as IBMarketDataTypeEnum @@ -26,6 +27,12 @@ from nautilus_trader.config import NautilusConfig +class SymbologyMethod(Enum): + DATABENTO = "databento" + IB_SIMPLIFIED = "simplified" + IB_RAW = "raw" + + class DockerizedIBGatewayConfig(NautilusConfig, frozen=True): """ Configuration for `DockerizedIBGateway` setup when working with containerized @@ -93,12 +100,18 @@ class InteractiveBrokersInstrumentProviderConfig(InstrumentProviderConfig, froze correspond to the instruments that the provider preloads. It's important to note that while the `load_ids` option can be used for loading individual instruments, using `load_contracts` allows for a more versatile loading of several related instruments like Futures and Options that share the same underlying asset. - strict_symbology : bool, optional - Determines the symbology format used for identifying instruments. If set to True, - a strict symbology format is used, as provided by InteractiveBrokers where instrument symbols - are detailed in the format `localSymbol=secType.exchange` (e.g., `EUR.USD=CASH.IDEALPRO`). - If False, a simplified symbology format is applied, using a notation like `EUR/USD.IDEALPRO`. - The default value is False, favoring simplified symbology unless specified otherwise. + symbology_method : SymbologyMethod, optional + Specifies the symbology format used for identifying financial instruments. The available options are: + - IB_RAW: Uses the raw symbology format provided by Interactive Brokers. Instrument symbols follow a detailed + format such as `localSymbol=secType.exchange` (e.g., `EUR.USD=CASH.IDEALPRO`). + While this format may lack visual clarity, it is robust and supports instruments from any region, + especially those with non-standard symbology where simplified parsing may fail. + - IB_SIMPLIFIED: Adopts a simplified symbology format specific to Interactive Brokers which uses Venue acronym. + Instrument symbols use a cleaner notation, such as `ESZ28.CME` or `EUR/USD.IDEALPRO`. + This format prioritizes ease of readability and usability and is default. + - DATABENTO: Utilizes the symbology format defined by the Databento adapter, ensuring seamless integration with + `DatabentoDataClient` when used alongside `InteractiveBrokersExecClientConfig`. Example notation includes + `ESZ8.GLBX`. Note that this symbology is only compatible with venues supported by Databento. build_options_chain: bool (default: None) Search for full option chain. Global setting for all applicable instruments. build_futures_chain: bool (default: None) @@ -142,7 +155,7 @@ def __hash__(self) -> int: ), ) - strict_symbology: bool = False + symbology_method: SymbologyMethod = SymbologyMethod.IB_SIMPLIFIED load_contracts: frozenset[IBContract] | None = None build_options_chain: bool | None = None build_futures_chain: bool | None = None diff --git a/nautilus_trader/adapters/interactive_brokers/execution.py b/nautilus_trader/adapters/interactive_brokers/execution.py index cf0fea5f8edb..890a83108631 100644 --- a/nautilus_trader/adapters/interactive_brokers/execution.py +++ b/nautilus_trader/adapters/interactive_brokers/execution.py @@ -32,6 +32,7 @@ from nautilus_trader.adapters.interactive_brokers.common import IB_VENUE from nautilus_trader.adapters.interactive_brokers.common import IBOrderTags from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersExecClientConfig +from nautilus_trader.adapters.interactive_brokers.config import SymbologyMethod from nautilus_trader.adapters.interactive_brokers.parsing.execution import MAP_ORDER_ACTION from nautilus_trader.adapters.interactive_brokers.parsing.execution import MAP_ORDER_FIELDS from nautilus_trader.adapters.interactive_brokers.parsing.execution import MAP_ORDER_STATUS @@ -502,7 +503,10 @@ async def generate_position_status_reports( ) continue - if not self._cache.instrument(instrument.id): + if ( + self.instrument_provider.config.symbology_method != SymbologyMethod.DATABENTO + and not self._cache.instrument(instrument.id) + ): self._handle_data(instrument) position_status = PositionStatusReport( @@ -532,7 +536,7 @@ def _transform_order_to_ib_order(self, order: Order) -> IBOrder: # noqa: C901 1 else: setattr(ib_order, field, fn(value)) - if self._cache.instrument(order.instrument_id).is_inverse: + if self.instrument_provider.find(order.instrument_id).is_inverse: ib_order.cashQty = int(ib_order.totalQuantity) ib_order.totalQuantity = 0 diff --git a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py index b061656bdfdc..8ea058ec7cd7 100644 --- a/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py +++ b/nautilus_trader/adapters/interactive_brokers/parsing/instruments.py @@ -24,6 +24,7 @@ # fmt: off from nautilus_trader.adapters.interactive_brokers.common import IBContract from nautilus_trader.adapters.interactive_brokers.common import IBContractDetails +from nautilus_trader.adapters.interactive_brokers.config import SymbologyMethod from nautilus_trader.core.correctness import PyCondition from nautilus_trader.model.enums import AssetClass from nautilus_trader.model.enums import OptionKind @@ -86,7 +87,7 @@ RE_CASH = re.compile(r"^(?P[A-Z]{3})\/(?P[A-Z]{3})$") RE_CFD_CASH = re.compile(r"^(?P[A-Z]{3})\.(?P[A-Z]{3})$") RE_OPT = re.compile( - r"^(?P^[A-Z]{1,6})(?P\d{6})(?P[CP])(?P\d{5})(?P\d{3})$", + r"^(?P^[A-Z. ]{1,6})(?P\d{6})(?P[CP])(?P\d{5})(?P\d{3})$", ) RE_FUT_UNDERLYING = re.compile(r"^(?P\w{1,3})$") RE_FUT = re.compile(r"^(?P\w{1,3})(?P[FGHJKMNQUVXZ])(?P\d{2})$") @@ -101,7 +102,7 @@ r"^(?P\w{1,3})(?P[FGHJKMNQUVXZ])(?P\d{2})(?P[CP])(?P.{4,5})$", ) RE_FOP_ORIGINAL = re.compile( - r"^(?P\w{1,3})(?P[FGHJKMNQUVXZ])(?P\d) (?P[CP])(?P.{4,5})$", + r"^(?P\w{1,3})(?P[FGHJKMNQUVXZ])(?P\d)\s(?P[CP])(?P\d{1,4}(?:\.\d)?)$", ) RE_CRYPTO = re.compile(r"^(?P[A-Z]*)\/(?P[A-Z]{3})$") @@ -139,12 +140,14 @@ def contract_details_to_ib_contract_details(details: ContractDetails) -> IBContr def parse_instrument( contract_details: IBContractDetails, - strict_symbology: bool = False, + symbology_method: SymbologyMethod = SymbologyMethod.IB_SIMPLIFIED, + databento_venue: str | None = None, ) -> Instrument: security_type = contract_details.contract.secType instrument_id = ib_contract_to_instrument_id( contract=contract_details.contract, - strict_symbology=strict_symbology, + symbology_method=symbology_method, + databento_venue=databento_venue, ) if security_type == "STK": return parse_equity_contract(details=contract_details, instrument_id=instrument_id) @@ -466,17 +469,23 @@ def decade_digit(last_digit: str, contract: IBContract) -> int: def ib_contract_to_instrument_id( contract: IBContract, - strict_symbology: bool = False, + symbology_method: SymbologyMethod = SymbologyMethod.IB_SIMPLIFIED, + databento_venue: str | None = None, ) -> InstrumentId: PyCondition.type(contract, IBContract, "IBContract") - if strict_symbology: - return ib_contract_to_instrument_id_strict_symbology(contract) - else: + if symbology_method == SymbologyMethod.DATABENTO: + assert databento_venue is not None + return InstrumentId.from_str(f"{contract.localSymbol}.{databento_venue}") + elif symbology_method == SymbologyMethod.IB_SIMPLIFIED: return ib_contract_to_instrument_id_simplified_symbology(contract) + elif symbology_method == SymbologyMethod.IB_RAW: + return ib_contract_to_instrument_id_raw_symbology(contract) + else: + raise NotImplementedError(f"{symbology_method} not implemented") -def ib_contract_to_instrument_id_strict_symbology(contract: IBContract) -> InstrumentId: +def ib_contract_to_instrument_id_raw_symbology(contract: IBContract) -> InstrumentId: if contract.secType == "CFD": symbol = f"{contract.localSymbol}={contract.secType}" venue = "IBCFD" @@ -539,17 +548,25 @@ def ib_contract_to_instrument_id_simplified_symbology( # noqa: C901 (too comple def instrument_id_to_ib_contract( instrument_id: InstrumentId, - strict_symbology: bool = False, + symbology_method: SymbologyMethod = SymbologyMethod.IB_SIMPLIFIED, + exchange: str | None = None, ) -> IBContract: PyCondition.type(instrument_id, InstrumentId, "InstrumentId") - if strict_symbology: - return instrument_id_to_ib_contract_strict_symbology(instrument_id) - else: + if symbology_method == SymbologyMethod.DATABENTO: + return instrument_id_to_ib_contract_databento_symbology( + instrument_id, + exchange=exchange or "SMART", + ) + elif symbology_method == SymbologyMethod.IB_SIMPLIFIED: return instrument_id_to_ib_contract_simplified_symbology(instrument_id) + elif symbology_method == SymbologyMethod.IB_RAW: + return instrument_id_to_ib_contract_raw_symbology(instrument_id) + else: + raise NotImplementedError(f"{symbology_method} not implemented") -def instrument_id_to_ib_contract_strict_symbology(instrument_id: InstrumentId) -> IBContract: +def instrument_id_to_ib_contract_raw_symbology(instrument_id: InstrumentId) -> IBContract: local_symbol, security_type = instrument_id.symbol.value.rsplit("=", 1) exchange = instrument_id.venue.value.replace("/", ".") if security_type == "STK": @@ -667,3 +684,45 @@ def instrument_id_to_ib_contract_simplified_symbology( # noqa: C901 (too comple primaryExchange=instrument_id.venue.value, localSymbol=f"{instrument_id.symbol.value}".replace("-", " "), ) + + +def instrument_id_to_ib_contract_databento_symbology( + instrument_id: InstrumentId, + exchange: str, +) -> IBContract: + if instrument_id.venue.value == "GLBX": + assert exchange is not None + if RE_FUT.match(instrument_id.symbol.value) or RE_FUT_ORIGINAL.match( + instrument_id.symbol.value, + ): + return IBContract( + secType="FUT", + exchange=exchange, + localSymbol=instrument_id.symbol.value, + ) + elif RE_FOP.match(instrument_id.symbol.value) or RE_FOP_ORIGINAL.match( + instrument_id.symbol.value, + ): + return IBContract( + secType="FOP", + exchange=exchange, + localSymbol=instrument_id.symbol.value, + ) + else: + raise ValueError(f"Cannot parse ib_contract for {instrument_id}") + elif instrument_id.venue.value == "IFEU": + raise ValueError(f"Cannot parse ib_contract for {instrument_id}") + else: + if RE_OPT.match(instrument_id.symbol.value): + return IBContract( + secType="OPT", + exchange=exchange, + localSymbol=instrument_id.symbol.value, + ) + else: + return IBContract( + secType="STK", + exchange=exchange, + localSymbol=instrument_id.symbol.value, + currency="USD", + ) diff --git a/nautilus_trader/adapters/interactive_brokers/providers.py b/nautilus_trader/adapters/interactive_brokers/providers.py index 113d6407a999..99be23ddb8e9 100644 --- a/nautilus_trader/adapters/interactive_brokers/providers.py +++ b/nautilus_trader/adapters/interactive_brokers/providers.py @@ -23,6 +23,7 @@ from nautilus_trader.adapters.interactive_brokers.common import IBContract from nautilus_trader.adapters.interactive_brokers.common import IBContractDetails from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersInstrumentProviderConfig +from nautilus_trader.adapters.interactive_brokers.config import SymbologyMethod from nautilus_trader.adapters.interactive_brokers.parsing.instruments import instrument_id_to_ib_contract from nautilus_trader.adapters.interactive_brokers.parsing.instruments import parse_instrument from nautilus_trader.common.providers import InstrumentProvider @@ -113,7 +114,7 @@ async def get_contract_details( try: details = await self._client.get_contract_details(contract=contract) if not details: - self._log.error(f"No contract details returned for {contract}") + self._log.debug(f"No contract details returned for {contract}") return [] [qualified] = details self._log.info( @@ -123,6 +124,8 @@ async def get_contract_details( ) self._log.debug(f"Got {details=}") except ValueError as e: + for d in details: + print(d.__dict__) self._log.error(f"No contract details found for the given kwargs {contract}, {e}") return [] min_expiry = pd.Timestamp.now() + pd.Timedelta( @@ -257,27 +260,52 @@ async def load_async( Not applicable in this case. """ + databento_venue = None if isinstance(instrument_id, InstrumentId): + databento_venue = ( + str(instrument_id.venue) + if self.config.symbology_method == SymbologyMethod.DATABENTO + else None + ) try: contract = instrument_id_to_ib_contract( instrument_id=instrument_id, - strict_symbology=self.config.strict_symbology, + symbology_method=self.config.symbology_method, ) except ValueError as e: self._log.error( - f"{self.config.strict_symbology=} failed to parse {instrument_id=}, {e}", + f"{self.config.symbology_method=} failed to parse {instrument_id=}, {e}", ) return elif isinstance(instrument_id, IBContract): + assert self.config.symbology_method != SymbologyMethod.DATABENTO contract = instrument_id else: self._log.error(f"Expected InstrumentId or IBContract, received {instrument_id}") return - self._log.debug(f"Attempting to find instrument for {contract=}") - contract_details: list[ContractDetails] - if not (contract_details := await self.get_contract_details(contract)): - return + self._log.info(f"Attempting to find instrument for {contract=}") + if databento_venue == "GLBX": + for exchange in ["CME", "CBOT", "NYMEX", "NYBOT"]: + contract = instrument_id_to_ib_contract( + instrument_id=instrument_id, + symbology_method=self.config.symbology_method, + exchange=exchange, + ) + contract_details = await self.get_contract_details(contract) + if contract_details: + break + else: + contract_details = await self.get_contract_details(contract) + + if contract_details: + await self._process_contract_details(contract_details, databento_venue=databento_venue) + + async def _process_contract_details( + self, + contract_details: list[ContractDetails], + databento_venue: str | None = None, + ) -> None: for details in copy.deepcopy(contract_details): details.contract = IBContract(**details.contract.__dict__) details = IBContractDetails(**details.__dict__) @@ -285,10 +313,11 @@ async def load_async( try: instrument: Instrument = parse_instrument( contract_details=details, - strict_symbology=self.config.strict_symbology, + symbology_method=self.config.symbology_method, + databento_venue=databento_venue, ) except ValueError as e: - self._log.error(f"{self.config.strict_symbology=} failed to parse {details=}, {e}") + self._log.error(f"{self.config.symbology_method=} failed to parse {details=}, {e}") continue if self.config.filter_callable is not None: filter_callable = resolve_path(self.config.filter_callable) @@ -296,7 +325,8 @@ async def load_async( continue self._log.info(f"Adding {instrument=} from InteractiveBrokersInstrumentProvider") self.add(instrument) - self._client._cache.add_instrument(instrument) + if self.config.symbology_method != SymbologyMethod.DATABENTO: + self._client._cache.add_instrument(instrument) self.contract_details[instrument.id.value] = details self.contract_id_to_instrument_id[details.contract.conId] = instrument.id diff --git a/tests/integration_tests/adapters/interactive_brokers/test_parsing.py b/tests/integration_tests/adapters/interactive_brokers/test_parsing.py index 1afb786da401..c8dbba31b17b 100644 --- a/tests/integration_tests/adapters/interactive_brokers/test_parsing.py +++ b/tests/integration_tests/adapters/interactive_brokers/test_parsing.py @@ -22,6 +22,7 @@ # fmt: off from nautilus_trader.adapters.interactive_brokers.common import IBContract from nautilus_trader.adapters.interactive_brokers.common import IBContractDetails +from nautilus_trader.adapters.interactive_brokers.config import SymbologyMethod from nautilus_trader.adapters.interactive_brokers.parsing.data import bar_spec_to_bar_size from nautilus_trader.adapters.interactive_brokers.parsing.data import timedelta_to_duration_str from nautilus_trader.adapters.interactive_brokers.parsing.instruments import RE_CASH @@ -75,7 +76,7 @@ # fmt: on ] -strict_symbology_params = [ +raw_symbology_params = [ # fmt: off (IBContract(secType="CASH", exchange="IDEALPRO", localSymbol="EUR.USD"), "EUR.USD=CASH.IDEALPRO"), (IBContract(secType="OPT", exchange="SMART", localSymbol="AAPL 230217P00155000"), "AAPL 230217P00155000=OPT.SMART"), @@ -123,8 +124,27 @@ ] +databento_symbology_params = [ + # fmt: off + (IBContract(secType="OPT", exchange="SMART", localSymbol="AAPL 230217P00155000"), "AAPL 230217P00155000.OPRA"), + (IBContract(secType="FUT", exchange="SMART", localSymbol="ESH3"), "ESH3.GLBX"), + (IBContract(secType="FUT", exchange="SMART", localSymbol="M6EH3"), "M6EH3.GLBX"), + (IBContract(secType="FUT", exchange="SMART", localSymbol="MYMM3"), "MYMM3.GLBX"), + (IBContract(secType="FUT", exchange="SMART", localSymbol="MCLV3"), "MCLV3.GLBX"), + (IBContract(secType="FOP", exchange="SMART", localSymbol="EX2G3 P4080"), "EX2G3 P4080.GLBX"), + (IBContract(secType="FOP", exchange="SMART", localSymbol="DXH3 P103.5"), "DXH3 P103.5.GLBX"), + (IBContract(secType="FOP", exchange="SMART", localSymbol="EX2G3 P4080"), "EX2G3 P4080.GLBX"), + (IBContract(secType="FOP", exchange="SMART", localSymbol="DXH3 P103.5"), "DXH3 P103.5.GLBX"), + (IBContract(secType="FOP", exchange="SMART", localSymbol="OJF6 C1.3"), "OJF6 C1.3.GLBX"), + (IBContract(secType="FOP", exchange="SMART", localSymbol="6NZ4 P0655"), "6NZ4 P0655.GLBX"), + (IBContract(secType="STK", exchange="SMART", currency="USD", localSymbol="SPY"), "SPY.XNAS"), + (IBContract(secType="STK", exchange="SMART", currency="USD", localSymbol="AAPL"), "AAPL.XNAS"), + # fmt: on +] + + @pytest.mark.parametrize("contract, instrument_id", simplified_symbology_params) -def test_ib_contract_to_instrument_id(contract, instrument_id): +def test_ib_contract_to_instrument_id_simplified_symbology(contract, instrument_id): # Arrange, Act result = ib_contract_to_instrument_id(contract) @@ -133,18 +153,37 @@ def test_ib_contract_to_instrument_id(contract, instrument_id): assert result == expected -@pytest.mark.parametrize("contract, instrument_id", strict_symbology_params) -def test_ib_contract_to_instrument_id_strict_symbology(contract, instrument_id): +@pytest.mark.parametrize("contract, instrument_id", raw_symbology_params) +def test_ib_contract_to_instrument_id_raw_symbology(contract, instrument_id): # Arrange, Act - result = ib_contract_to_instrument_id(contract=contract, strict_symbology=True) + result = ib_contract_to_instrument_id( + contract=contract, + symbology_method=SymbologyMethod.IB_RAW, + ) # Assert expected = InstrumentId.from_str(instrument_id) assert result == expected +@pytest.mark.parametrize("contract, instrument_id", databento_symbology_params) +def test_ib_contract_to_instrument_id_databento_symbology(contract, instrument_id): + # Arrange, Act + instrument_id_obj = InstrumentId.from_str(instrument_id) + databento_venue = instrument_id_obj.venue.value + result = ib_contract_to_instrument_id( + contract=contract, + symbology_method=SymbologyMethod.DATABENTO, + databento_venue=databento_venue, + ) + + # Assert + expected = instrument_id_obj + assert result == expected + + @pytest.mark.parametrize("contract, instrument_id", simplified_symbology_params) -def test_instrument_id_to_ib_contract(instrument_id, contract): +def test_instrument_id_to_ib_contract_simplified_symbology(instrument_id, contract): # Arrange, Act result = instrument_id_to_ib_contract(InstrumentId.from_str(instrument_id)) @@ -153,10 +192,26 @@ def test_instrument_id_to_ib_contract(instrument_id, contract): assert result == expected -@pytest.mark.parametrize("contract, instrument_id", simplified_symbology_params) -def test_instrument_id_to_ib_contract_strict_symbology(instrument_id, contract): +@pytest.mark.parametrize("contract, instrument_id", raw_symbology_params) +def test_instrument_id_to_ib_contract_raw_symbology(instrument_id, contract): # Arrange, Act - result = instrument_id_to_ib_contract(InstrumentId.from_str(instrument_id)) + result = instrument_id_to_ib_contract( + InstrumentId.from_str(instrument_id), + symbology_method=SymbologyMethod.IB_RAW, + ) + + # Assert + expected = contract + assert result == expected + + +@pytest.mark.parametrize("contract, instrument_id", databento_symbology_params) +def test_instrument_id_to_ib_contract_databento_symbology(instrument_id, contract): + # Arrange, Act + result = instrument_id_to_ib_contract( + InstrumentId.from_str(instrument_id), + symbology_method=SymbologyMethod.DATABENTO, + ) # Assert expected = contract From 30fdbc97a17b547dd396cf039294fb07e9cde9b5 Mon Sep 17 00:00:00 2001 From: David Blom Date: Wed, 27 Nov 2024 20:53:59 +0100 Subject: [PATCH 57/78] Reconcile order book for dYdX when inconsistent (#2077) --- nautilus_trader/adapters/dydx/data.py | 276 +++++++++++++++--- .../adapters/dydx/websocket/client.py | 5 +- 2 files changed, 239 insertions(+), 42 deletions(-) diff --git a/nautilus_trader/adapters/dydx/data.py b/nautilus_trader/adapters/dydx/data.py index a205f09c2bab..aba0dc25d232 100644 --- a/nautilus_trader/adapters/dydx/data.py +++ b/nautilus_trader/adapters/dydx/data.py @@ -49,12 +49,18 @@ from nautilus_trader.model.book import OrderBook from nautilus_trader.model.data import Bar from nautilus_trader.model.data import BarType +from nautilus_trader.model.data import BookOrder +from nautilus_trader.model.data import OrderBookDelta from nautilus_trader.model.data import OrderBookDeltas from nautilus_trader.model.data import QuoteTick +from nautilus_trader.model.enums import BookAction from nautilus_trader.model.enums import BookType +from nautilus_trader.model.enums import OrderSide from nautilus_trader.model.enums import PriceType +from nautilus_trader.model.enums import RecordFlag from nautilus_trader.model.identifiers import ClientId from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.objects import Quantity if TYPE_CHECKING: @@ -306,12 +312,13 @@ def _handle_orderbook(self, raw: bytes) -> None: self._log.error(f"Cannot parse orderbook data: no instrument for {instrument_id}") return + ts_init = self._clock.timestamp_ns() deltas = msg.parse_to_deltas( instrument_id=instrument_id, price_precision=instrument.price_precision, size_precision=instrument.size_precision, - ts_event=self._clock.timestamp_ns(), - ts_init=self._clock.timestamp_ns(), + ts_event=ts_init, + ts_init=ts_init, ) self._handle_deltas(instrument_id=instrument_id, deltas=deltas) @@ -332,12 +339,13 @@ def _handle_orderbook_batched(self, raw: bytes) -> None: self._log.error(f"Cannot parse orderbook data: no instrument for {instrument_id}") return + ts_init = self._clock.timestamp_ns() deltas = msg.parse_to_deltas( instrument_id=instrument_id, price_precision=instrument.price_precision, size_precision=instrument.size_precision, - ts_event=self._clock.timestamp_ns(), - ts_init=self._clock.timestamp_ns(), + ts_event=ts_init, + ts_init=ts_init, ) self._handle_deltas(instrument_id=instrument_id, deltas=deltas) @@ -362,12 +370,13 @@ def _handle_orderbook_snapshot(self, raw: bytes) -> None: ) return + ts_init = self._clock.timestamp_ns() deltas = msg.parse_to_snapshot( instrument_id=instrument_id, price_precision=instrument.price_precision, size_precision=instrument.size_precision, - ts_event=self._clock.timestamp_ns(), - ts_init=self._clock.timestamp_ns(), + ts_event=ts_init, + ts_init=ts_init, ) self._handle_deltas(instrument_id=instrument_id, deltas=deltas) @@ -375,51 +384,240 @@ def _handle_orderbook_snapshot(self, raw: bytes) -> None: except Exception as e: self._log.error(f"Failed to parse orderbook snapshot: {raw.decode()} with error {e}") - def _handle_deltas(self, instrument_id: InstrumentId, deltas: OrderBookDeltas) -> None: - self._handle_data(deltas) + def _resolve_crossed_order_book( + self, + book: OrderBook, + venue_deltas: OrderBookDeltas, + instrument_id: InstrumentId, + ) -> OrderBookDeltas: + """ + Reconcile the order book by generating new deltas when the order book is + crossed. + + One possible explanation for this behaviour could be that certain orders are not + acknowledged by other validators in the network. Another more unfortunate + explanation is that messages are missed or not send by the venue. - if instrument_id in self._books: - book = self._books[instrument_id] - book.apply_deltas(deltas) + """ + instrument = self._cache.instrument(instrument_id) + + if instrument is None: + self._log.error(f"Cannot resolve crossed order book: no instrument for {instrument_id}") + return + + book.apply_deltas(venue_deltas) + bid_price = book.best_bid_price() + ask_price = book.best_ask_price() + bid_size = book.best_bid_size() + ask_size = book.best_ask_size() + + if bid_price is None or ask_price is None: + return venue_deltas + + is_order_book_crossed = bid_price >= ask_price + ts_init = venue_deltas.ts_init + deltas: list[OrderBookDelta] = venue_deltas.deltas + + while is_order_book_crossed is True: + self._log.debug("Resolve crossed order book") + temp_deltas: list[OrderBookDelta] = [] + + if bid_size > ask_size: + # Remove ask price level from book and decrease bid size + delta = OrderBookDelta( + instrument_id=instrument_id, + action=BookAction.UPDATE, + order=BookOrder( + side=OrderSide.BUY, + price=bid_price, + size=Quantity(bid_size - ask_size, instrument.size_precision), + order_id=0, + ), + flags=0, + sequence=0, + ts_event=ts_init, + ts_init=ts_init, + ) + temp_deltas.append(delta) + + delta = OrderBookDelta( + instrument_id=instrument_id, + action=BookAction.DELETE, + order=BookOrder( + side=OrderSide.SELL, + price=ask_price, + size=Quantity(0, instrument.size_precision), + order_id=0, + ), + flags=0, + sequence=0, + ts_event=ts_init, + ts_init=ts_init, + ) + temp_deltas.append(delta) + elif bid_size < ask_size: + # Remove bid price level from book and decrease ask size + delta = OrderBookDelta( + instrument_id=instrument_id, + action=BookAction.UPDATE, + order=BookOrder( + side=OrderSide.SELL, + price=ask_price, + size=Quantity(ask_size - bid_size, instrument.size_precision), + order_id=0, + ), + flags=0, + sequence=0, + ts_event=ts_init, + ts_init=ts_init, + ) + temp_deltas.append(delta) + + delta = OrderBookDelta( + instrument_id=instrument_id, + action=BookAction.DELETE, + order=BookOrder( + side=OrderSide.BUY, + price=bid_price, + size=Quantity(0, instrument.size_precision), + order_id=0, + ), + flags=0, + sequence=0, + ts_event=ts_init, + ts_init=ts_init, + ) + temp_deltas.append(delta) + else: + # Remove bid price level and ask price level + delta = OrderBookDelta( + instrument_id=instrument_id, + action=BookAction.DELETE, + order=BookOrder( + side=OrderSide.BUY, + price=bid_price, + size=Quantity(0, instrument.size_precision), + order_id=0, + ), + flags=0, + sequence=0, + ts_event=ts_init, + ts_init=ts_init, + ) + temp_deltas.append(delta) + + delta = OrderBookDelta( + instrument_id=instrument_id, + action=BookAction.DELETE, + order=BookOrder( + side=OrderSide.SELL, + price=ask_price, + size=Quantity(0, instrument.size_precision), + order_id=0, + ), + flags=0, + sequence=0, + ts_event=ts_init, + ts_init=ts_init, + ) + temp_deltas.append(delta) - last_quote = self._last_quotes.get(instrument_id) + deltas += temp_deltas + order_book_deltas = OrderBookDeltas(instrument_id=instrument_id, deltas=temp_deltas) + book.apply_deltas(order_book_deltas) bid_price = book.best_bid_price() ask_price = book.best_ask_price() bid_size = book.best_bid_size() ask_size = book.best_ask_size() - if bid_price is None and last_quote is not None: - bid_price = last_quote.bid_price + if bid_price is None or ask_price is None: + break - if ask_price is None and last_quote is not None: - ask_price = last_quote.ask_price + is_order_book_crossed = bid_price >= ask_price - if bid_size is None and last_quote is not None: - bid_size = last_quote.bid_size + final_deltas = [] - if ask_size is None and last_quote is not None: - ask_size = last_quote.ask_size + for delta in deltas[0 : len(deltas) - 1]: + new_delta = OrderBookDelta( + instrument_id=delta.instrument_id, + action=delta.action, + order=delta.order, + flags=0, + sequence=delta.sequence, + ts_event=delta.ts_event, + ts_init=delta.ts_init, + ) + final_deltas.append(new_delta) + + delta = deltas[-1] + new_delta = OrderBookDelta( + instrument_id=delta.instrument_id, + action=delta.action, + order=delta.order, + flags=RecordFlag.F_LAST, + sequence=delta.sequence, + ts_event=delta.ts_event, + ts_init=delta.ts_init, + ) + final_deltas.append(new_delta) - quote = QuoteTick( - instrument_id=instrument_id, - bid_price=bid_price, - ask_price=ask_price, - bid_size=bid_size, - ask_size=ask_size, - ts_event=deltas.ts_event, - ts_init=deltas.ts_init, + return OrderBookDeltas(instrument_id=instrument_id, deltas=final_deltas) + + def _handle_deltas(self, instrument_id: InstrumentId, deltas: OrderBookDeltas) -> None: + book = self._books.get(instrument_id) + + if book is None: + self.log.error( + f"Cannot resolve crossed order book: order book not found for {instrument_id}", ) + return - if ( - last_quote is None - or last_quote.bid_price != quote.bid_price - or last_quote.ask_price != quote.ask_price - or last_quote.bid_size != quote.bid_size - or last_quote.ask_size != quote.ask_size - ): - self._handle_data(quote) - self._last_quotes[instrument_id] = quote + deltas = self._resolve_crossed_order_book( + book=book, + instrument_id=instrument_id, + venue_deltas=deltas, + ) + self._handle_data(deltas) + + last_quote = self._last_quotes.get(instrument_id) + + bid_price = book.best_bid_price() + ask_price = book.best_ask_price() + bid_size = book.best_bid_size() + ask_size = book.best_ask_size() + + if bid_price is None and last_quote is not None: + bid_price = last_quote.bid_price + + if ask_price is None and last_quote is not None: + ask_price = last_quote.ask_price + + if bid_size is None and last_quote is not None: + bid_size = last_quote.bid_size + + if ask_size is None and last_quote is not None: + ask_size = last_quote.ask_size + + quote = QuoteTick( + instrument_id=instrument_id, + bid_price=bid_price, + ask_price=ask_price, + bid_size=bid_size, + ask_size=ask_size, + ts_event=deltas.ts_event, + ts_init=deltas.ts_init, + ) + + if ( + last_quote is None + or last_quote.bid_price != quote.bid_price + or last_quote.ask_price != quote.ask_price + or last_quote.bid_size != quote.bid_size + or last_quote.ask_size != quote.ask_size + ): + self._handle_data(quote) + self._last_quotes[instrument_id] = quote def _handle_kline(self, raw: bytes) -> None: try: @@ -508,6 +706,9 @@ async def _subscribe_order_book_deltas( subscription = ("v4_orderbook", dydx_symbol.raw_symbol) self._orderbook_subscriptions.add(dydx_symbol.raw_symbol) + if instrument_id not in self._books: + self._books[instrument_id] = OrderBook(instrument_id, book_type) + if not self._ws_client.has_subscription(subscription): await self._ws_client.subscribe_order_book(dydx_symbol.raw_symbol) @@ -517,7 +718,6 @@ async def _subscribe_quote_ticks(self, instrument_id: InstrumentId) -> None: LogColor.MAGENTA, ) book_type = BookType.L2_MBP - self._books[instrument_id] = OrderBook(instrument_id, book_type) # Check if the websocket client is already subscribed. dydx_symbol = DYDXSymbol(instrument_id.symbol.value) diff --git a/nautilus_trader/adapters/dydx/websocket/client.py b/nautilus_trader/adapters/dydx/websocket/client.py index 3fd82886e0b9..a83ffd59ed89 100644 --- a/nautilus_trader/adapters/dydx/websocket/client.py +++ b/nautilus_trader/adapters/dydx/websocket/client.py @@ -323,7 +323,7 @@ async def subscribe_order_book( return self._subscriptions.add(subscription) - msg = {"type": "subscribe", "channel": "v4_orderbook", "id": symbol, "batched": True} + msg = {"type": "subscribe", "channel": "v4_orderbook", "id": symbol} self._log.debug(f"Subscribe to {symbol} order book") await self._send(msg) @@ -535,9 +535,6 @@ async def _subscribe_all(self) -> None: "id": subscription[1], } - if subscription[0] == "v4_orderbook": - msg["batched"] = True - await self._send(msg) async def _send(self, msg: dict[str, Any]) -> None: From 008d0239e99fa5e06c1e4660a34d88cc37c0d781 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 28 Nov 2024 06:27:29 +1100 Subject: [PATCH 58/78] Update release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index b99397799530..097bc3da765a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -8,6 +8,7 @@ Released on TBD (UTC). - Added `metadata` parameter for data requests (#2043), thanks @faysou - Added `STOP_MARKET` and `STOP_LIMIT` order support for dYdX (#2066), thanks @davidsblom - Added `max_reconnection_tries` to data client config for dYdX (#2066), thanks @davidsblom +- Added wallet subscription for Bybit (#2076), thanks @sunlei ### Internal Improvements - Ported `Portfolio` and `AccountManager` to Rust (#2058), thanks @Pushkarm029 From 9b03dfac3fad5ebbda54b7932d551487ac26c96e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 28 Nov 2024 06:38:14 +1100 Subject: [PATCH 59/78] Improve TradeId Debug impl --- RELEASES.md | 1 + nautilus_core/model/src/identifiers/trade_id.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 097bc3da765a..858c45c35669 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -15,6 +15,7 @@ Released on TBD (UTC). - Improved live engines error logging (will now log all exceptions rather than just `RuntimeError`) - Improved symbol normalization for Tardis - Improved historical bar request performance for Tardis +- Improved `TradeId` Debug implementation to display value as proper UTF-8 string - Refined `HttpClient` for use directly from Rust - Upgraded `datafusion` crate to v43.0.0 (#2056), thanks @twitu - Efficiently clean up expired timers in clocks (#2064), thanks @twitu diff --git a/nautilus_core/model/src/identifiers/trade_id.rs b/nautilus_core/model/src/identifiers/trade_id.rs index f409b8261cb8..f6f477d6bd68 100644 --- a/nautilus_core/model/src/identifiers/trade_id.rs +++ b/nautilus_core/model/src/identifiers/trade_id.rs @@ -38,7 +38,7 @@ pub const TRADE_ID_LEN: usize = 37; /// /// Maximum length is 36 characters. #[repr(C)] -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr( feature = "python", pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model") @@ -125,6 +125,12 @@ impl TradeId { } } +impl Debug for TradeId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}('{}')", stringify!(TradeId), self) + } +} + impl Display for TradeId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_cstr().to_str().unwrap()) @@ -231,6 +237,7 @@ mod tests { fn test_string_reprs(trade_id: TradeId) { assert_eq!(trade_id.to_string(), "1234567890"); assert_eq!(format!("{trade_id}"), "1234567890"); + assert_eq!(format!("{trade_id:?}"), "TradeId('1234567890')"); } #[rstest] From db0b4fa740ba07e00272799e56e89da6db0434c7 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 28 Nov 2024 06:45:18 +1100 Subject: [PATCH 60/78] Update dependencies and release notes --- RELEASES.md | 3 +- nautilus_core/Cargo.lock | 8 +- nautilus_core/Cargo.toml | 2 +- nautilus_core/cryptography/Cargo.toml | 2 +- nautilus_core/network/Cargo.toml | 2 +- poetry.lock | 154 +++++++++++++------------- 6 files changed, 86 insertions(+), 85 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 858c45c35669..401a31bbbb59 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,11 +4,12 @@ Released on TBD (UTC). ### Enhancements - Implemented mixed catalog data requests with catalog update (#2043), thanks @faysou -- Improved `Cache` behavior when adding more recent quotes, trades, or bars (now adds to cache) +- Added Databento symbology support for Interactive Brokers (#2073), thanks @rsmb7z - Added `metadata` parameter for data requests (#2043), thanks @faysou - Added `STOP_MARKET` and `STOP_LIMIT` order support for dYdX (#2066), thanks @davidsblom - Added `max_reconnection_tries` to data client config for dYdX (#2066), thanks @davidsblom - Added wallet subscription for Bybit (#2076), thanks @sunlei +- Improved `Cache` behavior when adding more recent quotes, trades, or bars (now adds to cache) ### Internal Improvements - Ported `Portfolio` and `AccountManager` to Rust (#2058), thanks @Pushkarm029 diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 2731e20e2c3e..b6b661fed1d5 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -4480,9 +4480,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.18" +version = "0.23.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" dependencies = [ "aws-lc-rs", "log", @@ -5593,9 +5593,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 40abffb68349..78bb23d0230e 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -63,7 +63,7 @@ strum = { version = "0.26.3", features = ["derive"] } thiserror = "2.0.3" thousands = "0.2.0" toml = "0.8.19" -tracing = "0.1.40" +tracing = "0.1.41" # Disable default feature "tracing-log" since it interferes with custom logging tracing-subscriber = { version = "0.3.18", default-features = false, features = ["smallvec", "fmt", "ansi", "std", "env-filter"] } tokio = { version = "1.41.1", features = ["full"] } diff --git a/nautilus_core/cryptography/Cargo.toml b/nautilus_core/cryptography/Cargo.toml index dafc89f78c55..4d0371d503b6 100644 --- a/nautilus_core/cryptography/Cargo.toml +++ b/nautilus_core/cryptography/Cargo.toml @@ -19,7 +19,7 @@ pyo3 = { workspace = true, optional = true } rand = { workspace = true } ring = { workspace = true } tracing = { workspace = true } -rustls = { version = "0.23.18", features = ["ring"] } +rustls = { version = "0.23.19", features = ["ring"] } rustls-native-certs = "0.8.0" pem = "3.0.4" diff --git a/nautilus_core/network/Cargo.toml b/nautilus_core/network/Cargo.toml index 5d9eab313505..fbb3a337be8d 100644 --- a/nautilus_core/network/Cargo.toml +++ b/nautilus_core/network/Cargo.toml @@ -26,7 +26,7 @@ tokio-tungstenite = { workspace = true } dashmap = "6.1.0" http = "1.1.0" nonzero_ext = "0.3.0" -rustls = { version = "0.23.18", features = ["ring"] } +rustls = { version = "0.23.19", features = ["ring"] } tokio-rustls = "0.26.0" [dev-dependencies] diff --git a/poetry.lock b/poetry.lock index 6d9a185e0bd1..7d8a80667e1b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,87 +13,87 @@ files = [ [[package]] name = "aiohttp" -version = "3.11.7" +version = "3.11.8" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" files = [ - {file = "aiohttp-3.11.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8bedb1f6cb919af3b6353921c71281b1491f948ca64408871465d889b4ee1b66"}, - {file = "aiohttp-3.11.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f5022504adab881e2d801a88b748ea63f2a9d130e0b2c430824682a96f6534be"}, - {file = "aiohttp-3.11.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e22d1721c978a6494adc824e0916f9d187fa57baeda34b55140315fa2f740184"}, - {file = "aiohttp-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e993676c71288618eb07e20622572b1250d8713e7e00ab3aabae28cb70f3640d"}, - {file = "aiohttp-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e13a05db87d3b241c186d0936808d0e4e12decc267c617d54e9c643807e968b6"}, - {file = "aiohttp-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ba8d043fed7ffa117024d7ba66fdea011c0e7602327c6d73cacaea38abe4491"}, - {file = "aiohttp-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda3ed0a7869d2fa16aa41f9961ade73aa2c2e3b2fcb0a352524e7b744881889"}, - {file = "aiohttp-3.11.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43bfd25113c1e98aec6c70e26d5f4331efbf4aa9037ba9ad88f090853bf64d7f"}, - {file = "aiohttp-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3dd3e7e7c9ef3e7214f014f1ae260892286647b3cf7c7f1b644a568fd410f8ca"}, - {file = "aiohttp-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78c657ece7a73b976905ab9ec8be9ef2df12ed8984c24598a1791c58ce3b4ce4"}, - {file = "aiohttp-3.11.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:db70a47987e34494b451a334605bee57a126fe8d290511349e86810b4be53b01"}, - {file = "aiohttp-3.11.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9e67531370a3b07e49b280c1f8c2df67985c790ad2834d1b288a2f13cd341c5f"}, - {file = "aiohttp-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9202f184cc0582b1db15056f2225ab4c1e3dac4d9ade50dd0613ac3c46352ac2"}, - {file = "aiohttp-3.11.7-cp310-cp310-win32.whl", hash = "sha256:2257bdd5cf54a4039a4337162cd8048f05a724380a2283df34620f55d4e29341"}, - {file = "aiohttp-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:b7215bf2b53bc6cb35808149980c2ae80a4ae4e273890ac85459c014d5aa60ac"}, - {file = "aiohttp-3.11.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cea52d11e02123f125f9055dfe0ccf1c3857225fb879e4a944fae12989e2aef2"}, - {file = "aiohttp-3.11.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3ce18f703b7298e7f7633efd6a90138d99a3f9a656cb52c1201e76cb5d79cf08"}, - {file = "aiohttp-3.11.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:670847ee6aeb3a569cd7cdfbe0c3bec1d44828bbfbe78c5d305f7f804870ef9e"}, - {file = "aiohttp-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dda726f89bfa5c465ba45b76515135a3ece0088dfa2da49b8bb278f3bdeea12"}, - {file = "aiohttp-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25b74a811dba37c7ea6a14d99eb9402d89c8d739d50748a75f3cf994cf19c43"}, - {file = "aiohttp-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5522ee72f95661e79db691310290c4618b86dff2d9b90baedf343fd7a08bf79"}, - {file = "aiohttp-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fbf41a6bbc319a7816ae0f0177c265b62f2a59ad301a0e49b395746eb2a9884"}, - {file = "aiohttp-3.11.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59ee1925b5a5efdf6c4e7be51deee93984d0ac14a6897bd521b498b9916f1544"}, - {file = "aiohttp-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:24054fce8c6d6f33a3e35d1c603ef1b91bbcba73e3f04a22b4f2f27dac59b347"}, - {file = "aiohttp-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:351849aca2c6f814575c1a485c01c17a4240413f960df1bf9f5deb0003c61a53"}, - {file = "aiohttp-3.11.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:12724f3a211fa243570e601f65a8831372caf1a149d2f1859f68479f07efec3d"}, - {file = "aiohttp-3.11.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7ea4490360b605804bea8173d2d086b6c379d6bb22ac434de605a9cbce006e7d"}, - {file = "aiohttp-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e0bf378db07df0a713a1e32381a1b277e62ad106d0dbe17b5479e76ec706d720"}, - {file = "aiohttp-3.11.7-cp311-cp311-win32.whl", hash = "sha256:cd8d62cab363dfe713067027a5adb4907515861f1e4ce63e7be810b83668b847"}, - {file = "aiohttp-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:bf0e6cce113596377cadda4e3ac5fb89f095bd492226e46d91b4baef1dd16f60"}, - {file = "aiohttp-3.11.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4bb7493c3e3a36d3012b8564bd0e2783259ddd7ef3a81a74f0dbfa000fce48b7"}, - {file = "aiohttp-3.11.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e143b0ef9cb1a2b4f74f56d4fbe50caa7c2bb93390aff52f9398d21d89bc73ea"}, - {file = "aiohttp-3.11.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7c58a240260822dc07f6ae32a0293dd5bccd618bb2d0f36d51c5dbd526f89c0"}, - {file = "aiohttp-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d20cfe63a1c135d26bde8c1d0ea46fd1200884afbc523466d2f1cf517d1fe33"}, - {file = "aiohttp-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12e4d45847a174f77b2b9919719203769f220058f642b08504cf8b1cf185dacf"}, - {file = "aiohttp-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf4efa2d01f697a7dbd0509891a286a4af0d86902fc594e20e3b1712c28c0106"}, - {file = "aiohttp-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee6a4cdcbf54b8083dc9723cdf5f41f722c00db40ccf9ec2616e27869151129"}, - {file = "aiohttp-3.11.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6095aaf852c34f42e1bd0cf0dc32d1e4b48a90bfb5054abdbb9d64b36acadcb"}, - {file = "aiohttp-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1cf03d27885f8c5ebf3993a220cc84fc66375e1e6e812731f51aab2b2748f4a6"}, - {file = "aiohttp-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1a17f6a230f81eb53282503823f59d61dff14fb2a93847bf0399dc8e87817307"}, - {file = "aiohttp-3.11.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:481f10a1a45c5f4c4a578bbd74cff22eb64460a6549819242a87a80788461fba"}, - {file = "aiohttp-3.11.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:db37248535d1ae40735d15bdf26ad43be19e3d93ab3f3dad8507eb0f85bb8124"}, - {file = "aiohttp-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9d18a8b44ec8502a7fde91446cd9c9b95ce7c49f1eacc1fb2358b8907d4369fd"}, - {file = "aiohttp-3.11.7-cp312-cp312-win32.whl", hash = "sha256:3d1c9c15d3999107cbb9b2d76ca6172e6710a12fda22434ee8bd3f432b7b17e8"}, - {file = "aiohttp-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:018f1b04883a12e77e7fc161934c0f298865d3a484aea536a6a2ca8d909f0ba0"}, - {file = "aiohttp-3.11.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:241a6ca732d2766836d62c58c49ca7a93d08251daef0c1e3c850df1d1ca0cbc4"}, - {file = "aiohttp-3.11.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aa3705a8d14de39898da0fbad920b2a37b7547c3afd2a18b9b81f0223b7d0f68"}, - {file = "aiohttp-3.11.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9acfc7f652b31853eed3b92095b0acf06fd5597eeea42e939bd23a17137679d5"}, - {file = "aiohttp-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcefcf2915a2dbdbce37e2fc1622129a1918abfe3d06721ce9f6cdac9b6d2eaa"}, - {file = "aiohttp-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c1f6490dd1862af5aae6cfcf2a274bffa9a5b32a8f5acb519a7ecf5a99a88866"}, - {file = "aiohttp-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac5462582d6561c1c1708853a9faf612ff4e5ea5e679e99be36143d6eabd8e"}, - {file = "aiohttp-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1a6309005acc4b2bcc577ba3b9169fea52638709ffacbd071f3503264620da"}, - {file = "aiohttp-3.11.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b973cce96793725ef63eb449adfb74f99c043c718acb76e0d2a447ae369962"}, - {file = "aiohttp-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ce91a24aac80de6be8512fb1c4838a9881aa713f44f4e91dd7bb3b34061b497d"}, - {file = "aiohttp-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:875f7100ce0e74af51d4139495eec4025affa1a605280f23990b6434b81df1bd"}, - {file = "aiohttp-3.11.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c171fc35d3174bbf4787381716564042a4cbc008824d8195eede3d9b938e29a8"}, - {file = "aiohttp-3.11.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ee9afa1b0d2293c46954f47f33e150798ad68b78925e3710044e0d67a9487791"}, - {file = "aiohttp-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8360c7cc620abb320e1b8d603c39095101391a82b1d0be05fb2225471c9c5c52"}, - {file = "aiohttp-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7a9318da4b4ada9a67c1dd84d1c0834123081e746bee311a16bb449f363d965e"}, - {file = "aiohttp-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:fc6da202068e0a268e298d7cd09b6e9f3997736cd9b060e2750963754552a0a9"}, - {file = "aiohttp-3.11.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:17829f37c0d31d89aa6b8b010475a10233774771f9b6dc2cc352ea4f8ce95d9a"}, - {file = "aiohttp-3.11.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d6177077a31b1aecfc3c9070bd2f11419dbb4a70f30f4c65b124714f525c2e48"}, - {file = "aiohttp-3.11.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:badda65ac99555791eed75e234afb94686ed2317670c68bff8a4498acdaee935"}, - {file = "aiohttp-3.11.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0de6466b9d742b4ee56fe1b2440706e225eb48c77c63152b1584864a236e7a50"}, - {file = "aiohttp-3.11.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04b0cc74d5a882c9dacaeeccc1444f0233212b6f5be8bc90833feef1e1ce14b9"}, - {file = "aiohttp-3.11.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c7af3e50e5903d21d7b935aceed901cc2475463bc16ddd5587653548661fdb"}, - {file = "aiohttp-3.11.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c63f898f683d1379b9be5afc3dd139e20b30b0b1e0bf69a3fc3681f364cf1629"}, - {file = "aiohttp-3.11.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdadc3f6a32d6eca45f9a900a254757fd7855dfb2d8f8dcf0e88f0fae3ff8eb1"}, - {file = "aiohttp-3.11.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d329300fb23e14ed1f8c6d688dfd867d1dcc3b1d7cd49b7f8c5b44e797ce0932"}, - {file = "aiohttp-3.11.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5578cf40440eafcb054cf859964bc120ab52ebe0e0562d2b898126d868749629"}, - {file = "aiohttp-3.11.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7b2f8107a3c329789f3c00b2daad0e35f548d0a55cda6291579136622099a46e"}, - {file = "aiohttp-3.11.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:43dd89a6194f6ab02a3fe36b09e42e2df19c211fc2050ce37374d96f39604997"}, - {file = "aiohttp-3.11.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d2fa6fc7cc865d26ff42480ac9b52b8c9b7da30a10a6442a9cdf429de840e949"}, - {file = "aiohttp-3.11.7-cp39-cp39-win32.whl", hash = "sha256:a7d9a606355655617fee25dd7e54d3af50804d002f1fd3118dd6312d26692d70"}, - {file = "aiohttp-3.11.7-cp39-cp39-win_amd64.whl", hash = "sha256:53c921b58fdc6485d6b2603e0132bb01cd59b8f0620ffc0907f525e0ba071687"}, - {file = "aiohttp-3.11.7.tar.gz", hash = "sha256:01a8aca4af3da85cea5c90141d23f4b0eee3cbecfd33b029a45a80f28c66c668"}, + {file = "aiohttp-3.11.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d2ca685c6a851ce64e511fbcb906e4dd97d13e567ca7ecb5cb30b184e15dc6d"}, + {file = "aiohttp-3.11.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52913bb8a0a72a57479f54b281300c9d23036aa9aa3ebbc9a32a643484eadfc2"}, + {file = "aiohttp-3.11.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:35dafc70051b6cbd6dafb533b4e3f0df6225a4896be373ef86367b2987409331"}, + {file = "aiohttp-3.11.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:561b9596a9f90266673ef0b950c27e04ab597cdb53785e2ac91b83b33c31b509"}, + {file = "aiohttp-3.11.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d479c1fdcc920056a06d04059db52eb8590ecbbb3acdcaeeea26a88ff782e94a"}, + {file = "aiohttp-3.11.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ce8eb6444bb6e862feca664ce365afa8e2e32db24dcf1a502719a8a002f9274"}, + {file = "aiohttp-3.11.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df9bf08eb93611b1d4d6245b6fecf88728e90eece00e00d554e1b0c445557d83"}, + {file = "aiohttp-3.11.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a20ddaa58fea717177fac9a4a1fb8b39be868aa4fed2af6de4313b7a08f0f71"}, + {file = "aiohttp-3.11.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9f4aadfea6b48cfa17aef1a68ba6bee5a0246374f5a588e299a4f4ff5bd1c77b"}, + {file = "aiohttp-3.11.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:aa7deebb4bc5143745e6282139d7b9de50beb6d06609df64d2c993ef496bc7eb"}, + {file = "aiohttp-3.11.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fe503a76b9e3a13b62e64545693c9463afe9d429e0909120f7bb66de91ed8bc2"}, + {file = "aiohttp-3.11.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1c5838a68e31712354129add1b5fe32b06aa05275f835130edc650e6288af05f"}, + {file = "aiohttp-3.11.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:832e58d9454fe501b0d092cdf660c0e34e16005f61acd06e1c79b0fc45019c94"}, + {file = "aiohttp-3.11.8-cp310-cp310-win32.whl", hash = "sha256:00618c37a350884c08e87cf9a6532be274d564227ac49e0b474cf41f27e1f190"}, + {file = "aiohttp-3.11.8-cp310-cp310-win_amd64.whl", hash = "sha256:8eeaac75203da1a54afe1faea3c855a1973026b54929112aa9b67bceadbcb0ca"}, + {file = "aiohttp-3.11.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f8dd02b44555893adfe7cc4b3b454fee04f9dcec45cf66ef5bb53ebf393f0505"}, + {file = "aiohttp-3.11.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:658052941324edea3dee1f681375e70779f55e437e07bdfc4b5bbe65ad53cefb"}, + {file = "aiohttp-3.11.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c829471a9e2266da4a0666f8a9e215f19320f79778af379c1c7db324ac24ed2"}, + {file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d21951756690f5d86d0215da38eb0fd65def03b5e2a1c08a4a39718a6d0d48f2"}, + {file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2fa50ddc6b21cc1ae23e13524d6f75b27e279fdf5cf905b2df6fd171891ac4e2"}, + {file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a5afbd805e449048ecebb1a256176e953d4ca9e48bab387d4d1c8524f1c7a95"}, + {file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea68db69f2a4ddc24b28b8e754fc0b963ed7f9b9a76137f06fe44643d6821fbd"}, + {file = "aiohttp-3.11.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80b3ac163145660ce660aed2f1005e6d4de840d39728990b7250525eeec4e4a8"}, + {file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e9ac0cce897904b77e109e5403ed713187dbdf96832bfd061ac07164264be16c"}, + {file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3260c77cff4e35245bc517658bd54d7a64787f71f3c4f723877c82f22835b032"}, + {file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7fd9c11ffad6b022bf02a41a70418cb2ab3b33f2c27842a5999e3ab78daf280"}, + {file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16bda233a7b159ab08107e8858fedca90a9de287057fab54cafde51bd83f9819"}, + {file = "aiohttp-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4867008617bbf86e9fb5b00f72dd0e3a00a579b32233caff834320867f9b7cac"}, + {file = "aiohttp-3.11.8-cp311-cp311-win32.whl", hash = "sha256:17e6b9d8e29e3bfc7f893f327e92c9769d3582cee2fb1652c1431ac3f60115a0"}, + {file = "aiohttp-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:7f3be4961a5c2c670f31caab7641a37ea2a97031f0d8ae15bcfd36b6bf273200"}, + {file = "aiohttp-3.11.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0e3b5bfef913d6be270c81976fbc0cbf66625cd92663bbb7e03b3adbd6aa4ac6"}, + {file = "aiohttp-3.11.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb51a81cb637b9a072c9cfae1839e35c6579638861eb3479eb5d6e6ce8bc6782"}, + {file = "aiohttp-3.11.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd2ca84e5f7a35f313a62eb7d6a50bac6760b60bafce34586750712731c0aeff"}, + {file = "aiohttp-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47c6663df9446aa848b478413219600da4b54bc0409e1ac4bc80fb1a81501363"}, + {file = "aiohttp-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c665ed4b52256614858b20711bbbd2755b0e19ec86870f8ff1645acf9ae9e760"}, + {file = "aiohttp-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35d4545e7684da7a954ffc2dce495462cb16a902dffdebe98572408f6aaaee83"}, + {file = "aiohttp-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85be3899e6860dd2cd3f4370ded6708e939d00d5ec922a8eb328d114db605a47"}, + {file = "aiohttp-3.11.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ed9f1f2697713c48efc9ec483ad5d062e4aa91854f090a3eba0b19c002851d"}, + {file = "aiohttp-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c0dbae99737badf3f5e862088a118e28d3b36f03eb608a6382eddfd68178e05b"}, + {file = "aiohttp-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:beae08f900b2980af4353a0200eb162b39f276fd8a6e43079a540f83964671f4"}, + {file = "aiohttp-3.11.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d6f9e5fd1b3ecbaca3e04a15a02d1fa213248608caee99fd5bdddd4759959cf7"}, + {file = "aiohttp-3.11.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a7def89a41fe32120d89cd4577f5efbab3c52234c5890066ced8a2f7202dff88"}, + {file = "aiohttp-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:98f596cf59292e779bc387f22378a3d2c5e052c9fe2bf822ac4f547c6fe57758"}, + {file = "aiohttp-3.11.8-cp312-cp312-win32.whl", hash = "sha256:b64fa6b76b35b695cd3e5c42a4e568cbea8d41c9e59165e2a43da00976e2027e"}, + {file = "aiohttp-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:afba47981ff73b1794c00dce774334dcfe62664b3b4f78f278b77d21ce9daf43"}, + {file = "aiohttp-3.11.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a81525430da5ca356fae6e889daeb6f5cc0d5f0cef88e59cdde48e2394ea1365"}, + {file = "aiohttp-3.11.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7565689e86a88c1d258351ebd14e343337b76a56ca5c0a2c1db96ec28149386f"}, + {file = "aiohttp-3.11.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0f9dbe9763c014c408ad51a027dc9582518e992dc63e2ffe359ac1b4840a560"}, + {file = "aiohttp-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca580edc3ccd7f6ea76ad9cf59f5a8756d338e770b5eda7be26bcda8fa7ef53"}, + {file = "aiohttp-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d141631a7348038fc7b5d1a81b3c9afa9aa056188ded7902fe754028fdea5c5"}, + {file = "aiohttp-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64e6b14608a56a4c76c60daac730b0c0eeaf9d10dfc3231f7fc26521a0d628fd"}, + {file = "aiohttp-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0983d0ce329f2f9dbeb355c3744bd6333f34e0dc56025b6b7d4f285b90acb51e"}, + {file = "aiohttp-3.11.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d96b93a46a3742880fa21bcb35c6c40cf27714ec0fb8ec85fe444d73b95131b9"}, + {file = "aiohttp-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f4f1779c3142d913c509c2ed1de8b8f920e07a5cd65ac1f57c61cfb6bfded5a4"}, + {file = "aiohttp-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:48be7cff468c9c0d86a02e6a826e1fe159094b16d5aa2c17703e7317f791b0f9"}, + {file = "aiohttp-3.11.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:daea456b79ca2bacc7f062845bbb1139c3b3231fc83169da5a682cf385416dd1"}, + {file = "aiohttp-3.11.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c92e763cf641e10ad9342597d20060ba23de5e411aada96660e679e3f9371189"}, + {file = "aiohttp-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a750ee5a177e0f873d6b2d7d0fa6e1e7c658fc0ca8ea56438dcba2ac94bedb09"}, + {file = "aiohttp-3.11.8-cp313-cp313-win32.whl", hash = "sha256:4448c9c7f77bad48a6569062c0c16deb77fbb7363de1dc71ed087f66fb3b3c96"}, + {file = "aiohttp-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:481075a1949de79a8a6841e0086f2f5f464785c592cf527ed0db2c0cbd0e1ba2"}, + {file = "aiohttp-3.11.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:72779bfb34d6d6b51e55a7f4901b410e416b5431738b367d49696928c91a2ca8"}, + {file = "aiohttp-3.11.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e6523f39071a01757048985e4cc22d04aa130bc40d9128503f3a61a3ee98328"}, + {file = "aiohttp-3.11.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:220bbce18b3046973465be45415430f1cab39d7fdc40cbcf0a8c05485c6902fe"}, + {file = "aiohttp-3.11.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:336bbf7a33dd8cb4a7afb98c70e9935a81e5e88f7ac595ba2e84b1fb5da190d6"}, + {file = "aiohttp-3.11.8-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c5e4f1ba5059b85e05c551961a448ce2689c6249ed6a2e2174796842c191d10"}, + {file = "aiohttp-3.11.8-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9f9fd5c672c962389429abd11ed32c9c93f7932fd58584cae1e43951b141c6b"}, + {file = "aiohttp-3.11.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58bd94ad48143e1d42e05fc055da41de0a9933f378ad87760595b8aec83d317b"}, + {file = "aiohttp-3.11.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bf52642b12d70d78c18882915201bc5345f7c8f0f2ab8919d99b886aa6475a7"}, + {file = "aiohttp-3.11.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fee12d8487b0df2b683424cca2a0d8fb7281d5607518d742e98119a74af01026"}, + {file = "aiohttp-3.11.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:65fd04f1fea668ad1af48ac31b752000e222dccffedcad3de8ccf9d34489ccd3"}, + {file = "aiohttp-3.11.8-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c3f397e0511a0ec4fe331e602fc057dfd336d352062deb9969ebd81e253a149c"}, + {file = "aiohttp-3.11.8-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:cf8f05f4abe3288fe2e106e1461fd20d8abf6103886ddfb6d746a5b8fb830d2b"}, + {file = "aiohttp-3.11.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7d71d4ac0792ff89541179394d303be846a0b6cd3821ae67286ee69ecec16f9f"}, + {file = "aiohttp-3.11.8-cp39-cp39-win32.whl", hash = "sha256:2b6f8716044ae5e5f2a3b4e4b6bfee48e97c8b2a92e56f43aadd728c7fd26b7d"}, + {file = "aiohttp-3.11.8-cp39-cp39-win_amd64.whl", hash = "sha256:da343903214bf9f9d314b913caa499fa19e26d73e6e23a3db7d4898ea6d47028"}, + {file = "aiohttp-3.11.8.tar.gz", hash = "sha256:7bc9d64a2350cbb29a9732334e1a0743cbb6844de1731cbdf5949b235653f3fd"}, ] [package.dependencies] From ed262521f08d508f0d056e86637f92dedb0d54f3 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 28 Nov 2024 07:13:05 +1100 Subject: [PATCH 61/78] Implement AsRef for more value types --- RELEASES.md | 2 ++ .../infrastructure/src/sql/models/instruments.rs | 4 ++-- nautilus_core/model/src/types/currency.rs | 14 +++----------- nautilus_core/model/src/types/money.rs | 12 +++--------- nautilus_core/model/src/types/price.rs | 12 +++--------- nautilus_core/model/src/types/quantity.rs | 13 +++++++++++++ 6 files changed, 26 insertions(+), 31 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 401a31bbbb59..e04447a46728 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -13,6 +13,7 @@ Released on TBD (UTC). ### Internal Improvements - Ported `Portfolio` and `AccountManager` to Rust (#2058), thanks @Pushkarm029 +- Implemented `AsRef` for `Price`, `Money`, and `Currency` - Improved live engines error logging (will now log all exceptions rather than just `RuntimeError`) - Improved symbol normalization for Tardis - Improved historical bar request performance for Tardis @@ -28,6 +29,7 @@ Released on TBD (UTC). - Fixed missing venue -> exchange mappings for Tardis integration - Fixed account balance and order status parsing for dYdX (#2067), thanks @davidsblom - Fixed parsing best effort opened order status for dYdX (#2068), thanks @davidsblom +- Reconcile order book for dYdX when inconsistent (#2077), thanks @davidsblom --- diff --git a/nautilus_core/infrastructure/src/sql/models/instruments.rs b/nautilus_core/infrastructure/src/sql/models/instruments.rs index 62178793465b..1f1b850f909f 100644 --- a/nautilus_core/infrastructure/src/sql/models/instruments.rs +++ b/nautilus_core/infrastructure/src/sql/models/instruments.rs @@ -150,10 +150,10 @@ impl<'r> FromRow<'r, PgRow> for BettingInstrumentModel { let size_precision = row.try_get::("size_precision")? as u8; let price_increment = row .try_get::("price_increment") - .map(|res| Price::from_str(res.as_str()).unwrap())?; + .map(Price::from)?; let size_increment = row .try_get::("size_increment") - .map(|res| Quantity::from_str(res.as_str()).unwrap())?; + .map(Quantity::from)?; let maker_fee = row .try_get::("maker_fee") .map(|res| Decimal::from_str(res.as_str()).unwrap())?; diff --git a/nautilus_core/model/src/types/currency.rs b/nautilus_core/model/src/types/currency.rs index 86740485a478..03fef1d1287d 100644 --- a/nautilus_core/model/src/types/currency.rs +++ b/nautilus_core/model/src/types/currency.rs @@ -211,17 +211,9 @@ impl FromStr for Currency { } } -impl From<&str> for Currency { - fn from(value: &str) -> Self { - value - .parse() - .expect("Currency string representation should be valid") - } -} - -impl From for Currency { - fn from(value: String) -> Self { - Self::from(value.as_str()) +impl> From for Currency { + fn from(value: T) -> Self { + Self::from_str(value.as_ref()).expect(FAILED) } } diff --git a/nautilus_core/model/src/types/money.rs b/nautilus_core/model/src/types/money.rs index 52677f4e5e64..1c437369cc39 100644 --- a/nautilus_core/model/src/types/money.rs +++ b/nautilus_core/model/src/types/money.rs @@ -151,15 +151,9 @@ impl FromStr for Money { } } -impl From<&str> for Money { - fn from(value: &str) -> Self { - Self::from_str(value).unwrap() - } -} - -impl From for Money { - fn from(value: String) -> Self { - Self::from_str(value.as_str()).unwrap() +impl> From for Money { + fn from(value: T) -> Self { + Self::from_str(value.as_ref()).expect(FAILED) } } diff --git a/nautilus_core/model/src/types/price.rs b/nautilus_core/model/src/types/price.rs index 6a7672b5669a..0dc3349624b6 100644 --- a/nautilus_core/model/src/types/price.rs +++ b/nautilus_core/model/src/types/price.rs @@ -207,15 +207,9 @@ impl FromStr for Price { } } -impl From<&str> for Price { - fn from(value: &str) -> Self { - Self::from_str(value).expect(FAILED) - } -} - -impl From for Price { - fn from(value: String) -> Self { - Self::from(value.as_str()) +impl> From for Price { + fn from(value: T) -> Self { + Self::from_str(value.as_ref()).expect(FAILED) } } diff --git a/nautilus_core/model/src/types/quantity.rs b/nautilus_core/model/src/types/quantity.rs index 8932b561dac6..0dafef037eac 100644 --- a/nautilus_core/model/src/types/quantity.rs +++ b/nautilus_core/model/src/types/quantity.rs @@ -327,12 +327,25 @@ impl FromStr for Quantity { } } +// Note: we can't implement `AsRef` due overlapping traits (maybe there is a way) impl From<&str> for Quantity { fn from(value: &str) -> Self { Self::from_str(value).expect("Valid string input for `Quantity`") } } +impl From for Quantity { + fn from(value: String) -> Self { + Self::from_str(&value).expect("Valid string input for `Quantity`") + } +} + +impl From<&String> for Quantity { + fn from(value: &String) -> Self { + Self::from_str(value).expect("Valid string input for `Quantity`") + } +} + impl> AddAssign for Quantity { fn add_assign(&mut self, other: T) { self.raw = self From 09e8c1cd3861004fed99a8cfb67ef38eecfba8c6 Mon Sep 17 00:00:00 2001 From: dodofarm <120017300+dodofarm@users.noreply.github.com> Date: Thu, 28 Nov 2024 07:18:20 +0000 Subject: [PATCH 62/78] Add docs clarity on loading historical bars (#2078) --- nautilus_trader/data/client.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_trader/data/client.pyx b/nautilus_trader/data/client.pyx index 97206c5de0f4..7b572593b1a0 100644 --- a/nautilus_trader/data/client.pyx +++ b/nautilus_trader/data/client.pyx @@ -923,7 +923,7 @@ cdef class MarketDataClient(DataClient): dict metadata = None, ): """ - Request historical `Bar` data. + Request historical `Bar` data. To load historical data from a catalog, you can pass a list[DataCatalogConfig] to the TradingNodeConfig or the BacktestEngineConfig. Parameters ---------- From 1669902c6e201d6af9053502623cd088e789554a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 28 Nov 2024 18:19:35 +1100 Subject: [PATCH 63/78] Update release notes --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index e04447a46728..5300c49b4d83 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,6 +9,7 @@ Released on TBD (UTC). - Added `STOP_MARKET` and `STOP_LIMIT` order support for dYdX (#2066), thanks @davidsblom - Added `max_reconnection_tries` to data client config for dYdX (#2066), thanks @davidsblom - Added wallet subscription for Bybit (#2076), thanks @sunlei +- Added docs clarity on loading historical bars (#2078), thanks @dodofarm - Improved `Cache` behavior when adding more recent quotes, trades, or bars (now adds to cache) ### Internal Improvements From 984d221050ca076d7fde59d171518f7fc582a1ca Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 29 Nov 2024 06:30:58 +1100 Subject: [PATCH 64/78] Upgrade Rust --- README.md | 6 +++--- nautilus_core/Cargo.toml | 2 +- rust-toolchain.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b7dd54e27024..41160a8ea066 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ | Platform | Rust | Python | | :----------------- | :------ | :----- | -| `Linux (x86_64)` | 1.82.0+ | 3.11+ | -| `macOS (arm64)` | 1.82.0+ | 3.11+ | -| `Windows (x86_64)` | 1.82.0+ | 3.11+ | +| `Linux (x86_64)` | 1.83.0+ | 3.11+ | +| `macOS (arm64)` | 1.83.0+ | 3.11+ | +| `Windows (x86_64)` | 1.83.0+ | 3.11+ | [![](https://dcbadge.limes.pink/api/server/AUWVs3XaCS)](https://discord.gg/AUWVs3XaCS) diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 78bb23d0230e..739bf50a027e 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -rust-version = "1.82.0" +rust-version = "1.83.0" version = "0.37.0" edition = "2021" authors = ["Nautech Systems "] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 3958e3484ca7..7b38006cd0c8 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -version = "1.82.0" +version = "1.83.0" channel = "stable" From 13fb75dd2ab2436fbb68efe2e152cf034e3ce3f5 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 29 Nov 2024 06:33:59 +1100 Subject: [PATCH 65/78] Update dependencies --- nautilus_core/Cargo.lock | 16 ++++++++-------- nautilus_core/Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index b6b661fed1d5..e41c705b9db0 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -730,9 +730,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" dependencies = [ "serde", ] @@ -1795,12 +1795,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4844,9 +4844,9 @@ checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", diff --git a/nautilus_core/Cargo.toml b/nautilus_core/Cargo.toml index 739bf50a027e..d0c8a16969c5 100644 --- a/nautilus_core/Cargo.toml +++ b/nautilus_core/Cargo.toml @@ -35,7 +35,7 @@ anyhow = "1.0.93" arrow = "53.2.0" # Keep in line with datafusion async-stream = "0.3.6" base64 = "0.22.1" -bytes = { version = "1.8.0", features = ["serde"] } +bytes = { version = "1.9.0", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] } derive_builder = "0.20.2" futures = "0.3.31" From d0533f6051f45e2f0f4d51714fdbdb51c180235a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 29 Nov 2024 06:38:35 +1100 Subject: [PATCH 66/78] Add requests as dev dependency Required by `fsspec` to use the github filesystem. --- poetry.lock | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 7d8a80667e1b..f419a77a898a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4123,4 +4123,4 @@ polymarket = ["py-clob-client"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "bd1f4cbf3b59b8de179b7ea0a2c5bcb699e88504e5d434176cf73c1269ae6215" +content-hash = "a77ead47b0a092c56779cb8471b312cd757f165cd122130624b191b251b861f5" diff --git a/pyproject.toml b/pyproject.toml index 36c146a6d536..9ab01a22d144 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,6 +92,7 @@ docformatter = "^1.7.5" mypy = "^1.13.0" pandas-stubs = "^2.2.2" pre-commit = "^4.0.1" +requests = "^2.32.3" # Required for fsspec github FS ruff = "^0.8.0" types-pytz = "^2024.1" types-requests = "^2.32" From 1ce3162b56bcedebca6e83ae74a00ea63cde267c Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 29 Nov 2024 07:23:43 +1100 Subject: [PATCH 67/78] Remove dead code --- nautilus_core/model/src/python/data/status.rs | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/nautilus_core/model/src/python/data/status.rs b/nautilus_core/model/src/python/data/status.rs index 4f3caa7cc76f..377ea8350646 100644 --- a/nautilus_core/model/src/python/data/status.rs +++ b/nautilus_core/model/src/python/data/status.rs @@ -185,27 +185,6 @@ impl InstrumentStatus { format!("{}:{}", PY_MODULE_MODEL, stringify!(InstrumentStatus)) } - // TODO: Implement for `Data` - /// Creates a `PyCapsule` containing a raw pointer to a `Data::Quote` object. - /// - /// This function takes the current object (assumed to be of a type that can be represented as - /// `Data::Quote`), and encapsulates a raw pointer to it within a `PyCapsule`. - /// - /// # Safety - /// - /// This function is safe as long as the following conditions are met: - /// - The `Data::Quote` object pointed to by the capsule must remain valid for the lifetime of the capsule. - /// - The consumer of the capsule must ensure proper handling to avoid dereferencing a dangling pointer. - /// - /// # Panics - /// - /// The function will panic if the `PyCapsule` creation fails, which can occur if the - /// `Data::Quote` object cannot be converted into a raw pointer. - // #[pyo3(name = "as_pycapsule")] - // fn py_as_pycapsule(&self, py: Python<'_>) -> PyObject { - // data_to_pycapsule(py, Data::Quote(*self)) - // } - /// Return a dictionary representation of the object. #[pyo3(name = "as_dict")] fn py_as_dict(&self, py: Python<'_>) -> PyResult> { From 17c895cb87fe9fadbdf183ccf3924a8c00f2181f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 29 Nov 2024 08:06:29 +1100 Subject: [PATCH 68/78] Add Currency::try_from_str --- nautilus_core/model/src/types/currency.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/nautilus_core/model/src/types/currency.rs b/nautilus_core/model/src/types/currency.rs index 03fef1d1287d..4d6b259934c7 100644 --- a/nautilus_core/model/src/types/currency.rs +++ b/nautilus_core/model/src/types/currency.rs @@ -126,6 +126,12 @@ impl Currency { Ok(()) } + /// Attempts to parse a [`Currency`] from a string, returning `None` if not found. + pub fn try_from_str(s: &str) -> Option { + let map_guard = CURRENCY_MAP.lock().ok()?; + map_guard.get(s).copied() + } + /// Checks if the currency identified by the given `code` is a fiat currency. /// /// # Errors @@ -297,6 +303,22 @@ mod tests { assert_eq!(currency.currency_type, CurrencyType::Crypto); } + #[rstest] + fn test_try_from_str_valid() { + let test_currency = Currency::new("TEST", 2, 999, "Test Currency", CurrencyType::Fiat); + Currency::register(test_currency, true).unwrap(); + + let currency = Currency::try_from_str("TEST"); + assert!(currency.is_some()); + assert_eq!(currency.unwrap(), test_currency); + } + + #[rstest] + fn test_try_from_str_invalid() { + let invalid_currency = Currency::try_from_str("INVALID"); + assert!(invalid_currency.is_none()); + } + #[rstest] fn test_equality() { let currency1 = Currency::new("USD", 2, 840, "United States dollar", CurrencyType::Fiat); From c66aa95c610041c6e30fe6a15d35a21c56ffdfe1 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Fri, 29 Nov 2024 08:34:50 +1100 Subject: [PATCH 69/78] Refine Databento decoder - Remove currency hard coding and use USD as fallback - Remove use of unsafe - Add tests --- RELEASES.md | 1 + .../live/databento/databento_subscriber.py | 2 +- nautilus_core/Cargo.lock | 1 + nautilus_core/adapters/Cargo.toml | 1 + .../adapters/src/databento/decode.rs | 395 +++++++++++------- .../adapters/src/databento/loader.rs | 7 +- .../src/databento/python/historical.rs | 4 +- 7 files changed, 255 insertions(+), 156 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 5300c49b4d83..7e64a45fec88 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -20,6 +20,7 @@ Released on TBD (UTC). - Improved historical bar request performance for Tardis - Improved `TradeId` Debug implementation to display value as proper UTF-8 string - Refined `HttpClient` for use directly from Rust +- Refined Databento decoder (removed currency hard coding and use of `unsafe`) - Upgraded `datafusion` crate to v43.0.0 (#2056), thanks @twitu - Efficiently clean up expired timers in clocks (#2064), thanks @twitu diff --git a/examples/live/databento/databento_subscriber.py b/examples/live/databento/databento_subscriber.py index a42989830957..c6e867041e03 100644 --- a/examples/live/databento/databento_subscriber.py +++ b/examples/live/databento/databento_subscriber.py @@ -91,7 +91,7 @@ mbo_subscriptions_delay=10.0, ), }, - timeout_connection=20.0, + timeout_connection=30.0, timeout_reconciliation=10.0, # Not applicable timeout_portfolio=10.0, timeout_disconnection=10.0, diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index e41c705b9db0..9095edd50a8c 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -2886,6 +2886,7 @@ dependencies = [ "heck 0.5.0", "indexmap", "itoa", + "log", "nautilus-common", "nautilus-core", "nautilus-model", diff --git a/nautilus_core/adapters/Cargo.toml b/nautilus_core/adapters/Cargo.toml index 3a0cb12f5dcb..6855b6698dff 100644 --- a/nautilus_core/adapters/Cargo.toml +++ b/nautilus_core/adapters/Cargo.toml @@ -39,6 +39,7 @@ futures-util = { workspace = true } heck = { workspace = true } indexmap = { workspace = true } itoa = { workspace = true } +log = { workspace = true } parquet = { workspace = true, optional = true } pyo3 = { workspace = true, optional = true } pyo3-async-runtimes = { workspace = true, optional = true } diff --git a/nautilus_core/adapters/src/databento/decode.rs b/nautilus_core/adapters/src/databento/decode.rs index 84e1ccda15bf..e3e16531f669 100644 --- a/nautilus_core/adapters/src/databento/decode.rs +++ b/nautilus_core/adapters/src/databento/decode.rs @@ -13,11 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use std::{ - cmp, - ffi::{c_char, CStr}, - str::FromStr, -}; +use std::{cmp, ffi::c_char}; use databento::dbn::{self}; use nautilus_core::{datetime::NANOSECONDS_IN_SECOND, nanos::UnixNanos}; @@ -51,6 +47,7 @@ use super::{ types::{DatabentoImbalance, DatabentoStatistics}, }; +// Represents $0.01 in nano units const ONE_CENT_INCREMENT: i64 = 10_000_000; const BAR_SPEC_1S: BarSpecification = BarSpecification { @@ -80,7 +77,7 @@ const BAR_CLOSE_ADJUSTMENT_1H: u64 = NANOSECONDS_IN_SECOND * 60 * 60; const BAR_CLOSE_ADJUSTMENT_1D: u64 = NANOSECONDS_IN_SECOND * 60 * 60 * 24; #[must_use] -pub const fn parse_ynblank_as_opt_bool(c: c_char) -> Option { +pub const fn parse_optional_bool(c: c_char) -> Option { match c as u8 as char { 'Y' => Some(true), 'N' => Some(false), @@ -113,7 +110,7 @@ pub fn parse_book_action(c: c_char) -> anyhow::Result { 'F' => Ok(BookAction::Update), 'M' => Ok(BookAction::Update), 'R' => Ok(BookAction::Clear), - _ => anyhow::bail!("Invalid `BookAction`, was '{c}'"), + invalid => anyhow::bail!("Invalid `BookAction`, was '{invalid}'"), } } @@ -121,7 +118,20 @@ pub fn parse_option_kind(c: c_char) -> anyhow::Result { match c as u8 as char { 'C' => Ok(OptionKind::Call), 'P' => Ok(OptionKind::Put), - _ => anyhow::bail!("Invalid `OptionKind`, was '{c}'"), + invalid => anyhow::bail!("Invalid `OptionKind`, was '{invalid}'"), + } +} + +fn parse_currency_or_usd_default(value: Result<&str, impl std::error::Error>) -> Currency { + match value { + Ok(value) if !value.is_empty() => { + Currency::try_from_str(value).unwrap_or_else(Currency::USD) + } + Ok(_) => Currency::USD(), + Err(e) => { + log::error!("Error parsing currency: {e}"); + Currency::USD() + } } } @@ -133,6 +143,7 @@ pub fn parse_cfi_iso10926( anyhow::bail!("Value string is too short"); } + // TODO: A proper CFI parser would be useful: https://en.wikipedia.org/wiki/ISO_10962 let cfi_category = chars[0]; let cfi_group = chars[1]; let cfi_attribute1 = chars[2]; @@ -159,6 +170,7 @@ pub fn parse_cfi_iso10926( Ok((asset_class, instrument_class)) } +// https://databento.com/docs/schemas-and-data-formats/status#types-of-status-reasons pub fn parse_status_reason(value: u16) -> anyhow::Result> { let value_str = match value { 0 => return Ok(None), @@ -195,7 +207,7 @@ pub fn parse_status_reason(value: u16) -> anyhow::Result> { 123 => "Market wide halt carryover", 124 => "Market wide halt resumption", 130 => "Quotation not available", - _ => anyhow::bail!("Invalid `StatusMsg` reason, was '{value}'"), + invalid => anyhow::bail!("Invalid `StatusMsg` reason, was '{invalid}'"), }; Ok(Some(Ustr::from(value_str))) @@ -228,37 +240,20 @@ pub fn decode_optional_price(value: i64, precision: u8) -> anyhow::Result