#![allow(clippy::unwrap_used, clippy::expect_used)]
use rust_decimal_macros::dec;
use rustrade_execution::{
client::{
ExecutionClient,
ibkr::{IbkrClient, IbkrConfig, contract::stock_contract},
},
order::{
OrderEvent, OrderKey, OrderKind, TimeInForce,
id::{ClientOrderId, StrategyId},
request::{RequestCancel, RequestOpen},
state::{ActiveOrderState, OrderState},
},
};
use rustrade_instrument::{
Side, asset::name::AssetNameExchange, exchange::ExchangeId,
instrument::name::InstrumentNameExchange,
};
use tracing::{info, warn};
#[tokio::main]
async fn main() {
init_logging();
let config = IbkrConfig {
host: "127.0.0.1".to_string(),
port: 4002, client_id: 102, account: std::env::var("IBKR_PAPER_ACCOUNT")
.unwrap_or_else(|_| "YOUR_PAPER_ACCOUNT_ID".to_string()),
contracts: vec![], };
info!("Connecting to IB Gateway...");
let client = match IbkrClient::connect_sync(config) {
Ok(c) => c,
Err(e) => {
warn!("Failed to connect: {e}");
warn!("Make sure TWS/Gateway is running with API enabled on port 4002");
return;
}
};
info!("Connected!");
let aapl_name: InstrumentNameExchange = "AAPL".into();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
info!("Registered AAPL contract");
info!("");
info!("=== Account Snapshot ===");
let assets: Vec<AssetNameExchange> = vec!["USD".into()];
let instruments: Vec<InstrumentNameExchange> = vec![aapl_name.clone()];
match client.account_snapshot(&assets, &instruments).await {
Ok(snapshot) => {
info!("Balances:");
for balance in &snapshot.balances {
info!(
" {}: total={}, free={}",
balance.asset, balance.balance.total, balance.balance.free
);
}
info!("Open Orders:");
if snapshot.instruments.is_empty() {
info!(" (no open orders)");
}
for instrument_snapshot in &snapshot.instruments {
info!(
" {}: {} open orders",
instrument_snapshot.instrument,
instrument_snapshot.orders.len()
);
}
}
Err(e) => {
warn!("Failed to get account snapshot: {e}");
}
}
info!("");
info!("=== Placing Limit Order ===");
let order_cid = ClientOrderId::new("example-order-001");
let strategy = StrategyId::new("demo-strategy");
let order_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: strategy.clone(),
cid: order_cid.clone(),
};
let request_open = RequestOpen {
side: Side::Buy,
price: Some(dec!(1.00)), quantity: dec!(1),
kind: OrderKind::Limit,
time_in_force: TimeInForce::GoodUntilEndOfDay,
position_id: None,
reduce_only: false,
};
let open_request = OrderEvent {
key: order_key.clone(),
state: request_open,
};
info!("Placing BUY 1 AAPL @ $1.00 (won't fill - too far from market)");
match client.open_order(open_request).await {
Some(response) => {
match &response.state {
OrderState::Active(ActiveOrderState::Open(open_state)) => {
info!("Order placed successfully!");
info!(" Client Order ID: {}", response.key.cid);
info!(" Exchange Order ID: {:?}", open_state.id);
info!("");
info!("=== Canceling Order ===");
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let cancel_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: response.key.strategy.clone(),
cid: response.key.cid.clone(),
};
let cancel_request = OrderEvent {
key: cancel_key,
state: RequestCancel {
id: Some(open_state.id.clone()),
},
};
match client.cancel_order(cancel_request).await {
Some(cancel_response) => match &cancel_response.state {
Ok(_cancelled) => {
info!("Order canceled successfully!");
}
Err(e) => {
warn!("Cancel rejected: {e:?}");
}
},
None => {
info!("Cancel request sent (no immediate response)");
}
}
}
OrderState::Inactive(inactive) => {
warn!("Order rejected: {inactive:?}");
warn!("This may be due to:");
warn!(" - 'Read-Only API' is checked in TWS settings");
warn!(" - Account not authorized for AAPL trading");
warn!(" - Invalid order parameters");
}
other => {
info!("Unexpected order state: {other:?}");
}
}
}
None => {
info!("Order request sent (no immediate response)");
}
}
info!("");
info!("Example complete");
}
fn init_logging() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::filter::EnvFilter::builder()
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
.from_env_lossy(),
)
.with_ansi(cfg!(debug_assertions))
.init()
}