From d6437beddeea6a217486f5cf22655d1a60946ef9 Mon Sep 17 00:00:00 2001 From: Wil Boayue Date: Thu, 14 Nov 2024 17:04:27 -0800 Subject: [PATCH] fixes parsing of scanner response (#177) --- Cargo.toml | 2 +- ... => scanner_subscription_active_stocks.rs} | 22 ++- .../scanner_subscription_complex_orders.rs | 38 +++++ src/messages.rs | 1 - src/messages/tests.rs | 3 +- src/scanner.rs | 57 +------ src/scanner/decoders.rs | 53 +++++++ src/scanner/decoders/tests.rs | 148 ++++++++++++++++++ src/testdata/responses.rs | 4 - 9 files changed, 257 insertions(+), 71 deletions(-) rename examples/{scanner_subscription.rs => scanner_subscription_active_stocks.rs} (73%) create mode 100644 examples/scanner_subscription_complex_orders.rs create mode 100644 src/scanner/decoders.rs create mode 100644 src/scanner/decoders/tests.rs diff --git a/Cargo.toml b/Cargo.toml index efbe4ef4..77707026 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ibapi" -version = "1.0.1" +version = "1.0.2" edition = "2021" authors = ["Wil Boayue "] description = "A Rust implementation of the Interactive Brokers TWS API, providing a reliable and user friendly interface for TWS and IB Gateway. Designed with a focus on simplicity and performance." diff --git a/examples/scanner_subscription.rs b/examples/scanner_subscription_active_stocks.rs similarity index 73% rename from examples/scanner_subscription.rs rename to examples/scanner_subscription_active_stocks.rs index d93d1dce..ce1c2346 100644 --- a/examples/scanner_subscription.rs +++ b/examples/scanner_subscription_active_stocks.rs @@ -7,17 +7,13 @@ fn main() { 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 scanner_subscription = most_active_stocks(10); + let filter = Vec::new(); let subscription = client - .scanner_subscription(&scanner_subscription, &Vec::default()) + .scanner_subscription(&scanner_subscription, &filter) .expect("request scanner parameters failed"); + for scan_results in subscription { for scan_data in scan_results.iter() { println!( @@ -28,3 +24,13 @@ fn main() { break; } } + +fn most_active_stocks(number_of_rows: i32) -> scanner::ScannerSubscription { + scanner::ScannerSubscription { + number_of_rows, + instrument: Some("STK".to_string()), + location_code: Some("STK.US.MAJOR".to_string()), + scan_code: Some("MOST_ACTIVE".to_string()), + ..Default::default() + } +} diff --git a/examples/scanner_subscription_complex_orders.rs b/examples/scanner_subscription_complex_orders.rs new file mode 100644 index 00000000..7f56305c --- /dev/null +++ b/examples/scanner_subscription_complex_orders.rs @@ -0,0 +1,38 @@ +use ibapi::{orders, 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 = complex_orders_and_trades(); + let filter = vec![orders::TagValue { + tag: "underConID".to_string(), + value: "265598".to_string(), + }]; + + let subscription = client + .scanner_subscription(&scanner_subscription, &filter) + .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; + } +} + +fn complex_orders_and_trades() -> scanner::ScannerSubscription { + scanner::ScannerSubscription { + instrument: Some("NATCOMB".to_string()), + location_code: Some("NATCOMB.OPT.US".to_string()), + scan_code: Some("COMBO_LATEST_TRADE".to_string()), + ..Default::default() + } +} diff --git a/src/messages.rs b/src/messages.rs index 73bc33bf..bac921b4 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -560,7 +560,6 @@ impl ResponseMessage { } pub fn from(fields: &str) -> ResponseMessage { - let fields = fields.replace("|", "\0"); ResponseMessage { i: 0, fields: fields.split('\x00').map(|x| x.to_string()).collect(), diff --git a/src/messages/tests.rs b/src/messages/tests.rs index 5661f754..98d61a0d 100644 --- a/src/messages/tests.rs +++ b/src/messages/tests.rs @@ -1,6 +1,5 @@ use crate::contracts::{ComboLegOpenClose, SecurityType}; use crate::orders::{Action, OrderCondition, OrderOpenClose, Rule80A}; -use crate::testdata::responses::NOTICE_DATA_FARM_CONNECTION; use super::*; @@ -278,7 +277,7 @@ fn test_request_id_index_invalid() { #[test] fn test_notice() { - let message = ResponseMessage::from(NOTICE_DATA_FARM_CONNECTION); + let message = ResponseMessage::from("4\02\0-1\02107\0HMDS data farm connection is inactive.\0"); let notice = Notice::from(&message); diff --git a/src/scanner.rs b/src/scanner.rs index e2a94fcc..8b3417ab 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -7,6 +7,8 @@ use crate::{ server_versions, Client, Error, }; +mod decoders; + // Requests an XML list of scanner parameters valid in TWS. pub(super) fn scanner_parameters(client: &Client) -> Result { let request = encoders::encode_scanner_parameters()?; @@ -199,58 +201,3 @@ mod encoders { 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 - - 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) - } -} diff --git a/src/scanner/decoders.rs b/src/scanner/decoders.rs new file mode 100644 index 00000000..2d6b12d3 --- /dev/null +++ b/src/scanner/decoders.rs @@ -0,0 +1,53 @@ +use crate::contracts::SecurityType; +use crate::messages::ResponseMessage; +use crate::Error; + +use super::ScannerData; + +#[cfg(test)] +mod tests; + +pub(super) fn decode_scanner_parameters(mut message: ResponseMessage) -> Result { + message.skip(); // skip message type + message.skip(); // skip message version + + message.next_string() +} + +pub(super) fn decode_scanner_data(mut message: ResponseMessage) -> Result, Error> { + message.skip(); // skip message type + message.skip(); // skip message version + 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() + }; + + 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 = message.next_string()?; + + matches.push(scanner_data); + } + + Ok(matches) +} diff --git a/src/scanner/decoders/tests.rs b/src/scanner/decoders/tests.rs new file mode 100644 index 00000000..55eb6c5b --- /dev/null +++ b/src/scanner/decoders/tests.rs @@ -0,0 +1,148 @@ +struct ScanRow { + rank: i32, + contract_id: i32, + symbol: String, + leg: String, +} + +#[test] +fn test_decode_scanner_data_top_10() { + let message = super::ResponseMessage::from("20\03\09000\010\00\0667434000\0ELAB\0STK\0\00\0\0SMART\0USD\0ELAB\0SCM\0SCM\0\0\0\0\01\0689954925\0XTIA\0STK\0\00\0\0SMART\0USD\0XTIA\0SCM\0SCM\0\0\0\0\02\0647805811\0MTEM\0STK\0\00\0\0SMART\0USD\0MTEM\0SCM\0SCM\0\0\0\0\03\0670777621\0SVMH\0STK\0\00\0\0SMART\0USD\0SVMH\0NMS\0NMS\0\0\0\0\04\0324651164\0QUBT\0STK\0\00\0\0SMART\0USD\0QUBT\0SCM\0SCM\0\0\0\0\05\0504717050\0MVST\0STK\0\00\0\0SMART\0USD\0MVST\0SCM\0SCM\0\0\0\0\06\0733727297\0UAVS\0STK\0\00\0\0SMART\0USD\0UAVS\0UAVS\0UAVS\0\0\0\0\07\04815747\0NVDA\0STK\0\00\0\0SMART\0USD\0NVDA\0NMS\0NMS\0\0\0\0\08\076792991\0TSLA\0STK\0\00\0\0SMART\0USD\0TSLA\0NMS\0NMS\0\0\0\0\09\0531212348\0NU\0STK\0\00\0\0SMART\0USD\0NU\0NU\0NU\0\0\0\0\0"); + + let scanner_data = super::decode_scanner_data(message).expect("error decoding pnl single"); + assert_eq!(scanner_data.len(), 10, "scanner_data.len()"); + + let expected = vec![ + ScanRow { + rank: 0, + contract_id: 667434000, + symbol: "ELAB".to_string(), + leg: "".to_string(), + }, + ScanRow { + rank: 1, + contract_id: 689954925, + symbol: "XTIA".to_string(), + leg: "".to_string(), + }, + ScanRow { + rank: 2, + contract_id: 647805811, + symbol: "MTEM".to_string(), + leg: "".to_string(), + }, + ScanRow { + rank: 3, + contract_id: 670777621, + symbol: "SVMH".to_string(), + leg: "".to_string(), + }, + ScanRow { + rank: 4, + contract_id: 324651164, + symbol: "QUBT".to_string(), + leg: "".to_string(), + }, + ScanRow { + rank: 5, + contract_id: 504717050, + symbol: "MVST".to_string(), + leg: "".to_string(), + }, + ScanRow { + rank: 6, + contract_id: 733727297, + symbol: "UAVS".to_string(), + leg: "".to_string(), + }, + ScanRow { + rank: 7, + contract_id: 4815747, + symbol: "NVDA".to_string(), + leg: "".to_string(), + }, + ScanRow { + rank: 8, + contract_id: 76792991, + symbol: "TSLA".to_string(), + leg: "".to_string(), + }, + ScanRow { + rank: 9, + contract_id: 531212348, + symbol: "NU".to_string(), + leg: "".to_string(), + }, + ]; + + for i in 0..10 { + assert_eq!(scanner_data[i].rank, expected[i].rank, "scanner_data[{}].rank", i); + assert_eq!( + scanner_data[i].contract_details.contract.contract_id, expected[i].contract_id, + "scanner_data[{}].contract_id", + i + ); + assert_eq!( + scanner_data[i].contract_details.contract.symbol, expected[i].symbol, + "scanner_data[{}].symbol", + i + ); + assert_eq!(scanner_data[i].leg, expected[i].leg, "scanner_data[{}].leg", i); + } +} + +#[test] +fn test_decode_scanner_data_complex_orders() { + let message = super::ResponseMessage::from("20\03\09000\050\00\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0738758309|1,738758426|-1\01\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0621537214|1,621537279|-1\02\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0734450979|1,734451081|-3,734451143|2\03\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0682678233|1,736727533|-1\04\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0739480291|1,740950238|-1\05\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0265598|100,584718266|-1,584719753|1\06\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0584718405|1,682678110|-1\07\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0682678233|1,736727485|1\08\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0739480710|1,740950685|-1\09\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0733172883|1,737589816|-1\010\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0265598|100,584718324|-1,584719827|1\011\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0592413828|1,592413865|-1\012\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0621535193|1,621535234|-1\013\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0733171244|2,739480710|-1\014\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0584718445|1,592413922|-1\015\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0682681481|1,739480710|-1\016\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0621535359|1,682678233|-1\017\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0584718476|1,592413501|-1\018\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0736727485|1,740950285|-1\019\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0733172408|1,733172961|-1\020\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0606138777|1,606138801|-1\021\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0736727912|1,736727939|-1\022\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0621535147|1,621535193|-1\023\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0733172883|11,739480710|-6\024\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0265598|100,733171197|-1,733173027|1\025\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0675816239|1,675816415|-1\026\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0740950238|1,740950345|-1\027\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0682678448|1,737588302|-1\028\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0265598|100,584718300|-1,584719802|1\029\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0682681585|1,733173027|-1\030\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0682678233|1,733171197|-1\031\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0621535193|1,740950238|-1\032\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0739480332|1,740950285|-1\033\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0682678233|1,736727485|-1\034\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0734451013|1,734451143|-1\035\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0739480660|1,739480710|-1\036\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0681166346|1,681166531|-1\037\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0682678110|1,722754599|-1\038\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0265598|100,621535037|-1\039\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0722765565|1,737588171|-1\040\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0682678162|1,740111939|-1\041\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0682678233|1,682681530|-1,736727485|-1,736727912|1\042\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0736727485|1,736727533|-1\043\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0618335050|1,682677693|-1\044\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0733170765|1,733171140|-1,734450778|-1,734450868|-1\045\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0722755002|2,722755094|-1\046\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0734452761|1,734452869|-1\047\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0682678216|1,733171140|-1\048\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0682681481|1,736727912|-1\049\028812380\0AAPL\0BAG\0\00\0\0SMART\0USD\0AAPL\0COMB\0COMB\0\0\0\0621537279|1,738758546|-1\0"); + + let scanner_data = super::decode_scanner_data(message).expect("error decoding pnl single"); + assert_eq!(scanner_data.len(), 50, "scanner_data.len()"); + + let expected = vec![ + ScanRow { + rank: 0, + contract_id: 28812380, + symbol: "AAPL".to_string(), + leg: "738758309|1,738758426|-1".to_string(), + }, + ScanRow { + rank: 1, + contract_id: 28812380, + symbol: "AAPL".to_string(), + leg: "621537214|1,621537279|-1".to_string(), + }, + ScanRow { + rank: 2, + contract_id: 28812380, + symbol: "AAPL".to_string(), + leg: "734450979|1,734451081|-3,734451143|2".to_string(), + }, + ScanRow { + rank: 3, + contract_id: 28812380, + symbol: "AAPL".to_string(), + leg: "682678233|1,736727533|-1".to_string(), + }, + ScanRow { + rank: 4, + contract_id: 28812380, + symbol: "AAPL".to_string(), + leg: "739480291|1,740950238|-1".to_string(), + }, + ]; + + for i in 0..5 { + assert_eq!(scanner_data[i].rank, expected[i].rank, "scanner_data[{}].rank", i); + assert_eq!( + scanner_data[i].contract_details.contract.contract_id, expected[i].contract_id, + "scanner_data[{}].contract_id", + i + ); + assert_eq!( + scanner_data[i].contract_details.contract.symbol, expected[i].symbol, + "scanner_data[{}].symbol", + i + ); + assert_eq!(scanner_data[i].leg, expected[i].leg, "scanner_data[{}].leg", i); + } +} diff --git a/src/testdata/responses.rs b/src/testdata/responses.rs index f8bdcaaf..b813d087 100644 --- a/src/testdata/responses.rs +++ b/src/testdata/responses.rs @@ -1,7 +1,3 @@ -// error - -pub const NOTICE_DATA_FARM_CONNECTION: &str = "4|2|-1|2107|HMDS data farm connection is inactive.|"; - // accounts pub const MANAGED_ACCOUNT: &str = "15|1|DU1234567,DU7654321|";