Send a Limit Order

Place a BUY LMT on SPY far below market, watch it acknowledge, then cancel it. Connect, take an id, place, read status, cancel, disconnect.

What this shows

  • client.next_order_id(), which reserves one id.
  • An Order with order_type: "LMT" and lmt_price set. outside_rth: true is what lets it be acknowledged outside regular hours.
  • order_status callbacks. PreSubmitted then Submitted while it rests, Cancelled once the cancel lands.
  • cancel_order(order_id, order_cancel). The second argument is what the withdrawal states about itself: an OrderCancel (who is withdrawing it, whether a person entered it, a manual time), or a time alone; "" states nothing. The operator and who entered it travel on the cancel. A time does not, and the caller is told so on error; a time a gateway cannot read is refused under 10301 and the order keeps working.

What comes back

order_status on every change and again on each fill. filled and remaining are quantities, avg_fill_price is the average of what has filled so far. An order that rests will sit at Submitted until you cancel it.

The perm_id on that callback is the number the order goes to the venue under, as a gateway states it, which for this order is its own order id. It is stated from the moment the order is sent and stays the same for the life of the order, including across a modify.

Limits

next_order_id counts from one past the highest id the account is working an order under, which the venue names at every connect, from every session. So it does not collide with an order placed elsewhere.

tif is checked against what the venue carries: DAY, GTC, IOC, FOK, OPG, GTD, GTX, DTC, AUC, NMIN, spelled exactly. An unrecognised value is refused here rather than sent as DAY and quietly expiring at the close.

Paper account only. The price is set far below market so it will not fill.

The shorter form

place takes the id itself, sends the order, and waits up to five seconds for the venue to settle it:

let report = client.place(&spy, &order)?;
println!("{} {} filled", report.status, report.filled);

It reports what is known either way. An order still working is reported as working; it is not cancelled.

Run it

IB_USERNAME=... IB_PASSWORD=... cargo run --example hello_limit_order

Source

//! Recipe: place a non-marketable BUY LMT on SPY, watch Submitted, then cancel.
//!
//! Usage: IB_USERNAME=... IB_PASSWORD=... cargo run --example hello_limit_order

use std::env;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use ib_dx::api::client::{Contract, EClient, EClientConfig, Order};
use ib_dx::api::wrapper::Wrapper;

#[derive(Default)]
struct State {
    statuses: Vec<(i64, String)>,
}

struct OrderWrapper {
    state: Arc<Mutex<State>>,
}

impl Wrapper for OrderWrapper {
    fn order_status(
        &mut self, order_id: i64, status: &str, _filled: ib_dx::api::Decimal, _remaining: ib_dx::api::Decimal,
        _avg_fill: f64, _perm_id: i64, _parent_id: i64, _last_fill: f64,
        _client_id: i64, _why_held: &str, _mkt_cap_price: f64,
    ) {
        println!("[status] oid={order_id} status={status}");
        self.state.lock().unwrap().statuses.push((order_id, status.into()));
    }
    fn error(&mut self, req_id: i64, _error_time: i64, code: i64, msg: &str, _adv: &str) {
        eprintln!("[error] req_id={req_id} code={code} msg={msg}");
    }
}

fn pump_until<F: Fn(&State) -> bool>(
    client: &EClient, wrapper: &mut OrderWrapper, state: &Arc<Mutex<State>>,
    timeout: Duration, done: F,
) -> bool {
    let deadline = Instant::now() + timeout;
    while Instant::now() < deadline {
        client.process_msgs(wrapper);
        if done(&state.lock().unwrap()) { return true; }
        std::thread::sleep(Duration::from_millis(20));
    }
    false
}

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 order_id = client.next_order_id();

    let state = Arc::new(Mutex::new(State::default()));
    let mut wrapper = OrderWrapper { state: state.clone() };

    let spy = Contract {
        con_id: 756733,
        symbol: "SPY".into(),
        sec_type: "STK".into(),
        exchange: "SMART".into(),
        currency: "USD".into(),
        ..Default::default()
    };
    let order = Order {
        action: "BUY".into(),
        total_quantity: 1.0,
        order_type: "LMT".into(),
        lmt_price: 1.00,
        tif: "DAY".into(),
        outside_rth: true,
        ..Default::default()
    };

    println!("placing BUY 1 SPY LMT 1.00 (oid={order_id})");
    client.place_order(order_id, &spy, &order);

    pump_until(&client, &mut wrapper, &state, Duration::from_secs(15),
               |s| s.statuses.iter().any(|(id, st)| *id == order_id && st == "Submitted"));

    println!("cancelling oid={order_id}");
    client.cancel_order(order_id, "");

    pump_until(&client, &mut wrapper, &state, Duration::from_secs(15),
               |s| s.statuses.iter().any(|(id, st)| *id == order_id && st == "Cancelled"));

    client.disconnect();
    Ok(())
}