1use crate::domain::{Money, OrderDirection};
4use chrono::{DateTime, Utc};
5use rust_decimal::Decimal;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct TradeId(String);
11
12impl TradeId {
13 #[must_use]
15 pub fn new(id: impl Into<String>) -> Self {
16 Self(id.into())
17 }
18
19 #[must_use]
21 pub fn random() -> Self {
22 Self(uuid::Uuid::new_v4().to_string())
23 }
24
25 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct Trade {
41 id: TradeId,
43 order_id: Option<String>,
45 symbol: String,
47 exchange: String,
49 direction: OrderDirection,
51 quantity_lots: u32,
53 price: Money,
55 value: Money,
57 commission: Option<Money>,
59 executed_at: DateTime<Utc>,
61}
62
63impl Trade {
64 #[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 #[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 #[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 #[must_use]
112 pub fn with_id(mut self, id: TradeId) -> Self {
113 self.id = id;
114 self
115 }
116
117 #[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 #[must_use]
126 pub fn with_commission(mut self, commission: Money) -> Self {
127 self.commission = Some(commission);
128 self
129 }
130
131 #[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 #[must_use]
140 pub fn id(&self) -> &TradeId {
141 &self.id
142 }
143
144 #[must_use]
146 pub fn order_id(&self) -> Option<&str> {
147 self.order_id.as_deref()
148 }
149
150 #[must_use]
152 pub fn symbol(&self) -> &str {
153 &self.symbol
154 }
155
156 #[must_use]
158 pub fn exchange(&self) -> &str {
159 &self.exchange
160 }
161
162 #[must_use]
164 pub fn direction(&self) -> OrderDirection {
165 self.direction
166 }
167
168 #[must_use]
170 pub fn quantity_lots(&self) -> u32 {
171 self.quantity_lots
172 }
173
174 #[must_use]
176 pub fn price(&self) -> &Money {
177 &self.price
178 }
179
180 #[must_use]
182 pub fn value(&self) -> &Money {
183 &self.value
184 }
185
186 #[must_use]
188 pub fn commission(&self) -> Option<&Money> {
189 self.commission.as_ref()
190 }
191
192 #[must_use]
194 pub fn executed_at(&self) -> DateTime<Utc> {
195 self.executed_at
196 }
197
198 #[must_use]
200 pub fn is_buy(&self) -> bool {
201 matches!(self.direction, OrderDirection::Buy)
202 }
203
204 #[must_use]
206 pub fn is_sell(&self) -> bool {
207 matches!(self.direction, OrderDirection::Sell)
208 }
209
210 #[must_use]
212 pub fn net_value(&self) -> Money {
213 match &self.commission {
214 Some(comm) if self.is_buy() => {
215 self.value.checked_add(comm).unwrap_or(self.value.clone())
217 }
218 Some(comm) if self.is_sell() => {
219 self.value.checked_sub(comm).unwrap_or(self.value.clone())
221 }
222 _ => self.value.clone(),
223 }
224 }
225}
226
227#[derive(Debug, Clone, Default, Serialize, Deserialize)]
229pub struct TradeHistory {
230 trades: Vec<Trade>,
231}
232
233impl TradeHistory {
234 #[must_use]
236 pub fn new() -> Self {
237 Self { trades: Vec::new() }
238 }
239
240 pub fn add(&mut self, trade: Trade) {
242 self.trades.push(trade);
243 }
244
245 #[must_use]
247 pub fn trades(&self) -> &[Trade] {
248 &self.trades
249 }
250
251 #[must_use]
253 pub fn len(&self) -> usize {
254 self.trades.len()
255 }
256
257 #[must_use]
259 pub fn is_empty(&self) -> bool {
260 self.trades.is_empty()
261 }
262
263 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 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 #[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 #[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 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 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 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}