diff --git a/README.md b/README.md index c06ed118..75b480e2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Introduction -This library provides a comprehensive Rust implementation of the Interactive Brokers[TWS API](https://ibkrcampus.com/campus/ibkr-api-page/twsapi-doc/), offering a robust and user-friendly interface for TWS and IB Gateway. Designed with performance and simplicity in mind, `ibapi` is a good fit for automated trading systems, market analysis, real-time data collection and portfolio management tools. +This library provides a comprehensive Rust implementation of the Interactive Brokers [TWS API](https://ibkrcampus.com/campus/ibkr-api-page/twsapi-doc/), offering a robust and user-friendly interface for TWS and IB Gateway. Designed with performance and simplicity in mind, `ibapi` is a good fit for automated trading systems, market analysis, real-time data collection and portfolio management tools. With this fully featured API, you can retrieve account information, access real-time and historical market data, manage orders, perform market scans, and access news and Wall Street Horizons (WSH) event data. Future updates will focus on bug fixes, maintaining parity with the official API, and enhancing usability. @@ -29,7 +29,7 @@ Or add the following line to your `Cargo.toml`: ```toml ibapi = "1.0.0" ``` -> **Note**: Check [crates.io/crates/ibapi](https://crates.io/crates/ibapi) for the latest version available version. +> **Note**: Check [crates.io/crates/ibapi](https://crates.io/crates/ibapi) for the latest available version. ## Examples @@ -189,10 +189,6 @@ fn main() { ### Placing Orders ```rust -use ibapi::contracts::Contract; -use ibapi::orders::{order_builder, Action, OrderNotification}; -use ibapi::Client; - pub fn main() { let connection_url = "127.0.0.1:4002"; let client = Client::connect(connection_url, 100).expect("connection to TWS failed!"); @@ -205,11 +201,11 @@ pub fn main() { let subscription = client.place_order(order_id, &contract, &order).expect("place order request failed!"); - for notice in subscription { - if let OrderNotification::ExecutionData(data) = notice { + for event in &subscription { + if let PlaceOrder::ExecutionData(data) = event { println!("{} {} shares of {}", data.execution.side, data.execution.shares, data.contract.symbol); } else { - println!("{:?}", notice); + println!("{:?}", event); } } } @@ -259,7 +255,6 @@ Some TWS API calls do not have a unique request ID and are mapped back to the in To avoid this issue, you can use a model of one client per thread. This ensures that each client instance handles only its own messages, reducing potential conflicts: ```rust -use std::sync::Arc; use std::thread; use ibapi::contracts::Contract; @@ -273,7 +268,7 @@ fn main() { for (symbol, client_id) in symbols { let handle = thread::spawn(move || { let connection_url = "127.0.0.1:4002"; - let client = Arc::new(Client::connect(connection_url, client_id).expect("connection to TWS failed!")); + let client = Client::connect(connection_url, client_id).expect("connection to TWS failed!"); let contract = Contract::stock(symbol); let subscription = client @@ -296,7 +291,7 @@ In this model, each client instance handles only the requests it initiates, impr # Fault Tolerance -The API will automatically attempt to reconnect to the TWS server if a disconnection is detected. The API will attempt to reconnect up to 30 times using a Fibonacci backoff strategy. In some cases, it will retry the request in progress. When receiving a response via a [Subscription](https://docs.rs/ibapi/latest/ibapi/client/struct.Subscription.html), the application may need to handle retries manually, as shown below. +The API will automatically attempt to reconnect to the TWS server if a disconnection is detected. The API will attempt to reconnect up to 30 times using a Fibonacci backoff strategy. In some cases, it will retry the request in progress. When receiving responses via a [Subscription](https://docs.rs/ibapi/latest/ibapi/client/struct.Subscription.html), the application may need to handle retries manually, as shown below. ```rust use ibapi::contracts::Contract; @@ -304,29 +299,28 @@ use ibapi::market_data::realtime::{BarSize, WhatToShow}; use ibapi::{Client, Error}; fn main() { - env_logger::init(); - let connection_url = "127.0.0.1:4002"; let client = Client::connect(connection_url, 100).expect("connection to TWS failed!"); let contract = Contract::stock("AAPL"); - stream_bars(&client, &contract); -} -// Request real-time bars data with 5-second intervals -fn stream_bars(client: &Client, contract: &Contract) { - let subscription = client - .realtime_bars(&contract, BarSize::Sec5, WhatToShow::Trades, false) - .expect("realtime bars request failed!"); + loop { + // Request real-time bars data with 5-second intervals + let subscription = client + .realtime_bars(&contract, BarSize::Sec5, WhatToShow::Trades, false) + .expect("realtime bars request failed!"); - for bar in &subscription { - // Process each bar here (e.g., print or use in calculations) - println!("bar: {bar:?}"); - } + for bar in &subscription { + // Process each bar here (e.g., print or use in calculations) + println!("bar: {bar:?}"); + } + + if let Some(Error::ConnectionReset) = subscription.error() { + eprintln!("Connection reset. Retrying stream..."); + continue; + } - if let Some(Error::ConnectionReset) = subscription.error() { - println!("Connection reset. Retrying stream..."); - stream_bars(client, contract); + break; } } ``` diff --git a/examples/calculate_implied_volatility.rs b/examples/calculate_implied_volatility.rs index 907d1aa1..a4ce5692 100644 --- a/examples/calculate_implied_volatility.rs +++ b/examples/calculate_implied_volatility.rs @@ -1,4 +1,4 @@ -use ibapi::contracts::{Contract, SecurityType}; +use ibapi::contracts::Contract; use ibapi::Client; fn main() { @@ -6,16 +6,7 @@ fn main() { let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); - let contract = Contract { - symbol: "AAPL".into(), - security_type: SecurityType::Option, - exchange: "SMART".into(), - currency: "USD".into(), - last_trade_date_or_contract_month: "20250620".into(), // Expiry date (YYYYMMDD) - strike: 240.0, - right: "C".into(), // Option type: "C" for Call, "P" for Put - ..Default::default() - }; + let contract = Contract::option("AAPL", "20250620", 240.0, "C"); let calculation = client.calculate_implied_volatility(&contract, 25.0, 235.0).expect("request failed"); println!("calculation: {:?}", calculation); diff --git a/examples/readme_multi_threading_2.rs b/examples/readme_multi_threading_2.rs index b918e6bb..bf0ea4cd 100644 --- a/examples/readme_multi_threading_2.rs +++ b/examples/readme_multi_threading_2.rs @@ -1,4 +1,3 @@ -use std::sync::Arc; use std::thread; use ibapi::contracts::Contract; @@ -14,7 +13,7 @@ fn main() { for (symbol, client_id) in symbols { let handle = thread::spawn(move || { let connection_url = "127.0.0.1:4002"; - let client = Arc::new(Client::connect(connection_url, client_id).expect("connection to TWS failed!")); + let client = Client::connect(connection_url, client_id).expect("connection to TWS failed!"); let contract = Contract::stock(symbol); let subscription = client diff --git a/examples/stream_retry.rs b/examples/stream_retry.rs index 52aaa979..bf2bdbe2 100644 --- a/examples/stream_retry.rs +++ b/examples/stream_retry.rs @@ -9,22 +9,23 @@ fn main() { let client = Client::connect(connection_url, 100).expect("connection to TWS failed!"); let contract = Contract::stock("AAPL"); - stream_bars(&client, &contract); -} -// Request real-time bars data with 5-second intervals -fn stream_bars(client: &Client, contract: &Contract) { - let subscription = client - .realtime_bars(&contract, BarSize::Sec5, WhatToShow::Trades, false) - .expect("realtime bars request failed!"); + loop { + // Request real-time bars data with 5-second intervals + let subscription = client + .realtime_bars(&contract, BarSize::Sec5, WhatToShow::Trades, false) + .expect("realtime bars request failed!"); - for bar in &subscription { - // Process each bar here (e.g., print or use in calculations) - println!("bar: {bar:?}"); - } + for bar in &subscription { + // Process each bar here (e.g., print or use in calculations) + println!("bar: {bar:?}"); + } + + if let Some(Error::ConnectionReset) = subscription.error() { + eprintln!("Connection reset. Retrying stream..."); + continue; + } - if let Some(Error::ConnectionReset) = subscription.error() { - println!("Connection reset. Retrying stream..."); - stream_bars(client, contract); + break; } } diff --git a/src/client.rs b/src/client.rs index f75df282..f6f007af 100644 --- a/src/client.rs +++ b/src/client.rs @@ -95,10 +95,35 @@ impl Client { } /// Returns and increments the order ID. + /// + /// The client maintains a sequence of order IDs. This function returns the next order ID in the sequence. pub fn next_order_id(&self) -> i32 { self.order_id.fetch_add(1, Ordering::Relaxed) } + /// Gets the next valid order ID from the TWS server. + /// + /// Unlike [Self::next_order_id], this function requests the next valid order ID from the TWS server and updates the client's internal order ID sequence. + /// This can be for ensuring that order IDs are unique across multiple clients. + /// + /// Use this method when coordinating order IDs across multiple client instances or when you need to synchronize with the server's order ID sequence at the start of a session. + /// + /// # Examples + /// + /// ```no_run + /// use ibapi::Client; + /// + /// // Connect to the TWS server at the given address with client ID. + /// let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); + /// + /// // Request the next valid order ID from the server. + /// let next_valid_order_id = client.next_valid_order_id().expect("request failed"); + /// println!("next_valid_order_id: {next_valid_order_id}"); + /// ``` + pub fn next_valid_order_id(&self) -> Result { + orders::next_valid_order_id(self) + } + /// Sets the current value of order ID. pub(crate) fn set_next_order_id(&self, order_id: i32) { self.order_id.store(order_id, Ordering::Relaxed) @@ -240,7 +265,7 @@ impl Client { /// /// let group = "All"; /// - /// let subscription = client.account_summary(group, AccountSummaryTags::ALL).expect("error requesting pnl"); + /// let subscription = client.account_summary(group, AccountSummaryTags::ALL).expect("error requesting account summary"); /// for summary in &subscription { /// println!("{summary:?}") /// } @@ -398,9 +423,9 @@ impl Client { /// Calculates an option’s price based on the provided volatility and its underlying’s price. /// /// # Arguments - /// * `contract` - The [Contract] object for which the depth is being requested. - /// * `volatility` - Hypothetical volatility. - /// * `underlying_price` - Hypothetical option’s underlying price. + /// * `contract` - The [Contract] object representing the option for which the calculation is being requested. + /// * `volatility` - Hypothetical volatility as a percentage (e.g., 20.0 for 20%). + /// * `underlying_price` - Hypothetical price of the underlying asset. /// /// # Examples /// @@ -418,12 +443,12 @@ impl Client { contracts::calculate_option_price(self, contract, volatility, underlying_price) } - /// Calculates the implied volatility based on hypothetical option and its underlying prices. + /// Calculates the implied volatility based on the hypothetical option price and underlying price. /// /// # Arguments - /// * `contract` - The [Contract] object for which the depth is being requested. - /// * `volatility` - Hypothetical option price. - /// * `underlying_price` - Hypothetical option’s underlying price. + /// * `contract` - The [Contract] object representing the option for which the calculation is being requested. + /// * `option_price` - Hypothetical option price. + /// * `underlying_price` - Hypothetical price of the underlying asset. /// /// # Examples /// @@ -433,7 +458,7 @@ impl Client { /// /// let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); /// - /// let contract = Contract::stock("AAPL"); + /// let contract = Contract::option("AAPL", "20230519", 150.0, "C"); /// let calculation = client.calculate_implied_volatility(&contract, 25.0, 235.0).expect("request failed"); /// println!("calculation: {:?}", calculation); /// ``` @@ -510,7 +535,7 @@ impl Client { /// ```no_run /// use ibapi::Client; /// - /// let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); + /// let client = Client::connect("127.0.0.1:4002", 0).expect("connection failed"); /// /// let subscription = client.auto_open_orders(false).expect("request failed"); /// for order_data in &subscription { @@ -521,11 +546,11 @@ impl Client { orders::auto_open_orders(self, auto_bind) } - /// Cancels an open [Order]. + /// Cancels an active [Order] placed by the same API client ID. /// /// # Arguments - /// * `order_id` - ID of [Order] to cancel. - /// * `manual_order_cancel_time` - can't find documentation. leave blank. + /// * `order_id` - ID of the [Order] to cancel. + /// * `manual_order_cancel_time` - Optional timestamp to specify the cancellation time. Use an empty string to use the current time. /// /// # Examples /// @@ -603,7 +628,7 @@ impl Client { /// ```no_run /// use ibapi::Client; /// - /// let mut client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); + /// let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); /// /// client.global_cancel().expect("request failed"); /// ``` @@ -611,22 +636,6 @@ impl Client { orders::global_cancel(self) } - /// Cancels all open [Order]s. - /// - /// # Examples - /// - /// ```no_run - /// use ibapi::Client; - /// - /// let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); - /// - /// let next_valid_order_id = client.next_valid_order_id().expect("request failed"); - /// println!("next_valid_order_id: {next_valid_order_id}"); - /// ``` - pub fn next_valid_order_id(&self) -> Result { - orders::next_valid_order_id(self) - } - /// Requests all open orders places by this specific API client (identified by the API client id). /// For client ID 0, this will bind previous manual TWS orders. /// @@ -646,10 +655,10 @@ impl Client { orders::open_orders(self) } - /// Submits an [Order]. + /// Places or modifies an [Order]. /// /// Submits an [Order] using [Client] for the given [Contract]. - /// Immediately after the order was submitted correctly, the TWS will start sending events concerning the order's activity via IBApi.EWrapper.openOrder and IBApi.EWrapper.orderStatus + /// Upon successful submission, the client will start receiving events related to the order's activity via the subscription, including order status updates and execution reports. /// /// # Arguments /// * `order_id` - ID for [Order]. Get next valid ID using [Client::next_order_id]. @@ -669,10 +678,10 @@ impl Client { /// let order = order_builder::market_order(Action::Buy, 100.0); /// let order_id = client.next_order_id(); /// - /// let notifications = client.place_order(order_id, &contract, &order).expect("request failed"); + /// let events = client.place_order(order_id, &contract, &order).expect("request failed"); /// - /// for notification in notifications { - /// match notification { + /// for event in &events { + /// match event { /// PlaceOrder::OrderStatus(order_status) => { /// println!("order status: {order_status:?}") /// } @@ -698,7 +707,7 @@ impl Client { /// * `account` - Destination account. /// * `ovrd` - Specifies whether your setting will override the system’s natural action. /// For example, if your action is "exercise" and the option is not in-the-money, by natural action the option would not exercise. If you have override set to true the natural action would be overridden and the out-of-the money option would be exercised. - /// * `manual_order_time - Specify the time at which the options should be exercised. An empty string will assume the current time. Required TWS API 10.26 or higher. + /// * `manual_order_time` - Specify the time at which the options should be exercised. If `None`, the current time will be used. Requires TWS API 10.26 or higher. pub fn exercise_options<'a>( &'a self, contract: &Contract, @@ -714,12 +723,13 @@ impl Client { // === Historical Market Data === /// Returns the timestamp of earliest available historical data for a contract and data type. + /// /// ```no_run /// use ibapi::Client; /// use ibapi::contracts::Contract; /// use ibapi::market_data::historical::{self, WhatToShow}; /// - /// let mut client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); + /// let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); /// /// let contract = Contract::stock("MSFT"); /// let what_to_show = WhatToShow::Trades; @@ -1260,8 +1270,8 @@ impl Client { /// TickTypes::EFP(tick_efp) => println!("{:?}", tick_efp), /// TickTypes::OptionComputation(option_computation) => println!("{:?}", option_computation), /// TickTypes::RequestParameters(tick_request_parameters) => println!("{:?}", tick_request_parameters), - /// TickTypes::SnapshotEnd => subscription.cancel(), /// TickTypes::Notice(notice) => println!("{:?}", notice), + /// TickTypes::SnapshotEnd => subscription.cancel(), /// } /// } /// ``` @@ -1401,7 +1411,7 @@ impl Client { /// let provider_codes = ["DJ-N"]; /// /// let subscription = client.contract_news(&contract, &provider_codes).expect("request contract news failed"); - /// for article in subscription { + /// for article in &subscription { /// println!("{:?}", article); /// } /// ``` @@ -1425,7 +1435,7 @@ impl Client { /// let provider_code = "BRFG"; /// /// let subscription = client.broad_tape_news(provider_code).expect("request broad tape news failed"); - /// for article in subscription { + /// for article in &subscription { /// println!("{:?}", article); /// } /// ``` diff --git a/src/contracts.rs b/src/contracts.rs index ce161472..318df19e 100644 --- a/src/contracts.rs +++ b/src/contracts.rs @@ -203,6 +203,27 @@ impl Contract { } } + /// Creates option contract from specified symbol, expiry date, strike price and option type. + /// Defaults currency to USD and exchange to SMART. + /// + /// # Arguments + /// * `symbol` - Symbols of the underlying asset. + /// * `expiration_date` - Expiration date of option contract (YYYYMMDD) + /// * `strike` - Strike price of the option contract. + /// * `right` - Option type: "C" for Call, "P" for Put + pub fn option(symbol: &str, expiration_date: &str, strike: f64, right: &str) -> Contract { + Contract { + symbol: symbol.into(), + security_type: SecurityType::Option, + exchange: "SMART".into(), + currency: "USD".into(), + last_trade_date_or_contract_month: expiration_date.into(), // Expiry date (YYYYMMDD) + strike, + right: right.into(), // Option type: "C" for Call, "P" for Put + ..Default::default() + } + } + /// Is Bag request pub fn is_bag(&self) -> bool { self.security_type == SecurityType::Spread diff --git a/src/market_data/realtime.rs b/src/market_data/realtime.rs index a5cd84b3..78ebf333 100644 --- a/src/market_data/realtime.rs +++ b/src/market_data/realtime.rs @@ -508,18 +508,20 @@ pub fn market_depth_exchanges(client: &Client) -> Result Ok(decoders::decode_market_depth_exchanges(client.server_version(), &mut message)?), - Some(Err(Error::ConnectionReset)) => { - debug!("connection reset. retrying market_depth_exchanges"); - market_depth_exchanges(client) + loop { + let request = encoders::encode_request_market_depth_exchanges()?; + let subscription = client.send_shared_request(OutgoingMessages::RequestMktDepthExchanges, request)?; + let response = subscription.next(); + + match response { + Some(Ok(mut message)) => return decoders::decode_market_depth_exchanges(client.server_version(), &mut message), + Some(Err(Error::ConnectionReset)) => { + debug!("connection reset. retrying market_depth_exchanges"); + continue; + } + Some(Err(e)) => return Err(e), + None => return Ok(Vec::new()), } - Some(Err(e)) => Err(e), - None => Ok(Vec::new()), } }