Skip to content

Commit

Permalink
fixes parsing of scanner response (#177)
Browse files Browse the repository at this point in the history
  • Loading branch information
wboayue authored Nov 15, 2024
1 parent 9de0bb1 commit d6437be
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 71 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ibapi"
version = "1.0.1"
version = "1.0.2"
edition = "2021"
authors = ["Wil Boayue <[email protected]>"]
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."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand All @@ -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()
}
}
38 changes: 38 additions & 0 deletions examples/scanner_subscription_complex_orders.rs
Original file line number Diff line number Diff line change
@@ -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()
}
}
1 change: 0 additions & 1 deletion src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
3 changes: 1 addition & 2 deletions src/messages/tests.rs
Original file line number Diff line number Diff line change
@@ -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::*;

Expand Down Expand Up @@ -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);

Expand Down
57 changes: 2 additions & 55 deletions src/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Error> {
let request = encoders::encode_scanner_parameters()?;
Expand Down Expand Up @@ -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<String, Error> {
message.skip(); // skip message type
message.skip(); // skip message version

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)
}
}
53 changes: 53 additions & 0 deletions src/scanner/decoders.rs
Original file line number Diff line number Diff line change
@@ -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<String, Error> {
message.skip(); // skip message type
message.skip(); // skip message version

message.next_string()
}

pub(super) fn decode_scanner_data(mut message: ResponseMessage) -> Result<Vec<ScannerData>, 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)
}
148 changes: 148 additions & 0 deletions src/scanner/decoders/tests.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
4 changes: 0 additions & 4 deletions src/testdata/responses.rs
Original file line number Diff line number Diff line change
@@ -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|";
Expand Down

0 comments on commit d6437be

Please sign in to comment.