diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0f8de48a0e58..4989bb719f60 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -92,7 +92,7 @@ repos: - id: cargo-doc name: cargo doc description: Check documentation builds without errors or warnings. - entry: bash -c 'RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo doc --all-features --no-deps --workspace --quiet' + entry: bash -c 'RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo doc --features "high-precision,ffi,python,extension-module" --no-deps --workspace --quiet' language: system files: '\.(rs|toml)$' types: [file] @@ -163,7 +163,7 @@ repos: ] - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.7.13 # uv version + rev: 0.7.15 # uv version hooks: - id: uv-lock diff --git a/Cargo.lock b/Cargo.lock index cc0ddb1fb52f..13539615f39f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1446,9 +1446,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byte-slice-cast" @@ -1974,9 +1974,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" @@ -2105,9 +2105,9 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "databento" -version = "0.27.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b19308b1932c34fa8eb51652691b11c064ca100c0f43cfbd8bdd4b78bd6344e" +checksum = "35dc6628199560fc4b1583dfeb734badab1384a2a4b1623024f2e0c848380e7e" dependencies = [ "async-compression", "dbn", @@ -4368,6 +4368,7 @@ dependencies = [ name = "nautilus-blockchain" version = "0.49.0" dependencies = [ + "ahash 0.8.12", "alloy", "anyhow", "async-stream", @@ -4511,6 +4512,7 @@ dependencies = [ "rand 0.9.1", "rmp-serde", "rstest", + "rust_decimal", "serde", "serde_json", "strum", @@ -5126,18 +5128,19 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index bd3173e85bac..cc2b98a9a106 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ clap = { version = "4.5.39", features = ["derive", "env"] } compare = "0.1.0" csv = "1.3.1" dashmap = "6.1.0" -databento = { version = "0.27.0", default-features = false, features = ["historical", "live"] } +databento = { version = "0.27.1", default-features = false, features = ["historical", "live"] } datafusion = { version = "48.0.0", default-features = false, features = [ "parquet", "regex_expressions", diff --git a/Makefile b/Makefile index eb87dc83d482..7742efdf800c 100644 --- a/Makefile +++ b/Makefile @@ -196,7 +196,7 @@ cargo-test-crate-%: RUST_BACKTRACE=1 cargo-test-crate-%: HIGH_PRECISION=true cargo-test-crate-%: check-nextest cargo-test-crate-%: - cargo nextest run --lib --no-default-features --features "ffi,python,high-precision,defi,stubs" --no-fail-fast --cargo-profile nextest -p $* + cargo nextest run --lib --no-default-features --all-features --no-fail-fast --cargo-profile nextest -p $* .PHONY: cargo-bench cargo-bench: diff --git a/RELEASES.md b/RELEASES.md index a6f627ea414c..795bde98dff2 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -30,12 +30,17 @@ Released on TBD (UTC). - Added property-based testing for `TestTimer` in Rust - Added property-based testing for `network` crate in Rust - Added chaos testing with `turmoil` for socket clients in Rust +- Added `check_positive_decimal` correctness function and use for instrument validations (#2736), thanks @nicolad +- Added `check_positive_money` correctness function and use for instrument validations (#2738), thanks @nicolad - Ported data catalog refactor to Rust (#2681, #2720), thanks @faysou - Consolidated the clocks and timers v2 feature from @twitu - Consolidated on pure Rust cryptography crates with no dependencies on native certs or openssl - Consolidated on `aws-lc-rs` cryptography for FIPS compliance - Confirmed parity between Cython and Rust indicators (#2700, #2710, #2713), thanks @nicolad - Implemented `From` -> `CurrencyPair` & `InstrumentAny` (#2693), thanks @nicolad +- Updated Tardis exchange mappings +- Improved handling of negative balances in backtests (#2730), thanks @ms32035 +- Improved implementation, validations and testing for Rust instruments (#2723, #2733), thanks @nicolad - Improved `Currency` equality to use `strcmp` to avoid C pointer comparison issues with `ustr` string interning - Improved unsubscribe cleanup(s) for Bybit adapter - Refactored IB adapter (#2647), thanks @faysou @@ -44,7 +49,7 @@ Released on TBD (UTC). - Refined signal serialization and tests (#2705), thanks @faysou - Refined CI/CD and build system (#2707), thanks @stastnypremysl - Upgraded Cython to v3.1.2 -- Upgraded `databento` crate to v0.27.0 +- Upgraded `databento` crate to v0.27.1 - Upgraded `datafusion` crate to v48.0.0 - Upgraded `pyo3` and `pyo3-async-runtimes` crates to v0.25.1 - Upgraded `redis` crate to v0.32.2 @@ -69,6 +74,7 @@ Released on TBD (UTC). - Fixed registration of encoder and decoder for `BinanceBar`, thanks for reporting @miller-moore - Fixed spot and futures sandbox for Binance (#2687), thanks @petioptrv - Fixed `clean` and `distclean` make targets entering `.venv` and corrupting the Python virtual env, thanks @faysou +- Fixed catalog identifier matching to exact match (#2732), thanks @faysou - Fixed last value updating for RSI indicator (#2703), thanks @bartlaw - Fixed gateway/TWS reconnect process for IBKR (#2710), thanks @bartlaw - Fixed Interactive Brokers options chain issue (#2711), thanks @FGU1 @@ -79,7 +85,7 @@ Released on TBD (UTC). - Restore task error logs for IBKR (#2716), thanks @bartlaw ### Documentation Updates -None +- Updated IB adapter documentation (#2729), thanks @faysou ### Deprecations - Deprecated `Portfolio.set_specific_venue(...)`, to be removed in a future release; use `Cache.set_specific_venue(...)` instead diff --git a/crates/adapters/blockchain/Cargo.toml b/crates/adapters/blockchain/Cargo.toml index 9147e6673b75..9ad57ec90e94 100644 --- a/crates/adapters/blockchain/Cargo.toml +++ b/crates/adapters/blockchain/Cargo.toml @@ -43,6 +43,7 @@ nautilus-model = { workspace = true, features = ["defi"] } nautilus-network = { workspace = true } nautilus-system = { workspace = true } +ahash = { workspace = true } alloy = { workspace = true } anyhow = { workspace = true } async-stream = { workspace = true } diff --git a/crates/adapters/blockchain/bin/node_test.rs b/crates/adapters/blockchain/bin/node_test.rs index 652c0dd874aa..56a0b8ec387b 100644 --- a/crates/adapters/blockchain/bin/node_test.rs +++ b/crates/adapters/blockchain/bin/node_test.rs @@ -15,6 +15,7 @@ use std::{ ops::{Deref, DerefMut}, + str::FromStr, sync::Arc, time::Duration, }; @@ -25,7 +26,8 @@ use nautilus_blockchain::{ }; use nautilus_common::{ actor::{DataActor, DataActorCore, data_actor::DataActorConfig}, - enums::Environment, + enums::{Environment, LogColor}, + logging::log_info, }; use nautilus_core::env::get_env_var; use nautilus_live::node::LiveNode; @@ -77,8 +79,7 @@ async fn main() -> Result<(), Box> { // Create and register a blockchain subscriber actor let client_id = ClientId::new(format!("BLOCKCHAIN-{}", chain.name)); let pools = vec![ - // Address::from("0xC31E54c7A869B9fCbECC14363CF510d1C41Fa443"), // WETH/USDC Arbitrum One - // Address::from(0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640), // USDC/ETH 0.05% on Uniswap V3 + Address::from_str("0xC31E54c7A869B9fCbECC14363CF510d1C41Fa443")?, // WETH/USDC Arbitrum One ]; let actor_config = BlockchainSubscriberActorConfig::new(client_id, chain.name, pools); @@ -120,7 +121,7 @@ impl BlockchainSubscriberActorConfig { /// A basic blockchain subscriber actor that monitors DeFi activities. /// /// This actor demonstrates how to use the `DataActor` trait to monitor blockchain data -/// from DEXs, pools, and other DeFi protocols. It logs received swaps and liquidity updates +/// from DEXs, pools, and other DeFi protocols. It logs received blocks and swaps /// to demonstrate the data flow. #[derive(Debug)] pub struct BlockchainSubscriberActor { @@ -198,11 +199,15 @@ impl DataActor for BlockchainSubscriberActor { } fn on_block(&mut self, block: &Block) -> anyhow::Result<()> { + log_info!("Received {block}", color = LogColor::Cyan); + self.received_blocks.push(block.clone()); Ok(()) } fn on_pool_swap(&mut self, swap: &PoolSwap) -> anyhow::Result<()> { + log_info!("Received {swap}", color = LogColor::Cyan); + self.received_pool_swaps.push(swap.clone()); Ok(()) } diff --git a/crates/adapters/blockchain/src/data.rs b/crates/adapters/blockchain/src/data.rs index 1292bc958043..2c0c87d2fa90 100644 --- a/crates/adapters/blockchain/src/data.rs +++ b/crates/adapters/blockchain/src/data.rs @@ -28,6 +28,7 @@ use nautilus_common::{ }, runner::get_data_event_sender, }; +use nautilus_core::UnixNanos; use nautilus_data::client::DataClient; use nautilus_infrastructure::sql::pg::PostgresConnectOptions; use nautilus_model::{ @@ -173,20 +174,18 @@ impl BlockchainDataClient { /// Spawns a unified task that handles both commands and data from the same client instances. /// This replaces both the command processor and hypersync forwarder with a single unified handler. fn spawn_process_task(&mut self) { - let command_rx = match self.command_rx.take() { - Some(r) => r, - None => { - tracing::error!("Command receiver already taken, not spawning handler"); - return; - } + let command_rx = if let Some(r) = self.command_rx.take() { + r + } else { + tracing::error!("Command receiver already taken, not spawning handler"); + return; }; - let hypersync_rx = match self.hypersync_rx.take() { - Some(r) => r, - None => { - tracing::error!("HyperSync receiver already taken, not spawning handler"); - return; - } + let hypersync_rx = if let Some(r) = self.hypersync_rx.take() { + r + } else { + tracing::error!("HyperSync receiver already taken, not spawning handler"); + return; }; let mut hypersync_client = std::mem::replace( @@ -205,43 +204,37 @@ impl BlockchainDataClient { loop { tokio::select! { command = command_rx.recv() => { - match command { - Some(cmd) => { - if let Err(e) = Self::process_command( - cmd, - &mut hypersync_client, - rpc_client.as_mut() - ).await { - tracing::error!("Error processing command: {e}"); - } - } - None => { - tracing::debug!("Command channel closed"); - break; + if let Some(cmd) = command { + if let Err(e) = Self::process_command( + cmd, + &mut hypersync_client, + rpc_client.as_mut() + ).await { + tracing::error!("Error processing command: {e}"); } + } else { + tracing::debug!("Command channel closed"); + break; } } data = hypersync_rx.recv() => { - match data { - Some(msg) => { - let data_event = match msg { - BlockchainMessage::Block(block) => { - DataEvent::DeFi(DefiData::Block(block)) - } - BlockchainMessage::Swap(swap) => { - DataEvent::DeFi(DefiData::PoolSwap(swap)) - } - }; - - if let Err(e) = data_sender.send(data_event) { - tracing::error!("Failed to send data event: {e}"); - break; + if let Some(msg) = data { + let data_event = match msg { + BlockchainMessage::Block(block) => { + DataEvent::DeFi(DefiData::Block(block)) } - } - None => { - tracing::debug!("HyperSync data channel closed"); + BlockchainMessage::Swap(swap) => { + DataEvent::DeFi(DefiData::PoolSwap(swap)) + } + }; + + if let Err(e) = data_sender.send(data_event) { + tracing::error!("Failed to send data event: {e}"); break; } + } else { + tracing::debug!("HyperSync data channel closed"); + break; } } } @@ -302,10 +295,21 @@ impl BlockchainDataClient { tracing::warn!("Pool subscriptions are handled at application level"); Ok(()) } - DefiSubscribeCommand::PoolSwaps(_cmd) => { - tracing::info!("Processing subscribe pool swaps command"); - tracing::warn!("Pool swaps subscription not yet implemented"); - // TODO: Implement actual pool swaps subscription logic + DefiSubscribeCommand::PoolSwaps(cmd) => { + tracing::info!( + "Processing subscribe pool swaps command for address: {}", + cmd.address + ); + + if let Some(ref mut _rpc) = rpc_client { + tracing::warn!( + "RPC pool swaps subscription not yet implemented, using HyperSync" + ); + } + + hypersync_client.subscribe_pool_swaps(cmd.address); + tracing::info!("Subscribed to pool swaps for address: {}", cmd.address); + Ok(()) } DefiSubscribeCommand::PoolLiquidityUpdates(_cmd) => { @@ -686,7 +690,7 @@ impl BlockchainDataClient { self.cache.get_token(&event.token1).cloned().unwrap(), event.fee, event.tick_spacing, - nautilus_core::UnixNanos::default(), // Use default timestamp for now + UnixNanos::default(), // TODO: Use default timestamp for now ); self.cache.add_pool(pool.clone()).await?; @@ -705,12 +709,11 @@ impl BlockchainDataClient { pub async fn process_hypersync_messages(&mut self) { tracing::info!("Starting task 'process_hypersync_messages'"); - let mut rx = match self.hypersync_rx.take() { - Some(r) => r, - None => { - tracing::warn!("HyperSync receiver already taken, not spawning forwarder"); - return; - } + let mut rx = if let Some(r) = self.hypersync_rx.take() { + r + } else { + tracing::warn!("HyperSync receiver already taken, not spawning forwarder"); + return; }; while let Some(msg) = rx.recv().await { diff --git a/crates/adapters/blockchain/src/exchanges/ethereum/uniswap_v3.rs b/crates/adapters/blockchain/src/exchanges/ethereum/uniswap_v3.rs index 852f28181304..65261c87fc69 100644 --- a/crates/adapters/blockchain/src/exchanges/ethereum/uniswap_v3.rs +++ b/crates/adapters/blockchain/src/exchanges/ethereum/uniswap_v3.rs @@ -374,9 +374,9 @@ mod tests { fn mint_event_log() -> Log { serde_json::from_str(r#"{ "removed": null, - "log_index": null, - "transaction_index": null, - "transaction_hash": null, + "log_index": "0xa", + "transaction_index": "0x5", + "transaction_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "block_hash": null, "block_number": "0x1581756", "address": null, diff --git a/crates/adapters/blockchain/src/hypersync/client.rs b/crates/adapters/blockchain/src/hypersync/client.rs index 82fb36e06147..c47e7f526b51 100644 --- a/crates/adapters/blockchain/src/hypersync/client.rs +++ b/crates/adapters/blockchain/src/hypersync/client.rs @@ -15,16 +15,21 @@ use std::{collections::BTreeSet, sync::Arc}; -use alloy::primitives::keccak256; +use ahash::AHashMap; +use alloy::primitives::{Address, keccak256}; use futures_util::Stream; use hypersync_client::{ net_types::{BlockSelection, FieldSelection, Query}, simple_types::Log, }; -use nautilus_model::defi::{Block, SharedChain}; +use nautilus_core::UnixNanos; +use nautilus_model::defi::{AmmType, Block, Dex, Pool, SharedChain, Token}; use reqwest::Url; -use crate::{hypersync::transform::transform_hypersync_block, rpc::types::BlockchainMessage}; +use crate::{ + hypersync::transform::{transform_hypersync_block, transform_hypersync_swap_log}, + rpc::types::BlockchainMessage, +}; /// The interval in milliseconds at which to check for new blocks when waiting /// for the hypersync to index the block. @@ -39,6 +44,8 @@ pub struct HyperSyncClient { client: Arc, /// Background task handle for the block subscription task. blocks_task: Option>, + /// Background task handles for swap subscription tasks (keyed by pool address). + swaps_tasks: AHashMap>, /// Channel for sending blockchain messages to the adapter data client. tx: tokio::sync::mpsc::UnboundedSender, } @@ -64,6 +71,7 @@ impl HyperSyncClient { chain, client: Arc::new(client), blocks_task: None, + swaps_tasks: AHashMap::new(), tx, } } @@ -140,6 +148,7 @@ impl HyperSyncClient { /// Disconnects from the HyperSync service and stops all background tasks. pub fn disconnect(&mut self) { self.unsubscribe_blocks(); + self.unsubscribe_all_swaps(); } /// Returns the current block @@ -238,10 +247,183 @@ impl HyperSyncClient { } } - /// Unsubscribes to the new blocks by stopping the background watch task. + /// Subscribes to swap events for a specific pool address. + pub fn subscribe_pool_swaps(&mut self, pool_address: Address) { + let chain_ref = self.chain.clone(); // Use existing SharedChain + let client = self.client.clone(); + let tx = self.tx.clone(); + + let task = tokio::spawn(async move { + tracing::debug!("Starting task 'swaps_feed' for pool: {pool_address}"); + + // TODO: These objects should be fetched from cache or RPC calls + // For now, create minimal objects just to get compilation working + let dex = std::sync::Arc::new(Dex::new( + (*chain_ref).clone(), + "Uniswap V3", + "0x1F98431c8aD98523631AE4a59f267346ea31F984", // Uniswap V3 factory + AmmType::CLAMM, + "PoolCreated(address,address,uint24,int24,address)", + "Swap(address,address,int256,int256,uint160,uint128,int24)", + "Mint(address,address,int24,int24,uint128,uint256,uint256)", + "Burn(address,int24,int24,uint128,uint256,uint256)", + )); + + let token0 = Token::new( + chain_ref.clone(), + "0xA0b86a33E6441b936662bb6B5d1F8Fb0E2b57A5D" + .parse() + .unwrap(), // WETH + "Wrapped Ether".to_string(), + "WETH".to_string(), + 18, + ); + + let token1 = Token::new( + chain_ref.clone(), + "0xdAC17F958D2ee523a2206206994597C13D831ec7" + .parse() + .unwrap(), // USDT + "Tether USD".to_string(), + "USDT".to_string(), + 6, // USDT has 6 decimals + ); + + let pool = std::sync::Arc::new(Pool::new( + chain_ref.clone(), + (*dex).clone(), + pool_address, + 0, // creation block - TODO: fetch from cache + token0, + token1, + 3000, // 0.3% fee tier + 60, // tick spacing + UnixNanos::default(), + )); + + let current_block_height = client.get_height().await.unwrap(); + let mut query = + Self::construct_pool_swaps_query(pool_address, current_block_height, None); + + loop { + let response = client.get(&query).await.unwrap(); + + // Process logs for swap events + for batch in response.data.logs { + for log in batch { + tracing::debug!( + "Received swap log from pool {pool_address}: topics={:?}, data={:?}, block={:?}, tx_hash={:?}", + log.topics, + log.data, + log.block_number, + log.transaction_hash + ); + match transform_hypersync_swap_log( + chain_ref.clone(), + dex.clone(), + pool.clone(), + UnixNanos::default(), // TODO: block timestamp placeholder + &log, + ) { + Ok(swap) => { + let msg = crate::rpc::types::BlockchainMessage::Swap(swap); + if let Err(e) = tx.send(msg) { + tracing::error!("Error sending swap message: {e}"); + } + } + Err(e) => { + tracing::warn!( + "Failed to transform swap log from pool {pool_address}: {e}" + ); + } + } + } + } + + if let Some(archive_block_height) = response.archive_height { + if archive_block_height < response.next_block { + while client.get_height().await.unwrap() < response.next_block { + tokio::time::sleep(std::time::Duration::from_millis( + BLOCK_POLLING_INTERVAL_MS, + )) + .await; + } + } + } + + query.from_block = response.next_block; + } + }); + + self.swaps_tasks.insert(pool_address, task); + } + + /// Constructs a HyperSync query for fetching swap events from a specific pool. + fn construct_pool_swaps_query( + pool_address: alloy::primitives::Address, + from_block: u64, + to_block: Option, + ) -> Query { + // Uniswap V3 Swap event signature: + // Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick) + let swap_topic = "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"; + + let mut query_value = serde_json::json!({ + "from_block": from_block, + "logs": [{ + "topics": [ + [swap_topic] + ], + "address": [ + pool_address.to_string(), + ] + }], + "field_selection": { + "log": [ + "block_number", + "transaction_hash", + "transaction_index", + "log_index", + "address", + "data", + "topic0", + "topic1", + "topic2", + "topic3", + ] + } + }); + + if let Some(to_block) = to_block { + if let Some(obj) = query_value.as_object_mut() { + obj.insert("to_block".to_string(), serde_json::json!(to_block)); + } + } + + serde_json::from_value(query_value).unwrap() + } + + /// Unsubscribes from swap events for a specific pool address. + pub fn unsubscribe_pool_swaps(&mut self, pool_address: Address) { + if let Some(task) = self.swaps_tasks.remove(&pool_address) { + task.abort(); + tracing::debug!("Unsubscribed from swaps for pool: {}", pool_address); + } + } + + /// Unsubscribes from all swap events by stopping all swap background tasks. + pub fn unsubscribe_all_swaps(&mut self) { + for (pool_address, task) in self.swaps_tasks.drain() { + task.abort(); + tracing::debug!("Unsubscribed from swaps for pool: {}", pool_address); + } + } + + /// Unsubscribes from new blocks by stopping the background watch task. pub fn unsubscribe_blocks(&mut self) { if let Some(task) = self.blocks_task.take() { task.abort(); + tracing::debug!("Unsubscribed from blocks"); } } } diff --git a/crates/adapters/blockchain/src/hypersync/helpers.rs b/crates/adapters/blockchain/src/hypersync/helpers.rs index 5717ea85505f..4e351be6e9ab 100644 --- a/crates/adapters/blockchain/src/hypersync/helpers.rs +++ b/crates/adapters/blockchain/src/hypersync/helpers.rs @@ -24,7 +24,7 @@ pub fn extract_transaction_hash( log.transaction_hash .as_ref() .map(ToString::to_string) - .ok_or_else(|| anyhow::anyhow!("Missing transaction hash in the log")) + .ok_or_else(|| anyhow::anyhow!("Missing transaction hash in log")) } /// Extracts the transaction index from a log entry diff --git a/crates/adapters/blockchain/src/hypersync/transform.rs b/crates/adapters/blockchain/src/hypersync/transform.rs index 3d2f021b9754..16788f4d3d26 100644 --- a/crates/adapters/blockchain/src/hypersync/transform.rs +++ b/crates/adapters/blockchain/src/hypersync/transform.rs @@ -13,13 +13,27 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -use alloy::primitives::U256; +use std::sync::Arc; + +use alloy::primitives::{Address, I256, U256}; use hypersync_client::format::Hex; use nautilus_core::{UnixNanos, datetime::NANOSECONDS_IN_SECOND}; -use nautilus_model::defi::{Block, Blockchain, hex::from_str_hex_to_u64}; +use nautilus_model::{ + defi::{Block, Blockchain, Chain, Dex, Pool, PoolSwap, hex::from_str_hex_to_u64}, + enums::OrderSide, + types::{Price, Quantity}, +}; use ustr::Ustr; -/// Converts a HyperSync block format to our internal Block type. +use crate::{ + decode::{u256_to_price, u256_to_quantity}, + hypersync::helpers::{ + extract_block_number, extract_log_index, extract_transaction_hash, + extract_transaction_index, + }, +}; + +/// Converts a HyperSync block format to our internal [`Block`] type. pub fn transform_hypersync_block( chain: Blockchain, received_block: hypersync_client::simple_types::Block, @@ -100,3 +114,292 @@ pub fn transform_hypersync_block( Ok(block) } + +/// Converts a HyperSync log entry to a [`PoolSwap`] using provided context. +pub fn transform_hypersync_swap_log( + chain_ref: Arc, + dex: Arc, + pool: Arc, + block_timestamp: UnixNanos, + log: &hypersync_client::simple_types::Log, +) -> Result { + let block_number = extract_block_number(log)?; + let transaction_hash = extract_transaction_hash(log)?; + let transaction_index = extract_transaction_index(log)?; + let log_index = extract_log_index(log)?; + + let sender = log + .topics + .get(1) + .and_then(|t| t.as_ref()) + .map(|t| Address::from_slice(&t[12..32])) + .ok_or_else(|| anyhow::anyhow!("Missing sender address in swap log"))?; + + let data = log + .data + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Missing data field in swap log"))?; + + if data.len() < 160 { + // 5 * 32 bytes = 160 bytes minimum + anyhow::bail!("Insufficient data length for Uniswap V3 swap event"); + } + + let amount0_bytes = &data[0..32]; + let amount1_bytes = &data[32..64]; + + // Convert signed integers (int256) - handle negative amounts + let amount0_signed = I256::from_be_bytes::<32>(amount0_bytes.try_into().unwrap()); + let amount1_signed = I256::from_be_bytes::<32>(amount1_bytes.try_into().unwrap()); + + // Get absolute values for quantity calculations + let amount0 = if amount0_signed.is_negative() { + U256::from(-amount0_signed) + } else { + U256::from(amount0_signed) + }; + let amount1 = if amount1_signed.is_negative() { + U256::from(-amount1_signed) + } else { + U256::from(amount1_signed) + }; + + tracing::debug!( + "Raw amounts: amount0_signed={}, amount1_signed={}, amount0={}, amount1={}", + amount0_signed, + amount1_signed, + amount0, + amount1 + ); + + let side = if amount0_signed.is_positive() { + OrderSide::Sell // Selling token0 (pool received token0) + } else { + OrderSide::Buy // Buying token0 (pool gave token0) + }; + + let quantity = if pool.token0.decimals == 18 { + Quantity::from_wei(amount0) + } else { + u256_to_quantity(amount0, pool.token0.decimals)? + }; + + let amount1_quantity = if pool.token1.decimals == 18 { + Quantity::from_wei(amount1) + } else { + u256_to_quantity(amount1, pool.token1.decimals)? + }; + + tracing::debug!( + "Converted amounts: amount0={} -> {} {}, amount1={} -> {} {}", + amount0, + quantity, + pool.token0.symbol, + amount1, + amount1_quantity, + pool.token1.symbol + ); + + let price = if !amount0.is_zero() && !amount1.is_zero() { + let price_precision = pool.token0.decimals.max(pool.token1.decimals); + let scaled_amount1 = amount1 * U256::from(10_u128.pow(u32::from(price_precision))); + let price_raw = scaled_amount1 / amount0; + + if price_precision == 18 { + Price::from_wei(price_raw) + } else { + u256_to_price(price_raw, price_precision)? + } + } else { + anyhow::bail!("Invalid swap: amount0 or amount1 is zero, cannot calculate price"); + }; + + let swap = PoolSwap::new( + chain_ref, + dex, + pool, + block_number, + transaction_hash, + transaction_index, + log_index, + block_timestamp, + sender, + side, + quantity, + price, + ); + + Ok(swap) +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use nautilus_model::defi::{AmmType, Chain, Dex, Token}; + use rstest::rstest; + use serde_json::json; + + use super::*; + + #[rstest] + fn test_transform_hypersync_swap_log() { + let chain = Arc::new(Chain::new(Blockchain::Ethereum, 1)); + + let dex = Arc::new(Dex::new( + (*chain).clone(), + "Uniswap V3", + "0x1F98431c8aD98523631AE4a59f267346ea31F984", + AmmType::CLAMM, + "PoolCreated(address,address,uint24,int24,address)", + "Swap(address,address,int256,int256,uint160,uint128,int24)", + "Mint(address,address,int24,int24,uint128,uint256,uint256)", + "Burn(address,int24,int24,uint128,uint256,uint256)", + )); + + let token0 = Token::new( + chain.clone(), + "0xA0b86a33E6441b936662bb6B5d1F8Fb0E2b57A5D" + .parse() + .unwrap(), + "Wrapped Ether".to_string(), + "WETH".to_string(), + 18, + ); + + let token1 = Token::new( + chain.clone(), + "0xdAC17F958D2ee523a2206206994597C13D831ec7" + .parse() + .unwrap(), + "Tether USD".to_string(), + "USDT".to_string(), + 6, + ); + + let pool = Arc::new(Pool::new( + chain.clone(), + (*dex).clone(), + "0x11b815efB8f581194ae79006d24E0d814B7697F6" + .parse() + .unwrap(), + 12345678, + token0, + token1, + 3000, + 60, + UnixNanos::default(), + )); + + let log_json = json!({ + "block_number": "0x1581b7e", + "transaction_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "transaction_index": "0x5", + "log_index": "0xa", + "data": "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000001dcd6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "topics": [ + "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67", + "0x0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad", + "0x0000000000000000000000003fc91a3afd70395cd496c647d5a6cc9d4b2b7fad" + ] + }); + + let log: hypersync_client::simple_types::Log = + serde_json::from_value(log_json).expect("Failed to deserialize log"); + + let result = transform_hypersync_swap_log( + chain.clone(), + dex.clone(), + pool.clone(), + UnixNanos::default(), + &log, + ); + + assert!( + result.is_ok(), + "Transform should succeed with valid log data" + ); + let swap = result.unwrap(); + + // Assert all fields are correctly transformed + assert_eq!(swap.block, 0x1581b7e); + assert_eq!( + swap.transaction_hash, + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + ); + assert_eq!(swap.transaction_index, 5); + assert_eq!(swap.log_index, 10); + assert_eq!(swap.timestamp, UnixNanos::default()); + assert_eq!( + swap.sender, + "0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad" + .parse() + .unwrap() + ); + assert_eq!(swap.side, OrderSide::Sell); // amount0 is positive (1 ETH), so selling token0 + + // Test data has amount0 = 1 ETH (0x0de0b6b3a7640000) and amount1 = 500 USDT (0x1dcd6500) + // amount0 = 1000000000000000000 wei = 1.0 ETH + assert_eq!(swap.quantity.as_f64(), 1.0); + assert_eq!(swap.quantity.precision, 18); + + // Price should be amount1/amount0 = 500 USDT / 1 ETH = 500.0 + assert_eq!(swap.price.as_f64(), 500.0); + assert_eq!(swap.price.precision, 18); + } + + #[rstest] + fn test_transform_hypersync_block() { + let block_json = json!({ + "number": 0x1581b7e_u64, + "hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "parent_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + "miner": "0x0000000000000000000000000000000000000000", + "gas_limit": "0x1c9c380", + "gas_used": "0x5208", + "timestamp": "0x61bc3f2d" + }); + + let block: hypersync_client::simple_types::Block = + serde_json::from_value(block_json).expect("Failed to deserialize block"); + + let result = transform_hypersync_block(Blockchain::Ethereum, block); + + assert!( + result.is_ok(), + "Transform should succeed with valid block data" + ); + let transformed_block = result.unwrap(); + + assert_eq!(transformed_block.number, 0x1581b7e); + assert_eq!( + transformed_block.hash, + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + ); + assert_eq!( + transformed_block.parent_hash, + "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + ); + assert_eq!( + transformed_block.miner.as_str(), + "0x0000000000000000000000000000000000000000" + ); + assert_eq!(transformed_block.gas_limit, 0x1c9c380); + assert_eq!(transformed_block.gas_used, 0x5208); + + // timestamp 0x61bc3f2d = 1639659309 seconds = 1639659309000000000 nanoseconds + let expected_timestamp = UnixNanos::new(1639659309 * NANOSECONDS_IN_SECOND); + assert_eq!(transformed_block.timestamp, expected_timestamp); + + assert_eq!(transformed_block.chain, Some(Blockchain::Ethereum)); + + // Optional fields should be None when not provided in test data + assert!(transformed_block.base_fee.is_none()); + assert!(transformed_block.blob_gas_used.is_none()); + assert!(transformed_block.excess_blob_gas.is_none()); + } +} diff --git a/crates/adapters/coinbase_intx/README.md b/crates/adapters/coinbase_intx/README.md index 4e1ccd72f614..d15e304f0136 100644 --- a/crates/adapters/coinbase_intx/README.md +++ b/crates/adapters/coinbase_intx/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-coinbase-intx)](https://docs.rs/nautilus-coinbase-intx/latest/nautilus-coinbase-intx/) [![crates.io version](https://img.shields.io/crates/v/nautilus-coinbase-intx.svg)](https://crates.io/crates/nautilus-coinbase-intx) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) ## Platform diff --git a/crates/adapters/databento/README.md b/crates/adapters/databento/README.md index 1b9ff456551e..ae964ca99836 100644 --- a/crates/adapters/databento/README.md +++ b/crates/adapters/databento/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-databento)](https://docs.rs/nautilus-databento/latest/nautilus-databento/) [![crates.io version](https://img.shields.io/crates/v/nautilus-databento.svg)](https://crates.io/crates/nautilus-databento) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) ## Platform diff --git a/crates/adapters/tardis/README.md b/crates/adapters/tardis/README.md index 0b94f0a06f33..b37117b9067a 100644 --- a/crates/adapters/tardis/README.md +++ b/crates/adapters/tardis/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-tardis)](https://docs.rs/nautilus-tardis/latest/nautilus-tardis/) [![crates.io version](https://img.shields.io/crates/v/nautilus-tardis.svg)](https://crates.io/crates/nautilus-tardis) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) ## Platform diff --git a/crates/adapters/tardis/src/enums.rs b/crates/adapters/tardis/src/enums.rs index 8c5ec7deb11f..5513039a9e99 100644 --- a/crates/adapters/tardis/src/enums.rs +++ b/crates/adapters/tardis/src/enums.rs @@ -134,6 +134,7 @@ pub enum Exchange { Binance, BinanceDelivery, BinanceDex, + BinanceEuropeanOptions, BinanceFutures, BinanceJersey, BinanceOptions, @@ -141,6 +142,8 @@ pub enum Exchange { Bitfinex, BitfinexDerivatives, Bitflyer, + Bitget, + BitgetFutures, Bitmex, Bitnomial, Bitstamp, @@ -149,6 +152,7 @@ pub enum Exchange { BybitOptions, BybitSpot, Coinbase, + CoinbaseInternational, Coinflex, CryptoCom, CryptoComDerivatives, @@ -156,6 +160,7 @@ pub enum Exchange { Delta, Deribit, Dydx, + DydxV4, Ftx, FtxUs, GateIo, @@ -167,13 +172,17 @@ pub enum Exchange { HuobiDmLinearSwap, HuobiDmOptions, HuobiDmSwap, + Hyperliquid, Kraken, + KrakenFutures, Kucoin, + KucoinFutures, Mango, Okcoin, Okex, OkexFutures, OkexOptions, + OkexSpreads, OkexSwap, Phemex, Poloniex, @@ -192,6 +201,7 @@ impl Exchange { "BINANCE" => vec![ Self::Binance, Self::BinanceDex, + Self::BinanceEuropeanOptions, Self::BinanceFutures, Self::BinanceJersey, Self::BinanceOptions, @@ -199,6 +209,7 @@ impl Exchange { "BINANCE_DELIVERY" => vec![Self::BinanceDelivery], "BINANCE_US" => vec![Self::BinanceUs], "BITFINEX" => vec![Self::Bitfinex, Self::BitfinexDerivatives], + "BITGET" => vec![Self::Bitget, Self::BitgetFutures], "BITFLYER" => vec![Self::Bitflyer], "BITMEX" => vec![Self::Bitmex], "BITNOMIAL" => vec![Self::Bitnomial], @@ -206,12 +217,14 @@ impl Exchange { "BLOCKCHAIN_COM" => vec![Self::BlockchainCom], "BYBIT" => vec![Self::Bybit, Self::BybitOptions, Self::BybitSpot], "COINBASE" => vec![Self::Coinbase], + "COINBASE_INTX" => vec![Self::CoinbaseInternational], "COINFLEX" => vec![Self::Coinflex], "CRYPTO_COM" => vec![Self::CryptoCom, Self::CryptoComDerivatives], "CRYPTOFACILITIES" => vec![Self::Cryptofacilities], "DELTA" => vec![Self::Delta], "DERIBIT" => vec![Self::Deribit], "DYDX" => vec![Self::Dydx], + "DYDX_V4" => vec![Self::DydxV4], "FTX" => vec![Self::Ftx, Self::FtxUs], "GATE_IO" => vec![Self::GateIo, Self::GateIoFutures], "GEMINI" => vec![Self::Gemini], @@ -223,14 +236,16 @@ impl Exchange { Self::HuobiDmOptions, ], "HUOBI_DELIVERY" => vec![Self::HuobiDmSwap], - "KRAKEN" => vec![Self::Kraken], - "KUCOIN" => vec![Self::Kucoin], + "HYPERLIQUID" => vec![Self::Hyperliquid], + "KRAKEN" => vec![Self::Kraken, Self::KrakenFutures], + "KUCOIN" => vec![Self::Kucoin, Self::KucoinFutures], "MANGO" => vec![Self::Mango], "OKCOIN" => vec![Self::Okcoin], "OKEX" => vec![ Self::Okex, Self::OkexFutures, Self::OkexOptions, + Self::OkexSpreads, Self::OkexSwap, ], "PHEMEX" => vec![Self::Phemex], @@ -250,6 +265,7 @@ impl Exchange { Self::Binance => "BINANCE", Self::BinanceDelivery => "BINANCE_DELIVERY", Self::BinanceDex => "BINANCE", + Self::BinanceEuropeanOptions => "BINANCE", Self::BinanceFutures => "BINANCE", Self::BinanceJersey => "BINANCE", Self::BinanceOptions => "BINANCE", @@ -257,6 +273,8 @@ impl Exchange { Self::Bitfinex => "BITFINEX", Self::BitfinexDerivatives => "BITFINEX", Self::Bitflyer => "BITFLYER", + Self::Bitget => "BITGET", + Self::BitgetFutures => "BITGET", Self::Bitmex => "BITMEX", Self::Bitnomial => "BITNOMIAL", Self::Bitstamp => "BITSTAMP", @@ -265,6 +283,7 @@ impl Exchange { Self::BybitOptions => "BYBIT", Self::BybitSpot => "BYBIT", Self::Coinbase => "COINBASE", + Self::CoinbaseInternational => "COINBASE_INTX", Self::Coinflex => "COINFLEX", Self::CryptoCom => "CRYPTO_COM", Self::CryptoComDerivatives => "CRYPTO_COM", @@ -272,6 +291,7 @@ impl Exchange { Self::Delta => "DELTA", Self::Deribit => "DERIBIT", Self::Dydx => "DYDX", + Self::DydxV4 => "DYDX_V4", Self::Ftx => "FTX", Self::FtxUs => "FTX", Self::GateIo => "GATE_IO", @@ -283,13 +303,17 @@ impl Exchange { Self::HuobiDmLinearSwap => "HUOBI", Self::HuobiDmOptions => "HUOBI", Self::HuobiDmSwap => "HUOBI_DELIVERY", + Self::Hyperliquid => "HYPERLIQUID", Self::Kraken => "KRAKEN", + Self::KrakenFutures => "KRAKEN", Self::Kucoin => "KUCOIN", + Self::KucoinFutures => "KUCOIN", Self::Mango => "MANGO", Self::Okcoin => "OKCOIN", Self::Okex => "OKEX", Self::OkexFutures => "OKEX", Self::OkexOptions => "OKEX", + Self::OkexSpreads => "OKEX", Self::OkexSwap => "OKEX", Self::Phemex => "PHEMEX", Self::Poloniex => "POLONIEX", diff --git a/crates/backtest/README.md b/crates/backtest/README.md index 8f4d755ef54b..924238548aae 100644 --- a/crates/backtest/README.md +++ b/crates/backtest/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-backtest)](https://docs.rs/nautilus-backtest/latest/nautilus-backtest/) [![crates.io version](https://img.shields.io/crates/v/nautilus-backtest.svg)](https://crates.io/crates/nautilus-backtest) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Backtest engine for [NautilusTrader](http://nautilustrader.io). diff --git a/crates/cli/README.md b/crates/cli/README.md index 7e75e2cd8a90..053dd6192540 100644 --- a/crates/cli/README.md +++ b/crates/cli/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-cli)](https://docs.rs/nautilus-cli/latest/nautilus-cli/) [![crates.io version](https://img.shields.io/crates/v/nautilus-cli.svg)](https://crates.io/crates/nautilus-cli) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Command-line interface and tools for [NautilusTrader](http://nautilustrader.io). diff --git a/crates/common/README.md b/crates/common/README.md index a5114f82698e..88f91b65fa6a 100644 --- a/crates/common/README.md +++ b/crates/common/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-common)](https://docs.rs/nautilus-common/latest/nautilus-common/) [![crates.io version](https://img.shields.io/crates/v/nautilus-common.svg)](https://crates.io/crates/nautilus-common) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Common componentry for [NautilusTrader](http://nautilustrader.io). @@ -30,7 +31,7 @@ or as part of a Rust only build. - `ffi`: Enables the C foreign function interface (FFI) from [cbindgen](https://github.com/mozilla/cbindgen). - `python`: Enables Python bindings from [PyO3](https://pyo3.rs). -- `stubs`: Enables type stubs for use in testing scenarios. +- `defi`: Enables DeFi (Decentralized Finance) support. ## Documentation diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index ec026701aefa..bd28bf277e20 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -39,6 +39,7 @@ //! - `ffi`: Enables the C foreign function interface (FFI) from [cbindgen](https://github.com/mozilla/cbindgen). //! - `python`: Enables Python bindings from [PyO3](https://pyo3.rs). //! - `stubs`: Enables type stubs for use in testing scenarios. +//! - `defi`: Enables DeFi (Decentralized Finance) support. #![warn(rustc::all)] #![deny(unsafe_code)] diff --git a/crates/common/src/messages/defi/mod.rs b/crates/common/src/messages/defi/mod.rs index a41eed744af3..cd3547cfeb46 100644 --- a/crates/common/src/messages/defi/mod.rs +++ b/crates/common/src/messages/defi/mod.rs @@ -39,11 +39,7 @@ pub enum DefiDataCommand { impl PartialEq for DefiDataCommand { fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Subscribe(cmd1), Self::Subscribe(cmd2)) => cmd1 == cmd2, - (Self::Unsubscribe(cmd1), Self::Unsubscribe(cmd2)) => cmd1 == cmd2, - _ => false, - } + self.command_id() == other.command_id() } } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index e075c4ac2e05..641cfdf61c7d 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -37,6 +37,7 @@ indexmap = { workspace = true } pyo3 = { workspace = true, optional = true } rand = { workspace = true } rmp-serde = { workspace = true } +rust_decimal = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } strum = { workspace = true, optional = true } diff --git a/crates/core/README.md b/crates/core/README.md index 7452f0716bcf..5a5e02b8d305 100644 --- a/crates/core/README.md +++ b/crates/core/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-core)](https://docs.rs/nautilus-core/latest/nautilus-core/) [![crates.io version](https://img.shields.io/crates/v/nautilus-core.svg)](https://crates.io/crates/nautilus-core) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Core foundational types and utilities for [NautilusTrader](http://nautilustrader.io). diff --git a/crates/core/src/correctness.rs b/crates/core/src/correctness.rs index 4c38fa612e10..feb0aa9aafd1 100644 --- a/crates/core/src/correctness.rs +++ b/crates/core/src/correctness.rs @@ -25,6 +25,8 @@ use std::fmt::{Debug, Display}; +use rust_decimal::Decimal; + use crate::collections::{MapLike, SetLike}; /// A message prefix that can be used with calls to `expect` or other assertion-related functions. @@ -507,6 +509,19 @@ where Ok(()) } +/// Checks the `Decimal` value is positive (> 0). +/// +/// # Errors +/// +/// Returns an error if the validation check fails. +#[inline(always)] +pub fn check_positive_decimal(value: Decimal, param: &str) -> anyhow::Result<()> { + if value <= Decimal::ZERO { + anyhow::bail!("invalid Decimal for '{param}' not positive, was {value}") + } + Ok(()) +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// @@ -515,9 +530,11 @@ mod tests { use std::{ collections::{HashMap, HashSet}, fmt::Display, + str::FromStr, }; use rstest::rstest; + use rust_decimal::Decimal; use super::*; @@ -906,4 +923,17 @@ mod tests { let result = check_member_in_set(&member, set, member_name, set_name).is_ok(); assert_eq!(result, expected); } + + #[rstest] + #[case("1", true)] // simple positive integer + #[case("0.0000000000000000000000000001", true)] // smallest positive (1 × 10⁻²⁸) + #[case("79228162514264337593543950335", true)] // very large positive (≈ Decimal::MAX) + #[case("0", false)] // zero should fail + #[case("-0.0000000000000000000000000001", false)] // tiny negative + #[case("-1", false)] // simple negative integer + fn test_check_positive_decimal(#[case] raw: &str, #[case] expected: bool) { + let value = Decimal::from_str(raw).expect("valid decimal literal"); + let result = super::check_positive_decimal(value, "param").is_ok(); + assert_eq!(result, expected); + } } diff --git a/crates/cryptography/README.md b/crates/cryptography/README.md index ba37ee08c089..442be3204fec 100644 --- a/crates/cryptography/README.md +++ b/crates/cryptography/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-cryptography)](https://docs.rs/nautilus-cryptography/latest/nautilus-cryptography/) [![crates.io version](https://img.shields.io/crates/v/nautilus-cryptography.svg)](https://crates.io/crates/nautilus-cryptography) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Cryptographic utilities and security functions for [NautilusTrader](http://nautilustrader.io). diff --git a/crates/data/README.md b/crates/data/README.md index fc01339290d6..ab57ffa19589 100644 --- a/crates/data/README.md +++ b/crates/data/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-data)](https://docs.rs/nautilus-data/latest/nautilus-data/) [![crates.io version](https://img.shields.io/crates/v/nautilus-data.svg)](https://crates.io/crates/nautilus-data) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Data engine and market data processing for [NautilusTrader](http://nautilustrader.io). @@ -37,6 +38,8 @@ or as part of a Rust only build. - `ffi`: Enables the C foreign function interface (FFI) from [cbindgen](https://github.com/mozilla/cbindgen). - `python`: Enables Python bindings from [PyO3](https://pyo3.rs). +- `high-precision`: Enables [high-precision mode](https://nautilustrader.io/docs/nightly/getting_started/installation#precision-mode) to use 128-bit value types. +- `defi`: Enables DeFi (Decentralized Finance) support. ## Documentation diff --git a/crates/data/src/lib.rs b/crates/data/src/lib.rs index f2b16252c0e2..470def3b0529 100644 --- a/crates/data/src/lib.rs +++ b/crates/data/src/lib.rs @@ -45,6 +45,8 @@ //! //! - `ffi`: Enables the C foreign function interface (FFI) from [cbindgen](https://github.com/mozilla/cbindgen). //! - `python`: Enables Python bindings from [PyO3](https://pyo3.rs). +//! - `high-precision`: Enables [high-precision mode](https://nautilustrader.io/docs/nightly/getting_started/installation#precision-mode) to use 128-bit value types. +//! - `defi`: Enables DeFi (Decentralized Finance) support. #![warn(rustc::all)] #![deny(unsafe_code)] diff --git a/crates/data/tests/engine.rs b/crates/data/tests/engine.rs index 53e1ce874aa4..9540edcc24cc 100644 --- a/crates/data/tests/engine.rs +++ b/crates/data/tests/engine.rs @@ -1672,8 +1672,8 @@ fn test_process_pool_swap(data_engine: Rc>, data_client: Dat dex.clone(), address, 0u64, - token0.clone(), - token1.clone(), + token0, + token1, 500u32, 10u32, UnixNanos::from(1), diff --git a/crates/execution/README.md b/crates/execution/README.md index 174bf77d1f7b..643b1a29d38b 100644 --- a/crates/execution/README.md +++ b/crates/execution/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-execution)](https://docs.rs/nautilus-execution/latest/nautilus-execution/) [![crates.io version](https://img.shields.io/crates/v/nautilus-execution.svg)](https://crates.io/crates/nautilus-execution) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Order execution engine for [NautilusTrader](http://nautilustrader.io). diff --git a/crates/indicators/README.md b/crates/indicators/README.md index d4cd5fc41359..a2ddaa464847 100644 --- a/crates/indicators/README.md +++ b/crates/indicators/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-indicators)](https://docs.rs/nautilus-indicators/latest/nautilus-indicators/) [![crates.io version](https://img.shields.io/crates/v/nautilus-indicators.svg)](https://crates.io/crates/nautilus-indicators) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Technical analysis indicators for [NautilusTrader](http://nautilustrader.io). diff --git a/crates/infrastructure/README.md b/crates/infrastructure/README.md index 2db56db1b671..5889b0c5e055 100644 --- a/crates/infrastructure/README.md +++ b/crates/infrastructure/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-infrastructure)](https://docs.rs/nautilus-infrastructure/latest/nautilus-infrastructure/) [![crates.io version](https://img.shields.io/crates/v/nautilus-infrastructure.svg)](https://crates.io/crates/nautilus-infrastructure) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Database and messaging infrastructure for [NautilusTrader](http://nautilustrader.io). diff --git a/crates/live/README.md b/crates/live/README.md index 7d84bef24823..c162992c9e40 100644 --- a/crates/live/README.md +++ b/crates/live/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-live)](https://docs.rs/nautilus-live/latest/nautilus-live/) [![crates.io version](https://img.shields.io/crates/v/nautilus-live.svg)](https://crates.io/crates/nautilus-live) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Live system node for [NautilusTrader](http://nautilustrader.io). @@ -25,6 +26,17 @@ and also deploy those same strategies live, with no code changes. NautilusTrader's design, architecture, and implementation philosophy prioritizes software correctness and safety at the highest level, with the aim of supporting mission-critical, trading system backtesting and live deployment workloads. +## Feature flags + +This crate provides feature flags to control source code inclusion during compilation, +depending on the intended use case, i.e. whether to provide Python bindings +for the [nautilus_trader](https://pypi.org/project/nautilus_trader) Python package, +or as part of a Rust only build. + +- `ffi`: Enables the C foreign function interface (FFI) from [cbindgen](https://github.com/mozilla/cbindgen). +- `python`: Enables Python bindings from [PyO3](https://pyo3.rs). +- `defi`: Enables DeFi (Decentralized Finance) support. + ## Documentation See [the docs](https://docs.rs/nautilus-live) for more detailed usage. diff --git a/crates/live/src/lib.rs b/crates/live/src/lib.rs index 27b9d55a2288..7e683b242f37 100644 --- a/crates/live/src/lib.rs +++ b/crates/live/src/lib.rs @@ -42,7 +42,7 @@ //! //! - `ffi`: Enables the C foreign function interface (FFI) from [cbindgen](https://github.com/mozilla/cbindgen). //! - `python`: Enables Python bindings from [PyO3](https://pyo3.rs). -//! - `high-precision`: Enables [high-precision mode](https://nautilustrader.io/docs/nightly/getting_started/installation#precision-mode) to use 128-bit value types. +//! - `defi`: Enables DeFi (Decentralized Finance) support. #![warn(rustc::all)] #![deny(unsafe_code)] diff --git a/crates/model/README.md b/crates/model/README.md index c9a26bbdea87..b029458ffad4 100644 --- a/crates/model/README.md +++ b/crates/model/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-model)](https://docs.rs/nautilus-model/latest/nautilus-model/) [![crates.io version](https://img.shields.io/crates/v/nautilus-model.svg)](https://crates.io/crates/nautilus-model) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Trading domain model for [NautilusTrader](http://nautilustrader.io). @@ -31,6 +32,7 @@ or as part of a Rust only build. - `python`: Enables Python bindings from [PyO3](https://pyo3.rs). - `stubs`: Enables type stubs for use in testing scenarios. - `high-precision`: Enables [high-precision mode](https://nautilustrader.io/docs/nightly/getting_started/installation#precision-mode) to use 128-bit value types. +- `defi`: Enables the DeFi (Decentralized Finance) domain model. ## Documentation diff --git a/crates/model/src/instruments/mod.rs b/crates/model/src/instruments/mod.rs index a8c7d9f2a8f5..aaa3bb1c1735 100644 --- a/crates/model/src/instruments/mod.rs +++ b/crates/model/src/instruments/mod.rs @@ -32,13 +32,18 @@ pub mod synthetic; #[cfg(any(test, feature = "stubs"))] pub mod stubs; +use std::{fmt::Display, str::FromStr}; + +use anyhow::{anyhow, bail}; use enum_dispatch::enum_dispatch; -use nautilus_core::UnixNanos; -use rust_decimal::Decimal; +use nautilus_core::{ + UnixNanos, + correctness::{check_equal_u8, check_positive_decimal, check_predicate_true}, +}; +use rust_decimal::{Decimal, RoundingStrategy, prelude::*}; use rust_decimal_macros::dec; use ustr::Ustr; -// Re-exports pub use crate::instruments::{ any::InstrumentAny, betting::BettingInstrument, binary_option::BinaryOption, crypto_future::CryptoFuture, crypto_option::CryptoOption, crypto_perpetual::CryptoPerpetual, @@ -49,12 +54,208 @@ pub use crate::instruments::{ use crate::{ enums::{AssetClass, InstrumentClass, OptionKind}, identifiers::{InstrumentId, Symbol, Venue}, - types::{Currency, Money, Price, Quantity}, + types::{ + Currency, Money, Price, Quantity, money::check_positive_money, price::check_positive_price, + quantity::check_positive_quantity, + }, }; +#[allow(clippy::missing_errors_doc, clippy::too_many_arguments)] +pub fn validate_instrument_common( + price_precision: u8, + size_precision: u8, + size_increment: Quantity, + multiplier: Quantity, + margin_init: Decimal, + margin_maint: Decimal, + price_increment: Option, + lot_size: Option, + max_quantity: Option, + min_quantity: Option, + max_notional: Option, + min_notional: Option, + max_price: Option, + min_price: Option, +) -> anyhow::Result<()> { + check_positive_quantity(size_increment, "size_increment")?; + check_equal_u8( + size_increment.precision, + size_precision, + "size_increment.precision", + "size_precision", + )?; + check_positive_quantity(multiplier, "multiplier")?; + check_positive_decimal(margin_init, "margin_init")?; + check_positive_decimal(margin_maint, "margin_maint")?; + + if let Some(price_increment) = price_increment { + check_positive_price(price_increment, "price_increment")?; + check_equal_u8( + price_increment.precision, + price_precision, + "price_increment.precision", + "price_precision", + )?; + } + + if let Some(lot) = lot_size { + check_positive_quantity(lot, "lot_size")?; + } + + if let Some(quantity) = max_quantity { + check_positive_quantity(quantity, "max_quantity")?; + } + + if let Some(quantity) = min_quantity { + check_positive_quantity(quantity, "max_quantity")?; + } + + if let Some(notional) = max_notional { + check_positive_money(notional, "max_notional")?; + } + + if let Some(notional) = min_notional { + check_positive_money(notional, "min_notional")?; + } + + if let Some(max_price) = max_price { + check_positive_price(max_price, "max_price")?; + check_equal_u8( + max_price.precision, + price_precision, + "max_price.precision", + "price_precision", + )?; + } + if let Some(min_price) = min_price { + check_positive_price(min_price, "min_price")?; + check_equal_u8( + min_price.precision, + price_precision, + "min_price.precision", + "price_precision", + )?; + } + + if let (Some(min), Some(max)) = (min_price, max_price) { + check_predicate_true(min.raw <= max.raw, "min_price exceeds max_price")?; + } + + Ok(()) +} + +pub trait TickSchemeRule: Display { + fn next_bid_price(&self, value: f64, n: i32, precision: u8) -> Option; + fn next_ask_price(&self, value: f64, n: i32, precision: u8) -> Option; +} + +#[derive(Clone, Copy, Debug)] +pub struct FixedTickScheme { + tick: f64, +} + +impl PartialEq for FixedTickScheme { + fn eq(&self, other: &Self) -> bool { + self.tick == other.tick + } +} +impl Eq for FixedTickScheme {} + +impl FixedTickScheme { + #[allow(clippy::missing_errors_doc)] + pub fn new(tick: f64) -> anyhow::Result { + check_predicate_true(tick > 0.0, "tick must be positive")?; + Ok(Self { tick }) + } +} + +impl TickSchemeRule for FixedTickScheme { + #[inline(always)] + fn next_bid_price(&self, value: f64, n: i32, precision: u8) -> Option { + let base = (value / self.tick).floor() * self.tick; + Some(Price::new(base - (n as f64) * self.tick, precision)) + } + + #[inline(always)] + fn next_ask_price(&self, value: f64, n: i32, precision: u8) -> Option { + let base = (value / self.tick).ceil() * self.tick; + Some(Price::new(base + (n as f64) * self.tick, precision)) + } +} + +impl Display for FixedTickScheme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "FIXED") + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TickScheme { + Fixed(FixedTickScheme), + Crypto, +} + +impl TickSchemeRule for TickScheme { + #[inline(always)] + fn next_bid_price(&self, value: f64, n: i32, precision: u8) -> Option { + match self { + TickScheme::Fixed(scheme) => scheme.next_bid_price(value, n, precision), + TickScheme::Crypto => { + let increment: f64 = 0.01; + let base = (value / increment).floor() * increment; + Some(Price::new(base - (n as f64) * increment, precision)) + } + } + } + + #[inline(always)] + fn next_ask_price(&self, value: f64, n: i32, precision: u8) -> Option { + match self { + TickScheme::Fixed(scheme) => scheme.next_ask_price(value, n, precision), + TickScheme::Crypto => { + let increment: f64 = 0.01; + let base = (value / increment).ceil() * increment; + Some(Price::new(base + (n as f64) * increment, precision)) + } + } + } +} + +impl Display for TickScheme { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TickScheme::Fixed(_) => write!(f, "FIXED"), + TickScheme::Crypto => write!(f, "CRYPTO_0_01"), + } + } +} + +impl FromStr for TickScheme { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.trim().to_ascii_uppercase().as_str() { + "FIXED" => Ok(TickScheme::Fixed(FixedTickScheme::new(1.0)?)), + "CRYPTO_0_01" => Ok(TickScheme::Crypto), + _ => Err(anyhow!("unknown tick scheme {}", s)), + } + } +} + #[enum_dispatch] pub trait Instrument: 'static + Send { - fn into_any(self) -> InstrumentAny; + fn tick_scheme(&self) -> Option<&dyn TickSchemeRule> { + None + } + + fn into_any(self) -> InstrumentAny + where + Self: Sized, + InstrumentAny: From, + { + self.into() + } + fn id(&self) -> InstrumentId; fn symbol(&self) -> Symbol { self.id().symbol @@ -62,39 +263,48 @@ pub trait Instrument: 'static + Send { fn venue(&self) -> Venue { self.id().venue } + fn raw_symbol(&self) -> Symbol; fn asset_class(&self) -> AssetClass; fn instrument_class(&self) -> InstrumentClass; + fn underlying(&self) -> Option; fn base_currency(&self) -> Option; fn quote_currency(&self) -> Currency; fn settlement_currency(&self) -> Currency; + + /// # Panics + /// + /// Panics if the instrument is inverse and does not have a base currency. fn cost_currency(&self) -> Currency { if self.is_inverse() { self.base_currency() - .expect("Inverse instruments must have a base currency") + .expect("inverse instrument without base_currency") } else { self.quote_currency() } } + fn isin(&self) -> Option; fn option_kind(&self) -> Option; fn exchange(&self) -> Option; fn strike_price(&self) -> Option; + fn activation_ns(&self) -> Option; fn expiration_ns(&self) -> Option; + fn is_inverse(&self) -> bool; fn is_quanto(&self) -> bool { - if let Some(base_currency) = self.base_currency() { - self.settlement_currency() != base_currency - } else { - false - } + self.base_currency() + .map(|currency| currency != self.settlement_currency()) + .unwrap_or(false) } + fn price_precision(&self) -> u8; fn size_precision(&self) -> u8; fn price_increment(&self) -> Price; fn size_increment(&self) -> Quantity; + fn multiplier(&self) -> Quantity; fn lot_size(&self) -> Option; fn max_quantity(&self) -> Option; @@ -103,79 +313,230 @@ pub trait Instrument: 'static + Send { fn min_notional(&self) -> Option; fn max_price(&self) -> Option; fn min_price(&self) -> Option; + fn margin_init(&self) -> Decimal { - dec!(0) // Temporary until separate fee models + dec!(0) } - fn margin_maint(&self) -> Decimal { - dec!(0) // Temporary until separate fee models + dec!(0) } - fn maker_fee(&self) -> Decimal { - dec!(0) // Temporary until separate fee models + dec!(0) } - fn taker_fee(&self) -> Decimal { - dec!(0) // Temporary until separate fee models + dec!(0) } + fn ts_event(&self) -> UnixNanos; fn ts_init(&self) -> UnixNanos; - /// Creates a new [`Price`] from the given `value` with the correct price precision for the instrument. + fn _min_price_increment_precision(&self) -> u8 { + self.price_increment().precision + } + + /// # Errors + /// + /// Returns an error if the value is not finite or cannot be converted to a `Price`. + #[inline(always)] + fn try_make_price(&self, value: f64) -> anyhow::Result { + check_predicate_true(value.is_finite(), "non-finite value passed to make_price")?; + let precision = self + .price_precision() + .min(self._min_price_increment_precision()) as u32; + let decimal_value = Decimal::from_f64_retain(value) + .ok_or_else(|| anyhow!("non-finite value passed to make_price"))?; + let rounded_decimal = + decimal_value.round_dp_with_strategy(precision, RoundingStrategy::MidpointNearestEven); + let rounded = rounded_decimal + .to_f64() + .ok_or_else(|| anyhow!("Decimal out of f64 range in make_price"))?; + Ok(Price::new(rounded, self.price_precision())) + } + fn make_price(&self, value: f64) -> Price { - Price::new(value, self.price_precision()) + self.try_make_price(value).unwrap() } - /// Creates a new [`Quantity`] from the given `value` with the correct size precision for the instrument. - fn make_qty(&self, value: f64, round_down: Option) -> Quantity { - if round_down.unwrap_or(false) { - // Round down to the nearest valid increment - let increment = 10f64.powi(-i32::from(self.size_precision())); - let rounded_value = (value / increment).floor() * increment; - Quantity::new(rounded_value, self.size_precision()) + /// # Errors + /// + /// Returns an error if the value is not finite or cannot be converted to a `Quantity`. + #[inline(always)] + fn try_make_qty(&self, value: f64, round_down: Option) -> anyhow::Result { + let precision_u8 = self.size_precision(); + let precision = precision_u8 as u32; + let decimal_value = Decimal::from_f64_retain(value) + .ok_or_else(|| anyhow!("non-finite value passed to make_qty"))?; + let rounded_decimal = if round_down.unwrap_or(false) { + decimal_value.round_dp_with_strategy(precision, RoundingStrategy::ToZero) } else { - // Use standard rounding behavior (banker's rounding) - Quantity::new(value, self.size_precision()) + decimal_value.round_dp_with_strategy(precision, RoundingStrategy::MidpointNearestEven) + }; + let rounded = rounded_decimal + .to_f64() + .ok_or_else(|| anyhow!("Decimal out of f64 range in make_qty"))?; + let increment = 10f64.powi(-(precision_u8 as i32)); + if value > 0.0 && rounded < increment * 0.1 { + bail!("value rounded to zero for quantity"); } + Ok(Quantity::new(rounded, precision_u8)) } - /// Calculates the notional value from the given parameters. - /// The `use_quote_for_inverse` flag is only applicable for inverse instruments. + fn make_qty(&self, value: f64, round_down: Option) -> Quantity { + self.try_make_qty(value, round_down).unwrap() + } + + /// # Errors /// + /// Returns an error if the quantity or price is not finite or cannot be converted to a `Quantity`. + fn try_calculate_base_quantity( + &self, + quantity: Quantity, + last_price: Price, + ) -> anyhow::Result { + check_predicate_true( + quantity.as_f64().is_finite(), + "non-finite quantity passed to calculate_base_quantity", + )?; + check_predicate_true( + last_price.as_f64().is_finite(), + "non-finite price passed to calculate_base_quantity", + )?; + let quantity_decimal = Decimal::from_f64_retain(quantity.as_f64()) + .ok_or_else(|| anyhow!("non-finite quantity passed to calculate_base_quantity"))?; + let price_decimal = Decimal::from_f64_retain(last_price.as_f64()) + .ok_or_else(|| anyhow!("non-finite price passed to calculate_base_quantity"))?; + let value_decimal = (quantity_decimal / price_decimal).round_dp_with_strategy( + self.size_precision().into(), + RoundingStrategy::MidpointNearestEven, + ); + let rounded = value_decimal + .to_f64() + .ok_or_else(|| anyhow!("Decimal out of f64 range in calculate_base_quantity"))?; + Ok(Quantity::new(rounded, self.size_precision())) + } + + fn calculate_base_quantity(&self, quantity: Quantity, last_price: Price) -> Quantity { + self.try_calculate_base_quantity(quantity, last_price) + .unwrap() + } + /// # Panics /// - /// This function panics if instrument is inverse and not `use_quote_for_inverse`, with no base currency. + /// Panics if the instrument is inverse and does not have a base currency. + #[inline(always)] fn calculate_notional_value( &self, quantity: Quantity, price: Price, use_quote_for_inverse: Option, ) -> Money { - let use_quote_for_inverse = use_quote_for_inverse.unwrap_or(false); - let (amount, currency) = if self.is_inverse() { - if use_quote_for_inverse { - (quantity.as_f64(), self.quote_currency()) + let use_quote_inverse = use_quote_for_inverse.unwrap_or(false); + if self.is_inverse() { + if use_quote_inverse { + Money::new(quantity.as_f64(), self.quote_currency()) } else { let amount = quantity.as_f64() * self.multiplier().as_f64() * (1.0 / price.as_f64()); let currency = self .base_currency() - .expect("Error: no base currency for notional calculation"); - (amount, currency) + .expect("inverse instrument without base_currency"); + Money::new(amount, currency) } + } else if self.is_quanto() { + let amount = quantity.as_f64() * self.multiplier().as_f64() * price.as_f64(); + Money::new(amount, self.settlement_currency()) } else { let amount = quantity.as_f64() * self.multiplier().as_f64() * price.as_f64(); - let currency = self.quote_currency(); - (amount, currency) + Money::new(amount, self.quote_currency()) + } + } + + #[inline(always)] + fn next_bid_price(&self, value: f64, n: i32) -> Option { + let price = if let Some(scheme) = self.tick_scheme() { + scheme.next_bid_price(value, n, self.price_precision())? + } else { + let increment = self.price_increment().as_f64().abs(); + if increment == 0.0 { + return None; + } + let base = (value / increment).floor() * increment; + Price::new(base - (n as f64) * increment, self.price_precision()) }; + if self.min_price().is_some_and(|min| price < min) + || self.max_price().is_some_and(|max| price > max) + { + return None; + } + Some(price) + } - Money::new(amount, currency) + #[inline(always)] + fn next_ask_price(&self, value: f64, n: i32) -> Option { + let price = if let Some(scheme) = self.tick_scheme() { + scheme.next_ask_price(value, n, self.price_precision())? + } else { + let increment = self.price_increment().as_f64().abs(); + if increment == 0.0 { + return None; + } + let base = (value / increment).ceil() * increment; + Price::new(base + (n as f64) * increment, self.price_precision()) + }; + if self.min_price().is_some_and(|min| price < min) + || self.max_price().is_some_and(|max| price > max) + { + return None; + } + Some(price) } - /// Returns the equivalent quantity of the base asset. - fn calculate_base_quantity(&self, quantity: Quantity, last_px: Price) -> Quantity { - let value = quantity.as_f64() * (1.0 / last_px.as_f64()); - Quantity::new(value, self.size_precision()) + #[inline] + fn next_bid_prices(&self, value: f64, n: usize) -> Vec { + let mut prices = Vec::with_capacity(n); + for i in 0..n { + if let Some(price) = self.next_bid_price(value, i as i32) { + prices.push(price); + } else { + break; + } + } + prices + } + + #[inline] + fn next_ask_prices(&self, value: f64, n: usize) -> Vec { + let mut prices = Vec::with_capacity(n); + for i in 0..n { + if let Some(price) = self.next_ask_price(value, i as i32) { + prices.push(price); + } else { + break; + } + } + prices + } +} + +impl Display for CurrencyPair { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}(instrument_id='{}', tick_scheme='{}', price_precision={}, size_precision={}, \ +price_increment={}, size_increment={}, multiplier={}, margin_init={}, margin_maint={})", + stringify!(CurrencyPair), + self.id, + self.tick_scheme() + .map(|s| s.to_string()) + .unwrap_or_else(|| "None".into()), + self.price_precision(), + self.size_precision(), + self.price_increment(), + self.size_increment(), + self.multiplier(), + self.margin_init(), + self.margin_maint(), + ) } } @@ -186,113 +547,732 @@ pub const EXPIRING_INSTRUMENT_TYPES: [InstrumentClass; 4] = [ InstrumentClass::OptionSpread, ]; +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// + #[cfg(test)] mod tests { + use std::str::FromStr; + + use proptest::prelude::*; use rstest::rstest; + use rust_decimal::Decimal; - use crate::instruments::{CurrencyPair, Instrument, stubs::*}; + use super::*; + use crate::{instruments::stubs::*, types::Money}; + + pub fn default_price_increment(precision: u8) -> Price { + let step = 10f64.powi(-(precision as i32)); + Price::new(step, precision) + } #[rstest] - fn test_make_qty_standard_rounding(currency_pair_btcusdt: CurrencyPair) { - assert_eq!( - currency_pair_btcusdt.make_qty(1.5, None).to_string(), - "1.500000" - ); // 1.5 -> 1.500000 - assert_eq!( - currency_pair_btcusdt.make_qty(2.5, None).to_string(), - "2.500000" - ); // 2.5 -> 2.500000 (banker's rounds to even) - assert_eq!( - currency_pair_btcusdt.make_qty(1.2345678, None).to_string(), - "1.234568" - ); // 1.2345678 -> 1.234568 (rounds to precision) + fn default_increment_precision() { + let inc = default_price_increment(2); + assert_eq!(inc, Price::new(0.01, 2)); } #[rstest] - fn test_make_qty_round_down(currency_pair_btcusdt: CurrencyPair) { - assert_eq!( - currency_pair_btcusdt.make_qty(1.5, Some(true)).to_string(), - "1.500000" - ); // 1.5 -> 1.500000 - assert_eq!( - currency_pair_btcusdt.make_qty(2.5, Some(true)).to_string(), - "2.500000" - ); // 2.5 -> 2.500000 + #[case(1.5, "1.500000")] + #[case(2.5, "2.500000")] + #[case(1.2345678, "1.234568")] + #[case(0.000123, "0.000123")] + #[case(99999.999999, "99999.999999")] + fn make_qty_rounding( + currency_pair_btcusdt: CurrencyPair, + #[case] input: f64, + #[case] expected: &str, + ) { assert_eq!( - currency_pair_btcusdt - .make_qty(1.2345678, Some(true)) - .to_string(), - "1.234567" - ); // 1.2345678 -> 1.234567 (rounds down) - assert_eq!( - currency_pair_btcusdt - .make_qty(1.9999999, Some(true)) - .to_string(), - "1.999999" - ); // 1.9999999 -> 1.999999 (rounds down) + currency_pair_btcusdt.make_qty(input, None).to_string(), + expected + ); } #[rstest] - fn test_make_qty_boundary_cases(currency_pair_btcusdt: CurrencyPair) { - // The instrument has size_precision=6, so increment = 0.000001 - let increment = 0.000001; - - // Testing behavior near increment boundaries - let value_just_above = 1.0 + (increment * 1.1); - assert_eq!( - currency_pair_btcusdt - .make_qty(value_just_above, Some(true)) - .to_string(), - "1.000001" - ); // Should round down to 1.000001 + #[case(1.2345678, "1.234567")] + #[case(1.9999999, "1.999999")] + #[case(0.00012345, "0.000123")] + #[case(10.9999999, "10.999999")] + fn make_qty_round_down( + currency_pair_btcusdt: CurrencyPair, + #[case] input: f64, + #[case] expected: &str, + ) { assert_eq!( currency_pair_btcusdt - .make_qty(value_just_above, None) + .make_qty(input, Some(true)) .to_string(), - "1.000001" - ); // Standard rounding should be 1.000001 + expected + ); + } - // Test with a value that should differ between round modes - let value_half_increment = 1.0000015; - assert_eq!( - currency_pair_btcusdt - .make_qty(value_half_increment, Some(true)) - .to_string(), - "1.000001" - ); // Should round down to 1.000001 + #[rstest] + #[case(1.2345678, "1.23457")] + #[case(2.3456781, "2.34568")] + #[case(0.00001, "0.00001")] + fn make_qty_precision( + currency_pair_ethusdt: CurrencyPair, + #[case] input: f64, + #[case] expected: &str, + ) { assert_eq!( - currency_pair_btcusdt - .make_qty(value_half_increment, None) - .to_string(), - "1.000002" - ); // Standard rounding should be 1.000002 + currency_pair_ethusdt.make_qty(input, None).to_string(), + expected + ); } #[rstest] - fn test_make_qty_zero_value(currency_pair_btcusdt: CurrencyPair) { - // Zero should remain zero with both rounding methods + #[case(1.2345675, "1.234568")] + #[case(1.2345665, "1.234566")] + fn make_qty_half_even( + currency_pair_btcusdt: CurrencyPair, + #[case] input: f64, + #[case] expected: &str, + ) { assert_eq!( - currency_pair_btcusdt.make_qty(0.0, None).to_string(), - "0.000000" + currency_pair_btcusdt.make_qty(input, None).to_string(), + expected ); - assert_eq!( - currency_pair_btcusdt.make_qty(0.0, Some(true)).to_string(), - "0.000000" + } + + #[rstest] + #[should_panic] + fn make_qty_rounds_to_zero(currency_pair_btcusdt: CurrencyPair) { + currency_pair_btcusdt.make_qty(1e-12, None); + } + + #[rstest] + fn notional_linear(currency_pair_btcusdt: CurrencyPair) { + let quantity = currency_pair_btcusdt.make_qty(2.0, None); + let price = currency_pair_btcusdt.make_price(10_000.0); + let notional = currency_pair_btcusdt.calculate_notional_value(quantity, price, None); + let expected = Money::new(20_000.0, currency_pair_btcusdt.quote_currency()); + assert_eq!(notional, expected); + } + + #[rstest] + fn tick_navigation(currency_pair_btcusdt: CurrencyPair) { + let start = 10_000.1234; + let bid_0 = currency_pair_btcusdt.next_bid_price(start, 0).unwrap(); + let bid_1 = currency_pair_btcusdt.next_bid_price(start, 1).unwrap(); + assert!(bid_1 < bid_0); + let asks = currency_pair_btcusdt.next_ask_prices(start, 3); + assert_eq!(asks.len(), 3); + assert!(asks[0] > bid_0); + } + + #[rstest] + #[should_panic] + fn validate_negative_margin_init() { + let size_increment = Quantity::new(0.01, 2); + let multiplier = Quantity::new(1.0, 0); + + validate_instrument_common( + 2, + 2, // size_precision + size_increment, // size_increment + multiplier, // multiplier + dec!(-0.01), // margin_init + dec!(0.01), // margin_maint + None, // price_increment + None, // lot_size + None, // max_quantity + None, // min_quantity + None, // max_notional + None, // min_notional + None, // max_price + None, // min_price + ) + .unwrap(); + } + + #[rstest] + #[should_panic] + fn validate_negative_margin_maint() { + let size_increment = Quantity::new(0.01, 2); + let multiplier = Quantity::new(1.0, 0); + + validate_instrument_common( + 2, + 2, // size_precision + size_increment, // size_increment + multiplier, // multiplier + dec!(0.01), // margin_init + dec!(-0.01), // margin_maint + None, // price_increment + None, // lot_size + None, // max_quantity + None, // min_quantity + None, // max_notional + None, // min_notional + None, // max_price + None, // min_price + ) + .unwrap(); + } + + #[rstest] + #[should_panic] + fn validate_negative_max_qty() { + let quantity = Quantity::new(0.0, 0); + validate_instrument_common( + 2, + 2, + Quantity::new(0.01, 2), + Quantity::new(1.0, 0), + dec!(0), + dec!(0), + None, + None, + Some(quantity), + None, + None, + None, + None, + None, + ) + .unwrap(); + } + + #[rstest] + fn make_price_negative_rounding(currency_pair_ethusdt: CurrencyPair) { + let price = currency_pair_ethusdt.make_price(-123.456_789); + assert!(price.as_f64() < 0.0); + } + + #[rstest] + fn base_quantity_linear(currency_pair_btcusdt: CurrencyPair) { + let quantity = currency_pair_btcusdt.make_qty(2.0, None); + let price = currency_pair_btcusdt.make_price(10_000.0); + let base = currency_pair_btcusdt.calculate_base_quantity(quantity, price); + assert_eq!(base.to_string(), "0.000200"); + } + + #[rstest] + fn fixed_tick_scheme_prices() { + let scheme = FixedTickScheme::new(0.5).unwrap(); + let bid = scheme.next_bid_price(10.3, 0, 2).unwrap(); + let ask = scheme.next_ask_price(10.3, 0, 2).unwrap(); + assert!(bid < ask); + } + + #[rstest] + #[should_panic] + fn fixed_tick_negative() { + FixedTickScheme::new(-0.01).unwrap(); + } + + #[rstest] + fn next_bid_prices_sequence(currency_pair_btcusdt: CurrencyPair) { + let start = 10_000.0; + let bids = currency_pair_btcusdt.next_bid_prices(start, 5); + assert_eq!(bids.len(), 5); + for i in 1..bids.len() { + assert!(bids[i] < bids[i - 1]); + } + } + + #[rstest] + fn next_ask_prices_sequence(currency_pair_btcusdt: CurrencyPair) { + let start = 10_000.0; + let asks = currency_pair_btcusdt.next_ask_prices(start, 5); + assert_eq!(asks.len(), 5); + for i in 1..asks.len() { + assert!(asks[i] > asks[i - 1]); + } + } + + #[rstest] + fn fixed_tick_boundary() { + let scheme = FixedTickScheme::new(0.5).unwrap(); + let price = scheme.next_bid_price(10.5, 0, 2).unwrap(); + assert_eq!(price, Price::new(10.5, 2)); + } + + #[rstest] + #[should_panic] + fn validate_price_increment_precision_mismatch() { + let size_increment = Quantity::new(0.01, 2); + let multiplier = Quantity::new(1.0, 0); + let price_increment = Price::new(0.001, 3); + validate_instrument_common( + 2, + 2, + size_increment, + multiplier, + dec!(0), + dec!(0), + Some(price_increment), + None, + None, + None, + None, + None, + None, + None, + ) + .unwrap(); + } + + #[rstest] + #[should_panic] + fn validate_min_price_exceeds_max_price() { + let size_increment = Quantity::new(0.01, 2); + let multiplier = Quantity::new(1.0, 0); + let min_price = Price::new(10.0, 2); + let max_price = Price::new(5.0, 2); + validate_instrument_common( + 2, + 2, + size_increment, + multiplier, + dec!(0), + dec!(0), + None, + None, + None, + None, + None, + None, + Some(max_price), + Some(min_price), + ) + .unwrap(); + } + + #[rstest] + fn validate_instrument_common_ok() { + let res = validate_instrument_common( + 2, + 4, + Quantity::new(0.0001, 4), + Quantity::new(1.0, 0), + dec!(0.02), + dec!(0.01), + Some(Price::new(0.01, 2)), + None, + None, + None, + None, + None, + None, + None, ); + assert!(matches!(res, Ok(()))); } #[rstest] - fn test_make_qty_different_precision(currency_pair_ethusdt: CurrencyPair) { - // ethusdt has size_precision=5 - assert_eq!( - currency_pair_ethusdt.make_qty(1.2345678, None).to_string(), - "1.23457" - ); // 1.2345678 -> 1.23457 (standard rounding) - assert_eq!( - currency_pair_ethusdt - .make_qty(1.2345678, Some(true)) - .to_string(), - "1.23456" - ); // 1.2345678 -> 1.23456 (rounds down) + #[should_panic] + fn validate_multiple_errors() { + validate_instrument_common( + 2, + 2, + Quantity::new(-0.01, 2), + Quantity::new(0.0, 0), + dec!(0), + dec!(0), + None, + None, + None, + None, + None, + None, + None, + None, + ) + .unwrap(); + } + + #[rstest] + #[case(1.234_9999, false, "1.235000")] + #[case(1.234_9999, true, "1.234999")] + fn make_qty_boundary( + currency_pair_btcusdt: CurrencyPair, + #[case] input: f64, + #[case] round_down: bool, + #[case] expected: &str, + ) { + let quantity = currency_pair_btcusdt.make_qty(input, Some(round_down)); + assert_eq!(quantity.to_string(), expected); + } + + #[rstest] + fn fixed_tick_multiple_steps() { + let scheme = FixedTickScheme::new(1.0).unwrap(); + let bid = scheme.next_bid_price(10.0, 2, 1).unwrap(); + let ask = scheme.next_ask_price(10.0, 3, 1).unwrap(); + assert_eq!(bid, Price::new(8.0, 1)); + assert_eq!(ask, Price::new(13.0, 1)); + } + + #[rstest] + #[case(1.234_999, 1.23)] + #[case(1.235, 1.24)] + #[case(1.235_001, 1.24)] + fn make_price_rounding_parity( + currency_pair_btcusdt: CurrencyPair, + #[case] input: f64, + #[case] expected: f64, + ) { + let price = currency_pair_btcusdt.make_price(input); + assert!((price.as_f64() - expected).abs() < 1e-9); + } + + #[rstest] + fn make_price_half_even_parity(currency_pair_btcusdt: CurrencyPair) { + let rounding_precision = std::cmp::min( + currency_pair_btcusdt.price_precision(), + currency_pair_btcusdt._min_price_increment_precision(), + ); + let step = 10f64.powi(-(rounding_precision as i32)); + let base_even_multiple = 42.0; + let base_value = step * base_even_multiple; + let delta = step / 2000.0; + let value_below = base_value + 0.5 * step - delta; + let value_exact = base_value + 0.5 * step; + let value_above = base_value + 0.5 * step + delta; + let price_below = currency_pair_btcusdt.make_price(value_below); + let price_exact = currency_pair_btcusdt.make_price(value_exact); + let price_above = currency_pair_btcusdt.make_price(value_above); + assert_eq!(price_below, price_exact); + assert_ne!(price_exact, price_above); + } + + #[rstest] + fn tick_scheme_round_trip() { + let scheme = TickScheme::from_str("CRYPTO_0_01").unwrap(); + assert_eq!(scheme.to_string(), "CRYPTO_0_01"); + } + + #[rstest] + fn is_quanto_flag(ethbtc_quanto: CryptoFuture) { + assert!(ethbtc_quanto.is_quanto()); + } + + #[rstest] + fn notional_quanto(ethbtc_quanto: CryptoFuture) { + let quantity = ethbtc_quanto.make_qty(5.0, None); + let price = ethbtc_quanto.make_price(0.036); + let notional = ethbtc_quanto.calculate_notional_value(quantity, price, None); + let expected = Money::new(0.18, ethbtc_quanto.settlement_currency()); + assert_eq!(notional, expected); + } + + #[rstest] + fn notional_inverse_base(xbtusd_inverse_perp: CryptoPerpetual) { + let quantity = xbtusd_inverse_perp.make_qty(100.0, None); + let price = xbtusd_inverse_perp.make_price(50_000.0); + let notional = xbtusd_inverse_perp.calculate_notional_value(quantity, price, Some(false)); + let expected = Money::new( + 100.0 * xbtusd_inverse_perp.multiplier().as_f64() * (1.0 / 50_000.0), + xbtusd_inverse_perp.base_currency().unwrap(), + ); + assert_eq!(notional, expected); + } + + #[rstest] + fn notional_inverse_quote_use_quote(xbtusd_inverse_perp: CryptoPerpetual) { + let quantity = xbtusd_inverse_perp.make_qty(100.0, None); + let price = xbtusd_inverse_perp.make_price(50_000.0); + let notional = xbtusd_inverse_perp.calculate_notional_value(quantity, price, Some(true)); + let expected = Money::new(100.0, xbtusd_inverse_perp.quote_currency()); + assert_eq!(notional, expected); + } + + #[rstest] + #[should_panic] + fn validate_non_positive_max_price() { + let size_increment = Quantity::new(0.01, 2); + let multiplier = Quantity::new(1.0, 0); + let max_price = Price::new(0.0, 2); + validate_instrument_common( + 2, + 2, + size_increment, + multiplier, + dec!(0), + dec!(0), + None, + None, + None, + None, + None, + None, + Some(max_price), + None, + ) + .unwrap(); + } + + #[rstest] + #[should_panic] + fn validate_non_positive_max_notional(currency_pair_btcusdt: CurrencyPair) { + let size_increment = Quantity::new(0.01, 2); + let multiplier = Quantity::new(1.0, 0); + let max_notional = Money::new(0.0, currency_pair_btcusdt.quote_currency()); + validate_instrument_common( + 2, + 2, + size_increment, + multiplier, + dec!(0), + dec!(0), + None, + None, + None, + None, + Some(max_notional), + None, + None, + None, + ) + .unwrap(); + } + + #[rstest] + #[should_panic] + fn validate_price_increment_min_price_precision_mismatch() { + let size_increment = Quantity::new(0.01, 2); + let multiplier = Quantity::new(1.0, 0); + let price_increment = Price::new(0.01, 2); + let min_price = Price::new(1.0, 3); + validate_instrument_common( + 2, + 2, + size_increment, + multiplier, + dec!(0), + dec!(0), + Some(price_increment), + None, + None, + None, + None, + None, + None, + Some(min_price), + ) + .unwrap(); + } + + #[rstest] + #[should_panic] + fn validate_negative_min_notional(currency_pair_btcusdt: CurrencyPair) { + let size_increment = Quantity::new(0.01, 2); + let multiplier = Quantity::new(1.0, 0); + let min_notional = Money::new(-1.0, currency_pair_btcusdt.quote_currency()); + let max_notional = Money::new(1.0, currency_pair_btcusdt.quote_currency()); + validate_instrument_common( + 2, + 2, + size_increment, + multiplier, + dec!(0), + dec!(0), + None, + None, + None, + None, + Some(max_notional), + Some(min_notional), + None, + None, + ) + .unwrap(); + } + + #[rstest] + #[case::dp0(Decimal::new(1_000, 0), Decimal::new(2, 0), 500.0)] + #[case::dp1(Decimal::new(10_000, 1), Decimal::new(2, 0), 500.0)] + #[case::dp2(Decimal::new(1_000_00, 2), Decimal::new(2, 0), 500.0)] + #[case::dp3(Decimal::new(1_000_000, 3), Decimal::new(2, 0), 500.0)] + #[case::dp4(Decimal::new(10_000_000, 4), Decimal::new(2, 0), 500.0)] + #[case::dp5(Decimal::new(100_000_000, 5), Decimal::new(2, 0), 500.0)] + #[case::dp6(Decimal::new(1_000_000_000, 6), Decimal::new(2, 0), 500.0)] + #[case::dp7(Decimal::new(10_000_000_000, 7), Decimal::new(2, 0), 500.0)] + #[case::dp8(Decimal::new(100_000_000_000, 8), Decimal::new(2, 0), 500.0)] + fn base_qty_rounding( + currency_pair_btcusdt: CurrencyPair, + #[case] q: Decimal, + #[case] px: Decimal, + #[case] expected: f64, + ) { + let qty = Quantity::new(q.to_f64().unwrap(), 8); + let price = Price::new(px.to_f64().unwrap(), 8); + let base = currency_pair_btcusdt.calculate_base_quantity(qty, price); + assert!((base.as_f64() - expected).abs() < 1e-9); + } + + proptest! { + #[test] + fn make_price_qty_fuzz(input in 0.0001f64..1e8) { + let instrument = currency_pair_btcusdt(); + let price = instrument.make_price(input); + prop_assert!(price.as_f64().is_finite()); + let quantity = instrument.make_qty(input, None); + prop_assert!(quantity.as_f64().is_finite()); + } + } + + #[rstest] + fn tick_walk_limits_btcusdt_ask(currency_pair_btcusdt: CurrencyPair) { + if let Some(max_price) = currency_pair_btcusdt.max_price() { + assert!( + currency_pair_btcusdt + .next_ask_price(max_price.as_f64(), 1) + .is_none() + ); + } + } + + #[rstest] + fn tick_walk_limits_ethusdt_ask(currency_pair_ethusdt: CurrencyPair) { + if let Some(max_price) = currency_pair_ethusdt.max_price() { + assert!( + currency_pair_ethusdt + .next_ask_price(max_price.as_f64(), 1) + .is_none() + ); + } + } + + #[rstest] + fn tick_walk_limits_btcusdt_bid(currency_pair_btcusdt: CurrencyPair) { + if let Some(min_price) = currency_pair_btcusdt.min_price() { + assert!( + currency_pair_btcusdt + .next_bid_price(min_price.as_f64(), 1) + .is_none() + ); + } + } + + #[rstest] + fn tick_walk_limits_ethusdt_bid(currency_pair_ethusdt: CurrencyPair) { + if let Some(min_price) = currency_pair_ethusdt.min_price() { + assert!( + currency_pair_ethusdt + .next_bid_price(min_price.as_f64(), 1) + .is_none() + ); + } + } + + #[rstest] + fn tick_walk_limits_quanto_ask(ethbtc_quanto: CryptoFuture) { + if let Some(max_price) = ethbtc_quanto.max_price() { + assert!( + ethbtc_quanto + .next_ask_price(max_price.as_f64(), 1) + .is_none() + ); + } + } + + #[rstest] + #[case(0.999_999, false)] + #[case(0.999_999, true)] + #[case(1.000_0001, false)] + #[case(1.000_0001, true)] + #[case(1.234_5, false)] + #[case(1.234_5, true)] + #[case(2.345_5, false)] + #[case(2.345_5, true)] + #[case(0.000_999_999, false)] + #[case(0.000_999_999, true)] + fn quantity_rounding_grid( + currency_pair_btcusdt: CurrencyPair, + #[case] input: f64, + #[case] round_down: bool, + ) { + let qty = currency_pair_btcusdt.make_qty(input, Some(round_down)); + assert!(qty.as_f64().is_finite()); + } + + #[rstest] + fn pyo3_failure_tick_scheme_unknown() { + assert!(TickScheme::from_str("UNKNOWN").is_err()); + } + + #[rstest] + fn pyo3_failure_fixed_tick_zero() { + assert!(FixedTickScheme::new(0.0).is_err()); + } + + #[rstest] + fn pyo3_failure_validate_price_increment_max_price_precision_mismatch() { + let size_increment = Quantity::new(0.01, 2); + let multiplier = Quantity::new(1.0, 0); + let price_increment = Price::new(0.01, 2); + let max_price = Price::new(1.0, 3); + let res = validate_instrument_common( + 2, + 2, + size_increment, + multiplier, + dec!(0), + dec!(0), + Some(price_increment), + None, + None, + None, + None, + None, + Some(max_price), + None, + ); + assert!(res.is_err()); + } + + #[rstest] + #[case::dp9(Decimal::new(1_000_000_000_000, 9), Decimal::new(2, 0), 500.0)] + #[case::dp10(Decimal::new(10_000_000_000_000, 10), Decimal::new(2, 0), 500.0)] + #[case::dp11(Decimal::new(100_000_000_000_000, 11), Decimal::new(2, 0), 500.0)] + #[case::dp12(Decimal::new(1_000_000_000_000_000, 12), Decimal::new(2, 0), 500.0)] + #[case::dp13(Decimal::new(10_000_000_000_000_000, 13), Decimal::new(2, 0), 500.0)] + #[case::dp14(Decimal::new(100_000_000_000_000_000, 14), Decimal::new(2, 0), 500.0)] + #[case::dp15(Decimal::new(1_000_000_000_000_000_000, 15), Decimal::new(2, 0), 500.0)] + #[case::dp16( + Decimal::from_i128_with_scale(10_000_000_000_000_000_000i128, 16), + Decimal::new(2, 0), + 500.0 + )] + #[case::dp17( + Decimal::from_i128_with_scale(100_000_000_000_000_000_000i128, 17), + Decimal::new(2, 0), + 500.0 + )] + fn base_qty_rounding_high_dp( + currency_pair_btcusdt: CurrencyPair, + #[case] q: Decimal, + #[case] px: Decimal, + #[case] expected: f64, + ) { + let qty = Quantity::new(q.to_f64().unwrap(), 8); + let price = Price::new(px.to_f64().unwrap(), 8); + let base = currency_pair_btcusdt.calculate_base_quantity(qty, price); + assert!((base.as_f64() - expected).abs() < 1e-9); + } + + #[rstest] + fn check_positive_money_ok(currency_pair_btcusdt: CurrencyPair) { + let money = Money::new(100.0, currency_pair_btcusdt.quote_currency()); + assert!(check_positive_money(money, "money").is_ok()); + } + + #[rstest] + #[should_panic] + fn check_positive_money_zero(currency_pair_btcusdt: CurrencyPair) { + let money = Money::new(0.0, currency_pair_btcusdt.quote_currency()); + check_positive_money(money, "money").unwrap(); + } + + #[rstest] + #[should_panic] + fn check_positive_money_negative(currency_pair_btcusdt: CurrencyPair) { + let money = Money::new(-0.01, currency_pair_btcusdt.quote_currency()); + check_positive_money(money, "money").unwrap(); } } diff --git a/crates/model/src/instruments/stubs.rs b/crates/model/src/instruments/stubs.rs index a7025666f0b7..5e883b3ed502 100644 --- a/crates/model/src/instruments/stubs.rs +++ b/crates/model/src/instruments/stubs.rs @@ -93,6 +93,86 @@ pub fn crypto_future_btcusdt( ) } +#[fixture] +pub fn ethbtc_quanto( + #[default(5)] price_precision: u8, + #[default(3)] size_precision: u8, + #[default(Price::from("0.00001"))] price_increment: Price, + #[default(Quantity::from("0.001"))] size_increment: Quantity, +) -> CryptoFuture { + let activation = Utc.with_ymd_and_hms(2014, 4, 8, 0, 0, 0).unwrap(); + let expiration = Utc.with_ymd_and_hms(2014, 7, 8, 0, 0, 0).unwrap(); + CryptoFuture::new( + InstrumentId::from("ETHBTC-123.BINANCE"), + Symbol::from("ETHBTC"), + Currency::from("ETH"), + Currency::from("BTC"), + Currency::from("BTC"), + false, + UnixNanos::from(activation.timestamp_nanos_opt().unwrap() as u64), + UnixNanos::from(expiration.timestamp_nanos_opt().unwrap() as u64), + price_precision, + size_precision, + price_increment, + size_increment, + None, + None, + Some(Quantity::from("9000.0")), + Some(Quantity::from("0.001")), + None, + Some(Money::new(1.0, Currency::from("BTC"))), + Some(Price::from("1.0")), + Some(Price::from("0.00001")), + None, + None, + None, + None, + 0.into(), + 0.into(), + ) +} + +//////////////////////////////////////////////////////////////////////////////// +// CryptoPerpetual – BitMEX inverse (XBTUSD) +// //////////////////////////////////////////////////////////////////////////// + +#[fixture] +pub fn xbtusd_inverse_perp( + // One-decimal tick (0.5 USD) and integer contract size + #[default(1)] price_precision: u8, + #[default(0)] size_precision: u8, + #[default(Price::from("0.5"))] price_increment: Price, + #[default(Quantity::from("1"))] size_increment: Quantity, +) -> CryptoPerpetual { + CryptoPerpetual::new( + // BitMEX uses XBT for BTC; keep the “-PERP” suffix for clarity + InstrumentId::from("XBTUSD-PERP.BITMEX"), + Symbol::from("XBTUSD"), + Currency::BTC(), // base + Currency::USD(), // quote + Currency::BTC(), // settlement (inverse) + true, // is_inverse + price_precision, + size_precision, + price_increment, + size_increment, + None, // lot_size + Some(Quantity::from("1")), // multiplier: 1 USD/contract + None, // max_quantity + None, // min_quantity + Some(Money::from("10000000 USD")), // max_notional + Some(Money::from("1 USD")), // min_notional + Some(Price::from("10000000")), // max_price + Some(Price::from("0.01")), // min_price + Some(dec!(0.01)), // margin_init + Some(dec!(0.0035)), // margin_maint + Some(dec!(-0.00025)), // maker_fee (rebate) + Some(dec!(0.00075)), // taker_fee + UnixNanos::default(), // ts_event + UnixNanos::default(), // ts_init + ) +} + //////////////////////////////////////////////////////////////////////////////// // CryptoOption //////////////////////////////////////////////////////////////////////////////// @@ -464,7 +544,7 @@ pub fn option_contract_appl() -> OptionContract { InstrumentId::from("AAPL211217C00150000.OPRA"), Symbol::from("AAPL211217C00150000"), AssetClass::Equity, - Some(Ustr::from("GMNI")), // Nasdaq GEMX + Some(Ustr::from("GMNI")), Ustr::from("AAPL"), OptionKind::Call, Price::from("149.0"), @@ -501,7 +581,7 @@ pub fn option_spread() -> OptionSpread { Symbol::from("UD:U$: GN 2534559"), AssetClass::FX, Some(Ustr::from("XCME")), - Ustr::from("SR3"), // British Pound futures (option on futures) + Ustr::from("SR3"), Ustr::from("GN"), UnixNanos::from(activation.timestamp_nanos_opt().unwrap() as u64), UnixNanos::from(expiration.timestamp_nanos_opt().unwrap() as u64), @@ -556,7 +636,7 @@ pub fn betting() -> BettingInstrument { ); let selection_id = 50214; let selection_name = Ustr::from("Kansas City Chiefs"); - let selection_handicap = 0.0; // As per betting convention, no handicap + let selection_handicap = 0.0; let currency = Currency::GBP(); let price_increment = Price::from("0.01"); let size_increment = Quantity::from("0.01"); @@ -570,8 +650,8 @@ pub fn betting() -> BettingInstrument { let margin_maint = Some(Decimal::from(1)); let maker_fee = Some(Decimal::from(0)); let taker_fee = Some(Decimal::from(0)); - let ts_event = UnixNanos::default(); // For testing purposes - let ts_init = UnixNanos::default(); // For testing purposes + let ts_event = UnixNanos::default(); + let ts_init = UnixNanos::default(); BettingInstrument::new( id, diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index 079d6e4cd63e..f8c724049aab 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -39,6 +39,7 @@ //! - `python`: Enables Python bindings from [PyO3](https://pyo3.rs). //! - `stubs`: Enables type stubs for use in testing scenarios. //! - `high-precision`: Enables [high-precision mode](https://nautilustrader.io/docs/nightly/getting_started/installation#precision-mode) to use 128-bit value types. +//! - `defi`: Enables the DeFi (Decentralized Finance) domain model. #![warn(rustc::all)] #![deny(unsafe_code)] diff --git a/crates/model/src/types/money.rs b/crates/model/src/types/money.rs index 1805f5867a67..6da57f1c289f 100644 --- a/crates/model/src/types/money.rs +++ b/crates/model/src/types/money.rs @@ -402,26 +402,34 @@ impl Div for Money { impl Debug for Money { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}({:.*}, {})", - stringify!(Money), - self.currency.precision as usize, - self.as_f64(), - self.currency, - ) + if self.currency.precision > crate::types::fixed::MAX_FLOAT_PRECISION { + write!(f, "{}({}, {})", stringify!(Money), self.raw, self.currency) + } else { + write!( + f, + "{}({:.*}, {})", + stringify!(Money), + self.currency.precision as usize, + self.as_f64(), + self.currency + ) + } } } impl Display for Money { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{:.*} {}", - self.currency.precision as usize, - self.as_f64(), - self.currency - ) + if self.currency.precision > crate::types::fixed::MAX_FLOAT_PRECISION { + write!(f, "{} {}", self.raw, self.currency) + } else { + write!( + f, + "{:.*} {}", + self.currency.precision as usize, + self.as_f64(), + self.currency + ) + } } } @@ -444,6 +452,19 @@ impl<'de> Deserialize<'de> for Money { } } +/// Checks if the money `value` is positive. +/// +/// # Errors +/// +/// Returns an error if `value` is not positive. +#[inline(always)] +pub fn check_positive_money(value: Money, param: &str) -> anyhow::Result<()> { + if value.raw <= 0 { + anyhow::bail!("invalid `Money` for '{param}' not positive, was {value}"); + } + Ok(()) +} + //////////////////////////////////////////////////////////////////////////////// // Tests //////////////////////////////////////////////////////////////////////////////// @@ -471,6 +492,67 @@ mod tests { assert_eq!(result, expected); } + #[rstest] + #[case(1010.12, 2, "USD", "Money(1010.12, USD)", "1010.12 USD")] // Normal precision + #[case(123.456789, 8, "BTC", "Money(123.45678900, BTC)", "123.45678900 BTC")] // At max normal precision + fn test_formatting_normal_precision( + #[case] value: f64, + #[case] precision: u8, + #[case] currency_code: &str, + #[case] expected_debug: &str, + #[case] expected_display: &str, + ) { + use crate::enums::CurrencyType; + let currency = Currency::new( + currency_code, + precision, + 0, + currency_code, + CurrencyType::Fiat, + ); + let money = Money::new(value, currency); + + assert_eq!(format!("{money:?}"), expected_debug); + assert_eq!(format!("{money}"), expected_display); + } + + #[rstest] + #[cfg(feature = "defi")] + #[case( + 1_000_000_000_000_000_000_i128, + 18, + "WEI", + "Money(1000000000000000000, WEI)", + "1000000000000000000 WEI" + )] // High precision + #[case( + 2_500_000_000_000_000_000_i128, + 18, + "ETH", + "Money(2500000000000000000, ETH)", + "2500000000000000000 ETH" + )] // High precision + fn test_formatting_high_precision( + #[case] raw_value: i128, + #[case] precision: u8, + #[case] currency_code: &str, + #[case] expected_debug: &str, + #[case] expected_display: &str, + ) { + use crate::enums::CurrencyType; + let currency = Currency::new( + currency_code, + precision, + 0, + currency_code, + CurrencyType::Crypto, + ); + let money = Money::from_raw(raw_value, currency); + + assert_eq!(format!("{money:?}"), expected_debug); + assert_eq!(format!("{money}"), expected_display); + } + #[rstest] fn test_zero_constructor() { let usd = Currency::USD(); @@ -939,4 +1021,30 @@ mod tests { } } } + + #[rstest] + #[case(42.0, true, "positive value")] + #[case(0.0, false, "zero value")] + #[case( -13.5, false, "negative value")] + fn test_check_positive_money( + #[case] amount: f64, + #[case] should_succeed: bool, + #[case] _case_name: &str, + ) { + let money = Money::new(amount, Currency::USD()); + + let res = check_positive_money(money, "money"); + + match should_succeed { + true => assert!(res.is_ok(), "expected Ok(..) for {amount}"), + false => { + assert!(res.is_err(), "expected Err(..) for {amount}"); + let msg = res.unwrap_err().to_string(); + assert!( + msg.contains("not positive"), + "error message should mention positivity; got: {msg:?}" + ); + } + } + } } diff --git a/crates/model/src/types/price.rs b/crates/model/src/types/price.rs index 5ea64774e4e0..c79ff2425f4d 100644 --- a/crates/model/src/types/price.rs +++ b/crates/model/src/types/price.rs @@ -503,19 +503,27 @@ impl Mul for Price { impl Debug for Price { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}({:.*})", - stringify!(Price), - self.precision as usize, - self.as_f64() - ) + if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION { + write!(f, "{}({})", stringify!(Price), self.raw) + } else { + write!( + f, + "{}({:.*})", + stringify!(Price), + self.precision as usize, + self.as_f64(), + ) + } } } impl Display for Price { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.*}", self.precision as usize, self.as_f64()) + if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION { + write!(f, "{}", self.raw) + } else { + write!(f, "{:.*}", self.precision as usize, self.as_f64(),) + } } } @@ -539,11 +547,11 @@ impl<'de> Deserialize<'de> for Price { } } -/// Checks the given `price` is positive. +/// Checks the price `value` is positive. /// /// # Errors /// -/// Returns an error if the validation check fails. +/// Returns an error if `value` is `PRICE_UNDEF` or not positive. pub fn check_positive_price(value: Price, param: &str) -> anyhow::Result<()> { if value.raw == PRICE_UNDEF { anyhow::bail!("invalid `Price` for '{param}', was PRICE_UNDEF") @@ -761,6 +769,38 @@ mod tests { assert_eq!(Price::new(1234.5678, 4).to_formatted_string(), "1_234.5678"); } + #[rstest] + #[case(1234.5678, 4, "Price(1234.5678)", "1234.5678")] // Normal precision + #[case(123.456789012345, 8, "Price(123.45678901)", "123.45678901")] // At max normal precision + #[cfg_attr( + feature = "defi", + case( + 2_000_000_000_000_000_000.0, + 18, + "Price(2000000000000000000)", + "2000000000000000000" + ) + )] // High precision + fn test_string_formatting_precision_handling( + #[case] value: f64, + #[case] precision: u8, + #[case] expected_debug: &str, + #[case] expected_display: &str, + ) { + let price = if precision > crate::types::fixed::MAX_FLOAT_PRECISION { + Price::from_raw(value as PriceRaw, precision) + } else { + Price::new(value, precision) + }; + + assert_eq!(format!("{price:?}"), expected_debug); + assert_eq!(format!("{price}"), expected_display); + assert_eq!( + price.to_formatted_string().replace("_", ""), + expected_display + ); + } + #[rstest] fn test_decimal_conversions() { let price = Price::new(123.456, 3); diff --git a/crates/model/src/types/quantity.rs b/crates/model/src/types/quantity.rs index 2bbf74c95502..95216273b7cd 100644 --- a/crates/model/src/types/quantity.rs +++ b/crates/model/src/types/quantity.rs @@ -517,19 +517,27 @@ impl> MulAssign for Quantity { impl Debug for Quantity { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}({:.*})", - stringify!(Quantity), - self.precision as usize, - self.as_f64(), - ) + if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION { + write!(f, "{}({})", stringify!(Quantity), self.raw) + } else { + write!( + f, + "{}({:.*})", + stringify!(Quantity), + self.precision as usize, + self.as_f64(), + ) + } } } impl Display for Quantity { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.*}", self.precision as usize, self.as_f64()) + if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION { + write!(f, "{}", self.raw) + } else { + write!(f, "{:.*}", self.precision as usize, self.as_f64(),) + } } } @@ -553,11 +561,11 @@ impl<'de> Deserialize<'de> for Quantity { } } -/// Checks if the given quantity is positive. +/// Checks if the quantity `value` is positive. /// /// # Errors /// -/// Returns an error if the quantity is not positive. +/// Returns an error if `value` is not positive. pub fn check_positive_quantity(value: Quantity, param: &str) -> anyhow::Result<()> { if !value.is_positive() { anyhow::bail!("invalid `Quantity` for '{param}' not positive, was {value}") @@ -906,6 +914,35 @@ mod tests { assert_eq!(result, "44.12"); } + #[rstest] + #[case(44.12, 2, "Quantity(44.12)", "44.12")] // Normal precision + #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] // At max normal precision + #[cfg_attr( + feature = "defi", + case( + 1_000_000_000_000_000_000.0, + 18, + "Quantity(1000000000000000000)", + "1000000000000000000" + ) + )] // High precision + fn test_debug_display_precision_handling( + #[case] value: f64, + #[case] precision: u8, + #[case] expected_debug: &str, + #[case] expected_display: &str, + ) { + let quantity = if precision > crate::types::fixed::MAX_FLOAT_PRECISION { + // For high precision, use from_raw to avoid f64 conversion issues + Quantity::from_raw(value as QuantityRaw, precision) + } else { + Quantity::new(value, precision) + }; + + assert_eq!(format!("{quantity:?}"), expected_debug); + assert_eq!(format!("{quantity}"), expected_display); + } + #[rstest] fn test_to_formatted_string() { let qty = Quantity::new(1234.5678, 4); diff --git a/crates/network/README.md b/crates/network/README.md index 4a1f997c3988..4794b4977353 100644 --- a/crates/network/README.md +++ b/crates/network/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-network)](https://docs.rs/nautilus-network/latest/nautilus-network/) [![crates.io version](https://img.shields.io/crates/v/nautilus-network.svg)](https://crates.io/crates/nautilus-network) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Network functionality for [NautilusTrader](http://nautilustrader.io). diff --git a/crates/persistence/README.md b/crates/persistence/README.md index a05e08343f01..5464683e111c 100644 --- a/crates/persistence/README.md +++ b/crates/persistence/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-persistence)](https://docs.rs/nautilus-persistence/latest/nautilus-persistence/) [![crates.io version](https://img.shields.io/crates/v/nautilus-persistence.svg)](https://crates.io/crates/nautilus-persistence) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) ## Platform diff --git a/crates/persistence/src/backend/catalog.rs b/crates/persistence/src/backend/catalog.rs index 67e93d18a24c..0b66f8a4fee3 100644 --- a/crates/persistence/src/backend/catalog.rs +++ b/crates/persistence/src/backend/catalog.rs @@ -1251,14 +1251,9 @@ impl ParquetDataCatalog { let start_u64 = start.map(|s| s.as_u64()); let end_u64 = end.map(|e| e.as_u64()); - let safe_ids = instrument_ids.as_ref().map(|ids| { - ids.iter() - .map(|id| urisafe_instrument_id(id)) - .collect::>() - }); - let base_dir = self.make_path(data_cls, None)?; + // Use recursive listing to match Python's glob behavior let list_result = self.execute_async(async { let prefix = ObjectPath::from(format!("{base_dir}/")); let mut stream = self.object_store.list(Some(&prefix)); @@ -1269,23 +1264,68 @@ impl ParquetDataCatalog { Ok::, anyhow::Error>(objects) })?; - for object in list_result { - let path_str = object.location.to_string(); - if path_str.ends_with(".parquet") { - if let Some(ids) = &safe_ids { - let matches_any_id = ids.iter().any(|safe_id| path_str.contains(safe_id)); - if !matches_any_id { - continue; - } + let mut file_paths: Vec = list_result + .into_iter() + .filter_map(|object| { + let path_str = object.location.to_string(); + if path_str.ends_with(".parquet") { + Some(path_str) + } else { + None } + }) + .collect(); - if query_intersects_filename(&path_str, start_u64, end_u64) { - let full_uri = self.reconstruct_full_uri(&path_str); - files.push(full_uri); - } + // Apply identifier filtering if provided + if let Some(identifiers) = instrument_ids { + let safe_identifiers: Vec = identifiers + .iter() + .map(|id| urisafe_instrument_id(id)) + .collect(); + + // Exact match by default for instrument_ids or bar_types + let exact_match_file_paths: Vec = file_paths + .iter() + .filter(|file_path| { + // Extract the directory name (second to last path component) + let path_parts: Vec<&str> = file_path.split('/').collect(); + if path_parts.len() >= 2 { + let dir_name = path_parts[path_parts.len() - 2]; + safe_identifiers.iter().any(|safe_id| safe_id == dir_name) + } else { + false + } + }) + .cloned() + .collect(); + + if exact_match_file_paths.is_empty() && data_cls == "bars" { + // Partial match of instrument_ids in bar_types for bars + file_paths.retain(|file_path| { + let path_parts: Vec<&str> = file_path.split('/').collect(); + if path_parts.len() >= 2 { + let dir_name = path_parts[path_parts.len() - 2]; + safe_identifiers + .iter() + .any(|safe_id| dir_name.starts_with(&format!("{safe_id}-"))) + } else { + false + } + }); + } else { + file_paths = exact_match_file_paths; } } + // Apply timestamp filtering + file_paths.retain(|file_path| query_intersects_filename(file_path, start_u64, end_u64)); + + // Convert to full URIs + for file_path in file_paths { + let full_uri = self.reconstruct_full_uri(&file_path); + files.push(full_uri); + } + Ok(files) } diff --git a/crates/portfolio/README.md b/crates/portfolio/README.md index f47979c3a714..88f1f43b0221 100644 --- a/crates/portfolio/README.md +++ b/crates/portfolio/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-portfolio)](https://docs.rs/nautilus-portfolio/latest/nautilus-portfolio/) [![crates.io version](https://img.shields.io/crates/v/nautilus-portfolio.svg)](https://crates.io/crates/nautilus-portfolio) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Portfolio management and risk analysis for [NautilusTrader](http://nautilustrader.io). diff --git a/crates/risk/README.md b/crates/risk/README.md index 55e3c6eda3e4..2b7edc1be197 100644 --- a/crates/risk/README.md +++ b/crates/risk/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-risk)](https://docs.rs/nautilus-risk/latest/nautilus-risk/) [![crates.io version](https://img.shields.io/crates/v/nautilus-risk.svg)](https://crates.io/crates/nautilus-risk) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Risk engine for [NautilusTrader](http://nautilustrader.io). diff --git a/crates/risk/src/engine/tests.rs b/crates/risk/src/engine/tests.rs index 4b0f47f5fc7e..a666e8dbb2e1 100644 --- a/crates/risk/src/engine/tests.rs +++ b/crates/risk/src/engine/tests.rs @@ -355,7 +355,6 @@ fn order_filled( ) } -// Tests #[rstest] fn test_bypass_config_risk_engine() { let risk_engine = get_risk_engine( diff --git a/crates/serialization/README.md b/crates/serialization/README.md index 7bf6627372ac..be60f75c6fba 100644 --- a/crates/serialization/README.md +++ b/crates/serialization/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-serialization)](https://docs.rs/nautilus-serialization/latest/nautilus-serialization/) [![crates.io version](https://img.shields.io/crates/v/nautilus-serialization.svg)](https://crates.io/crates/nautilus-serialization) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Data serialization and format conversion for [NautilusTrader](http://nautilustrader.io). diff --git a/crates/testkit/README.md b/crates/testkit/README.md index 0111ed240dd0..6584940d3829 100644 --- a/crates/testkit/README.md +++ b/crates/testkit/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-testkit)](https://docs.rs/nautilus-testkit/latest/nautilus-testkit/) [![crates.io version](https://img.shields.io/crates/v/nautilus-testkit.svg)](https://crates.io/crates/nautilus-testkit) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Test utilities and data management for [NautilusTrader](http://nautilustrader.io). diff --git a/crates/trading/README.md b/crates/trading/README.md index 004242e25246..a98e02b83b9c 100644 --- a/crates/trading/README.md +++ b/crates/trading/README.md @@ -4,6 +4,7 @@ [![Documentation](https://img.shields.io/docsrs/nautilus-trading)](https://docs.rs/nautilus-trading/latest/nautilus-trading/) [![crates.io version](https://img.shields.io/crates/v/nautilus-trading.svg)](https://crates.io/crates/nautilus-trading) ![license](https://img.shields.io/github/license/nautechsystems/nautilus_trader?color=blue) +[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.gg/NautilusTrader) Trading strategy machinery and orchestration for [NautilusTrader](http://nautilustrader.io). diff --git a/docs/api_reference/adapters/interactive_brokers.md b/docs/api_reference/adapters/interactive_brokers.md index d03815ee9c19..aa7a1c3a4b45 100644 --- a/docs/api_reference/adapters/interactive_brokers.md +++ b/docs/api_reference/adapters/interactive_brokers.md @@ -8,6 +8,16 @@ :member-order: bysource ``` +## Client + +```{eval-rst} +.. automodule:: nautilus_trader.adapters.interactive_brokers.client.client + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + ## Common ```{eval-rst} @@ -58,6 +68,44 @@ :member-order: bysource ``` +## Gateway + +```{eval-rst} +.. automodule:: nautilus_trader.adapters.interactive_brokers.gateway + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + +## Historical + +```{eval-rst} +.. automodule:: nautilus_trader.adapters.interactive_brokers.historical.client + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + +## Parsing + +```{eval-rst} +.. automodule:: nautilus_trader.adapters.interactive_brokers.parsing.execution + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + +```{eval-rst} +.. automodule:: nautilus_trader.adapters.interactive_brokers.parsing.instruments + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource +``` + ## Providers ```{eval-rst} diff --git a/docs/integrations/ib.md b/docs/integrations/ib.md index de24388cdb9e..3555b5fa4c48 100644 --- a/docs/integrations/ib.md +++ b/docs/integrations/ib.md @@ -36,29 +36,63 @@ You can find live example scripts [here](https://github.com/nautechsystems/nauti Before implementing your trading strategies, please ensure that either TWS (Trader Workstation) or IB Gateway is currently running. You have the option to log in to one of these standalone applications using your personal credentials or alternatively, via `DockerizedIBGateway`. +### Connection Methods + +There are two primary ways to connect to Interactive Brokers: + +1. **Connect to an existing TWS or IB Gateway instance** +2. **Use the dockerized IB Gateway (recommended for automated deployments)** + +### Default Ports + +Interactive Brokers uses different default ports depending on the application and trading mode: + +| Application | Paper Trading | Live Trading | +|-------------|---------------|--------------| +| TWS | 7497 | 7496 | +| IB Gateway | 4002 | 4001 | + ### Establish Connection to an Existing Gateway or TWS -Should you choose to connect to a pre-existing Gateway or TWS, it is crucial that you specify the `host` and `port` parameters in both the `InteractiveBrokersDataClientConfig` and `InteractiveBrokersExecClientConfig` to guarantee a successful connection. +When connecting to a pre-existing Gateway or TWS, specify the `ibg_host` and `ibg_port` parameters in both the `InteractiveBrokersDataClientConfig` and `InteractiveBrokersExecClientConfig`: + +```python +from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersDataClientConfig +from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersExecClientConfig + +# Example for TWS paper trading (default port 7497) +data_config = InteractiveBrokersDataClientConfig( + ibg_host="127.0.0.1", + ibg_port=7497, + ibg_client_id=1, +) + +exec_config = InteractiveBrokersExecClientConfig( + ibg_host="127.0.0.1", + ibg_port=7497, + ibg_client_id=1, + account_id="DU123456", # Your paper trading account ID +) +``` ### Establish Connection to DockerizedIBGateway -In this case, it's essential to supply `dockerized_gateway` with an instance of `DockerizedIBGatewayConfig` in both the `InteractiveBrokersDataClientConfig` and `InteractiveBrokersExecClientConfig`. It's important to stress, however, that `host` and `port` parameters aren't necessary in this context. -The following example provides a clear illustration of how to establish a connection to a Dockerized Gateway, which is judiciously managed internally by the Factories. +For automated deployments, the dockerized gateway is recommended. Supply `dockerized_gateway` with an instance of `DockerizedIBGatewayConfig` in both client configurations. The `ibg_host` and `ibg_port` parameters are not needed as they're managed automatically. ```python from nautilus_trader.adapters.interactive_brokers.config import DockerizedIBGatewayConfig from nautilus_trader.adapters.interactive_brokers.gateway import DockerizedIBGateway gateway_config = DockerizedIBGatewayConfig( - username="test", - password="test", - trading_mode="paper", + username="your_username", # Or set TWS_USERNAME env var + password="your_password", # Or set TWS_PASSWORD env var + trading_mode="paper", # "paper" or "live" + read_only_api=True, # Set to False to allow order execution + timeout=300, # Startup timeout in seconds ) # This may take a short while to start up, especially the first time -gateway = DockerizedIBGateway( - config=gateway_config -) +gateway = DockerizedIBGateway(config=gateway_config) gateway.start() # Confirm you are logged in @@ -68,33 +102,112 @@ print(gateway.is_logged_in(gateway.container)) print(gateway.container.logs()) ``` -**Note**: To supply credentials to the Interactive Brokers Gateway, either pass the `username` and `password` to the `DockerizedIBGatewayConfig`, or set the following environment variables: +### Environment Variables + +To supply credentials to the Interactive Brokers Gateway, either pass the `username` and `password` to the `DockerizedIBGatewayConfig`, or set the following environment variables: + +- `TWS_USERNAME` - Your IB account username +- `TWS_PASSWORD` - Your IB account password +- `TWS_ACCOUNT` - Your IB account ID (used as fallback for `account_id`) -- `TWS_USERNAME` -- `TWS_PASSWORD` +### Connection Management + +The adapter includes robust connection management features: + +- **Automatic reconnection**: Configurable via `IB_MAX_CONNECTION_ATTEMPTS` environment variable +- **Connection timeout**: Configurable via `connection_timeout` parameter (default: 300 seconds) +- **Connection watchdog**: Monitors connection health and triggers reconnection if needed +- **Graceful error handling**: Comprehensive error handling for various connection scenarios ## Overview -The adapter includes several major components: +The Interactive Brokers adapter provides a comprehensive integration with IB's TWS API. The adapter includes several major components: + +### Core Components + +- **`InteractiveBrokersClient`**: The central client that executes TWS API requests using `ibapi`. Manages connections, handles errors, and coordinates all API interactions. +- **`InteractiveBrokersDataClient`**: Connects to the Gateway for streaming market data including quotes, trades, and bars. +- **`InteractiveBrokersExecutionClient`**: Handles account information, order management, and trade execution. +- **`InteractiveBrokersInstrumentProvider`**: Retrieves and manages instrument definitions, including support for options and futures chains. +- **`HistoricInteractiveBrokersClient`**: Provides methods for retrieving instruments and historical data, useful for backtesting and research. + +### Supporting Components + +- **`DockerizedIBGateway`**: Manages dockerized IB Gateway instances for automated deployments. +- **Configuration Classes**: Comprehensive configuration options for all components. +- **Factory Classes**: Create and configure client instances with proper dependencies. -- `InteractiveBrokersClient`: Executes TWS API requests using `ibapi`. -- `HistoricInteractiveBrokersClient`: Provides methods for retrieving instruments and historical data, useful for backtesting. -- `InteractiveBrokersInstrumentProvider`: Retrieves or queries instruments for trading. -- `InteractiveBrokersDataClient`: Connects to the Gateway for streaming market data. -- `InteractiveBrokersExecutionClient`: Handles account information and executes trades. +### Supported Asset Classes + +The adapter supports trading across all major asset classes available through Interactive Brokers: + +- **Equities**: Stocks, ETFs, and equity options +- **Fixed Income**: Bonds and bond funds +- **Derivatives**: Futures, options, and warrants +- **Foreign Exchange**: Spot FX and FX forwards +- **Cryptocurrencies**: Bitcoin, Ethereum, and other digital assets +- **Commodities**: Physical commodities and commodity futures +- **Indices**: Index products and index options ## The Interactive Brokers Client The `InteractiveBrokersClient` serves as the central component of the IB adapter, overseeing a range of critical functions. These include establishing and maintaining connections, handling API errors, executing trades, and gathering various types of data such as market data, contract/instrument data, and account details. -To ensure efficient management of these diverse responsibilities, the `InteractiveBrokersClient` is divided into several specialized mixin classes. This modular approach enhances manageability and clarity. The key subcomponents are: +To ensure efficient management of these diverse responsibilities, the `InteractiveBrokersClient` is divided into several specialized mixin classes. This modular approach enhances manageability and clarity. + +### Client Architecture + +The client uses a mixin-based architecture where each mixin handles a specific aspect of the IB API: + +#### Connection Management (`InteractiveBrokersClientConnectionMixin`) + +- Establishes and maintains socket connections to TWS/Gateway +- Handles connection timeouts and reconnection logic +- Manages connection state and health monitoring +- Supports configurable reconnection attempts via `IB_MAX_CONNECTION_ATTEMPTS` environment variable + +#### Error Handling (`InteractiveBrokersClientErrorMixin`) + +- Processes all API errors and warnings +- Categorizes errors by type (client errors, connectivity issues, request errors) +- Handles subscription and request-specific error scenarios +- Provides comprehensive error logging and debugging information + +#### Account Management (`InteractiveBrokersClientAccountMixin`) + +- Retrieves account information and balances +- Manages position data and portfolio updates +- Handles multi-account scenarios +- Processes account-related notifications + +#### Contract/Instrument Management (`InteractiveBrokersClientContractMixin`) + +- Retrieves contract details and specifications +- Handles instrument searches and lookups +- Manages contract validation and verification +- Supports complex instrument types (options chains, futures chains) -- `InteractiveBrokersClientConnectionMixin`: This class is dedicated to managing the connection with TWS/Gateway. -- `InteractiveBrokersClientErrorMixin`: It focuses on addressing all encountered errors and warnings. -- `InteractiveBrokersClientAccountMixin`: Responsible for handling requests related to account information and positions. -- `InteractiveBrokersClientContractMixin`: Handles retrieving contracts (instruments) data. -- `InteractiveBrokersClientMarketDataMixin`: Handles market data requests, subscriptions and data processing. -- `InteractiveBrokersClientOrderMixin`: Oversees all aspects of order placement and management. +#### Market Data Management (`InteractiveBrokersClientMarketDataMixin`) + +- Handles real-time and historical market data subscriptions +- Processes quotes, trades, and bar data +- Manages market data type settings (real-time, delayed, frozen) +- Handles tick-by-tick data and market depth + +#### Order Management (`InteractiveBrokersClientOrderMixin`) + +- Processes order placement, modification, and cancellation +- Handles order status updates and execution reports +- Manages order validation and error handling +- Supports complex order types and conditions + +### Key Features + +- **Asynchronous Operation**: All operations are fully asynchronous using Python's asyncio +- **Robust Error Handling**: Comprehensive error categorization and handling +- **Connection Resilience**: Automatic reconnection with configurable retry logic +- **Message Processing**: Efficient message queue processing for high-throughput scenarios +- **State Management**: Proper state tracking for connections, subscriptions, and requests :::tip To troubleshoot TWS API incoming message issues, consider starting at the `InteractiveBrokersClient._process_message` method, which acts as the primary gateway for processing all messages received from the API. @@ -102,46 +215,190 @@ To troubleshoot TWS API incoming message issues, consider starting at the `Inter ## Symbology -The InteractiveBrokersInstrumentProvider supports three methods for constructing InstrumentId instances, which can be configured via the `symbology_method` enum in `InteractiveBrokersInstrumentProviderConfig`. +The `InteractiveBrokersInstrumentProvider` supports three methods for constructing `InstrumentId` instances, which can be configured via the `symbology_method` enum in `InteractiveBrokersInstrumentProviderConfig`. + +### Symbology Methods + +#### 1. Simplified Symbology (`IB_SIMPLIFIED`) - Default + +When `symbology_method` is set to `IB_SIMPLIFIED` (the default setting), the system uses intuitive, human-readable symbology rules: + +**Format Rules by Asset Class:** + +- **Forex**: `{symbol}/{currency}.{exchange}` + - Example: `EUR/USD.IDEALPRO` +- **Stocks**: `{localSymbol}.{primaryExchange}` + - Spaces in localSymbol are replaced with hyphens + - Example: `BF-B.NYSE`, `SPY.ARCA` +- **Futures**: `{localSymbol}.{exchange}` + - Individual contracts use single digit years + - Example: `ESM4.CME`, `CLZ7.NYMEX` +- **Continuous Futures**: `{symbol}.{exchange}` + - Represents front month, automatically rolling + - Example: `ES.CME`, `CL.NYMEX` +- **Options on Futures (FOP)**: `{localSymbol}.{exchange}` + - Format: `{symbol}{month}{year} {right}{strike}` + - Example: `ESM4 C4200.CME` +- **Options**: `{localSymbol}.{exchange}` + - All spaces removed from localSymbol + - Example: `AAPL230217P00155000.SMART` +- **Indices**: `^{localSymbol}.{exchange}` + - Example: `^SPX.CBOE`, `^NDX.NASDAQ` +- **Bonds**: `{localSymbol}.{exchange}` + - Example: `912828XE8.SMART` +- **Cryptocurrencies**: `{symbol}/{currency}.{exchange}` + - Example: `BTC/USD.PAXOS`, `ETH/USD.PAXOS` + +#### 2. Raw Symbology (`IB_RAW`) -### Simplified Symbology +Setting `symbology_method` to `IB_RAW` enforces stricter parsing rules that align directly with the fields defined in the IB API. This method provides maximum compatibility across all regions and instrument types: -When symbology_method is set to `IB_SIMPLIFIED` (the default setting), the system utilizes the following parsing rules for symbology: +**Format Rules:** -- 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`. -- Futures: The format is `{localSymbol}.{exchange}`. Single digit years are expanded to two digits, e.g., `ESM24.CME`. -- 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`. +- **CFDs**: `{localSymbol}={secType}.IBCFD` +- **Commodities**: `{localSymbol}={secType}.IBCMDTY` +- **Default for Other Types**: `{localSymbol}={secType}.{exchange}` -### Raw Symbology +**Examples:** -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: +- `IBUS30=CFD.IBCFD` +- `XAUUSD=CMDTY.IBCMDTY` +- `AAPL=STK.SMART` -- CFDs: `{localSymbol}={secType}.IBCFD` -- Commodities: `{localSymbol}={secType}.IBCMDTY` -- Default for Other Types: `{localSymbol}={secType}.{exchange}` +This configuration ensures explicit instrument identification and supports instruments from any region, especially those with non-standard symbology where simplified parsing may fail. -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. +### MIC Venue Conversion -### Databento Symbology +The adapter supports converting Interactive Brokers exchange codes to Market Identifier Codes (MIC) for standardized venue identification: -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. +#### `convert_exchange_to_mic_venue` + +When set to `True`, the adapter automatically converts IB exchange codes to their corresponding MIC codes: + +```python +instrument_provider_config = InteractiveBrokersInstrumentProviderConfig( + convert_exchange_to_mic_venue=True, # Enable MIC conversion + symbology_method=SymbologyMethod.IB_SIMPLIFIED, +) +``` + +**Examples of MIC Conversion:** + +- `CME` → `XCME` (Chicago Mercantile Exchange) +- `NASDAQ` → `XNAS` (Nasdaq Stock Market) +- `NYSE` → `XNYS` (New York Stock Exchange) +- `LSE` → `XLON` (London Stock Exchange) + +#### `symbol_to_mic_venue` + +For custom venue mapping, use the `symbol_to_mic_venue` dictionary to override default conversions: + +```python +instrument_provider_config = InteractiveBrokersInstrumentProviderConfig( + convert_exchange_to_mic_venue=True, + symbol_to_mic_venue={ + "ES": "XCME", # All ES futures/options use CME MIC + "SPY": "ARCX", # SPY specifically uses ARCA + }, +) +``` + +### Supported Instrument Formats + +The adapter supports various instrument formats based on Interactive Brokers' contract specifications: + +#### Futures Month Codes + +- **F** = January, **G** = February, **H** = March, **J** = April +- **K** = May, **M** = June, **N** = July, **Q** = August +- **U** = September, **V** = October, **X** = November, **Z** = December + +#### Supported Exchanges by Asset Class + +**Futures Exchanges:** + +- `CME`, `CBOT`, `NYMEX`, `COMEX`, `KCBT`, `MGE`, `NYBOT`, `SNFE` + +**Options Exchanges:** + +- `SMART` (IB's smart routing) + +**Forex Exchanges:** + +- `IDEALPRO` (IB's forex platform) + +**Cryptocurrency Exchanges:** + +- `PAXOS` (IB's crypto platform) + +**CFD/Commodity Exchanges:** + +- `IBCFD`, `IBCMDTY` (IB's internal routing) + +### Choosing the Right Symbology Method + +- **Use `IB_SIMPLIFIED`** (default) for most use cases - provides clean, readable instrument IDs +- **Use `IB_RAW`** when dealing with complex international instruments or when simplified parsing fails +- **Enable `convert_exchange_to_mic_venue`** when you need standardized MIC venue codes for compliance or data consistency ## Instruments & Contracts -In IB, a NautilusTrader `Instrument` is equivalent to a [Contract](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#contracts). Contracts can be either a [basic contract](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#contract-object) or a more [detailed](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#contract-details) version (ContractDetails). The adapter models these using `IBContract` and `IBContractDetails` classes. The latter includes critical data like order types and trading hours, which are absent in the basic contract. As a result, `IBContractDetails` can be converted to an `Instrument` while `IBContract` cannot. +In Interactive Brokers, a NautilusTrader `Instrument` corresponds to an IB [Contract](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#contracts). The adapter handles two types of contract representations: + +### Contract Types + +#### Basic Contract (`IBContract`) + +- Contains essential contract identification fields +- Used for contract searches and basic operations +- Cannot be directly converted to a NautilusTrader `Instrument` + +#### Contract Details (`IBContractDetails`) + +- Contains comprehensive contract information including: + - Order types supported + - Trading hours and calendar + - Margin requirements + - Price increments and multipliers + - Market data permissions +- Can be converted to a NautilusTrader `Instrument` +- Required for trading operations + +### Contract Discovery To search for contract information, use the [IB Contract Information Center](https://pennies.interactivebrokers.com/cstools/contract_info/). -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`. +### Loading Instruments + +There are two primary methods for loading instruments: + +#### 1. Using `load_ids` (Recommended) +Use `symbology_method=SymbologyMethod.IB_SIMPLIFIED` (default) with `load_ids` for clean, intuitive instrument identification: + +```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, + load_ids=frozenset([ + "EUR/USD.IDEALPRO", # Forex + "SPY.ARCA", # Stock + "ESM24.CME", # Future + "BTC/USD.PAXOS", # Crypto + "^SPX.CBOE", # Index + ]), +) +``` + +#### 2. Using `load_contracts` (For Complex Instruments) +Use `load_contracts` with `IBContract` instances for complex scenarios like options/futures chains: ```python -for_loading_instrument_expiry = IBContract( +from nautilus_trader.adapters.interactive_brokers.common import IBContract + +# Load options chain for specific expiry +options_chain_expiry = IBContract( secType="IND", symbol="SPX", exchange="CBOE", @@ -149,7 +406,8 @@ for_loading_instrument_expiry = IBContract( lastTradeDateOrContractMonth='20240718', ) -for_loading_instrument_range = IBContract( +# Load options chain for date range +options_chain_range = IBContract( secType="IND", symbol="SPX", exchange="CBOE", @@ -157,214 +415,655 @@ for_loading_instrument_range = IBContract( min_expiry_days=0, max_expiry_days=30, ) -``` -> **Note**: The `secType` and `symbol` should be specified for the Underlying Contract.\ -> To specify an options exchange other than the underlying's, use the `options_chain_exchange` parameter. +# Load futures chain +futures_chain = IBContract( + secType="CONTFUT", + exchange="CME", + symbol="ES", + build_futures_chain=True, +) + +instrument_provider_config = InteractiveBrokersInstrumentProviderConfig( + load_contracts=frozenset([ + options_chain_expiry, + options_chain_range, + futures_chain, + ]), +) +``` -Some more examples of building IBContracts: +### IBContract Examples by Asset Class ```python from nautilus_trader.adapters.interactive_brokers.common import IBContract -# Stock +# Stocks IBContract(secType='STK', exchange='SMART', primaryExchange='ARCA', symbol='SPY') +IBContract(secType='STK', exchange='SMART', primaryExchange='NASDAQ', symbol='AAPL') -# Bond +# Bonds IBContract(secType='BOND', secIdType='ISIN', secId='US03076KAA60') +IBContract(secType='BOND', secIdType='CUSIP', secId='912828XE8') -# Option -IBContract(secType='STK', exchange='SMART', primaryExchange='ARCA', symbol='SPY', lastTradeDateOrContractMonth='20251219', build_options_chain=True) +# Individual Options +IBContract(secType='OPT', exchange='SMART', symbol='SPY', + lastTradeDateOrContractMonth='20251219', strike=500, right='C') -# CFD +# Options Chain (loads all strikes/expirations) +IBContract(secType='STK', exchange='SMART', primaryExchange='ARCA', symbol='SPY', + build_options_chain=True, min_expiry_days=10, max_expiry_days=60) + +# CFDs IBContract(secType='CFD', symbol='IBUS30') +IBContract(secType='CFD', symbol='DE40EUR', exchange='SMART') + +# Individual Futures +IBContract(secType='FUT', exchange='CME', symbol='ES', + lastTradeDateOrContractMonth='20240315') -# Future +# Futures Chain (loads all expirations) IBContract(secType='CONTFUT', exchange='CME', symbol='ES', build_futures_chain=True) +# Options on Futures (FOP) - Individual +IBContract(secType='FOP', exchange='CME', symbol='ES', + lastTradeDateOrContractMonth='20240315', strike=4200, right='C') + +# Options on Futures Chain (loads all strikes/expirations) +IBContract(secType='CONTFUT', exchange='CME', symbol='ES', + build_options_chain=True, min_expiry_days=7, max_expiry_days=60) + # Forex -IBContract(secType='CASH', exchange='IDEALPRO', symbol='EUR', currency='GBP') +IBContract(secType='CASH', exchange='IDEALPRO', symbol='EUR', currency='USD') +IBContract(secType='CASH', exchange='IDEALPRO', symbol='GBP', currency='JPY') -# Crypto +# Cryptocurrencies +IBContract(secType='CRYPTO', symbol='BTC', exchange='PAXOS', currency='USD') IBContract(secType='CRYPTO', symbol='ETH', exchange='PAXOS', currency='USD') + +# Indices +IBContract(secType='IND', symbol='SPX', exchange='CBOE') +IBContract(secType='IND', symbol='NDX', exchange='NASDAQ') + +# Commodities +IBContract(secType='CMDTY', symbol='XAUUSD', exchange='SMART') ``` +### Advanced Configuration Options + +```python +# Options chain with custom exchange +IBContract( + secType="STK", + symbol="AAPL", + exchange="SMART", + primaryExchange="NASDAQ", + build_options_chain=True, + options_chain_exchange="CBOE", # Use CBOE for options instead of SMART + min_expiry_days=7, + max_expiry_days=45, +) + +# Futures chain with specific months +IBContract( + secType="CONTFUT", + exchange="NYMEX", + symbol="CL", # Crude Oil + build_futures_chain=True, + min_expiry_days=30, + max_expiry_days=180, +) +``` + +### Continuous Futures + +For continuous futures contracts (using `secType='CONTFUT'`), the adapter creates instrument IDs using just the symbol and venue: + +```python +# Continuous futures examples +IBContract(secType='CONTFUT', exchange='CME', symbol='ES') # → ES.CME +IBContract(secType='CONTFUT', exchange='NYMEX', symbol='CL') # → CL.NYMEX + +# With MIC venue conversion enabled +instrument_provider_config = InteractiveBrokersInstrumentProviderConfig( + convert_exchange_to_mic_venue=True, +) +# Results in: +# ES.XCME (instead of ES.CME) +# CL.XNYM (instead of CL.NYMEX) +``` + +**Continuous Futures vs Individual Futures:** + +- **Continuous**: `ES.CME` - Represents the front month contract, automatically rolls +- **Individual**: `ESM4.CME` - Specific March 2024 contract + +:::note +When using `build_options_chain=True` or `build_futures_chain=True`, the `secType` and `symbol` should be specified for the underlying contract. The adapter will automatically discover and load all related derivative contracts within the specified expiry range. +::: + ## Historical Data & Backtesting -When developing strategies with the IB adapter, the first step usually involves acquiring historical data for backtesting. The `HistoricInteractiveBrokersClient` offers methods to request and save this data. +The `HistoricInteractiveBrokersClient` provides comprehensive methods for retrieving historical data from Interactive Brokers for backtesting and research purposes. + +### Supported Data Types + +- **Bar Data**: OHLCV bars with various aggregations (time-based, tick-based, volume-based) +- **Tick Data**: Trade ticks and quote ticks with microsecond precision +- **Instrument Data**: Complete contract specifications and trading rules + +### Historical Data Client + +```python +from nautilus_trader.adapters.interactive_brokers.historical.client import HistoricInteractiveBrokersClient +from ibapi.common import MarketDataTypeEnum + +# Initialize the client +client = HistoricInteractiveBrokersClient( + host="127.0.0.1", + port=7497, + client_id=1, + market_data_type=MarketDataTypeEnum.DELAYED_FROZEN, # Use delayed data if no subscription + log_level="INFO" +) + +# Connect to TWS/Gateway +await client.connect() +``` + +### Retrieving Instruments + +```python +from nautilus_trader.adapters.interactive_brokers.common import IBContract + +# Define contracts +contracts = [ + IBContract(secType="STK", symbol="AAPL", exchange="SMART", primaryExchange="NASDAQ"), + IBContract(secType="STK", symbol="MSFT", exchange="SMART", primaryExchange="NASDAQ"), + IBContract(secType="CASH", symbol="EUR", currency="USD", exchange="IDEALPRO"), +] + +# Request instrument definitions +instruments = await client.request_instruments(contracts=contracts) +``` -Here's an example of retrieving and saving instrument and bar data. A more comprehensive example is available [here](https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/interactive_brokers/historic_download.py). +### Retrieving Historical Bars ```python import datetime + +# Request historical bars +bars = await client.request_bars( + bar_specifications=[ + "1-MINUTE-LAST", # 1-minute bars using last price + "5-MINUTE-MID", # 5-minute bars using midpoint + "1-HOUR-LAST", # 1-hour bars using last price + "1-DAY-LAST", # Daily bars using last price + ], + start_date_time=datetime.datetime(2023, 11, 1, 9, 30), + end_date_time=datetime.datetime(2023, 11, 6, 16, 30), + tz_name="America/New_York", + contracts=contracts, + use_rth=True, # Regular Trading Hours only + timeout=120, # Request timeout in seconds +) +``` + +### Retrieving Historical Ticks + +```python +# Request historical tick data +ticks = await client.request_ticks( + tick_types=["TRADES", "BID_ASK"], # Trade ticks and quote ticks + start_date_time=datetime.datetime(2023, 11, 6, 9, 30), + end_date_time=datetime.datetime(2023, 11, 6, 16, 30), + tz_name="America/New_York", + contracts=contracts, + use_rth=True, + timeout=120, +) +``` + +### Bar Specifications + +The adapter supports various bar specifications: + +#### Time-Based Bars + +- `"1-SECOND-LAST"`, `"5-SECOND-LAST"`, `"10-SECOND-LAST"`, `"15-SECOND-LAST"`, `"30-SECOND-LAST"` +- `"1-MINUTE-LAST"`, `"2-MINUTE-LAST"`, `"3-MINUTE-LAST"`, `"5-MINUTE-LAST"`, `"10-MINUTE-LAST"`, `"15-MINUTE-LAST"`, `"20-MINUTE-LAST"`, `"30-MINUTE-LAST"` +- `"1-HOUR-LAST"`, `"2-HOUR-LAST"`, `"3-HOUR-LAST"`, `"4-HOUR-LAST"`, `"8-HOUR-LAST"` +- `"1-DAY-LAST"`, `"1-WEEK-LAST"`, `"1-MONTH-LAST"` + +#### Price Types + +- `LAST` - Last traded price +- `MID` - Midpoint of bid/ask +- `BID` - Bid price +- `ASK` - Ask price + +### Complete Example + +```python +import asyncio +import datetime from nautilus_trader.adapters.interactive_brokers.common import IBContract -from nautilus_trader.adapters.interactive_brokers.historic import HistoricInteractiveBrokersClient +from nautilus_trader.adapters.interactive_brokers.historical.client import HistoricInteractiveBrokersClient from nautilus_trader.persistence.catalog import ParquetDataCatalog -async def main(): - contract = IBContract( - secType="STK", - symbol="AAPL", - exchange="SMART", - primaryExchange="NASDAQ", +async def download_historical_data(): + # Initialize client + client = HistoricInteractiveBrokersClient( + host="127.0.0.1", + port=7497, + client_id=5, ) - client = HistoricInteractiveBrokersClient() - instruments = await client.request_instruments( - contracts=[contract], - ) + # Connect + await client.connect() + await asyncio.sleep(2) # Allow connection to stabilize + + # Define contracts + contracts = [ + IBContract(secType="STK", symbol="AAPL", exchange="SMART", primaryExchange="NASDAQ"), + IBContract(secType="CASH", symbol="EUR", currency="USD", exchange="IDEALPRO"), + ] + # Request instruments + instruments = await client.request_instruments(contracts=contracts) + + # Request historical bars bars = await client.request_bars( - bar_specifications=["1-HOUR-LAST", "30-MINUTE-MID"], + bar_specifications=["1-HOUR-LAST", "1-DAY-LAST"], + start_date_time=datetime.datetime(2023, 11, 1, 9, 30), end_date_time=datetime.datetime(2023, 11, 6, 16, 30), tz_name="America/New_York", - duration="1 D", - contracts=[contract], + contracts=contracts, + use_rth=True, + ) + + # Request tick data + ticks = await client.request_ticks( + tick_types=["TRADES"], + start_date_time=datetime.datetime(2023, 11, 6, 14, 0), + end_date_time=datetime.datetime(2023, 11, 6, 15, 0), + tz_name="America/New_York", + contracts=contracts, ) + # Save to catalog catalog = ParquetDataCatalog("./catalog") catalog.write_data(instruments) catalog.write_data(bars) + catalog.write_data(ticks) + + print(f"Downloaded {len(instruments)} instruments") + print(f"Downloaded {len(bars)} bars") + print(f"Downloaded {len(ticks)} ticks") + + # Disconnect + await client.disconnect() + +# Run the example +if __name__ == "__main__": + asyncio.run(download_historical_data()) ``` +### Data Limitations + +Be aware of Interactive Brokers' historical data limitations: + +- **Rate Limits**: IB enforces rate limits on historical data requests +- **Data Availability**: Historical data availability varies by instrument and subscription level +- **Market Data Permissions**: Some data requires specific market data subscriptions +- **Time Ranges**: Maximum lookback periods vary by bar size and instrument type + +### Best Practices + +1. **Use Delayed Data**: For backtesting, `MarketDataTypeEnum.DELAYED_FROZEN` is often sufficient +2. **Batch Requests**: Group multiple instruments in single requests when possible +3. **Handle Timeouts**: Set appropriate timeout values for large data requests +4. **Respect Rate Limits**: Add delays between requests to avoid hitting rate limits +5. **Validate Data**: Always check data quality and completeness before backtesting + ## Live Trading -Engaging in live or paper trading requires constructing and running a `TradingNode`. -This node incorporates both `InteractiveBrokersDataClient` and `InteractiveBrokersExecutionClient`, -which depend on the `InteractiveBrokersInstrumentProvider` to operate. +Live trading with Interactive Brokers requires setting up a `TradingNode` that incorporates both `InteractiveBrokersDataClient` and `InteractiveBrokersExecutionClient`. These clients depend on the `InteractiveBrokersInstrumentProvider` for instrument management. + +### Architecture Overview + +The live trading setup consists of three main components: -### InstrumentProvider +1. **InstrumentProvider**: Manages instrument definitions and contract details +2. **DataClient**: Handles real-time market data subscriptions +3. **ExecutionClient**: Manages orders, positions, and account information -The `InteractiveBrokersInstrumentProvider` class functions as a bridge for accessing financial instrument data from IB. -Configurable through `InteractiveBrokersInstrumentProviderConfig`, it enables the customization of various instrument type parameters. -Additionally, this provider offers specialized methods to build and retrieve the entire futures and options chains. +### InstrumentProvider Configuration + +The `InteractiveBrokersInstrumentProvider` serves as the bridge for accessing financial instrument data from IB. It supports loading individual instruments, options chains, and futures chains. + +#### Basic Configuration ```python from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersInstrumentProviderConfig from nautilus_trader.adapters.interactive_brokers.config import SymbologyMethod - +from nautilus_trader.adapters.interactive_brokers.common import IBContract 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 - max_expiry_days=60, # Relevant for futures/options with expiration - load_ids=frozenset( - [ - "EUR/USD.IDEALPRO", - "BTC/USD.PAXOS", - "SPY.ARCA", - "V.NYSE", - "YMH24.CBOT", - "CLZ27.NYMEX", - "ESZ27.CME", - ], - ), - load_contracts=frozenset( - [ - IBContract(secType='STK', symbol='SPY', exchange='SMART', primaryExchange='ARCA'), - IBContract(secType='STK', symbol='AAPL', exchange='SMART', primaryExchange='NASDAQ') - ] - ), + build_futures_chain=False, # Set to True if fetching futures chains + build_options_chain=False, # Set to True if fetching options chains + min_expiry_days=10, # Minimum days to expiry for derivatives + max_expiry_days=60, # Maximum days to expiry for derivatives + convert_exchange_to_mic_venue=False, # Use MIC codes for venue mapping + cache_validity_days=1, # Cache instrument data for 1 day + load_ids=frozenset([ + # Individual instruments using simplified symbology + "EUR/USD.IDEALPRO", # Forex + "BTC/USD.PAXOS", # Cryptocurrency + "SPY.ARCA", # Stock ETF + "V.NYSE", # Individual stock + "ESM4.CME", # Future contract (single digit year) + "^SPX.CBOE", # Index + ]), + load_contracts=frozenset([ + # Complex instruments using IBContract + IBContract(secType='STK', symbol='AAPL', exchange='SMART', primaryExchange='NASDAQ'), + IBContract(secType='CASH', symbol='GBP', currency='USD', exchange='IDEALPRO'), + ]), +) +``` + +#### Advanced Configuration for Derivatives + +```python +# Configuration for options and futures chains +advanced_config = InteractiveBrokersInstrumentProviderConfig( + symbology_method=SymbologyMethod.IB_SIMPLIFIED, + build_futures_chain=True, # Enable futures chain loading + build_options_chain=True, # Enable options chain loading + min_expiry_days=7, # Load contracts expiring in 7+ days + max_expiry_days=90, # Load contracts expiring within 90 days + load_contracts=frozenset([ + # Load SPY options chain + IBContract( + secType='STK', + symbol='SPY', + exchange='SMART', + primaryExchange='ARCA', + build_options_chain=True, + ), + # Load ES futures chain + IBContract( + secType='CONTFUT', + exchange='CME', + symbol='ES', + build_futures_chain=True, + ), + ]), ) ``` -### Integration with Databento Data Client +### Integration with External Data Providers + +The Interactive Brokers adapter can be used alongside other data providers for enhanced market data coverage. When using multiple data sources: + +- Use consistent symbology methods across providers +- Consider using `convert_exchange_to_mic_venue=True` for standardized venue identification +- Ensure instrument cache management is handled properly to avoid conflicts -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. +### Data Client Configuration -When using this configuration: +The `InteractiveBrokersDataClient` interfaces with IB for streaming and retrieving real-time market data. Upon connection, it configures the [market data type](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#delayed-market-data) and loads instruments based on the `InteractiveBrokersInstrumentProviderConfig` settings. -- `InteractiveBrokersInstrumentProvider` will not publish instruments to the cache to prevent conflicts. -- Instruments Cache management must be handled exclusively by `DatabentoDataClient`. +#### Supported Data Types -### Data Client +- **Quote Ticks**: Real-time bid/ask prices and sizes +- **Trade Ticks**: Real-time trade prices and volumes +- **Bar Data**: Real-time OHLCV bars (1-second to 1-day intervals) +- **Market Depth**: Level 2 order book data (where available) -`InteractiveBrokersDataClient` interfaces with IB for streaming and retrieving market data. Upon -connection, it configures the [market data type](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/#delayed-market-data) -and loads instruments based on the settings in `InteractiveBrokersInstrumentProviderConfig`. -This client can subscribe to and unsubscribe from various market data types, including quotes, trades, and bars. +#### Market Data Types -Configurable through `InteractiveBrokersDataClientConfig`, it enables adjustments for handling revised bars, -trading hours preferences, and market data types (e.g., `IBMarketDataTypeEnum.REALTIME` or `IBMarketDataTypeEnum.DELAYED_FROZEN`). +Interactive Brokers supports several market data types: + +- `REALTIME`: Live market data (requires market data subscriptions) +- `DELAYED`: 15-20 minute delayed data (free for most markets) +- `DELAYED_FROZEN`: Delayed data that doesn't update (useful for testing) +- `FROZEN`: Last known real-time data (when market is closed) + +#### Basic Data Client Configuration ```python from nautilus_trader.adapters.interactive_brokers.config import IBMarketDataTypeEnum from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersDataClientConfig - data_client_config = InteractiveBrokersDataClientConfig( - ibg_port=4002, - handle_revised_bars=False, - use_regular_trading_hours=True, - market_data_type=IBMarketDataTypeEnum.DELAYED_FROZEN, # Default is REALTIME if not set + ibg_host="127.0.0.1", + ibg_port=7497, # TWS paper trading port + ibg_client_id=1, + use_regular_trading_hours=True, # RTH only for stocks + market_data_type=IBMarketDataTypeEnum.DELAYED_FROZEN, # Use delayed data + ignore_quote_tick_size_updates=False, # Include size-only updates instrument_provider=instrument_provider_config, - dockerized_gateway=dockerized_gateway_config, + connection_timeout=300, # 5 minutes + request_timeout=60, # 1 minute +) +``` + +#### Advanced Data Client Configuration + +```python +# Configuration for production with real-time data +production_data_config = InteractiveBrokersDataClientConfig( + ibg_host="127.0.0.1", + ibg_port=4001, # IB Gateway live trading port + ibg_client_id=1, + use_regular_trading_hours=False, # Include extended hours + market_data_type=IBMarketDataTypeEnum.REALTIME, # Real-time data + ignore_quote_tick_size_updates=True, # Reduce tick volume + handle_revised_bars=True, # Handle bar revisions + instrument_provider=instrument_provider_config, + dockerized_gateway=dockerized_gateway_config, # If using Docker + connection_timeout=300, + request_timeout=60, ) ``` -### Execution Client +#### Configuration Options Explained + +- **`use_regular_trading_hours`**: When `True`, only requests data during regular trading hours. Primarily affects bar data for stocks. +- **`ignore_quote_tick_size_updates`**: When `True`, filters out quote ticks where only the size changed (not price), reducing data volume. +- **`handle_revised_bars`**: When `True`, processes bar revisions from IB (bars can be updated after initial publication). +- **`connection_timeout`**: Maximum time to wait for initial connection establishment. +- **`request_timeout`**: Maximum time to wait for historical data requests. + +### Execution Client Configuration + +The `InteractiveBrokersExecutionClient` handles trade execution, order management, account information, and position tracking. It provides comprehensive order lifecycle management and real-time account updates. + +#### Supported Functionality -The `InteractiveBrokersExecutionClient` facilitates executing trades, accessing account information, -and processing order and trade-related details. It encompasses a range of methods for order management, -including reporting order statuses, placing new orders, and modifying or canceling existing ones. -Additionally, it generates position reports, although fill reports are not yet implemented. +- **Order Management**: Place, modify, and cancel orders +- **Order Types**: Market, limit, stop, stop-limit, trailing stop, and more +- **Account Information**: Real-time balance and margin updates +- **Position Tracking**: Real-time position updates and P&L +- **Trade Reporting**: Execution reports and fill notifications +- **Risk Management**: Pre-trade risk checks and position limits + +#### Supported Order Types + +The adapter supports most Interactive Brokers order types: + +- **Market Orders**: `OrderType.MARKET` +- **Limit Orders**: `OrderType.LIMIT` +- **Stop Orders**: `OrderType.STOP_MARKET` +- **Stop-Limit Orders**: `OrderType.STOP_LIMIT` +- **Market-If-Touched**: `OrderType.MARKET_IF_TOUCHED` +- **Limit-If-Touched**: `OrderType.LIMIT_IF_TOUCHED` +- **Trailing Stop Market**: `OrderType.TRAILING_STOP_MARKET` +- **Trailing Stop Limit**: `OrderType.TRAILING_STOP_LIMIT` +- **Market-on-Close**: `OrderType.MARKET` with `TimeInForce.AT_THE_CLOSE` +- **Limit-on-Close**: `OrderType.LIMIT` with `TimeInForce.AT_THE_CLOSE` + +#### Time-in-Force Options + +- **Day Orders**: `TimeInForce.DAY` +- **Good-Till-Canceled**: `TimeInForce.GTC` +- **Immediate-or-Cancel**: `TimeInForce.IOC` +- **Fill-or-Kill**: `TimeInForce.FOK` +- **Good-Till-Date**: `TimeInForce.GTD` +- **At-the-Open**: `TimeInForce.AT_THE_OPEN` +- **At-the-Close**: `TimeInForce.AT_THE_CLOSE` + +#### Basic Execution Client Configuration ```python from nautilus_trader.adapters.interactive_brokers.config import InteractiveBrokersExecClientConfig from nautilus_trader.config import RoutingConfig - exec_client_config = InteractiveBrokersExecClientConfig( - ibg_port=4002, - account_id="DU123456", # Must match the connected IB Gateway/TWS - dockerized_gateway=dockerized_gateway_config, + ibg_host="127.0.0.1", + ibg_port=7497, # TWS paper trading port + ibg_client_id=1, + account_id="DU123456", # Your IB account ID (paper or live) instrument_provider=instrument_provider_config, - routing=RoutingConfig( - default=True, - ) + connection_timeout=300, + routing=RoutingConfig(default=True), # Route all orders through this client ) ``` -### Full Configuration +#### Advanced Execution Client Configuration -Setting up a complete trading environment typically involves configuring a `TradingNodeConfig`, which -includes data and execution client configurations. Additional configurations are specified in `LiveDataEngineConfig` -to accommodate IB-specific requirements. A `TradingNode` is then instantiated from these configurations, -and factories for creating `InteractiveBrokersDataClient` and `InteractiveBrokersExecutionClient` are added. -Finally, the node is built and run. +```python +# Production configuration with dockerized gateway +production_exec_config = InteractiveBrokersExecClientConfig( + ibg_host="127.0.0.1", + ibg_port=4001, # IB Gateway live trading port + ibg_client_id=1, + account_id=None, # Will use TWS_ACCOUNT environment variable + instrument_provider=instrument_provider_config, + dockerized_gateway=dockerized_gateway_config, + connection_timeout=300, + routing=RoutingConfig(default=True), +) +``` -You can find additional examples here: +#### Account ID Configuration + +The `account_id` parameter is crucial and must match the account logged into TWS/Gateway: + +```python +# Option 1: Specify directly in config +exec_config = InteractiveBrokersExecClientConfig( + account_id="DU123456", # Paper trading account + # ... other parameters +) + +# Option 2: Use environment variable +import os +os.environ["TWS_ACCOUNT"] = "DU123456" +exec_config = InteractiveBrokersExecClientConfig( + account_id=None, # Will use TWS_ACCOUNT env var + # ... other parameters +) +``` + +#### Order Tags and Advanced Features + +The adapter supports IB-specific order parameters through order tags: + +```python +from nautilus_trader.adapters.interactive_brokers.common import IBOrderTags + +# Create order with IB-specific parameters +order_tags = IBOrderTags( + allOrNone=True, # All-or-none order + ocaGroup="MyGroup1", # One-cancels-all group + ocaType=1, # Cancel with block + activeStartTime="20240315 09:30:00 EST", # GTC activation time + activeStopTime="20240315 16:00:00 EST", # GTC deactivation time + goodAfterTime="20240315 09:35:00 EST", # Good after time +) + +# Apply tags to order (implementation depends on your strategy code) +``` + +### Complete Trading Node Configuration + +Setting up a complete trading environment involves configuring a `TradingNodeConfig` with all necessary components. Here are comprehensive examples for different scenarios. + +#### Paper Trading Configuration ```python +import os from nautilus_trader.adapters.interactive_brokers.common import IB from nautilus_trader.adapters.interactive_brokers.common import IB_VENUE +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 IBMarketDataTypeEnum +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 from nautilus_trader.config import LoggingConfig +from nautilus_trader.config import RoutingConfig from nautilus_trader.config import TradingNodeConfig from nautilus_trader.live.node import TradingNode +# Instrument provider configuration +instrument_provider_config = InteractiveBrokersInstrumentProviderConfig( + symbology_method=SymbologyMethod.IB_SIMPLIFIED, + load_ids=frozenset([ + "EUR/USD.IDEALPRO", + "GBP/USD.IDEALPRO", + "SPY.ARCA", + "QQQ.NASDAQ", + "AAPL.NASDAQ", + "MSFT.NASDAQ", + ]), +) -# ... [continuing from prior example code] ... +# Data client configuration +data_client_config = InteractiveBrokersDataClientConfig( + ibg_host="127.0.0.1", + ibg_port=7497, # TWS paper trading + ibg_client_id=1, + use_regular_trading_hours=True, + market_data_type=IBMarketDataTypeEnum.DELAYED_FROZEN, + instrument_provider=instrument_provider_config, +) -# Configure the trading node +# Execution client configuration +exec_client_config = InteractiveBrokersExecClientConfig( + ibg_host="127.0.0.1", + ibg_port=7497, # TWS paper trading + ibg_client_id=1, + account_id="DU123456", # Your paper trading account + instrument_provider=instrument_provider_config, + routing=RoutingConfig(default=True), +) + +# Trading node configuration config_node = TradingNodeConfig( - trader_id="TESTER-001", + trader_id="PAPER-TRADER-001", logging=LoggingConfig(log_level="INFO"), data_clients={IB: data_client_config}, exec_clients={IB: exec_client_config}, data_engine=LiveDataEngineConfig( - time_bars_timestamp_on_close=False, # Use opening time as `ts_event`, as per IB standard - validate_data_sequence=True, # Discards bars received out of sequence + time_bars_timestamp_on_close=False, # IB standard: use bar open time + validate_data_sequence=True, # Discard out-of-sequence bars ), + timeout_connection=90.0, + timeout_reconciliation=5.0, + timeout_portfolio=5.0, + timeout_disconnection=5.0, + timeout_post_stop=2.0, ) +# Create and configure the trading node node = TradingNode(config=config_node) node.add_data_client_factory(IB, InteractiveBrokersLiveDataClientFactory) node.add_exec_client_factory(IB, InteractiveBrokersLiveExecClientFactory) @@ -375,6 +1074,275 @@ if __name__ == "__main__": try: node.run() finally: - # Stop and dispose of the node with SIGINT/CTRL+C node.dispose() ``` + +#### Live Trading with Dockerized Gateway + +```python +from nautilus_trader.adapters.interactive_brokers.config import DockerizedIBGatewayConfig + +# Dockerized gateway configuration +dockerized_gateway_config = DockerizedIBGatewayConfig( + username=os.environ.get("TWS_USERNAME"), + password=os.environ.get("TWS_PASSWORD"), + trading_mode="live", # "paper" or "live" + read_only_api=False, # Allow order execution + timeout=300, +) + +# Data client with dockerized gateway +data_client_config = InteractiveBrokersDataClientConfig( + ibg_client_id=1, + use_regular_trading_hours=False, # Include extended hours + market_data_type=IBMarketDataTypeEnum.REALTIME, + instrument_provider=instrument_provider_config, + dockerized_gateway=dockerized_gateway_config, +) + +# Execution client with dockerized gateway +exec_client_config = InteractiveBrokersExecClientConfig( + ibg_client_id=1, + account_id=os.environ.get("TWS_ACCOUNT"), # Live account ID + instrument_provider=instrument_provider_config, + dockerized_gateway=dockerized_gateway_config, + routing=RoutingConfig(default=True), +) + +# Live trading node configuration +config_node = TradingNodeConfig( + trader_id="LIVE-TRADER-001", + logging=LoggingConfig(log_level="INFO"), + data_clients={IB: data_client_config}, + exec_clients={IB: exec_client_config}, + data_engine=LiveDataEngineConfig( + time_bars_timestamp_on_close=False, + validate_data_sequence=True, + ), +) +``` + +#### Multi-Client Configuration + +For advanced setups, you can configure multiple clients with different purposes: + +```python +# Separate data and execution clients with different client IDs +data_client_config = InteractiveBrokersDataClientConfig( + ibg_host="127.0.0.1", + ibg_port=7497, + ibg_client_id=1, # Data client uses ID 1 + market_data_type=IBMarketDataTypeEnum.REALTIME, + instrument_provider=instrument_provider_config, +) + +exec_client_config = InteractiveBrokersExecClientConfig( + ibg_host="127.0.0.1", + ibg_port=7497, + ibg_client_id=2, # Execution client uses ID 2 + account_id="DU123456", + instrument_provider=instrument_provider_config, + routing=RoutingConfig(default=True), +) +``` + +### Running the Trading Node + +```python +def run_trading_node(): + """Run the trading node with proper error handling.""" + node = None + try: + # Create and build node + node = TradingNode(config=config_node) + node.add_data_client_factory(IB, InteractiveBrokersLiveDataClientFactory) + node.add_exec_client_factory(IB, InteractiveBrokersLiveExecClientFactory) + node.build() + + # Set venue for portfolio + node.portfolio.set_specific_venue(IB_VENUE) + + # Add your strategies here + # node.trader.add_strategy(YourStrategy()) + + # Run the node + node.run() + + except KeyboardInterrupt: + print("Shutting down...") + except Exception as e: + print(f"Error: {e}") + finally: + if node: + node.dispose() + +if __name__ == "__main__": + run_trading_node() +``` + +### Additional Configuration Options + +#### Environment Variables + +Set these environment variables for easier configuration: + +```bash +export TWS_USERNAME="your_ib_username" +export TWS_PASSWORD="your_ib_password" +export TWS_ACCOUNT="your_account_id" +export IB_MAX_CONNECTION_ATTEMPTS="5" # Optional: limit reconnection attempts +``` + +#### Logging Configuration + +```python +# Enhanced logging configuration +logging_config = LoggingConfig( + log_level="INFO", + log_level_file="DEBUG", + log_file_format="json", # JSON format for structured logging + log_component_levels={ + "InteractiveBrokersClient": "DEBUG", + "InteractiveBrokersDataClient": "INFO", + "InteractiveBrokersExecutionClient": "INFO", + }, +) +``` + +You can find additional examples here: + +## Troubleshooting + +### Common Connection Issues + +#### Connection Refused + +- **Cause**: TWS/Gateway not running or wrong port +- **Solution**: Verify TWS/Gateway is running and check port configuration +- **Default Ports**: TWS (7497/7496), IB Gateway (4002/4001) + +#### Authentication Errors + +- **Cause**: Incorrect credentials or account not logged in +- **Solution**: Verify username/password and ensure account is logged into TWS/Gateway + +#### Client ID Conflicts + +- **Cause**: Multiple clients using the same client ID +- **Solution**: Use unique client IDs for each connection + +#### Market Data Permissions + +- **Cause**: Insufficient market data subscriptions +- **Solution**: Use `IBMarketDataTypeEnum.DELAYED_FROZEN` for testing or subscribe to required data feeds + +### Error Codes + +Interactive Brokers uses specific error codes. Common ones include: + +- **200**: No security definition found +- **201**: Order rejected - reason follows +- **202**: Order cancelled +- **300**: Can't find EId with ticker ID +- **354**: Requested market data is not subscribed +- **2104**: Market data farm connection is OK +- **2106**: HMDS data farm connection is OK + +### Performance Optimization + +#### Reduce Data Volume + +```python +# Reduce quote tick volume by ignoring size-only updates +data_config = InteractiveBrokersDataClientConfig( + ignore_quote_tick_size_updates=True, + # ... other config +) +``` + +#### Connection Management + +```python +# Set reasonable timeouts +config = InteractiveBrokersDataClientConfig( + connection_timeout=300, # 5 minutes + request_timeout=60, # 1 minute + # ... other config +) +``` + +#### Memory Management + +- Use appropriate bar sizes for your strategy +- Limit the number of simultaneous subscriptions +- Consider using historical data for backtesting instead of live data + +### Best Practices + +#### Security + +- Never hardcode credentials in source code +- Use environment variables for sensitive information +- Use paper trading for development and testing +- Set `read_only_api=True` for data-only applications + +#### Development Workflow + +1. **Start with Paper Trading**: Always test with paper trading first +2. **Use Delayed Data**: Use `DELAYED_FROZEN` market data for development +3. **Implement Proper Error Handling**: Handle connection losses and API errors gracefully +4. **Monitor Logs**: Enable appropriate logging levels for debugging +5. **Test Reconnection**: Test your strategy's behavior during connection interruptions + +#### Production Deployment + +- Use dockerized gateway for automated deployments +- Implement proper monitoring and alerting +- Set up log aggregation and analysis +- Use real-time data subscriptions only when necessary +- Implement circuit breakers and position limits + +#### Order Management + +- Always validate orders before submission +- Implement proper position sizing +- Use appropriate order types for your strategy +- Monitor order status and handle rejections +- Implement timeout handling for order operations + +### Debugging Tips + +#### Enable Debug Logging + +```python +logging_config = LoggingConfig( + log_level="DEBUG", + log_component_levels={ + "InteractiveBrokersClient": "DEBUG", + }, +) +``` + +#### Monitor Connection Status + +```python +# Check connection status in your strategy +if not self.data_client.is_connected: + self.log.warning("Data client disconnected") +``` + +#### Validate Instruments + +```python +# Ensure instruments are loaded before trading +instruments = self.cache.instruments() +if not instruments: + self.log.error("No instruments loaded") +``` + +### Support and Resources + +- **IB API Documentation**: [TWS API Guide](https://ibkrcampus.com/ibkr-api-page/trader-workstation-api/) +- **NautilusTrader Examples**: [GitHub Examples](https://github.com/nautechsystems/nautilus_trader/tree/develop/examples/live/interactive_brokers) +- **IB Contract Search**: [Contract Information Center](https://pennies.interactivebrokers.com/cstools/contract_info/) +- **Market Data Subscriptions**: [IB Market Data](https://www.interactivebrokers.com/en/trading/market-data.php) diff --git a/docs/integrations/tardis.md b/docs/integrations/tardis.md index a1c9b0db025a..d0e023a4ab78 100644 --- a/docs/integrations/tardis.md +++ b/docs/integrations/tardis.md @@ -108,35 +108,38 @@ The table below outlines the mappings between Nautilus venues and corresponding | Nautilus venue | Tardis exchange(s) | |:------------------------|:------------------------------------------------------| | `ASCENDEX` | `ascendex` | -| `BINANCE` | `binance`, `binance-dex`, `binance-futures`, `binance-jersey`, `binance-options`, `binance-us` | +| `BINANCE` | `binance`, `binance-dex`, `binance-european-options`, `binance-futures`, `binance-jersey`, `binance-options` | | `BINANCE_DELIVERY` | `binance-delivery` (*COIN-margined contracts*) | | `BINANCE_US` | `binance-us` | | `BITFINEX` | `bitfinex`, `bitfinex-derivatives` | | `BITFLYER` | `bitflyer` | +| `BITGET` | `bitget`, `bitget-futures` | | `BITMEX` | `bitmex` | | `BITNOMIAL` | `bitnomial` | | `BITSTAMP` | `bitstamp` | | `BLOCKCHAIN_COM` | `blockchain-com` | | `BYBIT` | `bybit`, `bybit-options`, `bybit-spot` | | `COINBASE` | `coinbase` | +| `COINBASE_INTX` | `coinbase-international` | | `COINFLEX` | `coinflex` (*for historical research*) | -| `CRYPTO_COM` | `crypto-com` | +| `CRYPTO_COM` | `crypto-com`, `crypto-com-derivatives` | | `CRYPTOFACILITIES` | `cryptofacilities` | | `DELTA` | `delta` | | `DERIBIT` | `deribit` | | `DYDX` | `dydx` | -| `FTX` | `ftx` (*historical research*) | -| `FTX_US` | `ftx-us` (*historical research*) | +| `DYDX_V4` | `dydx-v4` | +| `FTX` | `ftx`, `ftx-us` (*historical research*) | | `GATE_IO` | `gate-io`, `gate-io-futures` | | `GEMINI` | `gemini` | | `HITBTC` | `hitbtc` | | `HUOBI` | `huobi`, `huobi-dm`, `huobi-dm-linear-swap`, `huobi-dm-options` | | `HUOBI_DELIVERY` | `huobi-dm-swap` | -| `KRAKEN` | `kraken` | -| `KUCOIN` | `kucoin` | +| `HYPERLIQUID` | `hyperliquid` | +| `KRAKEN` | `kraken`, `kraken-futures` | +| `KUCOIN` | `kucoin`, `kucoin-futures` | | `MANGO` | `mango` | | `OKCOIN` | `okcoin` | -| `OKEX` | `okex`, `okex-futures`, `okex-options`, `okex-swap` | +| `OKEX` | `okex`, `okex-futures`, `okex-options`, `okex-spreads`, `okex-swap` | | `PHEMEX` | `phemex` | | `POLONIEX` | `poloniex` | | `SERUM` | `serum` (*historical research*) | @@ -150,7 +153,7 @@ The following environment variables are used by Tardis and NautilusTrader. - `TM_API_KEY`: API key for the Tardis Machine. - `TARDIS_API_KEY`: API key for NautilusTrader Tardis clients. -- `TARDIS_WS_URL` (optional): WebSocket URL for the `TardisMachineClient` in NautilusTrader. +- `TARDIS_MACHINE_WS_URL` (optional): WebSocket URL for the `TardisMachineClient` in NautilusTrader. - `TARDIS_BASE_URL` (optional): Base URL for the `TardisHttpClient` in NautilusTrader. - `NAUTILUS_CATALOG_PATH` (optional): Root directory for writing replay data in the Nautilus catalog. @@ -200,7 +203,7 @@ Next, ensure you have a configuration JSON file available. | Field | Type | Description | Default | |:--------------------|:------------------|:------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------| -| `tardis_ws_url` | string (optional) | The Tardis Machine WebSocket URL. | If `null` then will use the `TARDIS_WS_URL` env var. | +| `tardis_ws_url` | string (optional) | The Tardis Machine WebSocket URL. | If `null` then will use the `TARDIS_MACHINE_WS_URL` env var. | | `normalize_symbols` | bool (optional) | If Nautilus [symbol normalization](#symbology-and-normalization) should be applied. | If `null` then will default to `true`. | | `output_path` | string (optional) | The output directory path to write Nautilus Parquet data to. | If `null` then will use the `NAUTILUS_CATALOG_PATH` env var, otherwise the current working directory. | | `options` | JSON[] | An array of [ReplayNormalizedRequestOptions](https://docs.tardis.dev/api/tardis-machine#replay-normalized-options) objects. | diff --git a/examples/live/tardis/tardis_subscriber.py b/examples/live/tardis/tardis_subscriber.py index 0bd8ef588565..0e5c459a6911 100644 --- a/examples/live/tardis/tardis_subscriber.py +++ b/examples/live/tardis/tardis_subscriber.py @@ -40,6 +40,7 @@ # Run the following to start the tardis-machine server: # docker run -p 8000:8000 -p 8001:8001 -e "TM_API_KEY=YOUR_API_KEY" -d tardisdev/tardis-machine +# The TARDIS_MACHINE_WS_URL environment variable should be set to ws://localhost:8001 instrument_ids = [ InstrumentId.from_str("BTCUSDT-PERP.BINANCE"), diff --git a/nautilus_trader/accounting/manager.pyx b/nautilus_trader/accounting/manager.pyx index 4da9cfd80ba9..fea801aa185e 100644 --- a/nautilus_trader/accounting/manager.pyx +++ b/nautilus_trader/accounting/manager.pyx @@ -470,8 +470,15 @@ cdef class AccountsManager: return False # Calculate new balance + cdef Money new_total = balance.total.add(pnl) + if new_total._mem.raw < 0: + raise AccountBalanceNegative( + balance=new_total.as_decimal(), + currency=pnl.currency, + ) + cdef AccountBalance new_balance = AccountBalance( - total=balance.total.add(pnl), + total=new_total, locked=balance.locked, free=balance.free.add(pnl), ) diff --git a/nautilus_trader/adapters/tardis/common.py b/nautilus_trader/adapters/tardis/common.py index 09f32c8fbb1d..bd0e49d12a6f 100644 --- a/nautilus_trader/adapters/tardis/common.py +++ b/nautilus_trader/adapters/tardis/common.py @@ -102,6 +102,13 @@ def infer_tardis_exchange_str(instrument: Instrument) -> str: # noqa: C901 (too return "okex-futures" elif isinstance(instrument, CryptoOption): return "okex-options" + case "COINBASE_INTX": + return "coinbase-international" + case "BITGET": + if isinstance(instrument, CurrencyPair): + return "bitget" + else: + return "bitget-futures" return venue.lower().replace("_", "-") diff --git a/nautilus_trader/data/engine.pyx b/nautilus_trader/data/engine.pyx index 590db76e3232..37d1faff5d53 100644 --- a/nautilus_trader/data/engine.pyx +++ b/nautilus_trader/data/engine.pyx @@ -43,7 +43,6 @@ from nautilus_trader.persistence.catalog import ParquetDataCatalog from cpython.datetime cimport datetime from libc.stdint cimport uint64_t -from nautilus_trader.backtest.data_client cimport BacktestMarketDataClient from nautilus_trader.common.component cimport CMD from nautilus_trader.common.component cimport RECV from nautilus_trader.common.component cimport REQ @@ -1415,14 +1414,15 @@ cdef class DataEngine(Component): cpdef void _handle_request_data(self, DataClient client, RequestData request): self._handle_date_range_request(client, request) - cpdef void _handle_date_range_request( - self, - DataClient client, - RequestData request, - ): + cpdef void _handle_date_range_request(self, DataClient client, RequestData request): cdef DataClient used_client = client - if type(client) is BacktestMarketDataClient: + # Avoid importing `BacktestMarketDataClient` from the `backtest` subpackage at + # module import time – doing so creates a circular import between + # `nautilus_trader.data` and `nautilus_trader.backtest`. + from nautilus_trader.backtest.data_client import BacktestMarketDataClient + + if isinstance(client, BacktestMarketDataClient): used_client = None # Capping dates to the now datetime diff --git a/nautilus_trader/persistence/catalog/parquet.py b/nautilus_trader/persistence/catalog/parquet.py index 94b1a2efc539..c9fd1db27427 100644 --- a/nautilus_trader/persistence/catalog/parquet.py +++ b/nautilus_trader/persistence/catalog/parquet.py @@ -1195,7 +1195,7 @@ def query( An additional SQL WHERE clause to filter the data (used in Rust queries). files : list[str], optional A specific list of files to query from. If provided, these files are used - instead of discovering files through the normal process. Forces PyArrow backend. + instead of discovering files through the normal process. **kwargs : Any Additional keyword arguments passed to the underlying query implementation. @@ -1233,6 +1233,7 @@ def query( start=start, end=end, where=where, + files=files, **kwargs, ) else: @@ -1270,6 +1271,7 @@ def _query_rust( start: TimestampLike | None = None, end: TimestampLike | None = None, where: str | None = None, + files: list[str] | None = None, **kwargs: Any, ) -> list[Data]: query_data_cls = OrderBookDelta if data_cls == OrderBookDeltas else data_cls @@ -1279,6 +1281,7 @@ def _query_rust( start=start, end=end, where=where, + file=files, **kwargs, ) result = session.to_query_result() @@ -1304,6 +1307,7 @@ def backend_session( end: TimestampLike | None = None, where: str | None = None, session: DataBackendSession | None = None, + files: list[str] | None = None, **kwargs: Any, ) -> DataBackendSession: """ @@ -1327,6 +1331,9 @@ def backend_session( An additional SQL WHERE clause to filter the data. session : DataBackendSession, optional An existing session to update. If None, a new session is created. + files : list[str], optional + A specific list of files to query from. If provided, these files are used + instead of discovering files through the normal process. **kwargs : Any Additional keyword arguments. @@ -1351,7 +1358,7 @@ def backend_session( """ data_type: NautilusDataType = ParquetDataCatalog._nautilus_data_cls_to_data_type(data_cls) - files = self._query_files(data_cls, identifiers, start, end) + file_list = files if files else self._query_files(data_cls, identifiers, start, end) file_prefix = class_to_filename(data_cls) if session is None: @@ -1361,7 +1368,7 @@ def backend_session( if self.fs_protocol != "file": self._register_object_store_with_session(session) - for idx, file in enumerate(files): + for idx, file in enumerate(file_list): table = f"{file_prefix}_{idx}" query = self._build_query( table, @@ -1492,10 +1499,7 @@ def _query_pyarrow( **kwargs: Any, ) -> list[Data]: # Load dataset - use provided files or query for them - if files is not None: - file_list = files - else: - file_list = self._query_files(data_cls, identifiers, start, end) + file_list = files if files else self._query_files(data_cls, identifiers, start, end) if not file_list: return [] @@ -1536,32 +1540,50 @@ def _query_files( file_prefix = class_to_filename(data_cls) base_path = self.path.rstrip("/") glob_path = f"{base_path}/data/{file_prefix}/**/*.parquet" - file_names: list[str] = self.fs.glob(glob_path) + file_paths: list[str] = self.fs.glob(glob_path) if identifiers: if not isinstance(identifiers, list): identifiers = [identifiers] safe_identifiers = [urisafe_identifier(identifier) for identifier in identifiers] - file_names = [ - file_name - for file_name in file_names - if any(safe_identifier in file_name for safe_identifier in safe_identifiers) + + # Exact match by default for instrument_ids or bar_types + exact_match_file_paths = [ + file_path + for file_path in file_paths + if any( + safe_identifier == file_path.split("/")[-2] + for safe_identifier in safe_identifiers + ) ] + if not exact_match_file_paths and data_cls in [Bar, *Bar.__subclasses__()]: + # Partial match of instrument_ids in bar_types for bars + file_paths = [ + file_path + for file_path in file_paths + if any( + file_path.split("/")[-2].startswith(f"{safe_identifier}-") + for safe_identifier in safe_identifiers + ) + ] + else: + file_paths = exact_match_file_paths + used_start: pd.Timestamp | None = time_object_to_dt(start) used_end: pd.Timestamp | None = time_object_to_dt(end) - file_names = [ - file_name - for file_name in file_names - if _query_intersects_filename(file_name, used_start, used_end) + file_paths = [ + file_path + for file_path in file_paths + if _query_intersects_filename(file_path, used_start, used_end) ] if self.show_query_paths: - for file_name in file_names: - print(file_name) + for file_path in file_paths: + print(file_path) - return file_names + return file_paths @staticmethod def _handle_table_nautilus( diff --git a/pyproject.toml b/pyproject.toml index 64414a1ae119..3a493f194925 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,7 +126,7 @@ test = [ "pytest-xdist[psutil]>=3.7.0,<4.0.0", ] docs = [ - "numpydoc>=1.8.0,<2.0.0", + "numpydoc>=1.9.0,<2.0.0", "linkify-it-py>=2.0.3,<3.0.0", "myst-parser>=4.0.1,<5.0.0", "sphinx-comments>=0.0.3,<1.0.0", diff --git a/tests/unit_tests/portfolio/test_portfolio.py b/tests/unit_tests/portfolio/test_portfolio.py index 4fbe589d9148..58642dcb60f5 100644 --- a/tests/unit_tests/portfolio/test_portfolio.py +++ b/tests/unit_tests/portfolio/test_portfolio.py @@ -17,6 +17,7 @@ import pytest +from nautilus_trader.accounting.error import AccountBalanceNegative from nautilus_trader.accounting.factory import AccountFactory from nautilus_trader.common.component import MessageBus from nautilus_trader.common.component import TestClock @@ -256,7 +257,7 @@ def test_exceed_free_balance_single_currency_raises_account_balance_negative_exc self.exec_engine.process(TestEventStubs.order_submitted(order, account_id=account_id)) # Act, Assert: push account to negative balance (wouldn't normally be allowed by risk engine) - with pytest.raises(ValueError): + with pytest.raises(AccountBalanceNegative): fill = TestEventStubs.order_filled( order, instrument=AUDUSD_SIM, diff --git a/uv-version b/uv-version index a48658c94949..def4250351c5 100644 --- a/uv-version +++ b/uv-version @@ -1 +1 @@ -0.7.13 +0.7.15 diff --git a/uv.lock b/uv.lock index 2ba3625c1794..38f5d2837fee 100644 --- a/uv.lock +++ b/uv.lock @@ -170,52 +170,52 @@ wheels = [ [[package]] name = "bitarray" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/0d/15826c7c2d49a4518a1b24b0d432f1ecad2e0b68168f942058b5de498498/bitarray-3.4.2.tar.gz", hash = "sha256:78ed2b911aabede3a31e3329b1de8abdc8104bd5e0545184ddbd9c7f668f4059", size = 143756, upload-time = "2025-05-21T16:21:44.056Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/ba/508ba6a3ea16eb6c21baae33cd1b7bf6e299d21a496a1f90b8203a22d6d0/bitarray-3.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90ca8e260b75a7ac0c542093e5f29154e51fd0d2d0fa5041c038cb2b58415eeb", size = 141425, upload-time = "2025-05-21T16:18:24.452Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a1/44d9b88cd3daee3734ea98dac691acc2c935a3bfbd5bfc38267a59bd986d/bitarray-3.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549dabcae78fb8f9133e3138b9473c7648d6054bb6fec84d28d3861aaec5ddd1", size = 138172, upload-time = "2025-05-21T16:18:25.601Z" }, - { url = "https://files.pythonhosted.org/packages/5f/aa/5a8c33ab39e8a894978d42427ad0a1ba2d5c9cb61c8480101be555c0e3a7/bitarray-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3da536ac84e6911cbc8e86be0baf1cab0d4f4ccb80c0f39b4fa28509f2db1a", size = 313373, upload-time = "2025-05-21T16:18:26.796Z" }, - { url = "https://files.pythonhosted.org/packages/89/48/b0d28e21d91ec5c0477a320b9443096ddc816fbc59778b367f9e49094532/bitarray-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a5e84d6b737de2d773ab1bd538e6f37fa7f667ea734f00a48d9a973b181c751", size = 329657, upload-time = "2025-05-21T16:18:28.097Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d5/1f858bd559568286435a460e7a169a5185b2b29184684e6c6fa303af3ca9/bitarray-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e265c5eede8316ba64bb6029832f282f6284a557b625bb3207a7680fd5da7925", size = 321873, upload-time = "2025-05-21T16:18:29.511Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c8/23df4174142cccf6a8bd114651b8e9bf965005ab1ef741d37c9f72e8d2eb/bitarray-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63fb45c60c7ab7a724aa64203305e56f344489e12d41619bdc9d7887d6562e01", size = 314796, upload-time = "2025-05-21T16:18:31.2Z" }, - { url = "https://files.pythonhosted.org/packages/8f/21/329178b165f1aaf3f2ace3eb24aca5ad197febae908d7b41e552a69043e9/bitarray-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:083c2a9234dacf3e4e166a5844256da2a397941d3f6397e5b919bffca638f6ef", size = 302724, upload-time = "2025-05-21T16:18:32.729Z" }, - { url = "https://files.pythonhosted.org/packages/26/a8/a66d3c0d3410d01f51824f8476b060f96b3353db7d6b45c87dba6d1aa0e0/bitarray-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e72606adb2438002873cb0e8e81c3fce926386a59bbafa82fea07cdb2a6d8a05", size = 307434, upload-time = "2025-05-21T16:18:34.394Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ac/3052386e7ff80c80eb2549a22c890f511e9f9f7fbbe6244b04255adae031/bitarray-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dc994d22a3a563e1d402dc2f64c61e60605a1a3e66dd8aea7636f607b99f03cb", size = 299232, upload-time = "2025-05-21T16:18:35.708Z" }, - { url = "https://files.pythonhosted.org/packages/9d/46/91a32ccd39d40371ed7404d96a6f3cf1e381eaf36be5390c6bff5034f344/bitarray-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:214468391680ba1c831872a7949f1b563ab3cd832d10adc52df4f36e0af24446", size = 324056, upload-time = "2025-05-21T16:18:37.536Z" }, - { url = "https://files.pythonhosted.org/packages/39/0e/cb824f0e0302cd08809f67b35b3ae21b47af5dd122e99740bfe6bde1c824/bitarray-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c7483b97807bb018a7cd7f9741800c714c9c56ba4e5a7e962c5f956c4b858f3c", size = 327058, upload-time = "2025-05-21T16:18:38.856Z" }, - { url = "https://files.pythonhosted.org/packages/09/01/845e977d490e4e261179785540d1fdeff966c99296f503adc0e5407fc257/bitarray-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5774bf14ec451d5ac311cfcfe0b0cf2a1a9fa74b6ca81dfbc4f56a98872a5541", size = 306629, upload-time = "2025-05-21T16:18:40.211Z" }, - { url = "https://files.pythonhosted.org/packages/29/ef/33ee8533ff1b2a8cd0b9e84fd81b2a90d66c2774544c861e281c5361eaa2/bitarray-3.4.2-cp311-cp311-win32.whl", hash = "sha256:e6f35567347ddb8b9e8b6bf6ab7d64be88bdb6b6c107b8edbb2c3d426c1590a0", size = 134450, upload-time = "2025-05-21T16:18:42.435Z" }, - { url = "https://files.pythonhosted.org/packages/09/52/069c255d067319a9695c93369641d7f5539625069c1cf3ded2becff1bfbc/bitarray-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:ae5b0a8d3caf6284220232738dc7c05af81ec3a9f93d4a462295462dd0a492b2", size = 141596, upload-time = "2025-05-21T16:18:43.743Z" }, - { url = "https://files.pythonhosted.org/packages/05/57/0b2b50eb3f50c3144f705d0994171f17fda00ee3a72d563ba764ea235f66/bitarray-3.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a0e498563e0eefa96a1b92461d083de11256f6510b7706d5f2e6473cd9b7137a", size = 141191, upload-time = "2025-05-21T16:18:45.436Z" }, - { url = "https://files.pythonhosted.org/packages/81/c3/1d9ce4d0041c10ce90d924b8cea63afdda84a64623179045c0c67998922c/bitarray-3.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:114870ab71a0ebdac211aa0a120a54206c333b74b99fdf4b58fbe904979e1fef", size = 138158, upload-time = "2025-05-21T16:18:46.685Z" }, - { url = "https://files.pythonhosted.org/packages/5d/dd/a8653dac671ba97b1c68ee73b08a0eb2042f24e5e31f51b86afc09588c06/bitarray-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fbf6121978cba4313c31f7cc5961e481242def2b8ddfea34ca27ba9da52c9c1", size = 315834, upload-time = "2025-05-21T16:18:47.926Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a2/30547bea0a35f9f953e99f5157749d56304d3f3a96b01a982dd604a9dc48/bitarray-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:423bb4e1bec0bc5d63969e12bcc5cc0081cc5aec4d7b62a6cd8240342aa36107", size = 331317, upload-time = "2025-05-21T16:18:49.169Z" }, - { url = "https://files.pythonhosted.org/packages/2d/b9/1789476280f46455a9a30bcd252fda6fd995583d97d1b919ec0296393e2a/bitarray-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ef80a96487c82477e8def69a58a218491794f7989b3e191cbaaa7b450315a5c", size = 324416, upload-time = "2025-05-21T16:18:50.917Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/519c829ca641a3e7b8c9be56d177aaa05572b7e15e4298df4a77959b6a1e/bitarray-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35f5c69a79047e50bc1d54a777541b0a86b213e23559b1ac3d76fa9a42cc5522", size = 317634, upload-time = "2025-05-21T16:18:52.718Z" }, - { url = "https://files.pythonhosted.org/packages/0d/39/ebb6a6539261279c0994836b40b99384fa5e27ec239e70b203e310343f80/bitarray-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:002f7b128ed9d18d3ecb51ca78aeea5afffbe8e80d6be4ff2984d045b1c4b937", size = 305392, upload-time = "2025-05-21T16:18:54.888Z" }, - { url = "https://files.pythonhosted.org/packages/83/04/0ee0d57b2a60fdf881346f196fd92b824f44f4736026da1d8c7970745266/bitarray-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:999bccc72704afcf4a3d9868db4d149c032cdf910f9f7d91e30166978530af7f", size = 309740, upload-time = "2025-05-21T16:18:56.76Z" }, - { url = "https://files.pythonhosted.org/packages/f6/39/5ab0339e93097f2a2631ea281a6386c31707011499d5cf68b4e0e37ba124/bitarray-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2e44cfe2bc161cde3b11604f279e3048ef7bd3413837aadbd2ca30b5233c82cb", size = 301607, upload-time = "2025-05-21T16:18:58.144Z" }, - { url = "https://files.pythonhosted.org/packages/e8/bb/b8f697ba6a16c1e393afe75029d069e2dd457e62b112c3cb26768d2e65eb/bitarray-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f408ba3e6f706a0eabae405d1906ceb539f34a318562a91ab9799c5e1712e18c", size = 325942, upload-time = "2025-05-21T16:18:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/64/ec/77d866a96909c09c5a34f1716f015386f9d9bbbf4b5dc7219f642b8043e2/bitarray-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bf94513ae559b2525e6218e41b03790f866d75df5404490420f2c25e42cf55e7", size = 329491, upload-time = "2025-05-21T16:19:01.205Z" }, - { url = "https://files.pythonhosted.org/packages/37/6e/633b7d392a39df655c92035da9ee52f7332bb165ae72038692a33a6def6c/bitarray-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f2c88c792815d2755c49a3a1fca256e142c4adfadf1a2142b5a3a37e4d4b871", size = 309566, upload-time = "2025-05-21T16:19:02.762Z" }, - { url = "https://files.pythonhosted.org/packages/ab/38/9d7ad6eca72e09b81097176dd66eed3aeaabdea4c24cf6ce25609599ce7b/bitarray-3.4.2-cp312-cp312-win32.whl", hash = "sha256:f4dac6b942c4d7ae5f6eb555ee3993de1432bf9c8f46e3caf74b6671ac5571a3", size = 134600, upload-time = "2025-05-21T16:19:04.057Z" }, - { url = "https://files.pythonhosted.org/packages/d4/d3/c83ec3d912be73861a064f1a705436f270b8c5b5926350a875bd6c06b6df/bitarray-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:6c37e6814633041307f0df281651a86372b0ccdb1e4768247a87e83e2b68f9b9", size = 141844, upload-time = "2025-05-21T16:19:05.254Z" }, - { url = "https://files.pythonhosted.org/packages/f2/22/973d377477e1f27cf64f9e3292343219577136e32665a52667589380100d/bitarray-3.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:16263bdbb05ce379e7b8e9a9f3e0a61a9204a06a037bbc91322d2939b3079fd5", size = 141162, upload-time = "2025-05-21T16:19:06.488Z" }, - { url = "https://files.pythonhosted.org/packages/eb/53/65541b94fb6df1e8aa9a7359ac68f469c3243d8bc7302c5fb8ff8936dab2/bitarray-3.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:41fdc6fb8c3aabfcfe0073302c69fef0c74d6499491f133ba58755c3f2afb3d0", size = 138162, upload-time = "2025-05-21T16:19:07.688Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b2/83d587965f7969a5016a5bf5c9295a0651a34b668df41fa089d7c924ac08/bitarray-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c2571337b11c69206e339170516f3e72b4ec16250876c4f2bbb6e82b9caa15", size = 315760, upload-time = "2025-05-21T16:19:09.834Z" }, - { url = "https://files.pythonhosted.org/packages/4f/f5/2b2924181809debdb644143aa33d16facdce5763d5ff17e5301ecdaf89dc/bitarray-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c0e3d5f37217dde9b206418c37c4d86e173f072a892670e9714e6bb20b228e95", size = 331250, upload-time = "2025-05-21T16:19:11.449Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/8ed4eeb947e05ef54614feff4cc4badd03e29ec35d46aa0218513cc9f8ac/bitarray-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83202735f21fc781f27228daeae94b6c7df1a9f673b9dd6a1c0b3764d92b8e50", size = 324299, upload-time = "2025-05-21T16:19:13.236Z" }, - { url = "https://files.pythonhosted.org/packages/05/27/d7f1b15c079cbeffad76f97c41c27635873be4d5600f6896b2bbc4f5caff/bitarray-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53b3f8c35812d85a299d6c0ff097f93e18dfb7a324c129e20a4ec0ecfc4ba995", size = 317522, upload-time = "2025-05-21T16:19:14.832Z" }, - { url = "https://files.pythonhosted.org/packages/a5/db/e6a857a23222360dbc0b0d177e6060ecd88d63a1d6a3c2b52333c21a9683/bitarray-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef3f2e8ba5d6e0f38b57960d1bfb72aa9e2115f7cdca48561fadced652798d49", size = 305290, upload-time = "2025-05-21T16:19:16.57Z" }, - { url = "https://files.pythonhosted.org/packages/16/12/3b945e415233889c57c26f95a9a6a245da546e2c8d1de09991332cb796ff/bitarray-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:508ec6547bdd9f0c435c322fbb127a3dfd74c943a6c7f77fa5dfcb3e9ce1de66", size = 309764, upload-time = "2025-05-21T16:19:18.34Z" }, - { url = "https://files.pythonhosted.org/packages/6c/0e/9effb83e23ef5495c9078bdbac948df4fe2b202fb0ac5b33412848ab4b1e/bitarray-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1a3a08cc920f601258ea33d97b4454cd7cb04d17930e0a3bc7328ba3d732f8b0", size = 301690, upload-time = "2025-05-21T16:19:19.694Z" }, - { url = "https://files.pythonhosted.org/packages/cb/67/9a73476c8cd6a67ff5ab9c5c1d916307e4fb9178d76ee2781552451c995c/bitarray-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:60189130ae1ebaadbab27e3ad0a7d7ed44f5d9456bbfae07c72138501ce59053", size = 326049, upload-time = "2025-05-21T16:19:21.371Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b1/2a81f5f96c1ccc033d8c63b4584aedbd9e27499cf2276fc70d4f87ad673b/bitarray-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9e425eaf21a8d7b76531630029441c6d61f6064cbf4dd592af1607c79eb2e4d0", size = 329565, upload-time = "2025-05-21T16:19:22.88Z" }, - { url = "https://files.pythonhosted.org/packages/2e/30/670efe7771944b4b7d0aacdc076969adc9428c9d0939ee70230bdf4c8aed/bitarray-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:952cc40c593f663ba083be76d1ccdb6dc9dafab8fb6d949056405636b2e720f3", size = 309661, upload-time = "2025-05-21T16:19:24.574Z" }, - { url = "https://files.pythonhosted.org/packages/ee/2e/b2d8e842fe484d7d18fcd137289e396c7784b8484e0ec7e94ffe4bb7e8f9/bitarray-3.4.2-cp313-cp313-win32.whl", hash = "sha256:158f6b1a315eaf971f88e66f9b93431c3b580b46d2121c6a1166e7b761408fdf", size = 134614, upload-time = "2025-05-21T16:19:25.914Z" }, - { url = "https://files.pythonhosted.org/packages/0c/50/0ec25a51197410a66146eea7950e3597baedb000f2f2e2458bb6d5306b0a/bitarray-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:2d24658ac96a82beb4da2f5c71bef9790f3dcabadbe8ead8dda742ab207fe2f9", size = 141851, upload-time = "2025-05-21T16:19:27.388Z" }, +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/36/eef91e03e44be4b96a613333acfc0c636af2f3f3033b17e08e2052b649c5/bitarray-3.4.3.tar.gz", hash = "sha256:dddfb2bf086b66aec1c0110dc46642b7161f587a6441cfe74da9e323975f62f0", size = 143930, upload-time = "2025-06-23T23:23:20.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/fb/babcbe71bc7588cc0bdad72b4cb7165582e38f61cf1aee08139577bbae2c/bitarray-3.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dad06c638adb14c2ab2cdbe295f324e72c7068d65bb5612be5f170e5682a1e3e", size = 140940, upload-time = "2025-06-23T23:19:59.696Z" }, + { url = "https://files.pythonhosted.org/packages/0a/88/62296a8e4bf34d3cb87c623715de87e9de70300c60da4dbca59473fda264/bitarray-3.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0c6dc58399e2b1221f98b8696cdb414a8c42c2cea5c61f7cf9d691ee12c86cb3", size = 137545, upload-time = "2025-06-23T23:20:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/23/fd/e5885fbc65ba1a6bf6bd49f3fd90cc90889f03fe9a8a3e581531777135ee/bitarray-3.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:902b83896e0a4976186e3ec3c0064edd18dab886845644ef25c5e3c760999ed4", size = 314454, upload-time = "2025-06-23T23:20:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5a/b1511f7c3e33715a580a9f3d9ba8f45bce0ea745490fe8163bc4ae048ee8/bitarray-3.4.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e80131f03e4df4d9e70c355e5c939673eabaff47803fe1b85bf9676cb805e8a", size = 330842, upload-time = "2025-06-23T23:20:03.997Z" }, + { url = "https://files.pythonhosted.org/packages/86/df/fa11701e2ad8a8ffcabcfb82f9c7c78d47bc7aa1fe626bd320fc6b553e53/bitarray-3.4.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a544478f91a0239ce986c90af5dfbeb5ae758e4573194c94827705c138eb75b5", size = 323722, upload-time = "2025-06-23T23:20:05.846Z" }, + { url = "https://files.pythonhosted.org/packages/78/c1/fe8c84a3d3bde1eda2a222f7060278257d9a21318a27ba99fda5cfb6b801/bitarray-3.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3df507b700c5bd4f2d02056b9db1867e0a5c202fa22eb0d12a6dcca6098b1c0a", size = 316038, upload-time = "2025-06-23T23:20:07.571Z" }, + { url = "https://files.pythonhosted.org/packages/52/ea/3170ebc9c3c460b2e93f0bca19f79343e445064662203ffff5a752698227/bitarray-3.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ad9bf2403d69080bcd281fc3a4feab14fac8221362724e791df5d50aa105ea", size = 303936, upload-time = "2025-06-23T23:20:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/a2/56/a6dad0cee4ce7fc11e3ec1a616f8be058afede7c4ea05db66657a0384b44/bitarray-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cda69a119698a6ab00e30bc3530d6631245312f6b2287c24b02b3bcea482f512", size = 309117, upload-time = "2025-06-23T23:20:10.202Z" }, + { url = "https://files.pythonhosted.org/packages/1b/98/17d679e3ca3eefc3346adb08432e80ea8d283fe3cfa271c8b46dff92d09a/bitarray-3.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:350357713dd175788f1e43b85998d8290b8626eb8e5dcc55571a64f8e231dcc1", size = 300071, upload-time = "2025-06-23T23:20:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/66/c1/2c49e405a5df4dd8c8bf0b4ddbe48c966a5ec8799b0a8aed7cdc860dd312/bitarray-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:17d34aab4b70d8c67260d76810d4aca65ef8bc61e829da32f9fa7116338430e3", size = 325565, upload-time = "2025-06-23T23:20:13.566Z" }, + { url = "https://files.pythonhosted.org/packages/67/42/1df9d926af530fdf8d6cd26e9e618956b79db612a0d2b79e0864de875361/bitarray-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1f266e76e2819cfdd3522247fb33caccf661c7913e0a0e29e195b46a678be660", size = 328567, upload-time = "2025-06-23T23:20:15.09Z" }, + { url = "https://files.pythonhosted.org/packages/98/a5/cccffb02a3f3d2bf59e5a5950e7939b673a3aaa7061d9218bb8fac3f840d/bitarray-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:822963d34081d2d0b0767eaf1a161ac97b03f552fa21c2c7543d9433b88694b0", size = 308143, upload-time = "2025-06-23T23:20:16.811Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2d/0b7d2f79ca3b8e67cc1afa6335567ea6d7e46d89a5ead9644af8c7fcc5b7/bitarray-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6a345df7fa08af4d08f99a555d949003ff9309d5496c469b7f3dd50c402da973", size = 134586, upload-time = "2025-06-23T23:20:18.476Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8b/48c371ad2ea678eb1b1551ecfba603d8e153b5127445b89d549e9aee479a/bitarray-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:8dc0772146d39d6819d01b64d41a9bf5535e99d2b2df4343ec2686b23b3a9740", size = 141265, upload-time = "2025-06-23T23:20:20.004Z" }, + { url = "https://files.pythonhosted.org/packages/40/9f/803f016eb9d514cd3f0aeb3dd4b06066af7dd2b7d6fb315bfce7926240a7/bitarray-3.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:eb6d5b1c2d544e2691a9d519bdbbbc41630e71f0f5d3b4b02e072b1ecf7fe78a", size = 140680, upload-time = "2025-06-23T23:20:21.293Z" }, + { url = "https://files.pythonhosted.org/packages/41/ac/4cb6e0dd359e0c8498414ba9efc259a11cc5ae8463b3c9b4ec1ca1839945/bitarray-3.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e2b416291ba7817219296d2911fe06771b620541af26e6a4cc75e3559316d0af", size = 137524, upload-time = "2025-06-23T23:20:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/5e/08/4ad3f7cec01969c09f67da73023706e1661bf5a005afad9b7cfec73c6c6a/bitarray-3.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0522e602faf527e028a973e6260f2b6259a47d79fe8ddbf81b5176af36935e4", size = 317200, upload-time = "2025-06-23T23:20:24.707Z" }, + { url = "https://files.pythonhosted.org/packages/bf/55/19bc4d553654644623e9ae4b1381de9c67ae1e54d5b9a95c6ea48f46c950/bitarray-3.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd6b925a010d2749cba456ecd843f633594855f356d3ae66c49eb8cc6b3e0ba7", size = 332827, upload-time = "2025-06-23T23:20:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8a/0f7a3e971370fabb40c99a65145c3ae6f21dd858513f761a5d59f646d5cb/bitarray-3.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8dc18fb4e24affd29fbaf48a2c6714fa3dece01b7e06d7f0bb75a187f8f5cd", size = 326301, upload-time = "2025-06-23T23:20:28.308Z" }, + { url = "https://files.pythonhosted.org/packages/e1/cf/e35d81eabd1130e2725619106f9abf85f38bd140ce583e4ce4006a616d78/bitarray-3.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3e9ea27c5f877d6abeb02ee6affcf97804829b35a640c52a0e4ae340e401c9e", size = 319172, upload-time = "2025-06-23T23:20:29.722Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8a/e26e3478c506191e31d3e3f56011e2874afa232412765d3bb77777556b5e/bitarray-3.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3720b7e9816f61ff0dfa2d750c3cd2f989d1105d953606fb90471f45f5b8065", size = 306535, upload-time = "2025-06-23T23:20:31.199Z" }, + { url = "https://files.pythonhosted.org/packages/45/c1/c5b07a97ba12d1fe72a83372bfa25a06439ebe131c5ea9992120ef65c92b/bitarray-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:09abb161caada9ae4cd556c7d2f4d430f8eb2a8248f2e3fa93d5eea799ed1563", size = 311403, upload-time = "2025-06-23T23:20:33.068Z" }, + { url = "https://files.pythonhosted.org/packages/0a/46/187875c5976a81d0e73db0ac017a36e8a9fe3d880c11c432e8fe3057326a/bitarray-3.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bfb3fee5054a9a266d2d3d274987fbc5d75893ba8d28b849d6ffbdaefcad30f1", size = 302970, upload-time = "2025-06-23T23:20:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/24/36/88838419c29feefae55b7ca41db30c72f487fbb0bea5bd3de39cecc1af25/bitarray-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a4eed600da6de44f260d440a60c55722beacd9908a4a2d6550323e74a9bbbbd8", size = 327525, upload-time = "2025-06-23T23:20:36.233Z" }, + { url = "https://files.pythonhosted.org/packages/b1/cc/88fbedbb3c6b1432c425915777026d9030906c3a92630d18f74f4206efa1/bitarray-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98effdef57d3c60c3df67f203ee83b0716edd50b3ef9afaf1ae6468e5204c78f", size = 331324, upload-time = "2025-06-23T23:20:37.598Z" }, + { url = "https://files.pythonhosted.org/packages/ff/25/4e4806ac9b2497699698d6a0660d5323c082657e2259c5df96e6d2e2140a/bitarray-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71fe2e56394f81ed4d027938cf165f12b684c1d88fede535297f5ac94f54f5a0", size = 311275, upload-time = "2025-06-23T23:20:39.345Z" }, + { url = "https://files.pythonhosted.org/packages/ce/14/2400f4b9cddcf19ccd4d6ed3732bb700cb1909423cbe0b23f643e5ee5ba1/bitarray-3.4.3-cp312-cp312-win32.whl", hash = "sha256:28ea1d79c13a8443cdacf8711471d407ad402d55dac457a634be2dd739589a66", size = 134622, upload-time = "2025-06-23T23:20:41.036Z" }, + { url = "https://files.pythonhosted.org/packages/13/e1/54a8b7e498a5fbaeb3cf71537968884e0899410c4b33b208680da630a5c5/bitarray-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:ccb0bdca279d29286ef9bd973a975217927dfa7e0f0d6eac956df5b32ff7c57d", size = 141471, upload-time = "2025-06-23T23:20:42.274Z" }, + { url = "https://files.pythonhosted.org/packages/4b/dc/c0a56c0a01cbf36ac2d988f48ccbd4caf7fc78a8eeffea3046ceea17adfe/bitarray-3.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2162e97bbdb3a9d2dbf039df02baf9eefd2c13149fc615a5ce5a0189bff82fd4", size = 140655, upload-time = "2025-06-23T23:20:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/2b/62/5c10ba0ccf340e6744aef26135cef61ea7d0756e234ad9b175d2490e91c3/bitarray-3.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:254ab82798faf4636ffd3b5bfe2bf24ee6f70e0c8b794491da24f143329bf4c5", size = 137514, upload-time = "2025-06-23T23:20:45.232Z" }, + { url = "https://files.pythonhosted.org/packages/44/ba/6847f426473c02917cf5784c49dd4a5411cdf2aec1ca9df8fcdd98fcd2b8/bitarray-3.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9342795493deacc6bea42782fea777e180abb28cf2440e743f6c52b65b4bfddd", size = 317153, upload-time = "2025-06-23T23:20:47.556Z" }, + { url = "https://files.pythonhosted.org/packages/61/fe/2919d90da6fb81044d2ff5565ab7e85f1005ba8d1f65fca6cd914d4cab33/bitarray-3.4.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28347e92310a042e958c53336b03bea7e3eec451411ed0e27180d19c428ad7f2", size = 332706, upload-time = "2025-06-23T23:20:49.29Z" }, + { url = "https://files.pythonhosted.org/packages/60/9e/4d8a901744d28a17735050ac3564ee9d28b34885c772d9321f7af63e6944/bitarray-3.4.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f873f4506649575495ffc91febf3e573eabdb7b800e96326533a711807bbe7df", size = 326200, upload-time = "2025-06-23T23:20:51.176Z" }, + { url = "https://files.pythonhosted.org/packages/65/00/354655103f670c8051b10f597e8c70ba1959a92e9e73fa81ad246786b1e7/bitarray-3.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e9d9df7558497c72e832b1a29a1d3ec394c50c79829755b6237f9a76146f5e2", size = 319054, upload-time = "2025-06-23T23:20:52.652Z" }, + { url = "https://files.pythonhosted.org/packages/73/40/6ef40ca1b1d96dfe4102b96f3e0bf544f5872fae6a744f0a3aac649a9217/bitarray-3.4.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f849e428dd2c8c38b705e28b2baa6601fc9013e3a8dd4b922f128e474bcf687d", size = 306379, upload-time = "2025-06-23T23:20:54.024Z" }, + { url = "https://files.pythonhosted.org/packages/75/6e/46f2debcfa1ebffca1ae7e5644375c551618eda192dd0481df21e78e5e92/bitarray-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:438c50f2f9a5751fb12b1ae5c6283c94fc420c191ecd97f0d37483b3f1674a61", size = 311409, upload-time = "2025-06-23T23:20:55.796Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bf/bc0d5f371ea3a65d615663fc8f3ee03a2c1fade9bc18133504e60cbef2b4/bitarray-3.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cf5bdce1e64eb77cb10fd1046ec7ccd84a3e68cdeaf05da300adfc0a5ddcfa5", size = 302976, upload-time = "2025-06-23T23:20:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cf/c851e57a8bd681fe77086630330b8f374616dba3c676aaeb278e0cac8d34/bitarray-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2e981af7e4e0d33de3cd7132619c04484cc83846922507855d6d167ae2c444b5", size = 327525, upload-time = "2025-06-23T23:20:58.815Z" }, + { url = "https://files.pythonhosted.org/packages/62/0b/8868d01a41bd486736d75009e80122e67b453e07520b4565c81f2f79e50f/bitarray-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:da717081402de4b5d66865c9989cb586076773a11af85324fdad4db6950d36a4", size = 331270, upload-time = "2025-06-23T23:21:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a2/4e92ee5daf21ed200e31ee07b2f305c413332f1d54c51c8478c765414b20/bitarray-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d6b1a7764e178b127e1388015c00bbc20d4e7188129532c530f1a12979c491f2", size = 311288, upload-time = "2025-06-23T23:21:01.901Z" }, + { url = "https://files.pythonhosted.org/packages/89/b2/1152782423029d5af578069870e451c9d9589ffa63464c76fe0385f82f52/bitarray-3.4.3-cp313-cp313-win32.whl", hash = "sha256:23ec148e5db67efee6376eefc0d167d4a25610b9e333b05e4ccfdcf7c2ac8a9a", size = 134634, upload-time = "2025-06-23T23:21:03.66Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3c/00b002c5df85f30b9eb598edabcb8e10728d77014f2d04e38ec31b369be1/bitarray-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:c3113d40de1adfd3c4f08e4bb0a69ff88807085cf2916138f2b55839c9d8d1b2", size = 141522, upload-time = "2025-06-23T23:21:04.973Z" }, ] [[package]] @@ -1189,83 +1189,75 @@ wheels = [ [[package]] name = "multidict" -version = "6.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/b5/59f27b4ce9951a4bce56b88ba5ff5159486797ab18863f2b4c1c5e8465bd/multidict-6.5.0.tar.gz", hash = "sha256:942bd8002492ba819426a8d7aefde3189c1b87099cdf18aaaefefcf7f3f7b6d2", size = 98512, upload-time = "2025-06-17T14:15:56.556Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/ba/484f8e96ee58ec4fef42650eb9dbbedb24f9bc155780888398a4725d2270/multidict-6.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8b4bf6bb15a05796a07a248084e3e46e032860c899c7a9b981030e61368dba95", size = 73283, upload-time = "2025-06-17T14:13:50.406Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/01d62ea6199d76934c87746695b3ed16aeedfdd564e8d89184577037baac/multidict-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46bb05d50219655c42a4b8fcda9c7ee658a09adbb719c48e65a20284e36328ea", size = 42937, upload-time = "2025-06-17T14:13:51.45Z" }, - { url = "https://files.pythonhosted.org/packages/da/cf/bb462d920f26d9e2e0aff8a78aeb06af1225b826e9a5468870c57591910a/multidict-6.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:54f524d73f4d54e87e03c98f6af601af4777e4668a52b1bd2ae0a4d6fc7b392b", size = 42748, upload-time = "2025-06-17T14:13:52.505Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b1/d5c11ea0fdad68d3ed45f0e2527de6496d2fac8afe6b8ca6d407c20ad00f/multidict-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529b03600466480ecc502000d62e54f185a884ed4570dee90d9a273ee80e37b5", size = 236448, upload-time = "2025-06-17T14:13:53.562Z" }, - { url = "https://files.pythonhosted.org/packages/fc/69/c3ceb264994f5b338c812911a8d660084f37779daef298fc30bd817f75c7/multidict-6.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69ad681ad7c93a41ee7005cc83a144b5b34a3838bcf7261e2b5356057b0f78de", size = 228695, upload-time = "2025-06-17T14:13:54.775Z" }, - { url = "https://files.pythonhosted.org/packages/81/3d/c23dcc0d34a35ad29974184db2878021d28fe170ecb9192be6bfee73f1f2/multidict-6.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fe9fada8bc0839466b09fa3f6894f003137942984843ec0c3848846329a36ae", size = 247434, upload-time = "2025-06-17T14:13:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/06/b3/06cf7a049129ff52525a859277abb5648e61d7afae7fb7ed02e3806be34e/multidict-6.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f94c6ea6405fcf81baef1e459b209a78cda5442e61b5b7a57ede39d99b5204a0", size = 239431, upload-time = "2025-06-17T14:13:57.33Z" }, - { url = "https://files.pythonhosted.org/packages/8a/72/b2fe2fafa23af0c6123aebe23b4cd23fdad01dfe7009bb85624e4636d0dd/multidict-6.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ca75ad8a39ed75f079a8931435a5b51ee4c45d9b32e1740f99969a5d1cc2ee", size = 231542, upload-time = "2025-06-17T14:13:58.597Z" }, - { url = "https://files.pythonhosted.org/packages/a1/c9/a52ca0a342a02411a31b6af197a6428a5137d805293f10946eeab614ec06/multidict-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4c08f3a2a6cc42b414496017928d95898964fed84b1b2dace0c9ee763061f9", size = 233069, upload-time = "2025-06-17T14:13:59.834Z" }, - { url = "https://files.pythonhosted.org/packages/9b/55/a3328a3929b8e131e2678d5e65f552b0a6874fab62123e31f5a5625650b0/multidict-6.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:046a7540cfbb4d5dc846a1fd9843f3ba980c6523f2e0c5b8622b4a5c94138ae6", size = 250596, upload-time = "2025-06-17T14:14:01.178Z" }, - { url = "https://files.pythonhosted.org/packages/6c/b8/aa3905a38a8287013aeb0a54c73f79ccd8b32d2f1d53e5934643a36502c2/multidict-6.5.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64306121171d988af77d74be0d8c73ee1a69cf6f96aea7fa6030c88f32a152dd", size = 237858, upload-time = "2025-06-17T14:14:03.232Z" }, - { url = "https://files.pythonhosted.org/packages/d3/eb/f11d5af028014f402e5dd01ece74533964fa4e7bfae4af4824506fa8c398/multidict-6.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b4ac1dd5eb0ecf6f7351d5a9137f30a83f7182209c5d37f61614dfdce5714853", size = 249175, upload-time = "2025-06-17T14:14:04.561Z" }, - { url = "https://files.pythonhosted.org/packages/ac/57/d451905a62e5ef489cb4f92e8190d34ac5329427512afd7f893121da4e96/multidict-6.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bab4a8337235365f4111a7011a1f028826ca683834ebd12de4b85e2844359c36", size = 259532, upload-time = "2025-06-17T14:14:05.798Z" }, - { url = "https://files.pythonhosted.org/packages/d3/90/ff82b5ac5cabe3c79c50cf62a62f3837905aa717e67b6b4b7872804f23c8/multidict-6.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a05b5604c5a75df14a63eeeca598d11b2c3745b9008539b70826ea044063a572", size = 250554, upload-time = "2025-06-17T14:14:07.382Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5a/0cabc50d4bc16e61d8b0a8a74499a1409fa7b4ef32970b7662a423781fc7/multidict-6.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67c4a640952371c9ca65b6a710598be246ef3be5ca83ed38c16a7660d3980877", size = 248159, upload-time = "2025-06-17T14:14:08.65Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1d/adeabae0771544f140d9f42ab2c46eaf54e793325999c36106078b7f6600/multidict-6.5.0-cp311-cp311-win32.whl", hash = "sha256:fdeae096ca36c12d8aca2640b8407a9d94e961372c68435bef14e31cce726138", size = 40357, upload-time = "2025-06-17T14:14:09.91Z" }, - { url = "https://files.pythonhosted.org/packages/e1/fe/bbd85ae65c96de5c9910c332ee1f4b7be0bf0fb21563895167bcb6502a1f/multidict-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:e2977ef8b7ce27723ee8c610d1bd1765da4f3fbe5a64f9bf1fd3b4770e31fbc0", size = 44432, upload-time = "2025-06-17T14:14:11.013Z" }, - { url = "https://files.pythonhosted.org/packages/96/af/f9052d9c4e65195b210da9f7afdea06d3b7592b3221cc0ef1b407f762faa/multidict-6.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:82d0cf0ea49bae43d9e8c3851e21954eff716259ff42da401b668744d1760bcb", size = 41408, upload-time = "2025-06-17T14:14:12.112Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fa/18f4950e00924f7e84c8195f4fc303295e14df23f713d64e778b8fa8b903/multidict-6.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1bb986c8ea9d49947bc325c51eced1ada6d8d9b4c5b15fd3fcdc3c93edef5a74", size = 73474, upload-time = "2025-06-17T14:14:13.528Z" }, - { url = "https://files.pythonhosted.org/packages/6c/66/0392a2a8948bccff57e4793c9dde3e5c088f01e8b7f8867ee58a2f187fc5/multidict-6.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:03c0923da300120830fc467e23805d63bbb4e98b94032bd863bc7797ea5fa653", size = 43741, upload-time = "2025-06-17T14:14:15.188Z" }, - { url = "https://files.pythonhosted.org/packages/98/3e/f48487c91b2a070566cfbab876d7e1ebe7deb0a8002e4e896a97998ae066/multidict-6.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4c78d5ec00fdd35c91680ab5cf58368faad4bd1a8721f87127326270248de9bc", size = 42143, upload-time = "2025-06-17T14:14:16.612Z" }, - { url = "https://files.pythonhosted.org/packages/3f/49/439c6cc1cd00365cf561bdd3579cc3fa1a0d38effb3a59b8d9562839197f/multidict-6.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadc3cb78be90a887f8f6b73945b840da44b4a483d1c9750459ae69687940c97", size = 239303, upload-time = "2025-06-17T14:14:17.707Z" }, - { url = "https://files.pythonhosted.org/packages/c4/24/491786269e90081cb536e4d7429508725bc92ece176d1204a4449de7c41c/multidict-6.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b02e1ca495d71e07e652e4cef91adae3bf7ae4493507a263f56e617de65dafc", size = 236913, upload-time = "2025-06-17T14:14:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/e8/76/bbe2558b820ebeca8a317ab034541790e8160ca4b1e450415383ac69b339/multidict-6.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fe92a62326eef351668eec4e2dfc494927764a0840a1895cff16707fceffcd3", size = 250752, upload-time = "2025-06-17T14:14:20.297Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e3/3977f2c1123f553ceff9f53cd4de04be2c1912333c6fabbcd51531655476/multidict-6.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7673ee4f63879ecd526488deb1989041abcb101b2d30a9165e1e90c489f3f7fb", size = 243937, upload-time = "2025-06-17T14:14:21.935Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b8/7a6e9c13c79709cdd2f22ee849f058e6da76892d141a67acc0e6c30d845c/multidict-6.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa097ae2a29f573de7e2d86620cbdda5676d27772d4ed2669cfa9961a0d73955", size = 237419, upload-time = "2025-06-17T14:14:23.215Z" }, - { url = "https://files.pythonhosted.org/packages/84/9d/8557f5e88da71bc7e7a8ace1ada4c28197f3bfdc2dd6e51d3b88f2e16e8e/multidict-6.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:300da0fa4f8457d9c4bd579695496116563409e676ac79b5e4dca18e49d1c308", size = 237222, upload-time = "2025-06-17T14:14:24.516Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3b/8f023ad60e7969cb6bc0683738d0e1618f5ff5723d6d2d7818dc6df6ad3d/multidict-6.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a19bd108c35877b57393243d392d024cfbfdefe759fd137abb98f6fc910b64c", size = 247861, upload-time = "2025-06-17T14:14:25.839Z" }, - { url = "https://files.pythonhosted.org/packages/af/1c/9cf5a099ce7e3189906cf5daa72c44ee962dcb4c1983659f3a6f8a7446ab/multidict-6.5.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f32a1777465a35c35ddbbd7fc1293077938a69402fcc59e40b2846d04a120dd", size = 243917, upload-time = "2025-06-17T14:14:27.164Z" }, - { url = "https://files.pythonhosted.org/packages/6c/bb/88ee66ebeef56868044bac58feb1cc25658bff27b20e3cfc464edc181287/multidict-6.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9cc1e10c14ce8112d1e6d8971fe3cdbe13e314f68bea0e727429249d4a6ce164", size = 249214, upload-time = "2025-06-17T14:14:28.795Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/a90e88cc4a1309f33088ab1cdd5c0487718f49dfb82c5ffc845bb17c1973/multidict-6.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e95c5e07a06594bdc288117ca90e89156aee8cb2d7c330b920d9c3dd19c05414", size = 258682, upload-time = "2025-06-17T14:14:30.066Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d8/16dd69a6811920a31f4e06114ebe67b1cd922c8b05c9c82b050706d0b6fe/multidict-6.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:40ff26f58323795f5cd2855e2718a1720a1123fb90df4553426f0efd76135462", size = 254254, upload-time = "2025-06-17T14:14:31.323Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a8/90193a5f5ca1bdbf92633d69a25a2ef9bcac7b412b8d48c84d01a2732518/multidict-6.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76803a29fd71869a8b59c2118c9dcfb3b8f9c8723e2cce6baeb20705459505cf", size = 247741, upload-time = "2025-06-17T14:14:32.717Z" }, - { url = "https://files.pythonhosted.org/packages/cd/43/29c7a747153c05b41d1f67455426af39ed88d6de3f21c232b8f2724bde13/multidict-6.5.0-cp312-cp312-win32.whl", hash = "sha256:df7ecbc65a53a2ce1b3a0c82e6ad1a43dcfe7c6137733f9176a92516b9f5b851", size = 41049, upload-time = "2025-06-17T14:14:33.941Z" }, - { url = "https://files.pythonhosted.org/packages/1e/e8/8f3fc32b7e901f3a2719764d64aeaf6ae77b4ba961f1c3a3cf3867766636/multidict-6.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ec1c3fbbb0b655a6540bce408f48b9a7474fd94ed657dcd2e890671fefa7743", size = 44700, upload-time = "2025-06-17T14:14:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/24/e4/e250806adc98d524d41e69c8d4a42bc3513464adb88cb96224df12928617/multidict-6.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:2d24a00d34808b22c1f15902899b9d82d0faeca9f56281641c791d8605eacd35", size = 41703, upload-time = "2025-06-17T14:14:36.168Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c9/092c4e9402b6d16de761cff88cb842a5c8cc50ccecaf9c4481ba53264b9e/multidict-6.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:53d92df1752df67a928fa7f884aa51edae6f1cf00eeb38cbcf318cf841c17456", size = 73486, upload-time = "2025-06-17T14:14:37.238Z" }, - { url = "https://files.pythonhosted.org/packages/08/f9/6f7ddb8213f5fdf4db48d1d640b78e8aef89b63a5de8a2313286db709250/multidict-6.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:680210de2c38eef17ce46b8df8bf2c1ece489261a14a6e43c997d49843a27c99", size = 43745, upload-time = "2025-06-17T14:14:38.32Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a7/b9be0163bfeee3bb08a77a1705e24eb7e651d594ea554107fac8a1ca6a4d/multidict-6.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e279259bcb936732bfa1a8eec82b5d2352b3df69d2fa90d25808cfc403cee90a", size = 42135, upload-time = "2025-06-17T14:14:39.897Z" }, - { url = "https://files.pythonhosted.org/packages/8e/30/93c8203f943a417bda3c573a34d5db0cf733afdfffb0ca78545c7716dbd8/multidict-6.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1c185fc1069781e3fc8b622c4331fb3b433979850392daa5efbb97f7f9959bb", size = 238585, upload-time = "2025-06-17T14:14:41.332Z" }, - { url = "https://files.pythonhosted.org/packages/9d/fe/2582b56a1807604774f566eeef183b0d6b148f4b89d1612cd077567b2e1e/multidict-6.5.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6bb5f65ff91daf19ce97f48f63585e51595539a8a523258b34f7cef2ec7e0617", size = 236174, upload-time = "2025-06-17T14:14:42.602Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c4/d8b66d42d385bd4f974cbd1eaa8b265e6b8d297249009f312081d5ded5c7/multidict-6.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8646b4259450c59b9286db280dd57745897897284f6308edbdf437166d93855", size = 250145, upload-time = "2025-06-17T14:14:43.944Z" }, - { url = "https://files.pythonhosted.org/packages/bc/64/62feda5093ee852426aae3df86fab079f8bf1cdbe403e1078c94672ad3ec/multidict-6.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d245973d4ecc04eea0a8e5ebec7882cf515480036e1b48e65dffcfbdf86d00be", size = 243470, upload-time = "2025-06-17T14:14:45.343Z" }, - { url = "https://files.pythonhosted.org/packages/67/dc/9f6fa6e854625cf289c0e9f4464b40212a01f76b2f3edfe89b6779b4fb93/multidict-6.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a133e7ddc9bc7fb053733d0ff697ce78c7bf39b5aec4ac12857b6116324c8d75", size = 236968, upload-time = "2025-06-17T14:14:46.609Z" }, - { url = "https://files.pythonhosted.org/packages/46/ae/4b81c6e3745faee81a156f3f87402315bdccf04236f75c03e37be19c94ff/multidict-6.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80d696fa38d738fcebfd53eec4d2e3aeb86a67679fd5e53c325756682f152826", size = 236575, upload-time = "2025-06-17T14:14:47.929Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fa/4089d7642ea344226e1bfab60dd588761d4791754f8072e911836a39bedf/multidict-6.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:20d30c9410ac3908abbaa52ee5967a754c62142043cf2ba091e39681bd51d21a", size = 247632, upload-time = "2025-06-17T14:14:49.525Z" }, - { url = "https://files.pythonhosted.org/packages/16/ee/a353dac797de0f28fb7f078cc181c5f2eefe8dd16aa11a7100cbdc234037/multidict-6.5.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c65068cc026f217e815fa519d8e959a7188e94ec163ffa029c94ca3ef9d4a73", size = 243520, upload-time = "2025-06-17T14:14:50.83Z" }, - { url = "https://files.pythonhosted.org/packages/50/ec/560deb3d2d95822d6eb1bcb1f1cb728f8f0197ec25be7c936d5d6a5d133c/multidict-6.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e355ac668a8c3e49c2ca8daa4c92f0ad5b705d26da3d5af6f7d971e46c096da7", size = 248551, upload-time = "2025-06-17T14:14:52.229Z" }, - { url = "https://files.pythonhosted.org/packages/10/85/ddf277e67c78205f6695f2a7639be459bca9cc353b962fd8085a492a262f/multidict-6.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08db204213d0375a91a381cae0677ab95dd8c67a465eb370549daf6dbbf8ba10", size = 258362, upload-time = "2025-06-17T14:14:53.934Z" }, - { url = "https://files.pythonhosted.org/packages/02/fc/d64ee1df9b87c5210f2d4c419cab07f28589c81b4e5711eda05a122d0614/multidict-6.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ffa58e3e215af8f6536dc837a990e456129857bb6fd546b3991be470abd9597a", size = 253862, upload-time = "2025-06-17T14:14:55.323Z" }, - { url = "https://files.pythonhosted.org/packages/c9/7c/a2743c00d9e25f4826d3a77cc13d4746398872cf21c843eef96bb9945665/multidict-6.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e86eb90015c6f21658dbd257bb8e6aa18bdb365b92dd1fba27ec04e58cdc31b", size = 247391, upload-time = "2025-06-17T14:14:57.293Z" }, - { url = "https://files.pythonhosted.org/packages/9b/03/7773518db74c442904dbd349074f1e7f2a854cee4d9529fc59e623d3949e/multidict-6.5.0-cp313-cp313-win32.whl", hash = "sha256:f34a90fbd9959d0f857323bd3c52b3e6011ed48f78d7d7b9e04980b8a41da3af", size = 41115, upload-time = "2025-06-17T14:14:59.33Z" }, - { url = "https://files.pythonhosted.org/packages/eb/9a/6fc51b1dc11a7baa944bc101a92167d8b0f5929d376a8c65168fc0d35917/multidict-6.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fcb2aa79ac6aef8d5b709bbfc2fdb1d75210ba43038d70fbb595b35af470ce06", size = 44768, upload-time = "2025-06-17T14:15:00.427Z" }, - { url = "https://files.pythonhosted.org/packages/82/2d/0d010be24b663b3c16e3d3307bbba2de5ae8eec496f6027d5c0515b371a8/multidict-6.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:6dcee5e7e92060b4bb9bb6f01efcbb78c13d0e17d9bc6eec71660dd71dc7b0c2", size = 41770, upload-time = "2025-06-17T14:15:01.854Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d1/a71711a5f32f84b7b036e82182e3250b949a0ce70d51a2c6a4079e665449/multidict-6.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:cbbc88abea2388fde41dd574159dec2cda005cb61aa84950828610cb5010f21a", size = 80450, upload-time = "2025-06-17T14:15:02.968Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a2/953a9eede63a98fcec2c1a2c1a0d88de120056219931013b871884f51b43/multidict-6.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70b599f70ae6536e5976364d3c3cf36f40334708bd6cebdd1e2438395d5e7676", size = 46971, upload-time = "2025-06-17T14:15:04.149Z" }, - { url = "https://files.pythonhosted.org/packages/44/61/60250212953459edda2c729e1d85130912f23c67bd4f585546fe4bdb1578/multidict-6.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:828bab777aa8d29d59700018178061854e3a47727e0611cb9bec579d3882de3b", size = 45548, upload-time = "2025-06-17T14:15:05.666Z" }, - { url = "https://files.pythonhosted.org/packages/11/b6/e78ee82e96c495bc2582b303f68bed176b481c8d81a441fec07404fce2ca/multidict-6.5.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9695fc1462f17b131c111cf0856a22ff154b0480f86f539d24b2778571ff94d", size = 238545, upload-time = "2025-06-17T14:15:06.88Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0f/6132ca06670c8d7b374c3a4fd1ba896fc37fbb66b0de903f61db7d1020ec/multidict-6.5.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b5ac6ebaf5d9814b15f399337ebc6d3a7f4ce9331edd404e76c49a01620b68d", size = 229931, upload-time = "2025-06-17T14:15:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/c0/63/d9957c506e6df6b3e7a194f0eea62955c12875e454b978f18262a65d017b/multidict-6.5.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84a51e3baa77ded07be4766a9e41d977987b97e49884d4c94f6d30ab6acaee14", size = 248181, upload-time = "2025-06-17T14:15:09.907Z" }, - { url = "https://files.pythonhosted.org/packages/43/3f/7d5490579640db5999a948e2c41d4a0efd91a75989bda3e0a03a79c92be2/multidict-6.5.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8de67f79314d24179e9b1869ed15e88d6ba5452a73fc9891ac142e0ee018b5d6", size = 241846, upload-time = "2025-06-17T14:15:11.596Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f7/252b1ce949ece52bba4c0de7aa2e3a3d5964e800bce71fb778c2e6c66f7c/multidict-6.5.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17f78a52c214481d30550ec18208e287dfc4736f0c0148208334b105fd9e0887", size = 232893, upload-time = "2025-06-17T14:15:12.946Z" }, - { url = "https://files.pythonhosted.org/packages/45/7e/0070bfd48c16afc26e056f2acce49e853c0d604a69c7124bc0bbdb1bcc0a/multidict-6.5.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2966d0099cb2e2039f9b0e73e7fd5eb9c85805681aa2a7f867f9d95b35356921", size = 228567, upload-time = "2025-06-17T14:15:14.267Z" }, - { url = "https://files.pythonhosted.org/packages/2a/31/90551c75322113ebf5fd9c5422e8641d6952f6edaf6b6c07fdc49b1bebdd/multidict-6.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:86fb42ed5ed1971c642cc52acc82491af97567534a8e381a8d50c02169c4e684", size = 246188, upload-time = "2025-06-17T14:15:15.985Z" }, - { url = "https://files.pythonhosted.org/packages/cc/e2/aa4b02a55e7767ff292871023817fe4db83668d514dab7ccbce25eaf7659/multidict-6.5.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:4e990cbcb6382f9eae4ec720bcac6a1351509e6fc4a5bb70e4984b27973934e6", size = 235178, upload-time = "2025-06-17T14:15:17.395Z" }, - { url = "https://files.pythonhosted.org/packages/7d/5c/f67e726717c4b138b166be1700e2b56e06fbbcb84643d15f9a9d7335ff41/multidict-6.5.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d99a59d64bb1f7f2117bec837d9e534c5aeb5dcedf4c2b16b9753ed28fdc20a3", size = 243422, upload-time = "2025-06-17T14:15:18.939Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1c/15fa318285e26a50aa3fa979bbcffb90f9b4d5ec58882d0590eda067d0da/multidict-6.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:e8ef15cc97c9890212e1caf90f0d63f6560e1e101cf83aeaf63a57556689fb34", size = 254898, upload-time = "2025-06-17T14:15:20.31Z" }, - { url = "https://files.pythonhosted.org/packages/ad/3d/d6c6d1c2e9b61ca80313912d30bb90d4179335405e421ef0a164eac2c0f9/multidict-6.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:b8a09aec921b34bd8b9f842f0bcfd76c6a8c033dc5773511e15f2d517e7e1068", size = 247129, upload-time = "2025-06-17T14:15:21.665Z" }, - { url = "https://files.pythonhosted.org/packages/29/15/1568258cf0090bfa78d44be66247cfdb16e27dfd935c8136a1e8632d3057/multidict-6.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ff07b504c23b67f2044533244c230808a1258b3493aaf3ea2a0785f70b7be461", size = 243841, upload-time = "2025-06-17T14:15:23.38Z" }, - { url = "https://files.pythonhosted.org/packages/65/57/64af5dbcfd61427056e840c8e520b502879d480f9632fbe210929fd87393/multidict-6.5.0-cp313-cp313t-win32.whl", hash = "sha256:9232a117341e7e979d210e41c04e18f1dc3a1d251268df6c818f5334301274e1", size = 46761, upload-time = "2025-06-17T14:15:24.733Z" }, - { url = "https://files.pythonhosted.org/packages/26/a8/cac7f7d61e188ff44f28e46cb98f9cc21762e671c96e031f06c84a60556e/multidict-6.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:44cb5c53fb2d4cbcee70a768d796052b75d89b827643788a75ea68189f0980a1", size = 52112, upload-time = "2025-06-17T14:15:25.906Z" }, - { url = "https://files.pythonhosted.org/packages/51/9f/076533feb1b5488d22936da98b9c217205cfbf9f56f7174e8c5c86d86fe6/multidict-6.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:51d33fafa82640c0217391d4ce895d32b7e84a832b8aee0dcc1b04d8981ec7f4", size = 44358, upload-time = "2025-06-17T14:15:27.117Z" }, - { url = "https://files.pythonhosted.org/packages/44/d8/45e8fc9892a7386d074941429e033adb4640e59ff0780d96a8cf46fe788e/multidict-6.5.0-py3-none-any.whl", hash = "sha256:5634b35f225977605385f56153bd95a7133faffc0ffe12ad26e10517537e8dfc", size = 12181, upload-time = "2025-06-17T14:15:55.156Z" }, +version = "6.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/43/2d90c414d9efc4587d6e7cebae9f2c2d8001bcb4f89ed514ae837e9dcbe6/multidict-6.5.1.tar.gz", hash = "sha256:a835ea8103f4723915d7d621529c80ef48db48ae0c818afcabe0f95aa1febc3a", size = 98690, upload-time = "2025-06-24T22:16:05.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/65/439c3f595f68ee60d2c7abd14f36829b936b49c4939e35f24e65950b59b2/multidict-6.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:153d7ff738d9b67b94418b112dc5a662d89d2fc26846a9e942f039089048c804", size = 74129, upload-time = "2025-06-24T22:14:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7a/88b474366126ef7cd427dca84ea6692d81e6e8ebb46f810a565e60716951/multidict-6.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1d784c0a1974f00d87f632d0fb6b1078baf7e15d2d2d1408af92f54d120f136e", size = 43248, upload-time = "2025-06-24T22:14:10.017Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8f/c45ff8980c2f2d1ed8f4f0c682953861fbb840adc318da1b26145587e443/multidict-6.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dedf667cded1cdac5bfd3f3c2ff30010f484faccae4e871cc8a9316d2dc27363", size = 43250, upload-time = "2025-06-24T22:14:11.107Z" }, + { url = "https://files.pythonhosted.org/packages/ac/71/795e729385ecd8994d2033731ced3a80959e9c3c279766613565f5dcc7e1/multidict-6.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7cbf407313236a79ce9b8af11808c29756cfb9c9a49a7f24bb1324537eec174b", size = 254313, upload-time = "2025-06-24T22:14:12.216Z" }, + { url = "https://files.pythonhosted.org/packages/de/5a/36e8dd1306f8f6e5b252d6341e919c4a776745e2c38f86bc27d0640d3379/multidict-6.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bf0068fe9abb0ebed1436a4e415117386951cf598eb8146ded4baf8e1ff6d1e", size = 227162, upload-time = "2025-06-24T22:14:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c2/4e68fb3a8ef5b23bbf3d82a19f4ff71de8289b696c662572a6cb094eabf6/multidict-6.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195882f2f6272dacc88194ecd4de3608ad0ee29b161e541403b781a5f5dd346f", size = 265552, upload-time = "2025-06-24T22:14:14.846Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/b9ee059e39cd3fec2e1fe9ecb57165fba0518d79323a6f355275ed9ec956/multidict-6.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5776f9d2c3a1053f022f744af5f467c2f65b40d4cc00082bcf70e8c462c7dbad", size = 260935, upload-time = "2025-06-24T22:14:16.209Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0a/ea655a79d2d89dedb33f423b5dd3a733d97b1765a5e2155da883060fb48f/multidict-6.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a266373c604e49552d295d9f8ec4fd59bd364f2dd73eb18e7d36d5533b88f45", size = 251778, upload-time = "2025-06-24T22:14:17.963Z" }, + { url = "https://files.pythonhosted.org/packages/3f/58/8ff6b032f6c8956c8beb93a7191c80e4a6f385e9ffbe4a38c1cd758a7445/multidict-6.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:79101d58094419b6e8d07e24946eba440136b9095590271cd6ccc4a90674a57d", size = 249837, upload-time = "2025-06-24T22:14:19.344Z" }, + { url = "https://files.pythonhosted.org/packages/de/be/2fcdfd358ebc1be2ac3922a594daf660f99a23740f5177ba8b2fb6a66feb/multidict-6.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:62eb76be8c20d9017a82b74965db93ddcf472b929b6b2b78c56972c73bacf2e4", size = 240831, upload-time = "2025-06-24T22:14:20.647Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/1d3a4bb4ce34f314b919f4cb0da26430a6d88758f6d20b1c4f236a569085/multidict-6.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:70c742357dd6207be30922207f8d59c91e2776ddbefa23830c55c09020e59f8a", size = 262110, upload-time = "2025-06-24T22:14:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5a/4cabf6661aa18e43dca54d00de06ef287740ad6ddbba34be53b3a554a6ee/multidict-6.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:29eff1c9a905e298e9cd29f856f77485e58e59355f0ee323ac748203e002bbd3", size = 250845, upload-time = "2025-06-24T22:14:23.276Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/44c44312d48423327d22be8c7058f9da8e2a527c9230d89b582670327efd/multidict-6.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:090e0b37fde199b58ea050c472c21dc8a3fbf285f42b862fe1ff02aab8942239", size = 247351, upload-time = "2025-06-24T22:14:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/21/30/a12bbd76222be44c4f2d540c0d9cd1f932ab97e84a06098749f29b2908f5/multidict-6.5.1-cp311-cp311-win32.whl", hash = "sha256:6037beca8cb481307fb586ee0b73fae976a3e00d8f6ad7eb8af94a878a4893f0", size = 40644, upload-time = "2025-06-24T22:14:26.139Z" }, + { url = "https://files.pythonhosted.org/packages/90/58/2ce479dcb4611212eaa4808881d9a66a4362c48cd9f7b525b24a5d45764f/multidict-6.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:b632c1e4a2ff0bb4c1367d6c23871aa95dbd616bf4a847034732a142bb6eea94", size = 44693, upload-time = "2025-06-24T22:14:27.265Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d1/466a6cf48dcef796f2d75ba51af4475ac96c6ea33ef4dbf4cea1caf99532/multidict-6.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:2ec3aa63f0c668f591d43195f8e555f803826dee34208c29ade9d63355f9e095", size = 41822, upload-time = "2025-06-24T22:14:28.387Z" }, + { url = "https://files.pythonhosted.org/packages/33/36/225fb9b890607d740f61957febf622f5c9cd9e641a93502c7877934d57ef/multidict-6.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:48f95fe064f63d9601ef7a3dce2fc2a437d5fcc11bca960bc8be720330b13b6a", size = 74287, upload-time = "2025-06-24T22:14:29.456Z" }, + { url = "https://files.pythonhosted.org/packages/70/e5/c9eabb16ecf77275664413263527ab169e08371dfa6b168025d8f67261fd/multidict-6.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b7b6e1ce9b61f721417c68eeeb37599b769f3b631e6b25c21f50f8f619420b9", size = 44092, upload-time = "2025-06-24T22:14:30.686Z" }, + { url = "https://files.pythonhosted.org/packages/df/0b/dd9322a432c477a2e6d089bbb53acb68ed25515b8292dbc60f27e7e45d70/multidict-6.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8b83b055889bda09fc866c0a652cdb6c36eeeafc2858259c9a7171fe82df5773", size = 42565, upload-time = "2025-06-24T22:14:31.8Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/22f5b4e55a4bc99f9622de280f7da366c1d7f29ec4eec9d339cb2ba62019/multidict-6.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7bd4d655dc460c7aebb73b58ed1c074e85f7286105b012556cf0f25c6d1dba3", size = 254896, upload-time = "2025-06-24T22:14:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/09/dc/2f6d96d4a80ec731579cb69532fac33cbbda2a838079ae0c47c6e8f5545b/multidict-6.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa6dcf25ced31cdce10f004506dbc26129f28a911b32ed10e54453a0842a6173", size = 236854, upload-time = "2025-06-24T22:14:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/ef38a69ee75e8b72e5cff9ed4cff92379eadd057a99eaf4893494bf6ab64/multidict-6.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:059fb556c3e6ce1a168496f92ef139ad839a47f898eaa512b1d43e5e05d78c6b", size = 265131, upload-time = "2025-06-24T22:14:35.534Z" }, + { url = "https://files.pythonhosted.org/packages/c0/9e/85d9fe9e658e0edf566c02181248fa2aaf5e53134df0c80f7231ce5fc689/multidict-6.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f97680c839dd9fa208e9584b1c2a5f1224bd01d31961f7f7d94984408c4a6b9e", size = 262187, upload-time = "2025-06-24T22:14:36.891Z" }, + { url = "https://files.pythonhosted.org/packages/2b/1c/b46ec1dd78c3faa55bffb354410c48fadd81029a144cd056828c82ca15b4/multidict-6.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7710c716243525cc05cd038c6e09f1807ee0fef2510a6e484450712c389c8d7f", size = 251220, upload-time = "2025-06-24T22:14:38.584Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6b/481ec5179ddc7da8b05077ebae2dd51da3df3ae3e5842020fbfa939167c1/multidict-6.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:83eb172b4856ffff2814bdcf9c7792c0439302faab1b31376817b067b26cd8f5", size = 249949, upload-time = "2025-06-24T22:14:40.033Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/642f63e12c1b8e6662c23626a98e9d764fe5a63c3a6cb59002f6fdcb920f/multidict-6.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:562d4714fa43f6ebc043a657535e4575e7d6141a818c9b3055f0868d29a1a41b", size = 244438, upload-time = "2025-06-24T22:14:41.464Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cf/797397f6d38b011912504aef213a4be43ef4ec134859caa47f94d810bad8/multidict-6.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2d7def2fc47695c46a427b8f298fb5ace03d635c1fb17f30d6192c9a8fb69e70", size = 259921, upload-time = "2025-06-24T22:14:43.248Z" }, + { url = "https://files.pythonhosted.org/packages/82/b2/ae914a2d84eba21e956fa3727060248ca23ed4a5bf1beb057df0d10f9de3/multidict-6.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:77bc8ab5c6bfe696eff564824e73a451fdeca22f3b960261750836cee02bcbfa", size = 252691, upload-time = "2025-06-24T22:14:45.57Z" }, + { url = "https://files.pythonhosted.org/packages/01/fa/1ab4d79a236b871cfd40d36a1f9942906c630bd2b7822287bd3927addb62/multidict-6.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9eec51891d3c210948ead894ec1483d48748abec08db5ce9af52cc13fef37aee", size = 246224, upload-time = "2025-06-24T22:14:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/78/dd/bf002fe04e952db73cad8ce10a5b5347358d0d17221aef156e050aff690b/multidict-6.5.1-cp312-cp312-win32.whl", hash = "sha256:189f0c2bd1c0ae5509e453707d0e187e030c9e873a0116d1f32d1c870d0fc347", size = 41354, upload-time = "2025-06-24T22:14:48.567Z" }, + { url = "https://files.pythonhosted.org/packages/95/ce/508a8487d98fdc3e693755bc19c543a2af293f5ce96da398bd1974efb802/multidict-6.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:e81f23b4b6f2a588f15d5cb554b2d8b482bb6044223d64b86bc7079cae9ebaad", size = 45072, upload-time = "2025-06-24T22:14:50.898Z" }, + { url = "https://files.pythonhosted.org/packages/ae/da/4782cf2f274d0d56fff6c07fc5cc5a14acf821dec08350c17d66d0207a05/multidict-6.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:79d13e06d5241f9c8479dfeaf0f7cce8f453a4a302c9a0b1fa9b1a6869ff7757", size = 42149, upload-time = "2025-06-24T22:14:53.138Z" }, + { url = "https://files.pythonhosted.org/packages/19/3f/c2e07031111d2513d260157933a8697ad52a935d8a2a2b8b7b317ddd9a96/multidict-6.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:98011312f36d1e496f15454a95578d1212bc2ffc25650a8484752b06d304fd9b", size = 73588, upload-time = "2025-06-24T22:14:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/95/bb/f47aa21827202a9f889fd66de9a1db33d0e4bbaaa2567156e4efb3cc0e5e/multidict-6.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bae589fb902b47bd94e6f539b34eefe55a1736099f616f614ec1544a43f95b05", size = 43756, upload-time = "2025-06-24T22:14:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ec/24549de092c9b0bc3167e0beb31a11be58e8595dbcfed2b7821795bb3923/multidict-6.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6eb3bf26cd94eb306e4bc776d0964cc67a7967e4ad9299309f0ff5beec3c62be", size = 42222, upload-time = "2025-06-24T22:14:57.418Z" }, + { url = "https://files.pythonhosted.org/packages/13/45/54452027ebc0ba660667aab67ae11afb9aaba91f4b5d63cddef045279d94/multidict-6.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5e1a5a99c72d1531501406fcc06b6bf699ebd079dacd6807bb43fc0ff260e5c", size = 253014, upload-time = "2025-06-24T22:14:58.738Z" }, + { url = "https://files.pythonhosted.org/packages/97/3c/76e7b4c0ce3a8bb43efca679674fba421333fbc8429134072db80e13dcb8/multidict-6.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38755bcba18720cb2338bea23a5afcff234445ee75fa11518f6130e22f2ab970", size = 235939, upload-time = "2025-06-24T22:15:00.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/ce/48e3123a9af61ff2f60e3764b0b15cf4fca22b1299aac281252ac3a590d6/multidict-6.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f42fef9bcba3c32fd4e4a23c5757fc807d218b249573aaffa8634879f95feb73", size = 262940, upload-time = "2025-06-24T22:15:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ab/bccd739faf87051b55df619a0967c8545b4d4a4b90258c5f564ab1752f15/multidict-6.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:071b962f4cc87469cda90c7cc1c077b76496878b39851d7417a3d994e27fe2c6", size = 260652, upload-time = "2025-06-24T22:15:02.988Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9c/01f654aad28a5d0d74f2678c1541ae15e711f99603fd84c780078205966e/multidict-6.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:627ba4b7ce7c0115981f0fd91921f5d101dfb9972622178aeef84ccce1c2bbf3", size = 250011, upload-time = "2025-06-24T22:15:04.317Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bc/edf08906e1db7385c6bf36e4179957307f50c44a889493e9b251255be79c/multidict-6.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05dcaed3e5e54f0d0f99a39762b0195274b75016cbf246f600900305581cf1a2", size = 248242, upload-time = "2025-06-24T22:15:06.035Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c3/1ad054b88b889fda8b62ea9634ac7082567e8dc42b9b794a2c565ef102ab/multidict-6.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:11f5ecf3e741a18c578d118ad257c5588ca33cc7c46d51c0487d7ae76f072c32", size = 244683, upload-time = "2025-06-24T22:15:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/57/63/119a76b2095e1bb765816175cafeac7b520f564691abef2572fb80f4f246/multidict-6.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b948eb625411c20b15088fca862c51a39140b9cf7875b5fb47a72bb249fa2f42", size = 257626, upload-time = "2025-06-24T22:15:09.013Z" }, + { url = "https://files.pythonhosted.org/packages/26/a9/b91a76af5ff49bd088ee76d11eb6134227f5ea50bcd5f6738443b2fe8e05/multidict-6.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc993a96dfc8300befd03d03df46efdb1d8d5a46911b014e956a4443035f470d", size = 251077, upload-time = "2025-06-24T22:15:10.366Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fe/b1dc57aaa4de9f5a27543e28bd1f8bff00a316888b7344b5d33258b14b0a/multidict-6.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2d333380f22d35a56c6461f4579cfe186e143cd0b010b9524ac027de2a34cd", size = 244715, upload-time = "2025-06-24T22:15:11.76Z" }, + { url = "https://files.pythonhosted.org/packages/51/55/47a82690f71d0141eea49a623bbcc00a4d28770efc7cba8ead75602c9b90/multidict-6.5.1-cp313-cp313-win32.whl", hash = "sha256:5891e3327e6a426ddd443c87339b967c84feb8c022dd425e0c025fa0fcd71e68", size = 41156, upload-time = "2025-06-24T22:15:13.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/b3/43306e4d7d3a9898574d1dc156b9607540dad581b1d767c992030751b82d/multidict-6.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:fcdaa72261bff25fad93e7cb9bd7112bd4bac209148e698e380426489d8ed8a9", size = 44933, upload-time = "2025-06-24T22:15:14.639Z" }, + { url = "https://files.pythonhosted.org/packages/30/e2/34cb83c8a4e01b28e2abf30dc90178aa63c9db042be22fa02472cb744b86/multidict-6.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:84292145303f354a35558e601c665cdf87059d87b12777417e2e57ba3eb98903", size = 41967, upload-time = "2025-06-24T22:15:15.856Z" }, + { url = "https://files.pythonhosted.org/packages/64/08/17d2de9cf749ea9589ecfb7532ab4988e8b113b7624826dba6b7527a58f3/multidict-6.5.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f8316e58db799a1972afbc46770dfaaf20b0847003ab80de6fcb9861194faa3f", size = 80513, upload-time = "2025-06-24T22:15:16.946Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/c9392465a21f7dff164633348b4cf66eef55c4ee48bdcdc00f0a71792779/multidict-6.5.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3468f0db187aca59eb56e0aa9f7c8c5427bcb844ad1c86557b4886aeb4484d8", size = 46854, upload-time = "2025-06-24T22:15:18.116Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d79cbed5d0573304bc907dff0e5ad8788a4de891eec832809812b319930e/multidict-6.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:228533a5f99f1248cd79f6470779c424d63bc3e10d47c82511c65cc294458445", size = 45724, upload-time = "2025-06-24T22:15:19.241Z" }, + { url = "https://files.pythonhosted.org/packages/ec/22/232be6c077183719c78131f0e3c3d7134eb2d839e6e50e1c1e69e5ef5965/multidict-6.5.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527076fdf5854901b1246c589af9a8a18b4a308375acb0020b585f696a10c794", size = 251895, upload-time = "2025-06-24T22:15:20.564Z" }, + { url = "https://files.pythonhosted.org/packages/57/80/85985e1441864b946e79538355b7b47f36206bf6bbaa2fa6d74d8232f2ab/multidict-6.5.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9a17a17bad5c22f43e6a6b285dd9c16b1e8f8428202cd9bc22adaac68d0bbfed", size = 229357, upload-time = "2025-06-24T22:15:21.949Z" }, + { url = "https://files.pythonhosted.org/packages/b1/14/0024d1428b05aedaeea211da232aa6b6ad5c556a8a38b0942df1e54e1fa5/multidict-6.5.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:efd1951edab4a6cb65108d411867811f2b283f4b972337fb4269e40142f7f6a6", size = 259262, upload-time = "2025-06-24T22:15:23.455Z" }, + { url = "https://files.pythonhosted.org/packages/b1/cc/3fe63d61ffc9a48d62f36249e228e330144d990ac01f61169b615a3be471/multidict-6.5.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c07d5f38b39acb4f8f61a7aa4166d140ed628245ff0441630df15340532e3b3c", size = 257998, upload-time = "2025-06-24T22:15:24.907Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e4/46b38b9a565ccc5d86f55787090670582d51ab0a0d37cfeaf4313b053f7b/multidict-6.5.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a6605dc74cd333be279e1fcb568ea24f7bdf1cf09f83a77360ce4dd32d67f14", size = 247951, upload-time = "2025-06-24T22:15:26.274Z" }, + { url = "https://files.pythonhosted.org/packages/af/78/58a9bc0674401f1f26418cd58a5ebf35ce91ead76a22b578908acfe0f4e2/multidict-6.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d64e30ae9ba66ce303a567548a06d64455d97c5dff7052fe428d154274d7174", size = 246786, upload-time = "2025-06-24T22:15:27.695Z" }, + { url = "https://files.pythonhosted.org/packages/66/24/51142ccee295992e22881cccc54b291308423bbcc836fcf4d2edef1a88d0/multidict-6.5.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2fb5dde79a7f6d98ac5e26a4c9de77ccd2c5224a7ce89aeac6d99df7bbe06464", size = 235030, upload-time = "2025-06-24T22:15:29.391Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/a6f7b75460d3e35b16bf7745c9e3ebb3293324a4295e586563bf50d361f4/multidict-6.5.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8a0d22e8b07cf620e9aeb1582340d00f0031e6a1f3e39d9c2dcbefa8691443b4", size = 253964, upload-time = "2025-06-24T22:15:31.689Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f8/0b690674bf8f78604eb0a2b0a85d1380ff3003f270440d40def2a3de8cf4/multidict-6.5.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0120ed5cff2082c7a0ed62a8f80f4f6ac266010c722381816462f279bfa19487", size = 247370, upload-time = "2025-06-24T22:15:33.114Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7d/ca55049d1041c517f294c1755c786539cb7a8dc5033361f20ce3a3d817be/multidict-6.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3dea06ba27401c4b54317aa04791182dc9295e7aa623732dd459071a0e0f65db", size = 242920, upload-time = "2025-06-24T22:15:34.669Z" }, + { url = "https://files.pythonhosted.org/packages/1e/65/f4afa14f0921751864bb3ef80267f15ecae423483e8da9bc5d3757632bfa/multidict-6.5.1-cp313-cp313t-win32.whl", hash = "sha256:93b21be44f3cfee3be68ed5cd8848a3c0420d76dbd12d74f7776bde6b29e5f33", size = 46968, upload-time = "2025-06-24T22:15:36.023Z" }, + { url = "https://files.pythonhosted.org/packages/00/0a/13d08be1ca1523df515fb4efd3cf10f153e62d533f55c53f543cd73041e8/multidict-6.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c5c18f8646a520cc34d00f65f9f6f77782b8a8c59fd8de10713e0de7f470b5d0", size = 52353, upload-time = "2025-06-24T22:15:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/dd/84aaf725b236677597a9570d8c1c99af0ba03712149852347969e014d826/multidict-6.5.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eb27128141474a1d545f0531b496c7c2f1c4beff50cb5a828f36eb62fef16c67", size = 44500, upload-time = "2025-06-24T22:15:38.445Z" }, + { url = "https://files.pythonhosted.org/packages/07/9f/d4719ce55a1d8bf6619e8bb92f1e2e7399026ea85ae0c324ec77ee06c050/multidict-6.5.1-py3-none-any.whl", hash = "sha256:895354f4a38f53a1df2cc3fa2223fa714cff2b079a9f018a76cad35e7f0f044c", size = 12185, upload-time = "2025-06-24T22:16:03.816Z" }, ] [[package]] @@ -1456,7 +1448,7 @@ dev = [ docs = [ { name = "linkify-it-py", specifier = ">=2.0.3,<3.0.0" }, { name = "myst-parser", specifier = ">=4.0.1,<5.0.0" }, - { name = "numpydoc", specifier = ">=1.8.0,<2.0.0" }, + { name = "numpydoc", specifier = ">=1.9.0,<2.0.0" }, { name = "sphinx-comments", specifier = ">=0.0.3,<1.0.0" }, { name = "sphinx-markdown-builder", specifier = ">=0.6.8,<1.0.0" }, ] @@ -1484,73 +1476,72 @@ wheels = [ [[package]] name = "numpy" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/db/8e12381333aea300890829a0a36bfa738cac95475d88982d538725143fd9/numpy-2.3.0.tar.gz", hash = "sha256:581f87f9e9e9db2cba2141400e160e9dd644ee248788d6f90636eeb8fd9260a6", size = 20382813, upload-time = "2025-06-07T14:54:32.608Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/5f/df67435257d827eb3b8af66f585223dc2c3f2eb7ad0b50cb1dae2f35f494/numpy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3c9fdde0fa18afa1099d6257eb82890ea4f3102847e692193b54e00312a9ae9", size = 21199688, upload-time = "2025-06-07T14:36:52.067Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ce/aad219575055d6c9ef29c8c540c81e1c38815d3be1fe09cdbe53d48ee838/numpy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46d16f72c2192da7b83984aa5455baee640e33a9f1e61e656f29adf55e406c2b", size = 14359277, upload-time = "2025-06-07T14:37:15.325Z" }, - { url = "https://files.pythonhosted.org/packages/29/6b/2d31da8e6d2ec99bed54c185337a87f8fbeccc1cd9804e38217e92f3f5e2/numpy-2.3.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a0be278be9307c4ab06b788f2a077f05e180aea817b3e41cebbd5aaf7bd85ed3", size = 5376069, upload-time = "2025-06-07T14:37:25.636Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2a/6c59a062397553ec7045c53d5fcdad44e4536e54972faa2ba44153bca984/numpy-2.3.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:99224862d1412d2562248d4710126355d3a8db7672170a39d6909ac47687a8a4", size = 6913057, upload-time = "2025-06-07T14:37:37.215Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5a/8df16f258d28d033e4f359e29d3aeb54663243ac7b71504e89deeb813202/numpy-2.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2393a914db64b0ead0ab80c962e42d09d5f385802006a6c87835acb1f58adb96", size = 14568083, upload-time = "2025-06-07T14:37:59.337Z" }, - { url = "https://files.pythonhosted.org/packages/0a/92/0528a563dfc2cdccdcb208c0e241a4bb500d7cde218651ffb834e8febc50/numpy-2.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7729c8008d55e80784bd113787ce876ca117185c579c0d626f59b87d433ea779", size = 16929402, upload-time = "2025-06-07T14:38:24.343Z" }, - { url = "https://files.pythonhosted.org/packages/e4/2f/e7a8c8d4a2212c527568d84f31587012cf5497a7271ea1f23332142f634e/numpy-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:06d4fb37a8d383b769281714897420c5cc3545c79dc427df57fc9b852ee0bf58", size = 15879193, upload-time = "2025-06-07T14:38:48.007Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c3/dada3f005953847fe35f42ac0fe746f6e1ea90b4c6775e4be605dcd7b578/numpy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c39ec392b5db5088259c68250e342612db82dc80ce044cf16496cf14cf6bc6f8", size = 18665318, upload-time = "2025-06-07T14:39:15.794Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ae/3f448517dedefc8dd64d803f9d51a8904a48df730e00a3c5fb1e75a60620/numpy-2.3.0-cp311-cp311-win32.whl", hash = "sha256:ee9d3ee70d62827bc91f3ea5eee33153212c41f639918550ac0475e3588da59f", size = 6601108, upload-time = "2025-06-07T14:39:27.176Z" }, - { url = "https://files.pythonhosted.org/packages/8c/4a/556406d2bb2b9874c8cbc840c962683ac28f21efbc9b01177d78f0199ca1/numpy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c55b6a860b0eb44d42341438b03513cf3879cb3617afb749ad49307e164edd", size = 13021525, upload-time = "2025-06-07T14:39:46.637Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ee/bf54278aef30335ffa9a189f869ea09e1a195b3f4b93062164a3b02678a7/numpy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:2e6a1409eee0cb0316cb64640a49a49ca44deb1a537e6b1121dc7c458a1299a8", size = 10170327, upload-time = "2025-06-07T14:40:02.703Z" }, - { url = "https://files.pythonhosted.org/packages/89/59/9df493df81ac6f76e9f05cdbe013cdb0c9a37b434f6e594f5bd25e278908/numpy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:389b85335838155a9076e9ad7f8fdba0827496ec2d2dc32ce69ce7898bde03ba", size = 20897025, upload-time = "2025-06-07T14:40:33.558Z" }, - { url = "https://files.pythonhosted.org/packages/2f/86/4ff04335901d6cf3a6bb9c748b0097546ae5af35e455ae9b962ebff4ecd7/numpy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9498f60cd6bb8238d8eaf468a3d5bb031d34cd12556af53510f05fcf581c1b7e", size = 14129882, upload-time = "2025-06-07T14:40:55.034Z" }, - { url = "https://files.pythonhosted.org/packages/71/8d/a942cd4f959de7f08a79ab0c7e6cecb7431d5403dce78959a726f0f57aa1/numpy-2.3.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:622a65d40d8eb427d8e722fd410ac3ad4958002f109230bc714fa551044ebae2", size = 5110181, upload-time = "2025-06-07T14:41:04.4Z" }, - { url = "https://files.pythonhosted.org/packages/86/5d/45850982efc7b2c839c5626fb67fbbc520d5b0d7c1ba1ae3651f2f74c296/numpy-2.3.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b9446d9d8505aadadb686d51d838f2b6688c9e85636a0c3abaeb55ed54756459", size = 6647581, upload-time = "2025-06-07T14:41:14.695Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c0/c871d4a83f93b00373d3eebe4b01525eee8ef10b623a335ec262b58f4dc1/numpy-2.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:50080245365d75137a2bf46151e975de63146ae6d79f7e6bd5c0e85c9931d06a", size = 14262317, upload-time = "2025-06-07T14:41:35.862Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f6/bc47f5fa666d5ff4145254f9e618d56e6a4ef9b874654ca74c19113bb538/numpy-2.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c24bb4113c66936eeaa0dc1e47c74770453d34f46ee07ae4efd853a2ed1ad10a", size = 16633919, upload-time = "2025-06-07T14:42:00.622Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b4/65f48009ca0c9b76df5f404fccdea5a985a1bb2e34e97f21a17d9ad1a4ba/numpy-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d8d294287fdf685281e671886c6dcdf0291a7c19db3e5cb4178d07ccf6ecc67", size = 15567651, upload-time = "2025-06-07T14:42:24.429Z" }, - { url = "https://files.pythonhosted.org/packages/f1/62/5367855a2018578e9334ed08252ef67cc302e53edc869666f71641cad40b/numpy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6295f81f093b7f5769d1728a6bd8bf7466de2adfa771ede944ce6711382b89dc", size = 18361723, upload-time = "2025-06-07T14:42:51.167Z" }, - { url = "https://files.pythonhosted.org/packages/d4/75/5baed8cd867eabee8aad1e74d7197d73971d6a3d40c821f1848b8fab8b84/numpy-2.3.0-cp312-cp312-win32.whl", hash = "sha256:e6648078bdd974ef5d15cecc31b0c410e2e24178a6e10bf511e0557eed0f2570", size = 6318285, upload-time = "2025-06-07T14:43:02.052Z" }, - { url = "https://files.pythonhosted.org/packages/bc/49/d5781eaa1a15acb3b3a3f49dc9e2ff18d92d0ce5c2976f4ab5c0a7360250/numpy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:0898c67a58cdaaf29994bc0e2c65230fd4de0ac40afaf1584ed0b02cd74c6fdd", size = 12732594, upload-time = "2025-06-07T14:43:21.071Z" }, - { url = "https://files.pythonhosted.org/packages/c2/1c/6d343e030815c7c97a1f9fbad00211b47717c7fe446834c224bd5311e6f1/numpy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:bd8df082b6c4695753ad6193018c05aac465d634834dca47a3ae06d4bb22d9ea", size = 9891498, upload-time = "2025-06-07T14:43:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/73/fc/1d67f751fd4dbafc5780244fe699bc4084268bad44b7c5deb0492473127b/numpy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5754ab5595bfa2c2387d241296e0381c21f44a4b90a776c3c1d39eede13a746a", size = 20889633, upload-time = "2025-06-07T14:44:06.839Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/73ffdb69e5c3f19ec4530f8924c4386e7ba097efc94b9c0aff607178ad94/numpy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d11fa02f77752d8099573d64e5fe33de3229b6632036ec08f7080f46b6649959", size = 14151683, upload-time = "2025-06-07T14:44:28.847Z" }, - { url = "https://files.pythonhosted.org/packages/64/d5/06d4bb31bb65a1d9c419eb5676173a2f90fd8da3c59f816cc54c640ce265/numpy-2.3.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:aba48d17e87688a765ab1cd557882052f238e2f36545dfa8e29e6a91aef77afe", size = 5102683, upload-time = "2025-06-07T14:44:38.417Z" }, - { url = "https://files.pythonhosted.org/packages/12/8b/6c2cef44f8ccdc231f6b56013dff1d71138c48124334aded36b1a1b30c5a/numpy-2.3.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4dc58865623023b63b10d52f18abaac3729346a7a46a778381e0e3af4b7f3beb", size = 6640253, upload-time = "2025-06-07T14:44:49.359Z" }, - { url = "https://files.pythonhosted.org/packages/62/aa/fca4bf8de3396ddb59544df9b75ffe5b73096174de97a9492d426f5cd4aa/numpy-2.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:df470d376f54e052c76517393fa443758fefcdd634645bc9c1f84eafc67087f0", size = 14258658, upload-time = "2025-06-07T14:45:10.156Z" }, - { url = "https://files.pythonhosted.org/packages/1c/12/734dce1087eed1875f2297f687e671cfe53a091b6f2f55f0c7241aad041b/numpy-2.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:87717eb24d4a8a64683b7a4e91ace04e2f5c7c77872f823f02a94feee186168f", size = 16628765, upload-time = "2025-06-07T14:45:35.076Z" }, - { url = "https://files.pythonhosted.org/packages/48/03/ffa41ade0e825cbcd5606a5669962419528212a16082763fc051a7247d76/numpy-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fa264d56882b59dcb5ea4d6ab6f31d0c58a57b41aec605848b6eb2ef4a43e8", size = 15564335, upload-time = "2025-06-07T14:45:58.797Z" }, - { url = "https://files.pythonhosted.org/packages/07/58/869398a11863310aee0ff85a3e13b4c12f20d032b90c4b3ee93c3b728393/numpy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e651756066a0eaf900916497e20e02fe1ae544187cb0fe88de981671ee7f6270", size = 18360608, upload-time = "2025-06-07T14:46:25.687Z" }, - { url = "https://files.pythonhosted.org/packages/2f/8a/5756935752ad278c17e8a061eb2127c9a3edf4ba2c31779548b336f23c8d/numpy-2.3.0-cp313-cp313-win32.whl", hash = "sha256:e43c3cce3b6ae5f94696669ff2a6eafd9a6b9332008bafa4117af70f4b88be6f", size = 6310005, upload-time = "2025-06-07T14:50:13.138Z" }, - { url = "https://files.pythonhosted.org/packages/08/60/61d60cf0dfc0bf15381eaef46366ebc0c1a787856d1db0c80b006092af84/numpy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:81ae0bf2564cf475f94be4a27ef7bcf8af0c3e28da46770fc904da9abd5279b5", size = 12729093, upload-time = "2025-06-07T14:50:31.82Z" }, - { url = "https://files.pythonhosted.org/packages/66/31/2f2f2d2b3e3c32d5753d01437240feaa32220b73258c9eef2e42a0832866/numpy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:c8738baa52505fa6e82778580b23f945e3578412554d937093eac9205e845e6e", size = 9885689, upload-time = "2025-06-07T14:50:47.888Z" }, - { url = "https://files.pythonhosted.org/packages/f1/89/c7828f23cc50f607ceb912774bb4cff225ccae7131c431398ad8400e2c98/numpy-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39b27d8b38942a647f048b675f134dd5a567f95bfff481f9109ec308515c51d8", size = 20986612, upload-time = "2025-06-07T14:46:56.077Z" }, - { url = "https://files.pythonhosted.org/packages/dd/46/79ecf47da34c4c50eedec7511e53d57ffdfd31c742c00be7dc1d5ffdb917/numpy-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0eba4a1ea88f9a6f30f56fdafdeb8da3774349eacddab9581a21234b8535d3d3", size = 14298953, upload-time = "2025-06-07T14:47:18.053Z" }, - { url = "https://files.pythonhosted.org/packages/59/44/f6caf50713d6ff4480640bccb2a534ce1d8e6e0960c8f864947439f0ee95/numpy-2.3.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0f1f11d0a1da54927436505a5a7670b154eac27f5672afc389661013dfe3d4f", size = 5225806, upload-time = "2025-06-07T14:47:27.524Z" }, - { url = "https://files.pythonhosted.org/packages/a6/43/e1fd1aca7c97e234dd05e66de4ab7a5be54548257efcdd1bc33637e72102/numpy-2.3.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:690d0a5b60a47e1f9dcec7b77750a4854c0d690e9058b7bef3106e3ae9117808", size = 6735169, upload-time = "2025-06-07T14:47:38.057Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/f76f93b06a03177c0faa7ca94d0856c4e5c4bcaf3c5f77640c9ed0303e1c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8b51ead2b258284458e570942137155978583e407babc22e3d0ed7af33ce06f8", size = 14330701, upload-time = "2025-06-07T14:47:59.113Z" }, - { url = "https://files.pythonhosted.org/packages/aa/f5/4858c3e9ff7a7d64561b20580cf7cc5d085794bd465a19604945d6501f6c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:aaf81c7b82c73bd9b45e79cfb9476cb9c29e937494bfe9092c26aece812818ad", size = 16692983, upload-time = "2025-06-07T14:48:24.196Z" }, - { url = "https://files.pythonhosted.org/packages/08/17/0e3b4182e691a10e9483bcc62b4bb8693dbf9ea5dc9ba0b77a60435074bb/numpy-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f420033a20b4f6a2a11f585f93c843ac40686a7c3fa514060a97d9de93e5e72b", size = 15641435, upload-time = "2025-06-07T14:48:47.712Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d5/463279fda028d3c1efa74e7e8d507605ae87f33dbd0543cf4c4527c8b882/numpy-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d344ca32ab482bcf8735d8f95091ad081f97120546f3d250240868430ce52555", size = 18433798, upload-time = "2025-06-07T14:49:14.866Z" }, - { url = "https://files.pythonhosted.org/packages/0e/1e/7a9d98c886d4c39a2b4d3a7c026bffcf8fbcaf518782132d12a301cfc47a/numpy-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:48a2e8eaf76364c32a1feaa60d6925eaf32ed7a040183b807e02674305beef61", size = 6438632, upload-time = "2025-06-07T14:49:25.67Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ab/66fc909931d5eb230107d016861824f335ae2c0533f422e654e5ff556784/numpy-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ba17f93a94e503551f154de210e4d50c5e3ee20f7e7a1b5f6ce3f22d419b93bb", size = 12868491, upload-time = "2025-06-07T14:49:44.898Z" }, - { url = "https://files.pythonhosted.org/packages/ee/e8/2c8a1c9e34d6f6d600c83d5ce5b71646c32a13f34ca5c518cc060639841c/numpy-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f14e016d9409680959691c109be98c436c6249eaf7f118b424679793607b5944", size = 9935345, upload-time = "2025-06-07T14:50:02.311Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a2/f8c1133f90eaa1c11bbbec1dc28a42054d0ce74bc2c9838c5437ba5d4980/numpy-2.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80b46117c7359de8167cc00a2c7d823bdd505e8c7727ae0871025a86d668283b", size = 21070759, upload-time = "2025-06-07T14:51:18.241Z" }, - { url = "https://files.pythonhosted.org/packages/6c/e0/4c05fc44ba28463096eee5ae2a12832c8d2759cc5bcec34ae33386d3ff83/numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:5814a0f43e70c061f47abd5857d120179609ddc32a613138cbb6c4e9e2dbdda5", size = 5301054, upload-time = "2025-06-07T14:51:27.413Z" }, - { url = "https://files.pythonhosted.org/packages/8a/3b/6c06cdebe922bbc2a466fe2105f50f661238ea223972a69c7deb823821e7/numpy-2.3.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ef6c1e88fd6b81ac6d215ed71dc8cd027e54d4bf1d2682d362449097156267a2", size = 6817520, upload-time = "2025-06-07T14:51:38.015Z" }, - { url = "https://files.pythonhosted.org/packages/9d/a3/1e536797fd10eb3c5dbd2e376671667c9af19e241843548575267242ea02/numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33a5a12a45bb82d9997e2c0b12adae97507ad7c347546190a18ff14c28bbca12", size = 14398078, upload-time = "2025-06-07T14:52:00.122Z" }, - { url = "https://files.pythonhosted.org/packages/7c/61/9d574b10d9368ecb1a0c923952aa593510a20df4940aa615b3a71337c8db/numpy-2.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:54dfc8681c1906d239e95ab1508d0a533c4a9505e52ee2d71a5472b04437ef97", size = 16751324, upload-time = "2025-06-07T14:52:25.077Z" }, - { url = "https://files.pythonhosted.org/packages/39/de/bcad52ce972dc26232629ca3a99721fd4b22c1d2bda84d5db6541913ef9c/numpy-2.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e017a8a251ff4d18d71f139e28bdc7c31edba7a507f72b1414ed902cbe48c74d", size = 12924237, upload-time = "2025-06-07T14:52:44.713Z" }, +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/19/d7c972dfe90a353dbd3efbbe1d14a5951de80c99c9dc1b93cd998d51dc0f/numpy-2.3.1.tar.gz", hash = "sha256:1ec9ae20a4226da374362cca3c62cd753faf2f951440b0e3b98e93c235441d2b", size = 20390372, upload-time = "2025-06-21T12:28:33.469Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/c7/87c64d7ab426156530676000c94784ef55676df2f13b2796f97722464124/numpy-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ea9e48336a402551f52cd8f593343699003d2353daa4b72ce8d34f66b722070", size = 21199346, upload-time = "2025-06-21T11:47:47.57Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/0966c2f44beeac12af8d836e5b5f826a407cf34c45cb73ddcdfce9f5960b/numpy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ccb7336eaf0e77c1635b232c141846493a588ec9ea777a7c24d7166bb8533ae", size = 14361143, upload-time = "2025-06-21T11:48:10.766Z" }, + { url = "https://files.pythonhosted.org/packages/7d/31/6e35a247acb1bfc19226791dfc7d4c30002cd4e620e11e58b0ddf836fe52/numpy-2.3.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bb3a4a61e1d327e035275d2a993c96fa786e4913aa089843e6a2d9dd205c66a", size = 5378989, upload-time = "2025-06-21T11:48:19.998Z" }, + { url = "https://files.pythonhosted.org/packages/b0/25/93b621219bb6f5a2d4e713a824522c69ab1f06a57cd571cda70e2e31af44/numpy-2.3.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:e344eb79dab01f1e838ebb67aab09965fb271d6da6b00adda26328ac27d4a66e", size = 6912890, upload-time = "2025-06-21T11:48:31.376Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/6b06ed98d11fb32e27fb59468b42383f3877146d3ee639f733776b6ac596/numpy-2.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:467db865b392168ceb1ef1ffa6f5a86e62468c43e0cfb4ab6da667ede10e58db", size = 14569032, upload-time = "2025-06-21T11:48:52.563Z" }, + { url = "https://files.pythonhosted.org/packages/75/c9/9bec03675192077467a9c7c2bdd1f2e922bd01d3a69b15c3a0fdcd8548f6/numpy-2.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:afed2ce4a84f6b0fc6c1ce734ff368cbf5a5e24e8954a338f3bdffa0718adffb", size = 16930354, upload-time = "2025-06-21T11:49:17.473Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e2/5756a00cabcf50a3f527a0c968b2b4881c62b1379223931853114fa04cda/numpy-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0025048b3c1557a20bc80d06fdeb8cc7fc193721484cca82b2cfa072fec71a93", size = 15879605, upload-time = "2025-06-21T11:49:41.161Z" }, + { url = "https://files.pythonhosted.org/packages/ff/86/a471f65f0a86f1ca62dcc90b9fa46174dd48f50214e5446bc16a775646c5/numpy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5ee121b60aa509679b682819c602579e1df14a5b07fe95671c8849aad8f2115", size = 18666994, upload-time = "2025-06-21T11:50:08.516Z" }, + { url = "https://files.pythonhosted.org/packages/43/a6/482a53e469b32be6500aaf61cfafd1de7a0b0d484babf679209c3298852e/numpy-2.3.1-cp311-cp311-win32.whl", hash = "sha256:a8b740f5579ae4585831b3cf0e3b0425c667274f82a484866d2adf9570539369", size = 6603672, upload-time = "2025-06-21T11:50:19.584Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/bb613f4122c310a13ec67585c70e14b03bfc7ebabd24f4d5138b97371d7c/numpy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4580adadc53311b163444f877e0789f1c8861e2698f6b2a4ca852fda154f3ff", size = 13024015, upload-time = "2025-06-21T11:50:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/51/58/2d842825af9a0c041aca246dc92eb725e1bc5e1c9ac89712625db0c4e11c/numpy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:ec0bdafa906f95adc9a0c6f26a4871fa753f25caaa0e032578a30457bff0af6a", size = 10456989, upload-time = "2025-06-21T11:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/c6/56/71ad5022e2f63cfe0ca93559403d0edef14aea70a841d640bd13cdba578e/numpy-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2959d8f268f3d8ee402b04a9ec4bb7604555aeacf78b360dc4ec27f1d508177d", size = 20896664, upload-time = "2025-06-21T12:15:30.845Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/2db52ba049813670f7f987cc5db6dac9be7cd95e923cc6832b3d32d87cef/numpy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:762e0c0c6b56bdedfef9a8e1d4538556438288c4276901ea008ae44091954e29", size = 14131078, upload-time = "2025-06-21T12:15:52.23Z" }, + { url = "https://files.pythonhosted.org/packages/57/dd/28fa3c17b0e751047ac928c1e1b6990238faad76e9b147e585b573d9d1bd/numpy-2.3.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:867ef172a0976aaa1f1d1b63cf2090de8b636a7674607d514505fb7276ab08fc", size = 5112554, upload-time = "2025-06-21T12:16:01.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/fc/84ea0cba8e760c4644b708b6819d91784c290288c27aca916115e3311d17/numpy-2.3.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4e602e1b8682c2b833af89ba641ad4176053aaa50f5cacda1a27004352dde943", size = 6646560, upload-time = "2025-06-21T12:16:11.895Z" }, + { url = "https://files.pythonhosted.org/packages/61/b2/512b0c2ddec985ad1e496b0bd853eeb572315c0f07cd6997473ced8f15e2/numpy-2.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8e333040d069eba1652fb08962ec5b76af7f2c7bce1df7e1418c8055cf776f25", size = 14260638, upload-time = "2025-06-21T12:16:32.611Z" }, + { url = "https://files.pythonhosted.org/packages/6e/45/c51cb248e679a6c6ab14b7a8e3ead3f4a3fe7425fc7a6f98b3f147bec532/numpy-2.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e7cbf5a5eafd8d230a3ce356d892512185230e4781a361229bd902ff403bc660", size = 16632729, upload-time = "2025-06-21T12:16:57.439Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/feb4be2e5c09a3da161b412019caf47183099cbea1132fd98061808c2df2/numpy-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1b8f26d1086835f442286c1d9b64bb3974b0b1e41bb105358fd07d20872952", size = 15565330, upload-time = "2025-06-21T12:17:20.638Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6d/ceafe87587101e9ab0d370e4f6e5f3f3a85b9a697f2318738e5e7e176ce3/numpy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee8340cb48c9b7a5899d1149eece41ca535513a9698098edbade2a8e7a84da77", size = 18361734, upload-time = "2025-06-21T12:17:47.938Z" }, + { url = "https://files.pythonhosted.org/packages/2b/19/0fb49a3ea088be691f040c9bf1817e4669a339d6e98579f91859b902c636/numpy-2.3.1-cp312-cp312-win32.whl", hash = "sha256:e772dda20a6002ef7061713dc1e2585bc1b534e7909b2030b5a46dae8ff077ab", size = 6320411, upload-time = "2025-06-21T12:17:58.475Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3e/e28f4c1dd9e042eb57a3eb652f200225e311b608632bc727ae378623d4f8/numpy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfecc7822543abdea6de08758091da655ea2210b8ffa1faf116b940693d3df76", size = 12734973, upload-time = "2025-06-21T12:18:17.601Z" }, + { url = "https://files.pythonhosted.org/packages/04/a8/8a5e9079dc722acf53522b8f8842e79541ea81835e9b5483388701421073/numpy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:7be91b2239af2658653c5bb6f1b8bccafaf08226a258caf78ce44710a0160d30", size = 10191491, upload-time = "2025-06-21T12:18:33.585Z" }, + { url = "https://files.pythonhosted.org/packages/d4/bd/35ad97006d8abff8631293f8ea6adf07b0108ce6fec68da3c3fcca1197f2/numpy-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25a1992b0a3fdcdaec9f552ef10d8103186f5397ab45e2d25f8ac51b1a6b97e8", size = 20889381, upload-time = "2025-06-21T12:19:04.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/df5923874d8095b6062495b39729178eef4a922119cee32a12ee1bd4664c/numpy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dea630156d39b02a63c18f508f85010230409db5b2927ba59c8ba4ab3e8272e", size = 14152726, upload-time = "2025-06-21T12:19:25.599Z" }, + { url = "https://files.pythonhosted.org/packages/8c/0f/a1f269b125806212a876f7efb049b06c6f8772cf0121139f97774cd95626/numpy-2.3.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bada6058dd886061f10ea15f230ccf7dfff40572e99fef440a4a857c8728c9c0", size = 5105145, upload-time = "2025-06-21T12:19:34.782Z" }, + { url = "https://files.pythonhosted.org/packages/6d/63/a7f7fd5f375b0361682f6ffbf686787e82b7bbd561268e4f30afad2bb3c0/numpy-2.3.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:a894f3816eb17b29e4783e5873f92faf55b710c2519e5c351767c51f79d8526d", size = 6639409, upload-time = "2025-06-21T12:19:45.228Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0d/1854a4121af895aab383f4aa233748f1df4671ef331d898e32426756a8a6/numpy-2.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:18703df6c4a4fee55fd3d6e5a253d01c5d33a295409b03fda0c86b3ca2ff41a1", size = 14257630, upload-time = "2025-06-21T12:20:06.544Z" }, + { url = "https://files.pythonhosted.org/packages/50/30/af1b277b443f2fb08acf1c55ce9d68ee540043f158630d62cef012750f9f/numpy-2.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5902660491bd7a48b2ec16c23ccb9124b8abfd9583c5fdfa123fe6b421e03de1", size = 16627546, upload-time = "2025-06-21T12:20:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ec/3b68220c277e463095342d254c61be8144c31208db18d3fd8ef02712bcd6/numpy-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36890eb9e9d2081137bd78d29050ba63b8dab95dff7912eadf1185e80074b2a0", size = 15562538, upload-time = "2025-06-21T12:20:54.322Z" }, + { url = "https://files.pythonhosted.org/packages/77/2b/4014f2bcc4404484021c74d4c5ee8eb3de7e3f7ac75f06672f8dcf85140a/numpy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a780033466159c2270531e2b8ac063704592a0bc62ec4a1b991c7c40705eb0e8", size = 18360327, upload-time = "2025-06-21T12:21:21.053Z" }, + { url = "https://files.pythonhosted.org/packages/40/8d/2ddd6c9b30fcf920837b8672f6c65590c7d92e43084c25fc65edc22e93ca/numpy-2.3.1-cp313-cp313-win32.whl", hash = "sha256:39bff12c076812595c3a306f22bfe49919c5513aa1e0e70fac756a0be7c2a2b8", size = 6312330, upload-time = "2025-06-21T12:25:07.447Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c8/beaba449925988d415efccb45bf977ff8327a02f655090627318f6398c7b/numpy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d5ee6eec45f08ce507a6570e06f2f879b374a552087a4179ea7838edbcbfa42", size = 12731565, upload-time = "2025-06-21T12:25:26.444Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c3/5c0c575d7ec78c1126998071f58facfc124006635da75b090805e642c62e/numpy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c4d9e0a8368db90f93bd192bfa771ace63137c3488d198ee21dfb8e7771916e", size = 10190262, upload-time = "2025-06-21T12:25:42.196Z" }, + { url = "https://files.pythonhosted.org/packages/ea/19/a029cd335cf72f79d2644dcfc22d90f09caa86265cbbde3b5702ccef6890/numpy-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b0b5397374f32ec0649dd98c652a1798192042e715df918c20672c62fb52d4b8", size = 20987593, upload-time = "2025-06-21T12:21:51.664Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/8ea8894406209107d9ce19b66314194675d31761fe2cb3c84fe2eeae2f37/numpy-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c5bdf2015ccfcee8253fb8be695516ac4457c743473a43290fd36eba6a1777eb", size = 14300523, upload-time = "2025-06-21T12:22:13.583Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7f/06187b0066eefc9e7ce77d5f2ddb4e314a55220ad62dd0bfc9f2c44bac14/numpy-2.3.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d70f20df7f08b90a2062c1f07737dd340adccf2068d0f1b9b3d56e2038979fee", size = 5227993, upload-time = "2025-06-21T12:22:22.53Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ec/a926c293c605fa75e9cfb09f1e4840098ed46d2edaa6e2152ee35dc01ed3/numpy-2.3.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:2fb86b7e58f9ac50e1e9dd1290154107e47d1eef23a0ae9145ded06ea606f992", size = 6736652, upload-time = "2025-06-21T12:22:33.629Z" }, + { url = "https://files.pythonhosted.org/packages/e3/62/d68e52fb6fde5586650d4c0ce0b05ff3a48ad4df4ffd1b8866479d1d671d/numpy-2.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:23ab05b2d241f76cb883ce8b9a93a680752fbfcbd51c50eff0b88b979e471d8c", size = 14331561, upload-time = "2025-06-21T12:22:55.056Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ec/b74d3f2430960044bdad6900d9f5edc2dc0fb8bf5a0be0f65287bf2cbe27/numpy-2.3.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ce2ce9e5de4703a673e705183f64fd5da5bf36e7beddcb63a25ee2286e71ca48", size = 16693349, upload-time = "2025-06-21T12:23:20.53Z" }, + { url = "https://files.pythonhosted.org/packages/0d/15/def96774b9d7eb198ddadfcbd20281b20ebb510580419197e225f5c55c3e/numpy-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c4913079974eeb5c16ccfd2b1f09354b8fed7e0d6f2cab933104a09a6419b1ee", size = 15642053, upload-time = "2025-06-21T12:23:43.697Z" }, + { url = "https://files.pythonhosted.org/packages/2b/57/c3203974762a759540c6ae71d0ea2341c1fa41d84e4971a8e76d7141678a/numpy-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:010ce9b4f00d5c036053ca684c77441f2f2c934fd23bee058b4d6f196efd8280", size = 18434184, upload-time = "2025-06-21T12:24:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/22/8a/ccdf201457ed8ac6245187850aff4ca56a79edbea4829f4e9f14d46fa9a5/numpy-2.3.1-cp313-cp313t-win32.whl", hash = "sha256:6269b9edfe32912584ec496d91b00b6d34282ca1d07eb10e82dfc780907d6c2e", size = 6440678, upload-time = "2025-06-21T12:24:21.596Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/7f431d8bd8eb7e03d79294aed238b1b0b174b3148570d03a8a8a8f6a0da9/numpy-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2a809637460e88a113e186e87f228d74ae2852a2e0c44de275263376f17b5bdc", size = 12870697, upload-time = "2025-06-21T12:24:40.644Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ca/af82bf0fad4c3e573c6930ed743b5308492ff19917c7caaf2f9b6f9e2e98/numpy-2.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eccb9a159db9aed60800187bc47a6d3451553f0e1b08b068d8b277ddfbb9b244", size = 10260376, upload-time = "2025-06-21T12:24:56.884Z" }, + { url = "https://files.pythonhosted.org/packages/e8/34/facc13b9b42ddca30498fc51f7f73c3d0f2be179943a4b4da8686e259740/numpy-2.3.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad506d4b09e684394c42c966ec1527f6ebc25da7f4da4b1b056606ffe446b8a3", size = 21070637, upload-time = "2025-06-21T12:26:12.518Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/41b705d9dbae04649b529fc9bd3387664c3281c7cd78b404a4efe73dcc45/numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ebb8603d45bc86bbd5edb0d63e52c5fd9e7945d3a503b77e486bd88dde67a19b", size = 5304087, upload-time = "2025-06-21T12:26:22.294Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/fe3ac1902bff7a4934a22d49e1c9d71a623204d654d4cc43c6e8fe337fcb/numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:15aa4c392ac396e2ad3d0a2680c0f0dee420f9fed14eef09bdb9450ee6dcb7b7", size = 6817588, upload-time = "2025-06-21T12:26:32.939Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ee/89bedf69c36ace1ac8f59e97811c1f5031e179a37e4821c3a230bf750142/numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c6e0bf9d1a2f50d2b65a7cf56db37c095af17b59f6c132396f7c6d5dd76484df", size = 14399010, upload-time = "2025-06-21T12:26:54.086Z" }, + { url = "https://files.pythonhosted.org/packages/15/08/e00e7070ede29b2b176165eba18d6f9784d5349be3c0c1218338e79c27fd/numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eabd7e8740d494ce2b4ea0ff05afa1b7b291e978c0ae075487c51e8bd93c0c68", size = 16752042, upload-time = "2025-06-21T12:27:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/48/6b/1c6b515a83d5564b1698a61efa245727c8feecf308f4091f565988519d20/numpy-2.3.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e610832418a2bc09d974cc9fecebfa51e9532d6190223bc5ef6a7402ebf3b5cb", size = 12927246, upload-time = "2025-06-21T12:27:38.618Z" }, ] [[package]] name = "numpydoc" -version = "1.8.0" +version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sphinx" }, - { name = "tabulate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/59/5d1d1afb0b9598e21e7cda477935188e39ef845bcf59cb65ac20845bfd45/numpydoc-1.8.0.tar.gz", hash = "sha256:022390ab7464a44f8737f79f8b31ce1d3cfa4b4af79ccaa1aac5e8368db587fb", size = 90445, upload-time = "2024-08-09T15:52:38.679Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/19/7721093e25804cc82c7c1cdab0cce6b9343451828fc2ce249cee10646db5/numpydoc-1.9.0.tar.gz", hash = "sha256:5fec64908fe041acc4b3afc2a32c49aab1540cf581876f5563d68bb129e27c5b", size = 91451, upload-time = "2025-06-24T12:22:55.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/45/56d99ba9366476cd8548527667f01869279cedb9e66b28eb4dfb27701679/numpydoc-1.8.0-py3-none-any.whl", hash = "sha256:72024c7fd5e17375dec3608a27c03303e8ad00c81292667955c6fea7a3ccf541", size = 64003, upload-time = "2024-08-09T15:52:37.276Z" }, + { url = "https://files.pythonhosted.org/packages/26/62/5783d8924fca72529defb2c7dbe2070d49224d2dba03a85b20b37adb24d8/numpydoc-1.9.0-py3-none-any.whl", hash = "sha256:8a2983b2d62bfd0a8c470c7caa25e7e0c3d163875cdec12a8a1034020a9d1135", size = 64871, upload-time = "2025-06-24T12:22:53.701Z" }, ] [[package]] @@ -2056,11 +2047,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] @@ -2214,11 +2205,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.1.0" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, ] [[package]]