Skip to main content

trader_bot/simulator/
exchange.rs

1//! Simulated exchange implementation.
2
3use async_trait::async_trait;
4use chrono::Utc;
5use rust_decimal::Decimal;
6use std::collections::HashMap;
7use std::sync::{Arc, RwLock};
8
9use super::market::MarketManager;
10use crate::domain::{Money, Order, OrderDirection, OrderStatus, Position, Wallet};
11use crate::exchange::{
12    ExchangeError, ExchangeInfo, ExchangeProvider, ExchangeResult, Instrument, InstrumentType,
13    MarketData, MarketStatus, OrderBook, OrderBookEntry,
14};
15
16/// Simulated account state.
17#[derive(Debug, Clone)]
18struct AccountState {
19    cash: Money,
20    positions: HashMap<String, Position>,
21    orders: HashMap<String, Order>,
22}
23
24impl AccountState {
25    fn new(currency: &str, initial_cash: Decimal) -> Self {
26        Self {
27            cash: Money::new(initial_cash, currency),
28            positions: HashMap::new(),
29            orders: HashMap::new(),
30        }
31    }
32
33    fn to_wallet(&self, currency: &str) -> Wallet {
34        let mut wallet = Wallet::with_cash(currency, self.cash.clone());
35        for pos in self.positions.values() {
36            wallet.add_position(pos.clone());
37        }
38        wallet
39    }
40}
41
42/// A simulated exchange for testing.
43#[derive(Debug)]
44pub struct SimulatedExchange {
45    info: ExchangeInfo,
46    market: Arc<RwLock<MarketManager>>,
47    accounts: Arc<RwLock<HashMap<String, AccountState>>>,
48    base_currency: String,
49    /// Simulated latency in milliseconds.
50    latency_ms: u64,
51    /// Whether to fail the next operation (for testing error handling).
52    fail_next: Arc<RwLock<Option<ExchangeError>>>,
53}
54
55impl SimulatedExchange {
56    /// Creates a new simulated exchange.
57    #[must_use]
58    pub fn new(base_currency: impl Into<String>) -> Self {
59        let currency = base_currency.into();
60        Self {
61            info: ExchangeInfo {
62                id: "SIMULATOR".to_string(),
63                name: "Simulated Exchange".to_string(),
64                url: "https://localhost/".to_string(),
65                supported_types: vec![
66                    InstrumentType::Stock,
67                    InstrumentType::Etf,
68                    InstrumentType::Crypto,
69                ],
70            },
71            market: Arc::new(RwLock::new(MarketManager::new(&currency))),
72            accounts: Arc::new(RwLock::new(HashMap::new())),
73            base_currency: currency,
74            latency_ms: 0,
75            fail_next: Arc::new(RwLock::new(None)),
76        }
77    }
78
79    /// Sets the simulated latency.
80    pub fn set_latency(&mut self, ms: u64) {
81        self.latency_ms = ms;
82    }
83
84    /// Makes the next operation fail with the given error.
85    pub fn fail_next_with(&self, error: ExchangeError) {
86        *self.fail_next.write().unwrap() = Some(error);
87    }
88
89    /// Adds an instrument to the market.
90    pub fn add_instrument(
91        &self,
92        symbol: impl Into<String>,
93        name: impl Into<String>,
94        price: Decimal,
95        lot_size: u32,
96    ) {
97        self.market
98            .write()
99            .unwrap()
100            .add_instrument(symbol, name, price, lot_size);
101    }
102
103    /// Creates an account with initial cash.
104    pub fn create_account(&self, account_id: impl Into<String>, initial_cash: Decimal) {
105        let mut accounts = self.accounts.write().unwrap();
106        accounts.insert(
107            account_id.into(),
108            AccountState::new(&self.base_currency, initial_cash),
109        );
110    }
111
112    /// Adds a position to an account.
113    pub fn add_position(&self, account_id: &str, position: Position) {
114        let mut accounts = self.accounts.write().unwrap();
115        if let Some(account) = accounts.get_mut(account_id) {
116            account
117                .positions
118                .insert(position.symbol().to_string(), position);
119        }
120    }
121
122    /// Sets the price for an instrument.
123    pub fn set_price(&self, symbol: &str, price: Decimal) {
124        self.market.write().unwrap().set_price(symbol, price);
125    }
126
127    /// Sets the market status for all instruments.
128    pub fn set_market_status(&self, status: MarketStatus) {
129        self.market.write().unwrap().set_market_status(status);
130    }
131
132    /// Advances the market by one tick.
133    pub fn tick(&self) {
134        self.market.write().unwrap().tick();
135    }
136
137    /// Gets a reference to the market manager.
138    #[must_use]
139    pub fn market(&self) -> Arc<RwLock<MarketManager>> {
140        Arc::clone(&self.market)
141    }
142
143    /// Simulates network latency.
144    async fn simulate_latency(&self) {
145        if self.latency_ms > 0 {
146            tokio::time::sleep(std::time::Duration::from_millis(self.latency_ms)).await;
147        }
148    }
149
150    /// Checks and returns any pending failure.
151    fn check_failure(&self) -> ExchangeResult<()> {
152        let mut fail_next = self.fail_next.write().unwrap();
153        if let Some(error) = fail_next.take() {
154            return Err(error);
155        }
156        Ok(())
157    }
158}
159
160#[async_trait]
161impl ExchangeProvider for SimulatedExchange {
162    fn info(&self) -> &ExchangeInfo {
163        &self.info
164    }
165
166    async fn ping(&self) -> ExchangeResult<()> {
167        self.simulate_latency().await;
168        self.check_failure()?;
169        Ok(())
170    }
171
172    async fn get_wallet(&self, account_id: &str) -> ExchangeResult<Wallet> {
173        self.simulate_latency().await;
174        self.check_failure()?;
175
176        let accounts = self.accounts.read().unwrap();
177        let account = accounts.get(account_id).ok_or_else(|| {
178            ExchangeError::AuthenticationFailed(format!("Account {account_id} not found"))
179        })?;
180
181        // Update position prices from market
182        let market = self.market.read().unwrap();
183        let mut wallet = account.to_wallet(&self.base_currency);
184
185        for pos in wallet.positions_mut() {
186            if let Some(data) = market.get_market_data(pos.symbol()) {
187                pos.update_price(data.last_price);
188            }
189        }
190
191        Ok(wallet)
192    }
193
194    async fn get_instrument(&self, symbol: &str) -> ExchangeResult<Instrument> {
195        self.simulate_latency().await;
196        self.check_failure()?;
197
198        let market = self.market.read().unwrap();
199        market
200            .get_instrument(symbol)
201            .cloned()
202            .ok_or_else(|| ExchangeError::InstrumentNotFound(symbol.to_string()))
203    }
204
205    async fn list_instruments(&self) -> ExchangeResult<Vec<Instrument>> {
206        self.simulate_latency().await;
207        self.check_failure()?;
208
209        let market = self.market.read().unwrap();
210        Ok(market.list_instruments())
211    }
212
213    async fn get_market_data(&self, symbol: &str) -> ExchangeResult<MarketData> {
214        self.simulate_latency().await;
215        self.check_failure()?;
216
217        let market = self.market.read().unwrap();
218        market
219            .get_market_data(symbol)
220            .ok_or_else(|| ExchangeError::InstrumentNotFound(symbol.to_string()))
221    }
222
223    async fn get_order_book(&self, symbol: &str, depth: u32) -> ExchangeResult<OrderBook> {
224        self.simulate_latency().await;
225        self.check_failure()?;
226
227        let market = self.market.read().unwrap();
228        let state = market
229            .get_state(symbol)
230            .ok_or_else(|| ExchangeError::InstrumentNotFound(symbol.to_string()))?;
231
232        // Generate synthetic order book
233        let mut bids = Vec::new();
234        let mut asks = Vec::new();
235        let base_price = state.price;
236
237        for i in 0..depth {
238            let offset = Decimal::from(i + 1) * Decimal::new(1, 2); // 0.01 per level
239
240            bids.push(OrderBookEntry {
241                price: Money::new(base_price - offset, &self.base_currency),
242                quantity: Decimal::from(100 * (depth - i)),
243            });
244
245            asks.push(OrderBookEntry {
246                price: Money::new(base_price + offset, &self.base_currency),
247                quantity: Decimal::from(100 * (depth - i)),
248            });
249        }
250
251        Ok(OrderBook {
252            symbol: symbol.to_string(),
253            bids,
254            asks,
255            timestamp: Utc::now(),
256        })
257    }
258
259    async fn place_order(&self, account_id: &str, mut order: Order) -> ExchangeResult<Order> {
260        self.simulate_latency().await;
261        self.check_failure()?;
262
263        let mut accounts = self.accounts.write().unwrap();
264        let account = accounts.get_mut(account_id).ok_or_else(|| {
265            ExchangeError::AuthenticationFailed(format!("Account {account_id} not found"))
266        })?;
267
268        let market = self.market.read().unwrap();
269        let instrument = market
270            .get_instrument(order.symbol())
271            .ok_or_else(|| ExchangeError::InstrumentNotFound(order.symbol().to_string()))?;
272        let market_data = market
273            .get_market_data(order.symbol())
274            .ok_or_else(|| ExchangeError::InstrumentNotFound(order.symbol().to_string()))?;
275
276        // Check market status
277        if !market_data.market_status.is_tradeable() {
278            return Err(ExchangeError::MarketClosed {
279                instrument: order.symbol().to_string(),
280            });
281        }
282
283        // Calculate trade value
284        let price = market_data.last_price.amount();
285        let lot_value = price * Decimal::from(instrument.lot_size);
286        let total_value = lot_value * Decimal::from(order.quantity_lots());
287
288        match order.direction() {
289            OrderDirection::Buy => {
290                // Check sufficient funds
291                if account.cash.amount() < total_value {
292                    return Err(ExchangeError::InsufficientFunds {
293                        required: total_value.to_string(),
294                        available: account.cash.amount().to_string(),
295                    });
296                }
297
298                // Execute buy
299                let new_cash = account.cash.amount() - total_value;
300                account.cash = Money::new(new_cash, &self.base_currency);
301
302                let quantity = Decimal::from(order.quantity_lots() * instrument.lot_size);
303                if let Some(pos) = account.positions.get_mut(order.symbol()) {
304                    pos.update_quantity(pos.quantity() + quantity);
305                    pos.update_price(market_data.last_price.clone());
306                } else {
307                    let new_pos = Position::builder(order.symbol(), &instrument.exchange)
308                        .quantity(quantity)
309                        .lot_size(instrument.lot_size)
310                        .current_price(market_data.last_price.clone())
311                        .build();
312                    account
313                        .positions
314                        .insert(order.symbol().to_string(), new_pos);
315                }
316            }
317            OrderDirection::Sell => {
318                // Check sufficient position
319                let pos = account.positions.get_mut(order.symbol()).ok_or_else(|| {
320                    ExchangeError::InsufficientFunds {
321                        required: order.quantity_lots().to_string(),
322                        available: "0".to_string(),
323                    }
324                })?;
325
326                let sell_quantity = Decimal::from(order.quantity_lots() * instrument.lot_size);
327                if pos.quantity() < sell_quantity {
328                    return Err(ExchangeError::InsufficientFunds {
329                        required: sell_quantity.to_string(),
330                        available: pos.quantity().to_string(),
331                    });
332                }
333
334                // Execute sell
335                let new_quantity = pos.quantity() - sell_quantity;
336                if new_quantity.is_zero() {
337                    account.positions.remove(order.symbol());
338                } else {
339                    pos.update_quantity(new_quantity);
340                }
341
342                let new_cash = account.cash.amount() + total_value;
343                account.cash = Money::new(new_cash, &self.base_currency);
344            }
345        }
346
347        // Mark order as filled
348        order.set_exchange_order_id(format!(
349            "SIM-{}",
350            Utc::now().timestamp_nanos_opt().unwrap_or(0)
351        ));
352        order.record_fill(order.quantity_lots(), market_data.last_price);
353        order.set_status(OrderStatus::Filled);
354
355        // Store order
356        account.orders.insert(order.id().to_string(), order.clone());
357
358        Ok(order)
359    }
360
361    async fn cancel_order(&self, account_id: &str, order_id: &str) -> ExchangeResult<Order> {
362        self.simulate_latency().await;
363        self.check_failure()?;
364
365        let mut accounts = self.accounts.write().unwrap();
366        let account = accounts.get_mut(account_id).ok_or_else(|| {
367            ExchangeError::AuthenticationFailed(format!("Account {account_id} not found"))
368        })?;
369
370        let mut order = account
371            .orders
372            .get(order_id)
373            .cloned()
374            .ok_or_else(|| ExchangeError::OrderNotFound(order_id.to_string()))?;
375
376        if order.status().is_terminal() {
377            return Err(ExchangeError::InvalidOrder(format!(
378                "Order {order_id} is already in terminal state"
379            )));
380        }
381
382        order.set_status(OrderStatus::Cancelled);
383        account.orders.insert(order_id.to_string(), order.clone());
384
385        Ok(order)
386    }
387
388    async fn get_order(&self, account_id: &str, order_id: &str) -> ExchangeResult<Order> {
389        self.simulate_latency().await;
390        self.check_failure()?;
391
392        let accounts = self.accounts.read().unwrap();
393        let account = accounts.get(account_id).ok_or_else(|| {
394            ExchangeError::AuthenticationFailed(format!("Account {account_id} not found"))
395        })?;
396
397        account
398            .orders
399            .get(order_id)
400            .cloned()
401            .ok_or_else(|| ExchangeError::OrderNotFound(order_id.to_string()))
402    }
403
404    async fn get_active_orders(&self, account_id: &str) -> ExchangeResult<Vec<Order>> {
405        self.simulate_latency().await;
406        self.check_failure()?;
407
408        let accounts = self.accounts.read().unwrap();
409        let account = accounts.get(account_id).ok_or_else(|| {
410            ExchangeError::AuthenticationFailed(format!("Account {account_id} not found"))
411        })?;
412
413        Ok(account
414            .orders
415            .values()
416            .filter(|o| o.status().is_active())
417            .cloned()
418            .collect())
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425    use rust_decimal_macros::dec;
426
427    fn create_test_exchange() -> SimulatedExchange {
428        let exchange = SimulatedExchange::new("USD");
429        exchange.add_instrument("AAPL", "Apple Inc.", dec!(150), 1);
430        exchange.add_instrument("GOOGL", "Alphabet Inc.", dec!(2800), 1);
431        exchange.create_account("test_account", dec!(100000));
432        exchange
433    }
434
435    #[tokio::test]
436    async fn test_get_wallet() {
437        let exchange = create_test_exchange();
438
439        let wallet = exchange.get_wallet("test_account").await.unwrap();
440        assert_eq!(wallet.cash().amount(), dec!(100000));
441        assert_eq!(wallet.position_count(), 0);
442    }
443
444    #[tokio::test]
445    async fn test_place_buy_order() {
446        let exchange = create_test_exchange();
447
448        let order = Order::market("ORD1", "AAPL", "SIMULATOR", OrderDirection::Buy, 10);
449        let result = exchange.place_order("test_account", order).await.unwrap();
450
451        assert_eq!(result.status(), OrderStatus::Filled);
452        assert_eq!(result.filled_lots(), 10);
453
454        // Check wallet updated
455        let wallet = exchange.get_wallet("test_account").await.unwrap();
456        assert_eq!(wallet.cash().amount(), dec!(100000) - dec!(1500)); // 10 * 150
457        assert_eq!(wallet.get_position("AAPL").unwrap().quantity(), dec!(10));
458    }
459
460    #[tokio::test]
461    async fn test_place_sell_order() {
462        let exchange = create_test_exchange();
463
464        // First buy
465        let buy_order = Order::market("ORD1", "AAPL", "SIMULATOR", OrderDirection::Buy, 10);
466        exchange
467            .place_order("test_account", buy_order)
468            .await
469            .unwrap();
470
471        // Then sell
472        let sell_order = Order::market("ORD2", "AAPL", "SIMULATOR", OrderDirection::Sell, 5);
473        let result = exchange
474            .place_order("test_account", sell_order)
475            .await
476            .unwrap();
477
478        assert_eq!(result.status(), OrderStatus::Filled);
479
480        let wallet = exchange.get_wallet("test_account").await.unwrap();
481        assert_eq!(wallet.get_position("AAPL").unwrap().quantity(), dec!(5));
482    }
483
484    #[tokio::test]
485    async fn test_insufficient_funds() {
486        let exchange = SimulatedExchange::new("USD");
487        exchange.add_instrument("EXPENSIVE", "Expensive Stock", dec!(1000000), 1);
488        exchange.create_account("poor_account", dec!(100));
489
490        let order = Order::market("ORD1", "EXPENSIVE", "SIMULATOR", OrderDirection::Buy, 1);
491        let result = exchange.place_order("poor_account", order).await;
492
493        assert!(matches!(
494            result,
495            Err(ExchangeError::InsufficientFunds { .. })
496        ));
497    }
498
499    #[tokio::test]
500    async fn test_market_closed() {
501        let exchange = create_test_exchange();
502        exchange.set_market_status(MarketStatus::Closed);
503
504        let order = Order::market("ORD1", "AAPL", "SIMULATOR", OrderDirection::Buy, 10);
505        let result = exchange.place_order("test_account", order).await;
506
507        assert!(matches!(result, Err(ExchangeError::MarketClosed { .. })));
508    }
509
510    #[tokio::test]
511    async fn test_fail_next() {
512        let exchange = create_test_exchange();
513        exchange.fail_next_with(ExchangeError::NetworkError("Simulated failure".into()));
514
515        let result = exchange.ping().await;
516        assert!(matches!(result, Err(ExchangeError::NetworkError(_))));
517
518        // Next call should succeed
519        let result = exchange.ping().await;
520        assert!(result.is_ok());
521    }
522}