Send a Stop Order

Place a BUY STOP on SPY far above market. The trigger never fires, so the order rests. Watch it acknowledge, then cancel it.

What this shows

  • client.next_order_id(), which reserves one id.
  • An Order with order_type: "STP" and the trigger price in aux_price. lmt_price is unused for a plain stop.
  • tif: "GTC", so it survives the close rather than expiring with the day.
  • order_status callbacks: PreSubmitted then Submitted while it rests, Cancelled once the cancel lands.
  • cancel_order(order_id, "").

What comes back

The same order_status stream as any other order. A resting stop shows no fills: filled stays 0 and remaining stays at the full quantity until the trigger is touched.

Limits

A stop is held until its trigger is touched, then goes to the market as a market order. This example sets the trigger far above market so that never happens. On a real stop, the fill price is whatever the market gives you at that moment, not the trigger.

Paper account only.

Run it

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

Source

//! Recipe: place a BUY STP on SPY far above market, watch Submitted, then cancel.
//!
//! Usage: IB_USERNAME=... IB_PASSWORD=... cargo run --example hello_stop_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: "STP".into(),
        aux_price: 9999.0,
        tif: "GTC".into(),
        outside_rth: true,
        ..Default::default()
    };

    println!("placing BUY 1 SPY STP 9999.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(())
}