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), with scanner_setting_pairs "" when there are none.
  • Filters as TagValue pairs. The tag names are the ones req_scanner_parameters publishes, for example priceAbove = "5" or stkTypes = "inc:ETF". They are carried to the venue, not dropped.
  • Reading scanner_data rows, each a rank and a ContractDetails, until scanner_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(())
}