| Crates.io | yatws |
| lib.rs | yatws |
| version | 0.1.7 |
| created_at | 2025-06-07 22:32:59.200923+00 |
| updated_at | 2026-01-16 17:41:03.275837+00 |
| description | Yet Another TWS (Interactive Brokers TWS API) Implementation |
| homepage | |
| repository | https://github.com/drpngx/yatws |
| max_upload_size | |
| id | 1704476 |
| size | 2,249,031 |
A comprehensive, thread-safe, type-safe, and ergonomic Rust interface to the Interactive Brokers TWS API.
YATWS provides a modern Rust implementation for interacting with Interactive Brokers' Trader Workstation (TWS) and Gateway. The library's manager-based architecture and support for both synchronous and asynchronous patterns make it suitable for various trading applications, from simple data retrieval to complex automated trading systems.
This library was born out of the need to place orders in rapid succession in response to market events, which is not easily accomplished with existing Rust crates. It takes about 3ms to place an order with this library. While order management was the primary focus, other interfaces (market data, account information, etc.) have been implemented for completeness.
Current Status: Early stage but US equity trading is production-ready. The API has been used to trade 9 figures of dollars in volume.
YATWS follows a client-manager architecture:
IBKRClient): Central connection point to TWS/GatewayOrderManager: Order placement, modification, and trackingAccountManager: Account and portfolio data (summary, positions, P&L, executions, liquidation warnings)DataMarketManager: Real-time and historical market dataDataRefManager: Reference data (contract details, etc.)DataNewsManager: News headlines and articlesDataFundamentalsManager: Financial dataFinancialAdvisorManager: Financial Advisor configurations (groups, profiles, aliases)// Connect to TWS/Gateway
let client = IBKRClient::new("127.0.0.1", 7497, 0, None)?;
// Alternative: Record session to SQLite database
let client = IBKRClient::new(
"127.0.0.1",
7497,
0,
Some(("sessions.db", "my_trading_session"))
)?;
// Replay a recorded session
let replay_client = IBKRClient::from_db("sessions.db", "my_trading_session")?;
// Subscribe to account updates
client.account().subscribe_account_updates()?;
// Get account summary
let info = client.account().get_account_info()?;
println!("Account Net Liq: {}", info.net_liquidation);
// Get a single specific value.
use yatws::account::AccountValueKey;
let buying_power_value = client.account().get_account_value(AccountValueKey::BuyingPower)?;
if let Some(bp) = buying_power_value {
println!("Buying Power: {} {}", bp.value, bp.currency.unwrap_or_default());
}
// List positions
let positions = client.account().list_open_positions()?;
// Get today's executions
let executions = client.account().get_day_executions()?;
// Check for pre-liquidation warning
if client.account().has_received_pre_liquidation_warning() {
println!("Warning: Pre-liquidation warning received this session.");
}
// Create and place an order using OrderBuilder
let (contract, order_request) = OrderBuilder::new(OrderSide::Buy, 100.0)
.for_stock("AAPL")
.with_exchange("SMART")
.with_currency("USD")
.limit(150.0)
.with_tif(TimeInForce::Day)
.build()?;
let order_id = client.orders().place_order(contract, order_request)?;
// Wait for order to be filled (with timeout)
let status = client.orders().try_wait_order_executed(
&order_id,
Duration::from_secs(30)
)?;
For tracking order lifecycle with real-time updates:
use yatws::{OrderBuilder, OrderSide, TimeInForce};
use yatws::order_manager::OrderEvent;
use std::time::Duration;
// Create order specification
let (contract, order_request) = OrderBuilder::new(OrderSide::Buy, 100.0)
.for_stock("AAPL")
.market()
.with_tif(TimeInForce::Day)
.build()?;
// Subscribe to order lifecycle events
let order_subscription = client.orders().subscribe_new_order(contract, order_request)?;
let mut order_events = order_subscription.events();
// Process order events until completion or error
while let Some(event) = order_events.try_next(Duration::from_secs(1)) {
match event {
OrderEvent::Update(order) => {
println!("Order {} status: {:?}, filled: {}",
order.id, order.state.status, order.state.filled_quantity);
if order.state.status.is_terminal() {
println!("Order completed with status: {:?}", order.state.status);
break;
}
}
OrderEvent::Error(error) => {
eprintln!("Order error: {:?}", error);
break;
}
}
}
// Get a simple quote
let (bid, ask, last) = client.data_market().get_quote(
&contract,
None,
Duration::from_secs(5)
)?;
// Get historical data
use yatws::data::DurationUnit;
let bars = client.data_market().get_historical_data(
&contract,
None, // End time (None = now)
DurationUnit::Day(1),
yatws::contract::BarSize::Hour1,
yatws::contract::WhatToShow::Trades,
true, // Use RTH
1, // Date format
false, // Keep up to date?
None, // Market data type
&[] // Chart options
)?;
// Create a bull call spread
let builder = OptionsStrategyBuilder::new(
client.data_ref(),
"AAPL",
150.0, // Current price
10.0, // Quantity (10 spreads)
SecType::Stock
)?;
let (contract, order) = builder
.bull_call_spread(
NaiveDate::from_ymd_opt(2025, 12, 19).unwrap(),
150.0, // Lower strike
160.0 // Higher strike
)?
.with_limit_price(3.50) // Debit of $3.50 per spread
.build()?;
let order_id = client.orders().place_order(contract, order)?;
// Access the FinancialAdvisorManager
let fa_manager = client.financial_advisor();
// Request FA Groups data
fa_manager.request_fa_data(yatws::FADataType::Groups)?;
// Get the current FA configuration
let fa_config = fa_manager.get_config();
println!("FA Groups: {:?}", fa_config.groups);
YATWS supports an observer pattern for handling asynchronous events. This is a push programming model where events are pushed to you. You initiate the request and implement on_event methods. This is the closest to the official IBKR TWS API, except you have observers split by functionality rather than a single EWrapper.
This is best suited for event-driven trading, such as reacting to news, order fills and buying power depletion.
use yatws::{IBKRClient, IBKRError, contract::Contract, data::{MarketDataType, TickType, TickAttrib}, data_observer::MarketDataObserver};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
#[derive(Debug)]
struct MyMarketObserver {
name: String,
tick_count: Arc<Mutex<usize>>,
}
impl MarketDataObserver for MyMarketObserver {
fn on_tick_price(&self, req_id: i32, tick_type: TickType, price: f64, attrib: TickAttrib) {
println!("[{}] TickPrice: ReqID={}, Type={:?}, Price={}, Attrib={:?}", self.name, req_id, tick_type, price, attrib);
*self.tick_count.lock().unwrap() += 1;
}
// Implement other MarketDataObserver methods as needed (on_tick_size, on_error, etc.)
fn on_error(&self, req_id: i32, error_code: i32, error_message: &str) {
eprintln!("[{}] Error: ReqID={}, Code={}, Msg='{}'", self.name, req_id, error_code, error_message);
}
}
fn main() -> Result<(), IBKRError> {
let client = IBKRClient::new("127.0.0.1", 7497, 2, None)?;
let market_mgr = client.data_market();
let contract = Contract::stock("AAPL");
let observer = MyMarketObserver {
name: "AAPL-Observer".to_string(),
tick_count: Arc::new(Mutex::new(0)),
};
// Request streaming data and associate it with the observer
let (req_id, _observer_id) = market_mgr.request_observe_market_data(
&contract,
&[GenericTickType::MiscellaneousStats], // Empty generic_tick_list for default ticks
false, // snapshot = false for streaming
false, // regulatory_snapshot
&[], // mkt_data_options
Some(MarketDataType::Delayed), // Or RealTime if subscribed
observer,
)?;
println!("Observing market data for AAPL with ReqID: {}", req_id);
thread::sleep(Duration::from_secs(15)); // Observe for 15 seconds
market_mgr.cancel_market_data(req_id)?; // Cancel the stream
println!("Cancelled market data for AAPL.");
Ok(())
}
Simimlar to the ibapi crate, you can use a subscription programming model. This is in-between the sync and async models desribed above. This is best suited for receiving a stream of data. This is a pull model where you receive an iterator of events.
use yatws::{IBKRClient, IBKRError};
use yatws::account_subscription::{AccountSubscription, AccountEvent};
use std::time::Duration;
use std::thread;
fn main() -> Result<(), IBKRError> {
let client = IBKRClient::new("127.0.0.1", 7497, 1, None)?; // Use a unique client_id
let account_manager = client.account();
let mut account_sub = AccountSubscription::new(account_manager)?;
println!("AccountSubscription created for account: {}", account_sub.account_id());
// Example: Print initial summary
if let Some(summary) = account_sub.last_known_summary() {
println!("Initial Net Liquidation: {}", summary.net_liquidation);
}
// Spawn a thread to listen for events
let event_receiver = account_sub.events(); // Get a receiver clone
let event_thread = thread::spawn(move || {
for event in event_receiver { // Iterates until channel is disconnected
match event {
AccountEvent::SummaryUpdate { info, .. } => {
println!("[Thread] Account Summary Update: NetLiq = {}", info.net_liquidation);
}
AccountEvent::PositionUpdate { position, .. } => {
println!("[Thread] Position Update: {} {} @ {}",
position.symbol, position.quantity, position.market_price);
}
AccountEvent::ExecutionUpdate { execution, .. } => {
println!("[Thread] Execution: {} {} {} @ {}",
execution.side, execution.quantity, execution.symbol, execution.price);
}
AccountEvent::Error { error, .. } => {
eprintln!("[Thread] AccountSubscription Error: {:?}", error);
}
AccountEvent::Closed { account_id, .. } => {
println!("[Thread] AccountSubscription for {} closed.", account_id);
break; // Exit loop
}
}
}
});
// Let the subscription run for a bit
thread::sleep(Duration::from_secs(60));
// Explicitly close the subscription
println!("Main thread: Closing AccountSubscription...");
account_sub.close()?; // This will send AccountEvent::Closed and disconnect the channel
// Wait for the event thread to finish
event_thread.join().expect("Event thread panicked");
println!("Main thread: Done.");
Ok(())
}
use yatws::{IBKRClient, IBKRError, news_subscription::NewsEvent};
use std::time::Duration;
use std::thread;
fn main() -> Result<(), IBKRError> {
let client = IBKRClient::new("127.0.0.1", 7497, 3, None)?;
let news_manager = client.data_news();
// Subscribe to new news bulletins
let mut news_sub = news_manager.subscribe_news_bulletins_stream(false).submit()?;
println!("Subscribed to news bulletins (ReqID: {}).", news_sub.request_id());
let event_receiver = news_sub.events();
let news_thread = thread::spawn(move || {
for event in event_receiver {
match event {
NewsEvent::Bulletin { article, .. } => {
println!("[Thread] News Bulletin: Provider={}, ID={}, Headline='{}'",
article.provider_code, article.article_id, article.headline);
}
NewsEvent::Error(e) => {
eprintln!("[Thread] NewsSubscription Error: {:?}", e);
}
NewsEvent::Closed { .. } => {
println!("[Thread] NewsSubscription closed.");
break;
}
}
}
});
thread::sleep(Duration::from_secs(60)); // Listen for news for 60 seconds
println!("Main thread: Closing NewsSubscription...");
news_sub.cancel()?; // Explicitly cancel the subscription
news_thread.join().expect("News thread panicked");
println!("Main thread: Done with news.");
Ok(())
}
// Enable session recording
let client = IBKRClient::new(
"127.0.0.1",
7497,
0,
Some(("sessions.db", "my_trading_session"))
)?;
// Later, replay the session
let replay_client = IBKRClient::from_db("sessions.db", "my_trading_session")?;
// Create a price-conditional order
let (contract, order) = OrderBuilder::new(OrderSide::Buy, 100.0)
.for_stock("AAPL")
.limit(150.0)
.add_price_condition(
265598, // SPY con_id
"ISLAND", // Exchange
400.0, // Price
TriggerMethod::Last, // Trigger method
false // Is less than 400
)
.build()?;
YATWS provides built-in rate limiting to ensure compliance with Interactive Brokers' API limits:
// Enable rate limiting with default settings (50 msgs/sec, 50 historical requests, 100 market data lines)
client.enable_rate_limiting()?;
// Configure custom rate limiting settings
let mut config = RateLimiterConfig::default();
config.enabled = true;
config.max_messages_per_second = 40; // Be more conservative
config.max_historical_requests = 30; // Lower than default 50
config.rate_limit_wait_timeout = Duration::from_secs(10); // Longer timeout
client.configure_rate_limiter(config)?;
// Check current rate limiter status
if let Some(status) = client.get_rate_limiter_status() {
println!("Rate limiting enabled: {}", status.enabled);
println!("Current message rate: {:.2} msgs/sec", status.current_message_rate);
println!("Active historical requests: {}/{}",
status.active_historical_requests,
config.max_historical_requests);
}
// Disable rate limiting when needed
client.disable_rate_limiting()?;
// Clean up stale requests (useful for long-running applications)
let (hist_cleaned, mkt_cleaned) = client.cleanup_stale_rate_limiter_requests(
Duration::from_secs(300) // Clean up requests older than 5 minutes
)?;
println!("Cleaned up {} historical and {} market data requests", hist_cleaned, mkt_cleaned);
YATWS uses Rust's Result pattern consistently, with a custom IBKRError type:
match client.account().get_net_liquidation() {
Ok(net_liq_value) => println!("Account Net Liquidation Value: ${}", net_liq_value),
Err(IBKRError::Timeout) => println!("Operation timed out"),
Err(IBKRError::ApiError(code, msg)) => println!("API error {}: {}", code, msg),
Err(e) => println!("Other error: {:?}", e),
}
For detailed information about the available functions, structs, and traits, please refer to the API documentation on docs.rs or the API reference on GitHub.
Key components include:
IBKRClient: Primary client for interacting with the TWS APIOrderManager: Order-related operationsAccountManager: Account and portfolio data (including liquidation warning status)DataMarketManager: Market data operationsDataRefManager: Reference dataDataNewsManager: News dataDataFundamentalsManager: Financial dataFinancialAdvisorManager: Financial Advisor configurationsOrderBuilder: Fluent API for creating ordersOptionsStrategyBuilder: Factory for common options strategies