use crate::domain::{Money, Order, OrderDirection, Wallet};
use crate::exchange::{
ExchangeError, ExchangeInfo, ExchangeProvider, ExchangeResult, Instrument, InstrumentType,
MarketData, MarketStatus, OrderBook,
};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::RwLock;
#[derive(Debug)]
pub struct InteractiveBrokersAdapter {
info: ExchangeInfo,
host: String,
port: u16,
client_id: i32,
connected: RwLock<bool>,
accounts: RwLock<HashMap<String, AccountState>>,
}
#[derive(Debug, Clone)]
struct AccountState {
wallet: Wallet,
}
impl InteractiveBrokersAdapter {
#[must_use]
pub fn new() -> Self {
Self {
info: ExchangeInfo {
id: "ibkr".into(),
name: "Interactive Brokers".into(),
url: "https://www.interactivebrokers.com/".into(),
supported_types: vec![
InstrumentType::Stock,
InstrumentType::Etf,
InstrumentType::Bond,
InstrumentType::Currency,
InstrumentType::Futures,
InstrumentType::Options,
],
},
host: "127.0.0.1".into(),
port: 7496, client_id: 1,
connected: RwLock::new(false),
accounts: RwLock::new(HashMap::new()),
}
}
#[must_use]
pub fn with_connection(mut self, host: impl Into<String>, port: u16, client_id: i32) -> Self {
self.host = host.into();
self.port = port;
self.client_id = client_id;
self
}
pub fn connect(&self) -> ExchangeResult<()> {
*self.connected.write().unwrap() = true;
Ok(())
}
fn ensure_connected(&self) -> ExchangeResult<()> {
if !*self.connected.read().unwrap() {
return Err(ExchangeError::NetworkError("Not connected".into()));
}
Ok(())
}
}
impl Default for InteractiveBrokersAdapter {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ExchangeProvider for InteractiveBrokersAdapter {
fn info(&self) -> &ExchangeInfo {
&self.info
}
async fn ping(&self) -> ExchangeResult<()> {
self.ensure_connected()?;
Ok(())
}
async fn get_wallet(&self, account_id: &str) -> ExchangeResult<Wallet> {
self.ensure_connected()?;
let accounts = self.accounts.read().unwrap();
accounts
.get(account_id)
.map(|s| s.wallet.clone())
.ok_or_else(|| ExchangeError::Internal(format!("Account {} not found", account_id)))
}
async fn get_instrument(&self, symbol: &str) -> ExchangeResult<Instrument> {
self.ensure_connected()?;
Err(ExchangeError::InstrumentNotFound(symbol.into()))
}
async fn list_instruments(&self) -> ExchangeResult<Vec<Instrument>> {
self.ensure_connected()?;
Ok(Vec::new())
}
async fn get_market_data(&self, symbol: &str) -> ExchangeResult<MarketData> {
self.ensure_connected()?;
Ok(MarketData {
symbol: symbol.into(),
last_price: Money::zero("USD"),
bid: None,
ask: None,
volume_24h: None,
high_24h: None,
low_24h: None,
change_24h: None,
change_percent_24h: None,
market_status: MarketStatus::Unknown,
timestamp: chrono::Utc::now(),
})
}
async fn get_market_data_batch(&self, symbols: &[String]) -> ExchangeResult<Vec<MarketData>> {
self.ensure_connected()?;
let mut results = Vec::new();
for symbol in symbols {
results.push(self.get_market_data(symbol).await?);
}
Ok(results)
}
async fn get_order_book(&self, symbol: &str, _depth: u32) -> ExchangeResult<OrderBook> {
self.ensure_connected()?;
Ok(OrderBook::empty(symbol))
}
async fn place_order(&self, _account_id: &str, order: Order) -> ExchangeResult<Order> {
self.ensure_connected()?;
Err(ExchangeError::OrderRejected(format!(
"Order placement not implemented for {}",
order.symbol()
)))
}
async fn cancel_order(&self, _account_id: &str, order_id: &str) -> ExchangeResult<Order> {
self.ensure_connected()?;
Err(ExchangeError::OrderNotFound(order_id.into()))
}
async fn get_order(&self, _account_id: &str, order_id: &str) -> ExchangeResult<Order> {
self.ensure_connected()?;
Err(ExchangeError::OrderNotFound(order_id.into()))
}
async fn get_active_orders(&self, _account_id: &str) -> ExchangeResult<Vec<Order>> {
self.ensure_connected()?;
Ok(Vec::new())
}
async fn is_market_open(&self, _symbol: &str) -> ExchangeResult<bool> {
self.ensure_connected()?;
Ok(true)
}
async fn market_buy(&self, account_id: &str, symbol: &str, lots: u32) -> ExchangeResult<Order> {
let order = Order::market(
uuid::Uuid::new_v4().to_string(),
symbol,
&self.info.id,
OrderDirection::Buy,
lots,
);
self.place_order(account_id, order).await
}
async fn market_sell(
&self,
account_id: &str,
symbol: &str,
lots: u32,
) -> ExchangeResult<Order> {
let order = Order::market(
uuid::Uuid::new_v4().to_string(),
symbol,
&self.info.id,
OrderDirection::Sell,
lots,
);
self.place_order(account_id, order).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adapter_creation() {
let adapter = InteractiveBrokersAdapter::new();
assert_eq!(adapter.info().id, "ibkr");
assert_eq!(adapter.port, 7496);
}
#[test]
fn test_adapter_with_connection() {
let adapter = InteractiveBrokersAdapter::new().with_connection("192.168.1.100", 7497, 123);
assert_eq!(adapter.host, "192.168.1.100");
assert_eq!(adapter.port, 7497);
assert_eq!(adapter.client_id, 123);
}
#[test]
fn test_connect() {
let adapter = InteractiveBrokersAdapter::new();
let result = adapter.connect();
assert!(result.is_ok());
}
}