Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements scanner subscription #151

Merged
merged 8 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions examples/scanner_subscription.rs
Original file line number Diff line number Diff line change
@@ -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;
}
}
21 changes: 21 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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<orders::TagValue>,
) -> Result<Subscription<Vec<ScannerData>>, Error> {
scanner::scanner_subscription(self, subscription, filter)
}

// == Internal Use ==

#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ impl ToField for String {
}
}

impl ToField for Option<String> {
fn to_field(&self) -> String {
encode_option_field(self)
}
}

impl ToField for &str {
fn to_field(&self) -> String {
<&str>::clone(self).to_string()
Expand Down
1 change: 1 addition & 0 deletions src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ pub fn request_id_index(kind: IncomingMessages) -> Option<usize> {
| IncomingMessages::AccountUpdateMultiEnd
| IncomingMessages::MarketDepth
| IncomingMessages::MarketDepthL2
| IncomingMessages::ScannerData
| IncomingMessages::TickSnapshotEnd
| IncomingMessages::TickPrice
| IncomingMessages::TickSize
Expand Down
218 changes: 216 additions & 2 deletions src/scanner.rs
Original file line number Diff line number Diff line change
@@ -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<String, Error> {
Expand All @@ -12,11 +19,127 @@ pub(super) fn scanner_parameters(client: &Client) -> Result<String, Error> {
}
}

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<String>,
/// The request's location (STK.US, STK.US.MAJOR, etc).
pub location_code: Option<String>,
/// Same as TWS Market Scanner's "parameters" field, for example: TOP_PERC_GAIN
pub scan_code: Option<String>,
/// Filters out Contracts which price is below this value
pub above_price: Option<f64>,
/// Filters out contracts which price is above this value.
pub below_price: Option<f64>,
/// Filters out Contracts which volume is above this value.
pub above_volume: Option<i32>,
/// Filters out Contracts which option volume is above this value.
pub average_option_volume_above: Option<i32>,
/// Filters out Contracts which market cap is above this value.
pub market_cap_above: Option<f64>,
/// Filters out Contracts which market cap is below this value.
pub market_cap_below: Option<f64>,
/// Filters out Contracts which Moody's rating is below this value.
pub moody_rating_above: Option<String>,
/// Filters out Contracts which Moody's rating is above this value.
pub moody_rating_below: Option<String>,
/// Filters out Contracts with a S&P rating below this value.
pub sp_rating_above: Option<String>,
/// Filters out Contracts with a S&P rating above this value.
pub sp_rating_below: Option<String>,
/// Filter out Contracts with a maturity date earlier than this value.
pub maturity_date_above: Option<String>,
/// Filter out Contracts with a maturity date older than this value.
pub maturity_date_below: Option<String>,
/// Filter out Contracts with a coupon rate lower than this value.
pub coupon_rate_above: Option<f64>,
/// Filter out Contracts with a coupon rate higher than this value.
pub coupon_rate_below: Option<f64>,
/// 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<String>,
/// CORP = Corporation, ADR = American Depositary Receipt, ETF = Exchange Traded Fund, REIT = Real Estate Investment Trust, CEF = Closed End Fund
pub stock_type_filter: Option<String>,
}

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<Vec<ScannerData>> for Vec<ScannerData> {
fn decode(_client: &Client, message: &mut crate::messages::ResponseMessage) -> Result<Vec<ScannerData>, 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<TagValue>,
) -> Result<Subscription<'a, Vec<ScannerData>>, 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<RequestMessage, Error> {
const VERSION: i32 = 1;

Expand All @@ -27,16 +150,107 @@ mod encoders {

Ok(message)
}

pub(super) fn encode_scanner_subscription(
request_id: i32,
server_version: i32,
subscription: &ScannerSubscription,
filter: &Vec<TagValue>,
) -> Result<RequestMessage, Error> {
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<String, Error> {
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<Vec<ScannerData>, 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)
}
}
Loading