Market Scanner
Run the TOP_PERC_GAIN scan over US major stocks, filtered to names above $5,
and print the top ten.
What this shows
req_scanner_subscription(req_id, instrument, location_code, scan_code, max_items, filters, scanner_setting_pairs), withscanner_setting_pairs""when there are none.- Filters as
TagValuepairs. The tag names are the onesreq_scanner_parameterspublishes, for examplepriceAbove="5"orstkTypes="inc:ETF". They are carried to the venue, not dropped. - Reading
scanner_datarows, each a rank and aContractDetails, untilscanner_data_end. cancel_scanner_subscription(req_id)before disconnecting.
What comes back
One scanner_data callback per row, then scanner_data_end for the request id.
Rows are ranked; the example sorts by rank before printing because it collects
them into a Vec first.
A scanner subscription keeps answering until it is cancelled. The example cancels; if you leave one running it goes on delivering into a session nobody is reading.
The shorter form
scan runs the scan, waits, and withdraws the subscription before returning:
let rows = client.scan("STK", "STK.US.MAJOR", "TOP_PERC_GAIN", 25)?;
It sends no filters. Use req_scanner_subscription when the scan needs them.
Limits
Which scan codes and locations are valid is the venue's list.
req_scanner_parameters fetches it, and an unknown code comes back as a stated
refusal on the error callback. When the historical connection drops, a
running scan is told 165 as it drops, at each attempt to bring it back that
fails and once it is back, and is then subscribed again and goes on under its
number, as a gateway does; a 165 ends nothing.
Run it
IB_USERNAME=... IB_PASSWORD=... cargo run --example hello_scanner
Source
//! Recipe: subscribe to TOP_PERC_GAIN scanner, print first 10 results.
//!
//! Usage: IB_USERNAME=... IB_PASSWORD=... cargo run --example hello_scanner
use std::env;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use ib_dx::api::client::{EClient, EClientConfig};
use ib_dx::api::types::{ContractDetails, TagValue};
use ib_dx::api::wrapper::Wrapper;
#[derive(Default)]
struct State {
rows: Vec<(i32, ContractDetails)>,
end_seen: bool,
}
struct ScannerWrapper {
state: Arc<Mutex<State>>,
}
impl Wrapper for ScannerWrapper {
fn scanner_data(
&mut self, _req_id: i64, rank: i32, details: &ContractDetails,
_distance: &str, _benchmark: &str, _projection: &str, _legs: &str,
) {
self.state.lock().unwrap().rows.push((rank, details.clone()));
}
fn scanner_data_end(&mut self, _req_id: i64) {
self.state.lock().unwrap().end_seen = true;
}
fn error(&mut self, req_id: i64, _error_time: i64, code: i64, msg: &str, _adv: &str) {
if !matches!(code, 2104 | 2106 | 2158) {
eprintln!("[error] req_id={req_id} code={code} msg={msg}");
}
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = EClient::connect(&EClientConfig {
username: env::var("IB_USERNAME")?,
password: env::var("IB_PASSWORD")?,
paper: true,
..Default::default()
})?;
let state = Arc::new(Mutex::new(State::default()));
let mut wrapper = ScannerWrapper { state: state.clone() };
let req_id = 1;
println!("subscribing TOP_PERC_GAIN, STK.US.MAJOR…");
let filters = [TagValue { tag: "priceAbove".into(), value: "5".into() }];
client.req_scanner_subscription(req_id, "STK", "STK.US.MAJOR", "TOP_PERC_GAIN", 25, &filters, "");
let deadline = Instant::now() + Duration::from_secs(30);
while Instant::now() < deadline {
client.process_msgs(&mut wrapper);
if state.lock().unwrap().end_seen { break; }
std::thread::sleep(Duration::from_millis(20));
}
let mut s = state.lock().unwrap();
s.rows.sort_by_key(|(rank, _)| *rank);
println!("results: {}", s.rows.len());
for (rank, d) in s.rows.iter().take(10) {
println!(" #{rank:<3} {:<8} {:<6} con_id={}", d.contract.symbol, d.contract.primary_exchange, d.contract.con_id);
}
drop(s);
client.cancel_scanner_subscription(req_id);
client.disconnect();
Ok(())
}