#![cfg(feature = "ibkr")]
#![allow(clippy::unwrap_used, clippy::expect_used)]
use ibapi::{
contracts::{Contract, Currency, Exchange, SecurityType, Symbol},
market_data::{MarketDataType, realtime::generic_tick},
};
use rust_decimal_macros::dec;
use rustrade_execution::{
AccountEventKind,
client::{
ExecutionClient, OrderStatusClient, ShortabilityClient,
ibkr::{
ContractConfig, ContractSkipReason, IbkrClient, IbkrConfig, IbkrConnectError,
SkippedContract,
contract::{ContractConfigError, ResolveContractError, stock_contract},
},
},
error::{ApiError, ClientError},
order::{
OrderKey, OrderKind, TimeInForce, TrailingOffsetType,
id::{ClientOrderId, StrategyId},
request::RequestOpen,
state::{ActiveOrderState, InactiveOrderState, OrderState},
},
};
use rustrade_instrument::{
Side, asset::name::AssetNameExchange, exchange::ExchangeId, ibkr::ContractRegistryError,
instrument::name::InstrumentNameExchange,
};
use serial_test::serial;
use std::time::{Duration, Instant};
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_port() -> u16 {
std::env::var("IBKR_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(4002)
}
fn test_config(client_id_offset: i32) -> IbkrConfig {
IbkrConfig::new(
"127.0.0.1",
test_port(),
test_client_id_base() + client_id_offset,
std::env::var("IBKR_PAPER_ACCOUNT").expect("IBKR_PAPER_ACCOUNT env var required"),
)
}
fn resolved_aapl_config(client_id_offset: i32) -> IbkrConfig {
test_config(client_id_offset).with_contracts(vec![ContractConfig {
name: "AAPL".to_string(),
symbol: "AAPL".to_string(),
security_type: "STK".to_string(),
exchange: "SMART".to_string(),
currency: "USD".to_string(),
last_trade_date: None,
strike: None,
right: None,
}])
}
fn aapl_instrument() -> InstrumentNameExchange {
"AAPL".into()
}
fn partly_registrable_config(client_id_offset: i32) -> IbkrConfig {
let mut config = resolved_aapl_config(client_id_offset);
let aapl = config.contracts[0].clone();
config.contracts.extend([
ContractConfig {
name: "NOSUCH".to_string(),
symbol: "NOSUCHSYMBOLXQZ".to_string(),
..aapl.clone()
},
ContractConfig {
name: "ES-NODATE".to_string(),
symbol: "ES".to_string(),
security_type: "FUT".to_string(),
exchange: "CME".to_string(),
..aapl
},
]);
config
}
fn assert_partly_registrable_skips(skipped: &[SkippedContract]) {
let names: Vec<_> = skipped
.iter()
.map(|skip| skip.name.name().as_str())
.collect();
assert_eq!(names, ["NOSUCH", "ES-NODATE"], "{skipped:?}");
assert_eq!(
skipped[0].reason,
ContractSkipReason::Resolve(ResolveContractError::NoMatch)
);
assert_eq!(
skipped[1].reason,
ContractSkipReason::Config(ContractConfigError::MissingLastTradeDate)
);
assert!(skipped.iter().all(|skip| !skip.reason.is_transient()));
}
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 description = stock_contract("AAPL", "SMART", "USD");
assert_eq!(
client.register_contract(aapl_name.clone(), description.clone()),
Err(ContractRegistryError::Unresolved {
name: aapl_name.clone()
})
);
assert!(client.contract_registry().is_empty());
let aapl = client
.resolve_contract(&description)
.await
.expect("AAPL must resolve");
assert!(
aapl.contract_id > 0,
"resolved contract has no id: {aapl:?}"
);
client
.register_contract(aapl_name.clone(), aapl.clone())
.expect("a resolved contract registers");
assert_eq!(client.contract_registry().len(), 1);
assert_eq!(
client
.contract_registry()
.get_name_by_con_id(aapl.contract_id),
Some(aapl_name.clone())
);
assert!(matches!(
client.register_contract("AAPL-2".into(), aapl.clone()),
Err(ContractRegistryError::ContractIdTaken { .. })
));
let unknown = stock_contract("NOSUCHSYMBOLXQZ", "SMART", "USD");
assert_eq!(
client.resolve_contract(&unknown).await,
Err(ResolveContractError::NoMatch)
);
let every_es_month = Contract {
symbol: Symbol::from("ES"),
security_type: SecurityType::Future,
exchange: Exchange::from("CME"),
currency: Currency::from("USD"),
..Contract::default()
};
match client.resolve_contract(&every_es_month).await {
Err(ResolveContractError::Ambiguous { matches }) => {
println!("ES months: {}", matches.len());
assert!(matches.len() > 1);
}
other => panic!("expected an ambiguous description, got {other:?}"),
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_connect_refuses_a_configured_contract_it_cannot_register() {
init_logging();
let refused = tokio::task::spawn_blocking(|| {
IbkrClient::connect_sync(partly_registrable_config(23)).map(|_| ())
})
.await
.unwrap();
match refused {
Err(IbkrConnectError::Contracts(skipped)) => assert_partly_registrable_skips(&skipped),
other => panic!("expected the bad contracts to fail the connect, got {other:?}"),
}
let client = connect_client(resolved_aapl_config(23))
.await
.expect("the client ID was freed");
assert_eq!(client.contract_registry().len(), 1);
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_lenient_connect_reports_the_contracts_it_skips() {
init_logging();
let outcome = tokio::task::spawn_blocking(|| {
IbkrClient::connect_sync_lenient(partly_registrable_config(24))
})
.await
.unwrap()
.expect("connection failed");
assert_partly_registrable_skips(&outcome.skipped);
let registry = outcome.client.contract_registry();
assert_eq!(registry.len(), 1);
assert!(registry.contains(&aapl_instrument()));
}
#[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());
for snap in &snapshot.instruments {
println!(" {}: position {:?}", snap.instrument, snap.position);
}
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_fetch_open_orders() {
init_logging();
let config = resolved_aapl_config(4);
let client = connect_client(config).await.expect("connection failed");
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 = resolved_aapl_config(5);
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(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,
market: None,
};
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;
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;
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 = resolved_aapl_config(7);
let client = connect_client(config).await.expect("connection failed");
let since = chrono::Utc::now() - chrono::Duration::hours(24);
let instruments: Vec<InstrumentNameExchange> = vec![];
let result = client
.fetch_trades(since, chrono::Utc::now(), &instruments)
.await;
assert!(result.is_ok(), "fetch_trades failed: {:?}", result.err());
let read = result.unwrap();
assert_eq!(read.resume, None, "the venue reads a span whole");
let trades = read.trades;
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,
market: None,
};
let open_request = rustrade_execution::order::OrderEvent {
key: order_key,
state: request_open,
};
let response = client.open_order(open_request).await;
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.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 = resolved_aapl_config(11);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
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,
market: None,
};
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;
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;
println!("Cancel response: {:?}", cancel_response.state);
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 = resolved_aapl_config(12);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
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,
market: None,
};
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;
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;
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 = resolved_aapl_config(13);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
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,
market: None,
};
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;
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;
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 = resolved_aapl_config(14);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
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,
activation_price: None,
},
time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
position_id: None,
reduce_only: false,
market: None,
};
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;
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;
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);
}
}
}
async fn place_and_cancel_trailing_stop_limit(
client_id_offset: i32,
offset: rust_decimal::Decimal,
offset_type: TrailingOffsetType,
) {
init_logging();
let config = resolved_aapl_config(client_id_offset);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
let strategy = StrategyId::new("test-trailing-stop-limit");
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 kind = OrderKind::TrailingStopLimit {
offset,
offset_type,
stop_price: dec!(200),
limit_offset: dec!(1), };
let request_open = RequestOpen {
side: Side::Sell,
price: None, quantity: dec!(1),
kind,
time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
position_id: None,
reduce_only: false,
market: None,
};
let open_request = rustrade_execution::order::OrderEvent {
key: order_key.clone(),
state: request_open,
};
println!("Placing TrailingStopLimit order: SELL 1 AAPL, {kind} (won't trigger)");
let response = client.open_order(open_request).await;
let OrderState::Active(ActiveOrderState::Open(open_state)) = &response.state else {
panic!("TrailingStopLimit order not open: {:?}", response.state);
};
println!("TrailingStopLimit order placed: {:?}", open_state.id);
tokio::time::sleep(Duration::from_secs(3)).await;
let cancel_request = rustrade_execution::order::OrderEvent {
key: OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl_name,
strategy: strategy.clone(),
cid: order_cid.clone(),
},
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.state.is_ok(), "{cancel_response:?}");
let ended = ended_eventually(
&client,
OrderKey {
exchange: ExchangeId::Ibkr,
instrument: aapl_name.clone(),
strategy,
cid: order_cid,
},
)
.await;
assert!(
matches!(ended.state, InactiveOrderState::Cancelled(_)),
"not cancelled: {:?}",
ended.state
);
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_place_and_cancel_trailing_stop_limit_absolute() {
place_and_cancel_trailing_stop_limit(15, dec!(500), TrailingOffsetType::Absolute).await;
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_place_and_cancel_trailing_stop_limit_percentage() {
place_and_cancel_trailing_stop_limit(22, dec!(50), TrailingOffsetType::Percentage).await;
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_place_and_cancel_bracket_order() {
use rustrade_execution::client::ibkr::BracketOrderRequest;
init_logging();
let config = resolved_aapl_config(16);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
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;
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 = resolved_aapl_config(17);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
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 = resolved_aapl_config(18);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
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,
market: None,
};
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;
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;
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 = resolved_aapl_config(19);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
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,
market: None,
};
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;
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;
match &cancel_response.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 = resolved_aapl_config(20);
let client = connect_client(config).await.expect("connection failed");
let aapl_name = aapl_instrument();
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,
market: None,
};
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;
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;
match &cancel_response.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);
}
}
}
async fn ended_eventually(
client: &IbkrClient,
key: rustrade_execution::order::UnindexedOrderKey,
) -> rustrade_execution::order::UnindexedInactiveOrder {
for _ in 0..20 {
let mut ended = client
.fetch_ended_orders(std::slice::from_ref(&key))
.await
.expect("fetch_ended_orders failed");
if let Some(order) = ended.pop() {
println!("Ended: {order:?}");
assert!(ended.is_empty());
return order;
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
panic!("{} never listed completed", key.cid);
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_orders_found_by_client_order_id_across_a_restart() {
init_logging();
let aapl = aapl_instrument();
let strategy = StrategyId::new("test-strategy");
let client = connect_client(resolved_aapl_config(21))
.await
.expect("connection failed");
let refused = client
.open_order(rustrade_execution::order::OrderEvent {
key: OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl,
strategy: strategy.clone(),
cid: ClientOrderId::new("ordre-\u{e9}"),
},
state: RequestOpen {
side: Side::Buy,
price: Some(dec!(1.00)),
quantity: dec!(1),
kind: OrderKind::Limit,
time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
position_id: None,
reduce_only: false,
market: None,
},
})
.await;
assert!(
matches!(
refused.state,
OrderState::Inactive(InactiveOrderState::OpenFailed(_))
),
"{refused:?}"
);
let cid = ClientOrderId::new(format!("restart-{}", chrono::Utc::now().timestamp_millis()));
let placed = client
.open_order(rustrade_execution::order::OrderEvent {
key: OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl,
strategy: strategy.clone(),
cid: cid.clone(),
},
state: RequestOpen {
side: Side::Buy,
price: Some(dec!(1.00)),
quantity: dec!(1),
kind: OrderKind::Limit,
time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
position_id: None,
reduce_only: false,
market: None,
},
})
.await;
assert!(
matches!(placed.state, OrderState::Active(ActiveOrderState::Open(_))),
"{placed:?}"
);
let snapshot = client
.account_snapshot(&[], std::slice::from_ref(&aapl))
.await
.expect("account_snapshot failed");
let entry = snapshot
.instruments
.iter()
.find(|entry| entry.instrument == aapl)
.expect("AAPL has an open order");
println!(
"AAPL: {} order(s), complete: {}",
entry.orders.len(),
entry.orders_complete
);
assert!(
entry.orders.iter().any(|order| order.key.cid == cid),
"{entry:?}"
);
assert!(entry.orders_complete, "{entry:?}");
let placed_here = ClientOrderId::new(format!(
"restart-here-{}",
chrono::Utc::now().timestamp_millis()
));
let order = client
.open_order(rustrade_execution::order::OrderEvent {
key: OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl,
strategy: strategy.clone(),
cid: placed_here.clone(),
},
state: RequestOpen {
side: Side::Buy,
price: Some(dec!(1.00)),
quantity: dec!(1),
kind: OrderKind::Limit,
time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
position_id: None,
reduce_only: false,
market: None,
},
})
.await;
assert!(
matches!(order.state, OrderState::Active(ActiveOrderState::Open(_))),
"{order:?}"
);
let cancelled = client
.cancel_order(rustrade_execution::order::OrderEvent {
key: OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl,
strategy: strategy.clone(),
cid: placed_here.clone(),
},
state: rustrade_execution::order::request::RequestCancel { id: None },
})
.await;
assert!(cancelled.state.is_ok(), "{cancelled:?}");
let ended = ended_eventually(
&client,
OrderKey {
exchange: ExchangeId::Ibkr,
instrument: aapl.clone(),
strategy: strategy.clone(),
cid: placed_here,
},
)
.await;
let InactiveOrderState::Cancelled(cancelled) = &ended.state else {
panic!("not cancelled: {:?}", ended.state);
};
assert_eq!(cancelled.filled_quantity, Some(rust_decimal::Decimal::ZERO));
client.disconnect();
drop(client);
let mut restarted = Err(String::new());
for _ in 0..15 {
tokio::time::sleep(Duration::from_secs(1)).await;
restarted = connect_client(resolved_aapl_config(21)).await;
if restarted.is_ok() {
break;
}
}
let restarted = restarted.expect("reconnection failed");
let open = restarted
.fetch_open_orders(std::slice::from_ref(&aapl))
.await
.expect("fetch_open_orders failed");
let listed = open
.iter()
.find(|order| order.key.cid == cid)
.expect("the order is listed under its client order id");
assert_eq!(listed.kind, OrderKind::Limit);
assert_eq!(listed.price, Some(dec!(1.00)));
for leftover in open
.iter()
.filter(|order| order.key.cid != cid && order.key.cid.0.starts_with("restart-"))
{
let _ = restarted
.cancel_order(rustrade_execution::order::OrderEvent {
key: OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl,
strategy: strategy.clone(),
cid: leftover.key.cid.clone(),
},
state: rustrade_execution::order::request::RequestCancel { id: None },
})
.await;
}
let cancelled = restarted
.cancel_order(rustrade_execution::order::OrderEvent {
key: OrderKey {
exchange: ExchangeId::Ibkr,
instrument: &aapl,
strategy: strategy.clone(),
cid: cid.clone(),
},
state: rustrade_execution::order::request::RequestCancel { id: None },
})
.await;
assert!(cancelled.state.is_ok(), "{cancelled:?}");
let key = OrderKey {
exchange: ExchangeId::Ibkr,
instrument: aapl.clone(),
strategy,
cid: cid.clone(),
};
let ended = ended_eventually(&restarted, key.clone()).await;
assert_eq!(ended.key, key);
let InactiveOrderState::Cancelled(cancelled) = &ended.state else {
panic!("not cancelled: {:?}", ended.state);
};
assert_eq!(cancelled.filled_quantity, None);
let unknown = OrderKey {
cid: ClientOrderId::new("never-placed"),
..key
};
assert!(
restarted
.fetch_ended_orders(&[unknown])
.await
.expect("fetch_ended_orders failed")
.is_empty()
);
}
fn shortability_symbols() -> Vec<String> {
std::env::var("IBKR_SHORTABILITY_SYMBOLS")
.ok()
.map(|symbols| {
symbols
.split(',')
.map(str::trim)
.filter(|symbol| !symbol.is_empty())
.map(str::to_owned)
.collect::<Vec<_>>()
})
.filter(|symbols| !symbols.is_empty())
.unwrap_or_else(|| vec!["AAPL".to_owned()])
}
#[tokio::test]
#[ignore]
#[serial]
async fn test_fetch_shortability() {
init_logging();
let symbols = shortability_symbols();
let contracts = symbols
.iter()
.map(|symbol| ContractConfig {
name: symbol.clone(),
symbol: symbol.clone(),
security_type: "STK".to_string(),
exchange: "SMART".to_string(),
currency: "USD".to_string(),
last_trade_date: None,
strike: None,
right: None,
})
.collect();
let config = test_config(25).with_contracts(contracts);
let client = connect_client(config).await.expect("connection failed");
let mut results = Vec::new();
for symbol in &symbols {
let started = Instant::now();
let result = client
.fetch_shortability(&InstrumentNameExchange::from(symbol.as_str()))
.await;
println!("{symbol}: {result:?} in {:?}", started.elapsed());
results.push((symbol, result));
}
for (symbol, result) in results {
result.unwrap_or_else(|e| panic!("{symbol}: {e}"));
}
let unknown = client.fetch_shortability(&"UNKNOWN_SYMBOL".into()).await;
assert!(
matches!(
unknown,
Err(ClientError::Api(ApiError::InstrumentInvalid(..)))
),
"{unknown:?}"
);
}
#[test]
#[ignore]
#[serial]
fn test_shortability_raw_ticks() {
init_logging();
let window = Duration::from_secs(
std::env::var("IBKR_PROBE_SECS")
.ok()
.and_then(|secs| secs.parse().ok())
.unwrap_or(30),
);
let client = ibapi::client::blocking::Client::connect(
&format!("127.0.0.1:{}", test_port()),
test_client_id_base() + 26,
)
.expect("connection failed");
if let Some(data_type) = std::env::var("IBKR_MARKET_DATA_TYPE")
.ok()
.and_then(|value| value.parse::<u8>().ok())
{
let data_type = match data_type {
1 => MarketDataType::Realtime,
2 => MarketDataType::Frozen,
3 => MarketDataType::Delayed,
4 => MarketDataType::DelayedFrozen,
other => panic!("IBKR_MARKET_DATA_TYPE must be 1 to 4, not {other}"),
};
println!("market data type: {data_type:?}");
client
.switch_market_data_type(data_type)
.expect("switch_market_data_type failed");
}
for symbol in shortability_symbols() {
let contract = Contract::stock(symbol.as_str()).build();
let subscription = match client
.market_data(&contract)
.generic_ticks(&[generic_tick::SHORTABLE])
.subscribe()
{
Ok(subscription) => subscription,
Err(e) => {
println!("{symbol}: subscribe failed: {e}");
continue;
}
};
let started = Instant::now();
loop {
let remaining = window.saturating_sub(started.elapsed());
if remaining.is_zero() {
break;
}
match subscription.next_timeout(remaining) {
Some(item) => {
println!("{symbol} +{:>6} ms {item:?}", started.elapsed().as_millis())
}
None => {
println!(
"{symbol} +{:>6} ms end of the read",
started.elapsed().as_millis()
);
break;
}
}
}
}
}