diff --git a/examples/scanner_subscription.rs b/examples/scanner_subscription.rs new file mode 100644 index 00000000..d93d1dce --- /dev/null +++ b/examples/scanner_subscription.rs @@ -0,0 +1,30 @@ +use ibapi::{scanner, Client}; + +// This example demonstrates setting up a market scanner. + +fn main() { + env_logger::init(); + + let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); + + let scanner_subscription = scanner::ScannerSubscription { + number_of_rows: 10, + instrument: Some("STK".to_string()), + location_code: Some("STK.US.MAJOR".to_string()), + scan_code: Some("MOST_ACTIVE".to_string()), + ..Default::default() + }; + + let subscription = client + .scanner_subscription(&scanner_subscription, &Vec::default()) + .expect("request scanner parameters failed"); + for scan_results in subscription { + for scan_data in scan_results.iter() { + println!( + "rank: {}, contract_id: {}, symbol: {}", + scan_data.rank, scan_data.contract_details.contract.contract_id, scan_data.contract_details.contract.symbol + ); + } + break; + } +} diff --git a/src/client.rs b/src/client.rs index 51be0f48..cf8e7efa 100644 --- a/src/client.rs +++ b/src/client.rs @@ -18,6 +18,7 @@ use crate::messages::{IncomingMessages, OutgoingMessages}; use crate::messages::{RequestMessage, ResponseMessage}; use crate::news::NewsArticle; use crate::orders::{CancelOrder, Executions, ExerciseOptions, Order, Orders, PlaceOrder}; +use crate::scanner::ScannerData; use crate::transport::{Connection, ConnectionMetadata, InternalSubscription, MessageBus, TcpMessageBus}; use crate::{accounts, contracts, market_data, news, orders, scanner}; @@ -1375,6 +1376,26 @@ impl Client { scanner::scanner_parameters(self) } + /// Starts a subscription to market scan results based on the provided parameters. + /// + /// # Examples + /// + /// ```no_run + /// use ibapi::Client; + /// + /// let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); + /// + /// let parameters = client.scanner_parameters().expect("request scanner parameters failed"); + /// println!("{:?}", parameters); + /// ``` + pub fn scanner_subscription( + &self, + subscription: &scanner::ScannerSubscription, + filter: &Vec, + ) -> Result>, Error> { + scanner::scanner_subscription(self, subscription, filter) + } + // == Internal Use == #[cfg(test)] diff --git a/src/contracts.rs b/src/contracts.rs index 0cc18075..9eae837a 100644 --- a/src/contracts.rs +++ b/src/contracts.rs @@ -292,7 +292,7 @@ pub struct DeltaNeutralContract { } /// ContractDetails provides extended contract details. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct ContractDetails { /// A fully-defined Contract object. pub contract: Contract, diff --git a/src/lib.rs b/src/lib.rs index 916f5b19..0f728f65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,6 +160,12 @@ impl ToField for String { } } +impl ToField for Option { + fn to_field(&self) -> String { + encode_option_field(self) + } +} + impl ToField for &str { fn to_field(&self) -> String { <&str>::clone(self).to_string() diff --git a/src/messages.rs b/src/messages.rs index ae825b81..c5e16953 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -238,6 +238,7 @@ pub fn request_id_index(kind: IncomingMessages) -> Option { | IncomingMessages::AccountUpdateMultiEnd | IncomingMessages::MarketDepth | IncomingMessages::MarketDepthL2 + | IncomingMessages::ScannerData | IncomingMessages::TickSnapshotEnd | IncomingMessages::TickPrice | IncomingMessages::TickSize diff --git a/src/scanner.rs b/src/scanner.rs index dd47c471..e2a94fcc 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -1,4 +1,11 @@ -use crate::{messages::OutgoingMessages, Client, Error}; +use serde::{Deserialize, Serialize}; + +use crate::{ + client::{DataStream, ResponseContext, Subscription}, + messages::{IncomingMessages, OutgoingMessages}, + orders::TagValue, + server_versions, Client, Error, +}; // Requests an XML list of scanner parameters valid in TWS. pub(super) fn scanner_parameters(client: &Client) -> Result { @@ -12,11 +19,127 @@ pub(super) fn scanner_parameters(client: &Client) -> Result { } } +pub struct ScannerSubscription { + /// The number of rows to be returned for the query + pub number_of_rows: i32, + /// The instrument's type for the scan. I.e. STK, FUT.HK, etc. + pub instrument: Option, + /// The request's location (STK.US, STK.US.MAJOR, etc). + pub location_code: Option, + /// Same as TWS Market Scanner's "parameters" field, for example: TOP_PERC_GAIN + pub scan_code: Option, + /// Filters out Contracts which price is below this value + pub above_price: Option, + /// Filters out contracts which price is above this value. + pub below_price: Option, + /// Filters out Contracts which volume is above this value. + pub above_volume: Option, + /// Filters out Contracts which option volume is above this value. + pub average_option_volume_above: Option, + /// Filters out Contracts which market cap is above this value. + pub market_cap_above: Option, + /// Filters out Contracts which market cap is below this value. + pub market_cap_below: Option, + /// Filters out Contracts which Moody's rating is below this value. + pub moody_rating_above: Option, + /// Filters out Contracts which Moody's rating is above this value. + pub moody_rating_below: Option, + /// Filters out Contracts with a S&P rating below this value. + pub sp_rating_above: Option, + /// Filters out Contracts with a S&P rating above this value. + pub sp_rating_below: Option, + /// Filter out Contracts with a maturity date earlier than this value. + pub maturity_date_above: Option, + /// Filter out Contracts with a maturity date older than this value. + pub maturity_date_below: Option, + /// Filter out Contracts with a coupon rate lower than this value. + pub coupon_rate_above: Option, + /// Filter out Contracts with a coupon rate higher than this value. + pub coupon_rate_below: Option, + /// Filters out Convertible bonds + pub exclude_convertible: bool, + /// For example, a pairing "Annual, true" used on the "top Option Implied Vol % Gainers" scan would return annualized volatilities. + pub scanner_setting_pairs: Option, + /// CORP = Corporation, ADR = American Depositary Receipt, ETF = Exchange Traded Fund, REIT = Real Estate Investment Trust, CEF = Closed End Fund + pub stock_type_filter: Option, +} + +impl Default for ScannerSubscription { + fn default() -> Self { + ScannerSubscription { + number_of_rows: -1, + instrument: None, + location_code: None, + scan_code: None, + above_price: None, + below_price: None, + above_volume: None, + average_option_volume_above: None, + market_cap_above: None, + market_cap_below: None, + moody_rating_above: None, + moody_rating_below: None, + sp_rating_above: None, + sp_rating_below: None, + maturity_date_above: None, + maturity_date_below: None, + coupon_rate_above: None, + coupon_rate_below: None, + exclude_convertible: false, + scanner_setting_pairs: None, + stock_type_filter: None, + } + } +} + +impl DataStream> for Vec { + fn decode(_client: &Client, message: &mut crate::messages::ResponseMessage) -> Result, Error> { + match message.message_type() { + IncomingMessages::ScannerData => Ok(decoders::decode_scanner_data(message.clone())?), + _ => Err(Error::UnexpectedResponse(message.clone())), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] +/// Provides the data resulting from the market scanner request. +pub struct ScannerData { + /// The ranking position of the contract in the scanner sort. + pub rank: i32, + /// The contract matching the scanner subscription. + pub contract_details: crate::contracts::ContractDetails, + /// Describes the combo legs when the scanner is returning EFP. + pub leg: String, +} + +pub(super) fn scanner_subscription<'a>( + client: &'a Client, + subscription: &ScannerSubscription, + filter: &Vec, +) -> Result>, Error> { + if !filter.is_empty() { + client.check_server_version( + server_versions::SCANNER_GENERIC_OPTS, + "It does not support API scanner subscription generic filter options.", + )? + } + + let request_id = client.next_request_id(); + let request = encoders::encode_scanner_subscription(request_id, client.server_version, subscription, filter)?; + let subscription = client.send_request(request_id, request)?; + + Ok(Subscription::new(client, subscription, ResponseContext::default())) +} + mod encoders { use crate::messages::OutgoingMessages; use crate::messages::RequestMessage; + use crate::orders::TagValue; + use crate::server_versions; use crate::Error; + use super::ScannerSubscription; + pub(super) fn encode_scanner_parameters() -> Result { const VERSION: i32 = 1; @@ -27,16 +150,107 @@ mod encoders { Ok(message) } + + pub(super) fn encode_scanner_subscription( + request_id: i32, + server_version: i32, + subscription: &ScannerSubscription, + filter: &Vec, + ) -> Result { + const VERSION: i32 = 4; + + let mut message = RequestMessage::new(); + + message.push_field(&OutgoingMessages::RequestScannerSubscription); + if server_version < server_versions::SCANNER_GENERIC_OPTS { + message.push_field(&VERSION); + } + message.push_field(&request_id); + message.push_field(&subscription.number_of_rows); + message.push_field(&subscription.instrument); + message.push_field(&subscription.location_code); + message.push_field(&subscription.scan_code); + + message.push_field(&subscription.above_price); + message.push_field(&subscription.below_price); + message.push_field(&subscription.above_volume); + message.push_field(&subscription.market_cap_above); + message.push_field(&subscription.market_cap_below); + message.push_field(&subscription.moody_rating_above); + message.push_field(&subscription.moody_rating_below); + message.push_field(&subscription.sp_rating_above); + message.push_field(&subscription.sp_rating_below); + message.push_field(&subscription.maturity_date_above); + message.push_field(&subscription.maturity_date_below); + message.push_field(&subscription.coupon_rate_above); + message.push_field(&subscription.coupon_rate_below); + message.push_field(&subscription.exclude_convertible); + message.push_field(&subscription.average_option_volume_above); + message.push_field(&subscription.scanner_setting_pairs); + message.push_field(&subscription.stock_type_filter); + + if server_version >= server_versions::SCANNER_GENERIC_OPTS { + message.push_field(filter); + } + if server_version >= server_versions::LINKING { + message.push_field(&""); // ignore subscription options + } + + Ok(message) + } } mod decoders { + use crate::contracts::SecurityType; use crate::messages::ResponseMessage; use crate::Error; + use super::ScannerData; + pub(super) fn decode_scanner_parameters(mut message: ResponseMessage) -> Result { message.skip(); // skip message type message.skip(); // skip message version - Ok(message.next_string()?) + message.next_string() + } + + pub(super) fn decode_scanner_data(mut message: ResponseMessage) -> Result, Error> { + message.skip(); // skip message type + let message_version = message.next_int()?; + message.skip(); // request id + + let number_of_elements = message.next_int()?; + let mut matches = Vec::with_capacity(number_of_elements as usize); + + for _ in 0..number_of_elements { + let mut scanner_data = ScannerData { + rank: message.next_int()?, + ..Default::default() + }; + + if message_version >= 3 { + scanner_data.contract_details.contract.contract_id = message.next_int()?; + } + scanner_data.contract_details.contract.symbol = message.next_string()?; + scanner_data.contract_details.contract.security_type = SecurityType::from(&message.next_string()?); + scanner_data.contract_details.contract.last_trade_date_or_contract_month = message.next_string()?; + scanner_data.contract_details.contract.strike = message.next_double()?; + scanner_data.contract_details.contract.right = message.next_string()?; + scanner_data.contract_details.contract.exchange = message.next_string()?; + scanner_data.contract_details.contract.currency = message.next_string()?; + scanner_data.contract_details.contract.local_symbol = message.next_string()?; + scanner_data.contract_details.market_name = message.next_string()?; + scanner_data.contract_details.contract.trading_class = message.next_string()?; + + message.skip(); // distance + message.skip(); // benchmark + message.skip(); // projection + + scanner_data.leg = if message_version >= 2 { message.next_string()? } else { "".to_string() }; + + matches.push(scanner_data); + } + + Ok(matches) } }