#![cfg(feature = "ibkr")]
#![allow(clippy::unwrap_used, clippy::expect_used)]
use rust_decimal_macros::dec;
use rustrade_execution::{
AccountEventKind,
client::{
ExecutionClient,
ibkr::{IbkrClient, IbkrConfig, contract::stock_contract},
},
order::{
OrderKey, OrderKind, TimeInForce, TrailingOffsetType,
id::{ClientOrderId, StrategyId},
request::RequestOpen,
state::{ActiveOrderState, InactiveOrderState, OrderState},
},
};
use rustrade_instrument::{
Side, asset::name::AssetNameExchange, exchange::ExchangeId,
instrument::name::InstrumentNameExchange,
};
use serial_test::serial;
use std::time::Duration;
use tokio_stream::StreamExt;
use tracing_subscriber::{EnvFilter, fmt};
fn init_logging() {
let _ = fmt()
.with_env_filter(
EnvFilter::builder()
.with_default_directive(tracing::Level::DEBUG.into())
.from_env_lossy(),
)
.try_init();
}
fn test_client_id_base() -> i32 {
std::env::var("IBKR_CLIENT_ID")
.ok()
.and_then(|id| id.parse().ok())
.unwrap_or(200)
}
fn test_config(client_id_offset: i32) -> IbkrConfig {
IbkrConfig {
host: "127.0.0.1".to_string(),
port: std::env::var("IBKR_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(4002),
client_id: test_client_id_base() + client_id_offset,
account: std::env::var("IBKR_PAPER_ACCOUNT").expect("IBKR_PAPER_ACCOUNT env var required"),
contracts: vec![],
}
}
fn aapl_instrument() -> InstrumentNameExchange {
"AAPL".into()
}
async fn connect_client(config: IbkrConfig) -> Result<IbkrClient, String> {
tokio::task::spawn_blocking(move || IbkrClient::connect_sync(config).map_err(|e| e.to_string()))
.await
.map_err(|e| format!("task join: {e}"))?
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_connection() {
init_logging();
let config = test_config(0);
let client = connect_client(config).await;
assert!(client.is_ok(), "Failed to connect: {:?}", client.err());
let client = client.unwrap();
assert_eq!(IbkrClient::EXCHANGE, ExchangeId::Ibkr);
assert_eq!(client.contract_registry().len(), 0);
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_contract_registration() {
init_logging();
let config = test_config(1);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
assert_eq!(client.contract_registry().len(), 1);
assert!(
client
.contract_registry()
.get_contract(&aapl_name)
.is_some()
);
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_fetch_balances() {
init_logging();
let config = test_config(2);
let client = connect_client(config).await.expect("connection failed");
let assets: Vec<AssetNameExchange> = vec![];
let result = client.fetch_balances(&assets).await;
assert!(result.is_ok(), "fetch_balances failed: {:?}", result.err());
let balances = result.unwrap();
println!("Fetched {} balance(s)", balances.len());
for balance in &balances {
println!(
" {}: total={}, free={}",
balance.asset, balance.balance.total, balance.balance.free
);
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_account_snapshot() {
init_logging();
let config = test_config(3);
let client = connect_client(config).await.expect("connection failed");
let assets: Vec<AssetNameExchange> = vec![];
let instruments: Vec<InstrumentNameExchange> = vec![];
let result = client.account_snapshot(&assets, &instruments).await;
assert!(
result.is_ok(),
"account_snapshot failed: {:?}",
result.err()
);
let snapshot = result.unwrap();
println!("Exchange: {:?}", snapshot.exchange);
println!("Balances: {}", snapshot.balances.len());
println!("Instruments: {}", snapshot.instruments.len());
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_fetch_open_orders() {
init_logging();
let config = test_config(4);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
let instruments: Vec<InstrumentNameExchange> = vec![];
let result = client.fetch_open_orders(&instruments).await;
assert!(
result.is_ok(),
"fetch_open_orders failed: {:?}",
result.err()
);
let orders = result.unwrap();
println!("Open orders: {}", orders.len());
for order in &orders {
println!(
" {:?} {} {} @ {:?}",
order.side, order.quantity, order.key.instrument, order.price
);
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_place_and_cancel_limit_order() {
init_logging();
let config = test_config(5);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
let strategy = StrategyId::new("test-strategy");
let order_cid = ClientOrderId::new(format!(
"test-order-{}",
chrono::Utc::now().timestamp_millis()
));
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 = rustrade_execution::order::OrderEvent {
key: order_key.clone(),
state: request_open,
};
println!("Placing limit order: BUY 1 AAPL @ $1.00 (won't fill)");
let response = client.open_order(open_request).await;
assert!(response.is_some(), "Expected order response");
let response = response.unwrap();
match &response.state {
OrderState::Active(ActiveOrderState::Open(open_state)) => {
println!("Order placed successfully!");
println!(" Client Order ID: {}", response.key.cid);
println!(" Exchange Order ID: {:?}", open_state.id);
tokio::time::sleep(Duration::from_millis(100)).await;
let cancel_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: response.key.strategy.clone(),
cid: response.key.cid.clone(),
};
let cancel_request = rustrade_execution::order::OrderEvent {
key: cancel_key,
state: rustrade_execution::order::request::RequestCancel {
id: Some(open_state.id.clone()),
},
};
println!("Canceling order...");
let cancel_response = client.cancel_order(cancel_request).await;
assert!(cancel_response.is_some(), "Expected cancel response");
let cancel_response = cancel_response.unwrap();
match &cancel_response.state {
Ok(_cancelled) => {
println!("Order canceled successfully!");
}
Err(e) => {
panic!("Cancel rejected: {:?}", e);
}
}
}
OrderState::Inactive(e) => {
panic!("Order rejected: {:?}", e);
}
other => {
panic!("Unexpected order state: {:?}", other);
}
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_account_stream() {
init_logging();
let config = test_config(6);
let client = connect_client(config).await.expect("connection failed");
let assets: Vec<AssetNameExchange> = vec![];
let instruments: Vec<InstrumentNameExchange> = vec![];
let stream_result = client.account_stream(&assets, &instruments).await;
assert!(
stream_result.is_ok(),
"account_stream failed: {:?}",
stream_result.err()
);
let mut stream = stream_result.unwrap();
println!("Account stream started. Waiting for events (5 second timeout)...");
let timeout = tokio::time::timeout(Duration::from_secs(5), async {
let mut count = 0;
while let Some(event) = stream.next().await {
println!("Event: {:?}", event.kind);
count += 1;
if count >= 3 {
break;
}
}
count
})
.await;
match timeout {
Ok(count) => println!("Received {} events", count),
Err(_) => println!("Timeout reached (this is normal if no orders are active)"),
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_fetch_trades() {
init_logging();
let config = test_config(7);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
let since = chrono::Utc::now() - chrono::Duration::hours(24);
let instruments: Vec<InstrumentNameExchange> = vec![];
let result = client.fetch_trades(since, &instruments).await;
assert!(result.is_ok(), "fetch_trades failed: {:?}", result.err());
let trades = result.unwrap();
println!("Trades in last 24h: {}", trades.len());
for trade in trades.iter().take(5) {
println!(
" {} {} {} @ {} (fees: {:?})",
trade.time_exchange.format("%Y-%m-%d %H:%M:%S"),
trade.side,
trade.quantity,
trade.price,
trade.fees
);
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_order_id_mapping_cleanup() {
init_logging();
let config = test_config(8);
let client = connect_client(config).await.expect("connection failed");
assert_eq!(client.pending_execution_count(), 0);
let cleared_execs = client.clear_stale_executions(Duration::from_secs(3600));
let cleared_orders = client.clear_stale_order_ids(Duration::from_secs(3600));
println!("Cleared {} stale executions", cleared_execs);
println!("Cleared {} stale order IDs", cleared_orders);
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_order_without_registered_contract() {
init_logging();
let config = test_config(9);
let client = connect_client(config).await.expect("connection failed");
let unknown_instrument: InstrumentNameExchange = "UNKNOWN_SYMBOL".into();
let strategy = StrategyId::new("test-strategy");
let order_cid = ClientOrderId::new("test-order-unknown");
let order_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &unknown_instrument,
strategy: strategy.clone(),
cid: order_cid.clone(),
};
let request_open = RequestOpen {
side: Side::Buy,
price: Some(dec!(100.00)),
quantity: dec!(1),
kind: OrderKind::Limit,
time_in_force: TimeInForce::GoodUntilEndOfDay,
position_id: None,
reduce_only: false,
};
let open_request = rustrade_execution::order::OrderEvent {
key: order_key,
state: request_open,
};
let response = client.open_order(open_request).await;
assert!(response.is_some());
let response = response.unwrap();
assert!(
response.state.is_failed(),
"Expected rejection for unregistered contract"
);
println!("Order correctly rejected: {:?}", response.state);
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_cancel_nonexistent_order() {
init_logging();
let config = test_config(10);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let strategy = StrategyId::new("test-strategy");
let order_cid = ClientOrderId::new("nonexistent-order");
let cancel_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy,
cid: order_cid,
};
let cancel_request = rustrade_execution::order::OrderEvent {
key: cancel_key,
state: rustrade_execution::order::request::RequestCancel { id: None },
};
let response = client.cancel_order(cancel_request).await;
assert!(response.is_some());
let response = response.unwrap();
assert!(
response.state.is_err(),
"Expected rejection for nonexistent order"
);
println!("Cancel correctly rejected: {:?}", response.state.err());
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_cancel_produces_cancelled_not_expired() {
init_logging();
let config = test_config(11);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
let assets: Vec<AssetNameExchange> = vec![];
let instruments: Vec<InstrumentNameExchange> = vec![];
let mut stream = client
.account_stream(&assets, &instruments)
.await
.expect("account_stream failed");
let strategy = StrategyId::new("test-cancel-vs-expire");
let order_cid = ClientOrderId::new(format!(
"cancel-test-{}",
chrono::Utc::now().timestamp_millis()
));
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 = rustrade_execution::order::OrderEvent {
key: order_key.clone(),
state: request_open,
};
println!("Placing DAY limit order: BUY 1 AAPL @ $1.00");
let response = client.open_order(open_request).await;
assert!(response.is_some(), "Expected order response");
let response = response.unwrap();
let exchange_order_id = match &response.state {
OrderState::Active(ActiveOrderState::Open(open_state)) => {
println!("Order placed: {:?}", open_state.id);
open_state.id.clone()
}
other => panic!("Expected Open state, got: {:?}", other),
};
tokio::time::sleep(Duration::from_millis(200)).await;
let cancel_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: response.key.strategy.clone(),
cid: response.key.cid.clone(),
};
let cancel_request = rustrade_execution::order::OrderEvent {
key: cancel_key,
state: rustrade_execution::order::request::RequestCancel {
id: Some(exchange_order_id.clone()),
},
};
println!("Cancelling order...");
let cancel_response = client.cancel_order(cancel_request).await;
assert!(cancel_response.is_some(), "Expected cancel response");
println!("Waiting for stream to emit Cancelled state...");
let mut found_cancelled = false;
let mut found_expired = false;
let timeout_result = tokio::time::timeout(Duration::from_secs(5), async {
while let Some(event) = stream.next().await {
if let AccountEventKind::OrderSnapshot(snapshot) = &event.kind {
let order = snapshot.value();
if order.key.cid == order_cid {
println!("Order event: {:?}", order.state);
match &order.state {
OrderState::Inactive(InactiveOrderState::Cancelled(_)) => {
found_cancelled = true;
break;
}
OrderState::Inactive(InactiveOrderState::Expired(_)) => {
found_expired = true;
break;
}
_ => {}
}
}
}
}
})
.await;
if timeout_result.is_err() {
println!("Timeout waiting for order state (this may happen if stream doesn't emit)");
}
if found_cancelled {
println!("SUCCESS: Order state is Cancelled (correct for user-initiated cancel)");
} else if found_expired {
panic!("FAILURE: Order state is Expired (should be Cancelled for user-initiated cancel)");
} else {
println!("WARNING: No terminal state observed in stream (cancel_order response was OK)");
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_place_and_cancel_stop_order() {
init_logging();
let config = test_config(12);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
let strategy = StrategyId::new("test-stop-order");
let order_cid = ClientOrderId::new(format!(
"stop-order-{}",
chrono::Utc::now().timestamp_millis()
));
let order_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: strategy.clone(),
cid: order_cid.clone(),
};
let request_open = RequestOpen {
side: Side::Sell,
price: None, quantity: dec!(1),
kind: OrderKind::Stop {
trigger_price: dec!(0.01),
},
time_in_force: TimeInForce::GoodUntilEndOfDay,
position_id: None,
reduce_only: false,
};
let open_request = rustrade_execution::order::OrderEvent {
key: order_key.clone(),
state: request_open,
};
println!("Placing Stop order: SELL 1 AAPL @ Stop $0.01 (won't trigger)");
let response = client.open_order(open_request).await;
assert!(response.is_some(), "Expected order response");
let response = response.unwrap();
match &response.state {
OrderState::Active(ActiveOrderState::Open(open_state)) => {
println!("Stop order placed successfully!");
println!(" Client Order ID: {}", response.key.cid);
println!(" Exchange Order ID: {:?}", open_state.id);
tokio::time::sleep(Duration::from_millis(100)).await;
let cancel_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: response.key.strategy.clone(),
cid: response.key.cid.clone(),
};
let cancel_request = rustrade_execution::order::OrderEvent {
key: cancel_key,
state: rustrade_execution::order::request::RequestCancel {
id: Some(open_state.id.clone()),
},
};
println!("Canceling Stop order...");
let cancel_response = client.cancel_order(cancel_request).await;
assert!(cancel_response.is_some(), "Expected cancel response");
let cancel_response = cancel_response.unwrap();
match &cancel_response.state {
Ok(_cancelled) => {
println!("Stop order canceled successfully!");
}
Err(e) => {
panic!("Cancel rejected: {:?}", e);
}
}
}
OrderState::Inactive(e) => {
panic!("Stop order rejected: {:?}", e);
}
other => {
panic!("Unexpected order state: {:?}", other);
}
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_place_and_cancel_stop_limit_order() {
init_logging();
let config = test_config(13);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
let strategy = StrategyId::new("test-stop-limit-order");
let order_cid = ClientOrderId::new(format!(
"stop-limit-{}",
chrono::Utc::now().timestamp_millis()
));
let order_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: strategy.clone(),
cid: order_cid.clone(),
};
let request_open = RequestOpen {
side: Side::Sell,
price: Some(dec!(0.01)), quantity: dec!(1),
kind: OrderKind::StopLimit {
trigger_price: dec!(0.01),
},
time_in_force: TimeInForce::GoodUntilEndOfDay,
position_id: None,
reduce_only: false,
};
let open_request = rustrade_execution::order::OrderEvent {
key: order_key.clone(),
state: request_open,
};
println!("Placing StopLimit order: SELL 1 AAPL @ Stop $0.01, Limit $0.01 (won't trigger)");
let response = client.open_order(open_request).await;
assert!(response.is_some(), "Expected order response");
let response = response.unwrap();
match &response.state {
OrderState::Active(ActiveOrderState::Open(open_state)) => {
println!("StopLimit order placed successfully!");
println!(" Client Order ID: {}", response.key.cid);
println!(" Exchange Order ID: {:?}", open_state.id);
tokio::time::sleep(Duration::from_millis(100)).await;
let cancel_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: response.key.strategy.clone(),
cid: response.key.cid.clone(),
};
let cancel_request = rustrade_execution::order::OrderEvent {
key: cancel_key,
state: rustrade_execution::order::request::RequestCancel {
id: Some(open_state.id.clone()),
},
};
println!("Canceling StopLimit order...");
let cancel_response = client.cancel_order(cancel_request).await;
assert!(cancel_response.is_some(), "Expected cancel response");
let cancel_response = cancel_response.unwrap();
match &cancel_response.state {
Ok(_cancelled) => {
println!("StopLimit order canceled successfully!");
}
Err(e) => {
panic!("Cancel rejected: {:?}", e);
}
}
}
OrderState::Inactive(e) => {
panic!("StopLimit order rejected: {:?}", e);
}
other => {
panic!("Unexpected order state: {:?}", other);
}
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_place_and_cancel_trailing_stop_percentage() {
init_logging();
let config = test_config(14);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
let strategy = StrategyId::new("test-trailing-stop-pct");
let order_cid = ClientOrderId::new(format!(
"trail-stop-pct-{}",
chrono::Utc::now().timestamp_millis()
));
let order_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: strategy.clone(),
cid: order_cid.clone(),
};
let request_open = RequestOpen {
side: Side::Sell,
price: None, quantity: dec!(1),
kind: OrderKind::TrailingStop {
offset: dec!(50), offset_type: TrailingOffsetType::Percentage,
},
time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
position_id: None,
reduce_only: false,
};
let open_request = rustrade_execution::order::OrderEvent {
key: order_key.clone(),
state: request_open,
};
println!("Placing TrailingStop order: SELL 1 AAPL @ 50% trail (won't trigger)");
let response = client.open_order(open_request).await;
assert!(response.is_some(), "Expected order response");
let response = response.unwrap();
match &response.state {
OrderState::Active(ActiveOrderState::Open(open_state)) => {
println!("TrailingStop (percentage) order placed successfully!");
println!(" Client Order ID: {}", response.key.cid);
println!(" Exchange Order ID: {:?}", open_state.id);
tokio::time::sleep(Duration::from_millis(100)).await;
let cancel_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: response.key.strategy.clone(),
cid: response.key.cid.clone(),
};
let cancel_request = rustrade_execution::order::OrderEvent {
key: cancel_key,
state: rustrade_execution::order::request::RequestCancel {
id: Some(open_state.id.clone()),
},
};
println!("Canceling TrailingStop order...");
let cancel_response = client.cancel_order(cancel_request).await;
assert!(cancel_response.is_some(), "Expected cancel response");
let cancel_response = cancel_response.unwrap();
match &cancel_response.state {
Ok(_cancelled) => {
println!("TrailingStop (percentage) order canceled successfully!");
}
Err(e) => {
panic!("Cancel rejected: {:?}", e);
}
}
}
OrderState::Inactive(e) => {
panic!("TrailingStop order rejected: {:?}", e);
}
other => {
panic!("Unexpected order state: {:?}", other);
}
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_place_and_cancel_trailing_stop_limit_absolute() {
init_logging();
let config = test_config(15);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
let strategy = StrategyId::new("test-trailing-stop-limit-abs");
let order_cid = ClientOrderId::new(format!(
"trail-stop-limit-{}",
chrono::Utc::now().timestamp_millis()
));
let order_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: strategy.clone(),
cid: order_cid.clone(),
};
let request_open = RequestOpen {
side: Side::Sell,
price: None, quantity: dec!(1),
kind: OrderKind::TrailingStopLimit {
offset: dec!(500), offset_type: TrailingOffsetType::Absolute,
limit_offset: dec!(1), },
time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
position_id: None,
reduce_only: false,
};
let open_request = rustrade_execution::order::OrderEvent {
key: order_key.clone(),
state: request_open,
};
println!(
"Placing TrailingStopLimit order: SELL 1 AAPL @ $500 trail, $1 limit offset (won't trigger)"
);
let response = client.open_order(open_request).await;
assert!(response.is_some(), "Expected order response");
let response = response.unwrap();
match &response.state {
OrderState::Active(ActiveOrderState::Open(open_state)) => {
println!("TrailingStopLimit (absolute) order placed successfully!");
println!(" Client Order ID: {}", response.key.cid);
println!(" Exchange Order ID: {:?}", open_state.id);
tokio::time::sleep(Duration::from_millis(100)).await;
let cancel_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: response.key.strategy.clone(),
cid: response.key.cid.clone(),
};
let cancel_request = rustrade_execution::order::OrderEvent {
key: cancel_key,
state: rustrade_execution::order::request::RequestCancel {
id: Some(open_state.id.clone()),
},
};
println!("Canceling TrailingStopLimit order...");
let cancel_response = client.cancel_order(cancel_request).await;
assert!(cancel_response.is_some(), "Expected cancel response");
let cancel_response = cancel_response.unwrap();
match &cancel_response.state {
Ok(_cancelled) => {
println!("TrailingStopLimit (absolute) order canceled successfully!");
}
Err(e) => {
panic!("Cancel rejected: {:?}", e);
}
}
}
OrderState::Inactive(e) => {
panic!("TrailingStopLimit order rejected: {:?}", e);
}
other => {
panic!("Unexpected order state: {:?}", other);
}
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_place_and_cancel_bracket_order() {
use rustrade_execution::client::ibkr::BracketOrderRequest;
init_logging();
let config = test_config(16);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
let strategy = StrategyId::new("test-bracket-order");
let parent_cid =
ClientOrderId::new(format!("bracket-{}", chrono::Utc::now().timestamp_millis()));
let request = BracketOrderRequest {
instrument: aapl_name.clone(),
strategy: strategy.clone(),
parent_cid: parent_cid.clone(),
side: Side::Buy,
quantity: dec!(1),
entry_price: dec!(1.00), take_profit_price: dec!(2.00), stop_loss_price: dec!(0.50), time_in_force: TimeInForce::GoodUntilEndOfDay,
};
println!("Placing bracket order: BUY 1 AAPL @ $1.00 entry, $2.00 TP, $0.50 SL");
let result = client.open_bracket_order(request).await;
match &result.parent.state {
OrderState::Active(ActiveOrderState::Open(open_state)) => {
println!("Parent order placed successfully!");
println!(" Client Order ID: {}", result.parent.key.cid);
println!(" Exchange Order ID: {:?}", open_state.id);
}
OrderState::Inactive(e) => {
panic!("Parent order rejected: {:?}", e);
}
other => {
panic!("Unexpected parent order state: {:?}", other);
}
}
match &result.take_profit.state {
OrderState::Active(ActiveOrderState::Open(open_state)) => {
println!("Take profit order placed successfully!");
println!(" Client Order ID: {}", result.take_profit.key.cid);
println!(" Exchange Order ID: {:?}", open_state.id);
}
OrderState::Inactive(e) => {
panic!("Take profit order rejected: {:?}", e);
}
other => {
panic!("Unexpected take profit order state: {:?}", other);
}
}
match &result.stop_loss.state {
OrderState::Active(ActiveOrderState::Open(open_state)) => {
println!("Stop loss order placed successfully!");
println!(" Client Order ID: {}", result.stop_loss.key.cid);
println!(" Exchange Order ID: {:?}", open_state.id);
}
OrderState::Inactive(e) => {
panic!("Stop loss order rejected: {:?}", e);
}
other => {
panic!("Unexpected stop loss order state: {:?}", other);
}
}
tokio::time::sleep(Duration::from_millis(200)).await;
if let OrderState::Active(ActiveOrderState::Open(open_state)) = &result.parent.state {
let cancel_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: result.parent.key.strategy.clone(),
cid: result.parent.key.cid.clone(),
};
let cancel_request = rustrade_execution::order::OrderEvent {
key: cancel_key,
state: rustrade_execution::order::request::RequestCancel {
id: Some(open_state.id.clone()),
},
};
println!("Canceling bracket order (parent)...");
let cancel_response = client.cancel_order(cancel_request).await;
assert!(cancel_response.is_some(), "Expected cancel response");
let cancel_response = cancel_response.unwrap();
match &cancel_response.state {
Ok(_cancelled) => {
println!("Bracket order canceled successfully!");
println!(" (Children should be auto-cancelled by IB via parent_id linkage)");
}
Err(e) => {
panic!("Cancel rejected: {:?}", e);
}
}
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_bracket_order_oca_group_linkage() {
use rustrade_execution::client::ibkr::BracketOrderRequest;
init_logging();
let config = test_config(17);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
let strategy = StrategyId::new("test-bracket-oca");
let parent_cid = ClientOrderId::new(format!(
"bracket-oca-{}",
chrono::Utc::now().timestamp_millis()
));
let request = BracketOrderRequest {
instrument: aapl_name.clone(),
strategy: strategy.clone(),
parent_cid: parent_cid.clone(),
side: Side::Buy,
quantity: dec!(1),
entry_price: dec!(1.00),
take_profit_price: dec!(2.00),
stop_loss_price: dec!(0.50),
time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
};
println!("Placing bracket order to verify OCA linkage...");
let result = client.open_bracket_order(request).await;
assert!(
matches!(result.parent.state, OrderState::Active(_)),
"Parent should be active"
);
assert!(
matches!(result.take_profit.state, OrderState::Active(_)),
"TP should be active"
);
assert!(
matches!(result.stop_loss.state, OrderState::Active(_)),
"SL should be active"
);
println!("All three legs placed successfully.");
println!(" Parent CID: {}", result.parent.key.cid);
println!(
" TP CID: {} (should end with _tp)",
result.take_profit.key.cid
);
println!(
" SL CID: {} (should end with _sl)",
result.stop_loss.key.cid
);
assert!(
result.take_profit.key.cid.0.ends_with("_tp"),
"TP CID should end with _tp"
);
assert!(
result.stop_loss.key.cid.0.ends_with("_sl"),
"SL CID should end with _sl"
);
tokio::time::sleep(Duration::from_millis(200)).await;
if let OrderState::Active(ActiveOrderState::Open(open_state)) = &result.parent.state {
let cancel_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: result.parent.key.strategy.clone(),
cid: result.parent.key.cid.clone(),
};
let cancel_request = rustrade_execution::order::OrderEvent {
key: cancel_key,
state: rustrade_execution::order::request::RequestCancel {
id: Some(open_state.id.clone()),
},
};
let _ = client.cancel_order(cancel_request).await;
println!("Cleanup: bracket order cancelled.");
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_place_and_cancel_gtd_order() {
init_logging();
let config = test_config(18);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
let strategy = StrategyId::new("test-gtd-order");
let order_cid = ClientOrderId::new(format!(
"gtd-order-{}",
chrono::Utc::now().timestamp_millis()
));
let order_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: strategy.clone(),
cid: order_cid.clone(),
};
let expiry = chrono::Utc::now() + chrono::Duration::days(1);
let request_open = RequestOpen {
side: Side::Buy,
price: Some(dec!(1.00)),
quantity: dec!(1),
kind: OrderKind::Limit,
time_in_force: TimeInForce::GoodTillDate { expiry },
position_id: None,
reduce_only: false,
};
let open_request = rustrade_execution::order::OrderEvent {
key: order_key.clone(),
state: request_open,
};
println!(
"Placing GTD limit order: BUY 1 AAPL @ $1.00, expires {}",
expiry.format("%Y-%m-%d %H:%M:%S UTC")
);
let response = client.open_order(open_request).await;
assert!(response.is_some(), "Expected order response");
let response = response.unwrap();
match &response.state {
OrderState::Active(ActiveOrderState::Open(open_state)) => {
println!("GTD order placed successfully!");
println!(" Client Order ID: {}", response.key.cid);
println!(" Exchange Order ID: {:?}", open_state.id);
tokio::time::sleep(Duration::from_millis(100)).await;
let cancel_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: response.key.strategy.clone(),
cid: response.key.cid.clone(),
};
let cancel_request = rustrade_execution::order::OrderEvent {
key: cancel_key,
state: rustrade_execution::order::request::RequestCancel {
id: Some(open_state.id.clone()),
},
};
println!("Canceling GTD order...");
let cancel_response = client.cancel_order(cancel_request).await;
assert!(cancel_response.is_some(), "Expected cancel response");
let cancel_response = cancel_response.unwrap();
match &cancel_response.state {
Ok(_cancelled) => {
println!("GTD order canceled successfully!");
}
Err(e) => {
panic!("Cancel rejected: {:?}", e);
}
}
}
OrderState::Inactive(e) => {
panic!("GTD order rejected: {:?}", e);
}
other => {
panic!("Unexpected order state: {:?}", other);
}
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_place_moo_order_premarket() {
init_logging();
let config = test_config(19);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
let strategy = StrategyId::new("test-moo-order");
let order_cid = ClientOrderId::new(format!(
"moo-order-{}",
chrono::Utc::now().timestamp_millis()
));
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: None, quantity: dec!(1),
kind: OrderKind::Market,
time_in_force: TimeInForce::AtOpen,
position_id: None,
reduce_only: false,
};
let open_request = rustrade_execution::order::OrderEvent {
key: order_key.clone(),
state: request_open,
};
println!("Placing MOO order: BUY 1 AAPL at market open");
println!("Note: This test should be run during pre-market hours");
let response = client.open_order(open_request).await;
assert!(response.is_some(), "Expected order response");
let response = response.unwrap();
match &response.state {
OrderState::Active(ActiveOrderState::Open(open_state)) => {
println!("MOO order placed successfully (pre-market)!");
println!(" Client Order ID: {}", response.key.cid);
println!(" Exchange Order ID: {:?}", open_state.id);
tokio::time::sleep(Duration::from_millis(100)).await;
let cancel_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: response.key.strategy.clone(),
cid: response.key.cid.clone(),
};
let cancel_request = rustrade_execution::order::OrderEvent {
key: cancel_key,
state: rustrade_execution::order::request::RequestCancel {
id: Some(open_state.id.clone()),
},
};
println!("Canceling MOO order...");
let cancel_response = client.cancel_order(cancel_request).await;
assert!(cancel_response.is_some(), "Expected cancel response");
match &cancel_response.unwrap().state {
Ok(_) => println!("MOO order canceled successfully!"),
Err(e) => panic!("Cancel rejected: {:?}", e),
}
}
OrderState::Inactive(e) => {
println!("MOO order rejected (expected if not pre-market): {:?}", e);
println!("Test is timing-sensitive - run during pre-market for success");
}
other => {
panic!("Unexpected order state: {:?}", other);
}
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_place_loo_order_premarket() {
init_logging();
let config = test_config(20);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let aapl_contract = stock_contract("AAPL", "SMART", "USD");
client.register_contract(aapl_name.clone(), aapl_contract);
let strategy = StrategyId::new("test-loo-order");
let order_cid = ClientOrderId::new(format!(
"loo-order-{}",
chrono::Utc::now().timestamp_millis()
));
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::AtOpen,
position_id: None,
reduce_only: false,
};
let open_request = rustrade_execution::order::OrderEvent {
key: order_key.clone(),
state: request_open,
};
println!("Placing LOO order: BUY 1 AAPL @ $1.00 at market open");
let response = client.open_order(open_request).await;
assert!(response.is_some(), "Expected order response");
let response = response.unwrap();
match &response.state {
OrderState::Active(ActiveOrderState::Open(open_state)) => {
println!("LOO order placed successfully!");
println!(" Client Order ID: {}", response.key.cid);
println!(" Exchange Order ID: {:?}", open_state.id);
tokio::time::sleep(Duration::from_millis(100)).await;
let cancel_key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: response.key.strategy.clone(),
cid: response.key.cid.clone(),
};
let cancel_request = rustrade_execution::order::OrderEvent {
key: cancel_key,
state: rustrade_execution::order::request::RequestCancel {
id: Some(open_state.id.clone()),
},
};
println!("Canceling LOO order...");
let cancel_response = client.cancel_order(cancel_request).await;
assert!(cancel_response.is_some(), "Expected cancel response");
match &cancel_response.unwrap().state {
Ok(_) => println!("LOO order canceled successfully!"),
Err(e) => panic!("Cancel rejected: {:?}", e),
}
}
OrderState::Inactive(e) => {
println!("LOO order rejected (may be timing-related): {:?}", e);
}
other => {
panic!("Unexpected order state: {:?}", other);
}
}
}