use crate::domain::{Money, OrderDirection};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TradeId(String);
impl TradeId {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
#[must_use]
pub fn random() -> Self {
Self(uuid::Uuid::new_v4().to_string())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for TradeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trade {
id: TradeId,
order_id: Option<String>,
symbol: String,
exchange: String,
direction: OrderDirection,
quantity_lots: u32,
price: Money,
value: Money,
commission: Option<Money>,
executed_at: DateTime<Utc>,
}
impl Trade {
#[must_use]
pub fn new(
symbol: impl Into<String>,
exchange: impl Into<String>,
direction: OrderDirection,
quantity_lots: u32,
price: Money,
) -> Self {
let value = price.multiply(Decimal::from(quantity_lots));
Self {
id: TradeId::random(),
order_id: None,
symbol: symbol.into(),
exchange: exchange.into(),
direction,
quantity_lots,
price,
value,
commission: None,
executed_at: Utc::now(),
}
}
#[must_use]
pub fn buy(
symbol: impl Into<String>,
exchange: impl Into<String>,
quantity_lots: u32,
price: Money,
) -> Self {
Self::new(symbol, exchange, OrderDirection::Buy, quantity_lots, price)
}
#[must_use]
pub fn sell(
symbol: impl Into<String>,
exchange: impl Into<String>,
quantity_lots: u32,
price: Money,
) -> Self {
Self::new(symbol, exchange, OrderDirection::Sell, quantity_lots, price)
}
#[must_use]
pub fn with_id(mut self, id: TradeId) -> Self {
self.id = id;
self
}
#[must_use]
pub fn with_order_id(mut self, order_id: impl Into<String>) -> Self {
self.order_id = Some(order_id.into());
self
}
#[must_use]
pub fn with_commission(mut self, commission: Money) -> Self {
self.commission = Some(commission);
self
}
#[must_use]
pub fn with_executed_at(mut self, executed_at: DateTime<Utc>) -> Self {
self.executed_at = executed_at;
self
}
#[must_use]
pub fn id(&self) -> &TradeId {
&self.id
}
#[must_use]
pub fn order_id(&self) -> Option<&str> {
self.order_id.as_deref()
}
#[must_use]
pub fn symbol(&self) -> &str {
&self.symbol
}
#[must_use]
pub fn exchange(&self) -> &str {
&self.exchange
}
#[must_use]
pub fn direction(&self) -> OrderDirection {
self.direction
}
#[must_use]
pub fn quantity_lots(&self) -> u32 {
self.quantity_lots
}
#[must_use]
pub fn price(&self) -> &Money {
&self.price
}
#[must_use]
pub fn value(&self) -> &Money {
&self.value
}
#[must_use]
pub fn commission(&self) -> Option<&Money> {
self.commission.as_ref()
}
#[must_use]
pub fn executed_at(&self) -> DateTime<Utc> {
self.executed_at
}
#[must_use]
pub fn is_buy(&self) -> bool {
matches!(self.direction, OrderDirection::Buy)
}
#[must_use]
pub fn is_sell(&self) -> bool {
matches!(self.direction, OrderDirection::Sell)
}
#[must_use]
pub fn net_value(&self) -> Money {
match &self.commission {
Some(comm) if self.is_buy() => {
self.value.checked_add(comm).unwrap_or(self.value.clone())
}
Some(comm) if self.is_sell() => {
self.value.checked_sub(comm).unwrap_or(self.value.clone())
}
_ => self.value.clone(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TradeHistory {
trades: Vec<Trade>,
}
impl TradeHistory {
#[must_use]
pub fn new() -> Self {
Self { trades: Vec::new() }
}
pub fn add(&mut self, trade: Trade) {
self.trades.push(trade);
}
#[must_use]
pub fn trades(&self) -> &[Trade] {
&self.trades
}
#[must_use]
pub fn len(&self) -> usize {
self.trades.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.trades.is_empty()
}
pub fn for_symbol<'a>(&'a self, symbol: &'a str) -> impl Iterator<Item = &'a Trade> + 'a {
self.trades.iter().filter(move |t| t.symbol() == symbol)
}
pub fn in_range(
&self,
start: DateTime<Utc>,
end: DateTime<Utc>,
) -> impl Iterator<Item = &Trade> + '_ {
self.trades
.iter()
.filter(move |t| t.executed_at >= start && t.executed_at <= end)
}
#[must_use]
pub fn total_buy_value(&self, symbol: &str, currency: &str) -> Money {
self.for_symbol(symbol)
.filter(|t| t.is_buy())
.fold(Money::zero(currency), |acc, t| {
acc.checked_add(t.value()).unwrap_or(acc)
})
}
#[must_use]
pub fn total_sell_value(&self, symbol: &str, currency: &str) -> Money {
self.for_symbol(symbol)
.filter(|t| t.is_sell())
.fold(Money::zero(currency), |acc, t| {
acc.checked_add(t.value()).unwrap_or(acc)
})
}
pub fn clear(&mut self) {
self.trades.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn make_usd(amount: Decimal) -> Money {
Money::new(amount, "USD")
}
#[test]
fn test_trade_creation() {
let trade = Trade::buy("AAPL", "nasdaq", 10, make_usd(dec!(150)));
assert_eq!(trade.symbol(), "AAPL");
assert_eq!(trade.quantity_lots(), 10);
assert_eq!(trade.price().amount(), dec!(150));
assert_eq!(trade.value().amount(), dec!(1500));
assert!(trade.is_buy());
}
#[test]
fn test_trade_with_commission() {
let trade = Trade::buy("AAPL", "nasdaq", 10, make_usd(dec!(100)))
.with_commission(make_usd(dec!(5)));
assert_eq!(trade.commission().unwrap().amount(), dec!(5));
assert_eq!(trade.net_value().amount(), dec!(1005));
}
#[test]
fn test_sell_net_value() {
let trade = Trade::sell("AAPL", "nasdaq", 10, make_usd(dec!(100)))
.with_commission(make_usd(dec!(5)));
assert_eq!(trade.net_value().amount(), dec!(995));
}
#[test]
fn test_trade_history() {
let mut history = TradeHistory::new();
history.add(Trade::buy("AAPL", "nasdaq", 10, make_usd(dec!(100))));
history.add(Trade::sell("AAPL", "nasdaq", 5, make_usd(dec!(110))));
history.add(Trade::buy("GOOGL", "nasdaq", 2, make_usd(dec!(2800))));
assert_eq!(history.len(), 3);
assert_eq!(history.for_symbol("AAPL").count(), 2);
assert_eq!(history.for_symbol("GOOGL").count(), 1);
}
#[test]
fn test_trade_id() {
let id = TradeId::new("test-123");
assert_eq!(id.as_str(), "test-123");
assert_eq!(id.to_string(), "test-123");
}
}