Skip to main content

trader_bot/domain/
trade.rs

1//! Trade type for recording executed trades.
2
3use crate::domain::{Money, OrderDirection};
4use chrono::{DateTime, Utc};
5use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7
8/// A unique trade identifier.
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct TradeId(String);
11
12impl TradeId {
13    /// Creates a new trade ID.
14    #[must_use]
15    pub fn new(id: impl Into<String>) -> Self {
16        Self(id.into())
17    }
18
19    /// Creates a new random trade ID.
20    #[must_use]
21    pub fn random() -> Self {
22        Self(uuid::Uuid::new_v4().to_string())
23    }
24
25    /// Returns the ID as a string slice.
26    #[must_use]
27    pub fn as_str(&self) -> &str {
28        &self.0
29    }
30}
31
32impl std::fmt::Display for TradeId {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(f, "{}", self.0)
35    }
36}
37
38/// A completed trade execution.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct Trade {
41    /// Unique trade identifier.
42    id: TradeId,
43    /// Related order ID (if any).
44    order_id: Option<String>,
45    /// Trading symbol.
46    symbol: String,
47    /// Exchange where trade occurred.
48    exchange: String,
49    /// Trade direction (buy/sell).
50    direction: OrderDirection,
51    /// Number of lots traded.
52    quantity_lots: u32,
53    /// Price per lot.
54    price: Money,
55    /// Total value of trade (quantity * price).
56    value: Money,
57    /// Commission/fee paid.
58    commission: Option<Money>,
59    /// Time of trade execution.
60    executed_at: DateTime<Utc>,
61}
62
63impl Trade {
64    /// Creates a new trade.
65    #[must_use]
66    pub fn new(
67        symbol: impl Into<String>,
68        exchange: impl Into<String>,
69        direction: OrderDirection,
70        quantity_lots: u32,
71        price: Money,
72    ) -> Self {
73        let value = price.multiply(Decimal::from(quantity_lots));
74        Self {
75            id: TradeId::random(),
76            order_id: None,
77            symbol: symbol.into(),
78            exchange: exchange.into(),
79            direction,
80            quantity_lots,
81            price,
82            value,
83            commission: None,
84            executed_at: Utc::now(),
85        }
86    }
87
88    /// Creates a buy trade.
89    #[must_use]
90    pub fn buy(
91        symbol: impl Into<String>,
92        exchange: impl Into<String>,
93        quantity_lots: u32,
94        price: Money,
95    ) -> Self {
96        Self::new(symbol, exchange, OrderDirection::Buy, quantity_lots, price)
97    }
98
99    /// Creates a sell trade.
100    #[must_use]
101    pub fn sell(
102        symbol: impl Into<String>,
103        exchange: impl Into<String>,
104        quantity_lots: u32,
105        price: Money,
106    ) -> Self {
107        Self::new(symbol, exchange, OrderDirection::Sell, quantity_lots, price)
108    }
109
110    /// Sets the trade ID.
111    #[must_use]
112    pub fn with_id(mut self, id: TradeId) -> Self {
113        self.id = id;
114        self
115    }
116
117    /// Sets the order ID.
118    #[must_use]
119    pub fn with_order_id(mut self, order_id: impl Into<String>) -> Self {
120        self.order_id = Some(order_id.into());
121        self
122    }
123
124    /// Sets the commission.
125    #[must_use]
126    pub fn with_commission(mut self, commission: Money) -> Self {
127        self.commission = Some(commission);
128        self
129    }
130
131    /// Sets the execution time.
132    #[must_use]
133    pub fn with_executed_at(mut self, executed_at: DateTime<Utc>) -> Self {
134        self.executed_at = executed_at;
135        self
136    }
137
138    /// Returns the trade ID.
139    #[must_use]
140    pub fn id(&self) -> &TradeId {
141        &self.id
142    }
143
144    /// Returns the order ID if any.
145    #[must_use]
146    pub fn order_id(&self) -> Option<&str> {
147        self.order_id.as_deref()
148    }
149
150    /// Returns the symbol.
151    #[must_use]
152    pub fn symbol(&self) -> &str {
153        &self.symbol
154    }
155
156    /// Returns the exchange.
157    #[must_use]
158    pub fn exchange(&self) -> &str {
159        &self.exchange
160    }
161
162    /// Returns the trade direction.
163    #[must_use]
164    pub fn direction(&self) -> OrderDirection {
165        self.direction
166    }
167
168    /// Returns the quantity in lots.
169    #[must_use]
170    pub fn quantity_lots(&self) -> u32 {
171        self.quantity_lots
172    }
173
174    /// Returns the price per lot.
175    #[must_use]
176    pub fn price(&self) -> &Money {
177        &self.price
178    }
179
180    /// Returns the total trade value.
181    #[must_use]
182    pub fn value(&self) -> &Money {
183        &self.value
184    }
185
186    /// Returns the commission if any.
187    #[must_use]
188    pub fn commission(&self) -> Option<&Money> {
189        self.commission.as_ref()
190    }
191
192    /// Returns the execution time.
193    #[must_use]
194    pub fn executed_at(&self) -> DateTime<Utc> {
195        self.executed_at
196    }
197
198    /// Returns true if this is a buy trade.
199    #[must_use]
200    pub fn is_buy(&self) -> bool {
201        matches!(self.direction, OrderDirection::Buy)
202    }
203
204    /// Returns true if this is a sell trade.
205    #[must_use]
206    pub fn is_sell(&self) -> bool {
207        matches!(self.direction, OrderDirection::Sell)
208    }
209
210    /// Returns the net value (value minus commission).
211    #[must_use]
212    pub fn net_value(&self) -> Money {
213        match &self.commission {
214            Some(comm) if self.is_buy() => {
215                // For buys, we pay value + commission
216                self.value.checked_add(comm).unwrap_or(self.value.clone())
217            }
218            Some(comm) if self.is_sell() => {
219                // For sells, we receive value - commission
220                self.value.checked_sub(comm).unwrap_or(self.value.clone())
221            }
222            _ => self.value.clone(),
223        }
224    }
225}
226
227/// A collection of trades for history tracking.
228#[derive(Debug, Clone, Default, Serialize, Deserialize)]
229pub struct TradeHistory {
230    trades: Vec<Trade>,
231}
232
233impl TradeHistory {
234    /// Creates a new empty trade history.
235    #[must_use]
236    pub fn new() -> Self {
237        Self { trades: Vec::new() }
238    }
239
240    /// Adds a trade to the history.
241    pub fn add(&mut self, trade: Trade) {
242        self.trades.push(trade);
243    }
244
245    /// Returns all trades.
246    #[must_use]
247    pub fn trades(&self) -> &[Trade] {
248        &self.trades
249    }
250
251    /// Returns the number of trades.
252    #[must_use]
253    pub fn len(&self) -> usize {
254        self.trades.len()
255    }
256
257    /// Returns true if there are no trades.
258    #[must_use]
259    pub fn is_empty(&self) -> bool {
260        self.trades.is_empty()
261    }
262
263    /// Returns trades for a specific symbol.
264    pub fn for_symbol<'a>(&'a self, symbol: &'a str) -> impl Iterator<Item = &'a Trade> + 'a {
265        self.trades.iter().filter(move |t| t.symbol() == symbol)
266    }
267
268    /// Returns trades in a time range.
269    pub fn in_range(
270        &self,
271        start: DateTime<Utc>,
272        end: DateTime<Utc>,
273    ) -> impl Iterator<Item = &Trade> + '_ {
274        self.trades
275            .iter()
276            .filter(move |t| t.executed_at >= start && t.executed_at <= end)
277    }
278
279    /// Calculates total buy value for a symbol.
280    #[must_use]
281    pub fn total_buy_value(&self, symbol: &str, currency: &str) -> Money {
282        self.for_symbol(symbol)
283            .filter(|t| t.is_buy())
284            .fold(Money::zero(currency), |acc, t| {
285                acc.checked_add(t.value()).unwrap_or(acc)
286            })
287    }
288
289    /// Calculates total sell value for a symbol.
290    #[must_use]
291    pub fn total_sell_value(&self, symbol: &str, currency: &str) -> Money {
292        self.for_symbol(symbol)
293            .filter(|t| t.is_sell())
294            .fold(Money::zero(currency), |acc, t| {
295                acc.checked_add(t.value()).unwrap_or(acc)
296            })
297    }
298
299    /// Clears all trades.
300    pub fn clear(&mut self) {
301        self.trades.clear();
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use rust_decimal_macros::dec;
309
310    fn make_usd(amount: Decimal) -> Money {
311        Money::new(amount, "USD")
312    }
313
314    #[test]
315    fn test_trade_creation() {
316        let trade = Trade::buy("AAPL", "nasdaq", 10, make_usd(dec!(150)));
317
318        assert_eq!(trade.symbol(), "AAPL");
319        assert_eq!(trade.quantity_lots(), 10);
320        assert_eq!(trade.price().amount(), dec!(150));
321        assert_eq!(trade.value().amount(), dec!(1500));
322        assert!(trade.is_buy());
323    }
324
325    #[test]
326    fn test_trade_with_commission() {
327        let trade = Trade::buy("AAPL", "nasdaq", 10, make_usd(dec!(100)))
328            .with_commission(make_usd(dec!(5)));
329
330        assert_eq!(trade.commission().unwrap().amount(), dec!(5));
331        // Net value for buy = value + commission
332        assert_eq!(trade.net_value().amount(), dec!(1005));
333    }
334
335    #[test]
336    fn test_sell_net_value() {
337        let trade = Trade::sell("AAPL", "nasdaq", 10, make_usd(dec!(100)))
338            .with_commission(make_usd(dec!(5)));
339
340        // Net value for sell = value - commission
341        assert_eq!(trade.net_value().amount(), dec!(995));
342    }
343
344    #[test]
345    fn test_trade_history() {
346        let mut history = TradeHistory::new();
347
348        history.add(Trade::buy("AAPL", "nasdaq", 10, make_usd(dec!(100))));
349        history.add(Trade::sell("AAPL", "nasdaq", 5, make_usd(dec!(110))));
350        history.add(Trade::buy("GOOGL", "nasdaq", 2, make_usd(dec!(2800))));
351
352        assert_eq!(history.len(), 3);
353        assert_eq!(history.for_symbol("AAPL").count(), 2);
354        assert_eq!(history.for_symbol("GOOGL").count(), 1);
355    }
356
357    #[test]
358    fn test_trade_id() {
359        let id = TradeId::new("test-123");
360        assert_eq!(id.as_str(), "test-123");
361        assert_eq!(id.to_string(), "test-123");
362    }
363}