use crate::model::Trade;
pub use crate::pnl::PnLCalculator;
use chrono::{DateTime, Utc};
use positive::Positive;
use pretty_simple_display::{DebugPretty, DisplaySimple};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::iter::Sum;
use std::ops::Add;
use utoipa::ToSchema;
#[derive(
DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq, Default, ToSchema,
)]
pub struct PnL {
pub realized: Option<Decimal>,
pub unrealized: Option<Decimal>,
pub initial_costs: Positive,
pub initial_income: Positive,
pub date_time: DateTime<Utc>,
}
impl PnL {
#[inline]
#[must_use]
pub fn new(
realized: Option<Decimal>,
unrealized: Option<Decimal>,
initial_costs: Positive,
initial_income: Positive,
date_time: DateTime<Utc>,
) -> Self {
PnL {
realized,
unrealized,
initial_costs,
initial_income,
date_time,
}
}
#[inline]
#[must_use]
pub fn total_pnl(&self) -> Option<Decimal> {
match (self.realized, self.unrealized) {
(Some(r), Some(u)) => Some(r + u),
(Some(r), None) => Some(r),
(None, Some(u)) => Some(u),
(None, None) => None,
}
}
}
impl Sum for PnL {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
iter.fold(PnL::default(), |acc, x| PnL {
realized: match (acc.realized, x.realized) {
(Some(a), Some(b)) => Some(a + b),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
},
unrealized: match (acc.unrealized, x.unrealized) {
(Some(a), Some(b)) => Some(a + b),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
},
initial_costs: acc.initial_costs + x.initial_costs,
initial_income: acc.initial_income + x.initial_income,
date_time: x.date_time, })
}
}
impl<'a> Sum<&'a PnL> for PnL {
fn sum<I: Iterator<Item = &'a PnL>>(iter: I) -> Self {
iter.fold(PnL::default(), |acc, x| PnL {
realized: match (acc.realized, x.realized) {
(Some(a), Some(b)) => Some(a + b),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
},
unrealized: match (acc.unrealized, x.unrealized) {
(Some(a), Some(b)) => Some(a + b),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
},
initial_costs: acc.initial_costs + x.initial_costs,
initial_income: acc.initial_income + x.initial_income,
date_time: x.date_time, })
}
}
impl Add for PnL {
type Output = Self;
fn add(self, other: Self) -> Self {
PnL {
realized: match (self.realized, other.realized) {
(Some(a), Some(b)) => Some(a + b),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
},
unrealized: match (self.unrealized, other.unrealized) {
(Some(a), Some(b)) => Some(a + b),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
},
initial_costs: self.initial_costs + other.initial_costs,
initial_income: self.initial_income + other.initial_income,
date_time: if self.date_time > other.date_time {
self.date_time
} else {
other.date_time
},
}
}
}
impl Add for &PnL {
type Output = PnL;
fn add(self, other: Self) -> PnL {
PnL {
realized: match (self.realized, other.realized) {
(Some(a), Some(b)) => Some(a + b),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
},
unrealized: match (self.unrealized, other.unrealized) {
(Some(a), Some(b)) => Some(a + b),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
},
initial_costs: self.initial_costs + other.initial_costs,
initial_income: self.initial_income + other.initial_income,
date_time: if self.date_time > other.date_time {
self.date_time
} else {
other.date_time
},
}
}
}
impl From<Trade> for PnL {
fn from(value: Trade) -> Self {
PnL {
realized: Some(value.net()),
unrealized: None,
initial_costs: value.cost(),
initial_income: value.income(),
date_time: value.datetime(),
}
}
}
impl From<&Trade> for PnL {
fn from(value: &Trade) -> Self {
PnL {
realized: Some(value.net()),
unrealized: None,
initial_costs: value.cost(),
initial_income: value.income(),
date_time: value.datetime(),
}
}
}
#[cfg(test)]
mod tests_sum {
use super::*;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
#[test]
fn test_pnl_sum() {
let pnl1 = PnL {
realized: Some(dec!(10.0)),
unrealized: Some(dec!(5.0)),
initial_costs: Positive::TWO,
initial_income: Positive::ONE,
date_time: Utc::now(),
};
let pnl2 = PnL {
realized: Some(dec!(20.0)),
unrealized: Some(dec!(10.0)),
initial_costs: pos_or_panic!(3.0),
initial_income: Positive::TWO,
date_time: Utc::now(),
};
let sum: PnL = vec![pnl1.clone(), pnl2.clone()].into_iter().sum();
assert_eq!(sum.realized, Some(dec!(30.0)));
assert_eq!(sum.unrealized, Some(dec!(15.0)));
assert_eq!(sum.initial_costs, pos_or_panic!(5.0));
assert_eq!(sum.initial_income, pos_or_panic!(3.0));
}
#[test]
fn test_pnl_sum_both_none() {
let pnl1 = PnL {
realized: None,
unrealized: None,
initial_costs: Positive::TWO,
initial_income: Positive::ONE,
date_time: Utc::now(),
};
let pnl2 = PnL {
realized: None,
unrealized: None,
initial_costs: pos_or_panic!(3.0),
initial_income: Positive::TWO,
date_time: Utc::now(),
};
let sum: PnL = vec![pnl1, pnl2].into_iter().sum();
assert_eq!(sum.realized, None);
assert_eq!(sum.unrealized, None);
assert_eq!(sum.initial_costs, pos_or_panic!(5.0));
assert_eq!(sum.initial_income, pos_or_panic!(3.0));
}
#[test]
fn test_pnl_sum_with_none() {
let pnl1 = PnL {
realized: None,
unrealized: Some(dec!(5.0)),
initial_costs: Positive::TWO,
initial_income: Positive::ONE,
date_time: Utc::now(),
};
let pnl2 = PnL {
realized: Some(dec!(20.0)),
unrealized: None,
initial_costs: pos_or_panic!(3.0),
initial_income: Positive::TWO,
date_time: Utc::now(),
};
let sum: PnL = vec![pnl1.clone(), pnl2.clone()].into_iter().sum();
assert_eq!(sum.realized, Some(dec!(20.0)));
assert_eq!(sum.unrealized, Some(dec!(5.0)));
assert_eq!(sum.initial_costs, pos_or_panic!(5.0));
assert_eq!(sum.initial_income, pos_or_panic!(3.0));
}
#[test]
fn test_pnl_sum_reference() {
let pnl1 = PnL {
realized: Some(dec!(10.0)),
unrealized: Some(dec!(5.0)),
initial_costs: Positive::TWO,
initial_income: Positive::ONE,
date_time: Utc::now(),
};
let pnl2 = PnL {
realized: Some(dec!(20.0)),
unrealized: Some(dec!(10.0)),
initial_costs: pos_or_panic!(3.0),
initial_income: Positive::TWO,
date_time: Utc::now(),
};
let sum: PnL = vec![&pnl1, &pnl2].into_iter().sum();
assert_eq!(sum.realized, Some(dec!(30.0)));
assert_eq!(sum.unrealized, Some(dec!(15.0)));
assert_eq!(sum.initial_costs, pos_or_panic!(5.0));
assert_eq!(sum.initial_income, pos_or_panic!(3.0));
}
}
#[cfg(test)]
mod tests_add {
use super::*;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
#[test]
fn test_pnl_add() {
let pnl1 = PnL {
realized: Some(dec!(10.0)),
unrealized: Some(dec!(5.0)),
initial_costs: Positive::TWO,
initial_income: Positive::ONE,
date_time: Utc::now(),
};
let pnl2 = PnL {
realized: Some(dec!(20.0)),
unrealized: Some(dec!(10.0)),
initial_costs: pos_or_panic!(3.0),
initial_income: Positive::TWO,
date_time: Utc::now(),
};
let sum = pnl1 + pnl2;
assert_eq!(sum.realized, Some(dec!(30.0)));
assert_eq!(sum.unrealized, Some(dec!(15.0)));
assert_eq!(sum.initial_costs, pos_or_panic!(5.0));
assert_eq!(sum.initial_income, pos_or_panic!(3.0));
}
#[test]
fn test_pnl_add_ref() {
let pnl1 = PnL {
realized: Some(dec!(10.0)),
unrealized: Some(dec!(5.0)),
initial_costs: Positive::TWO,
initial_income: Positive::ONE,
date_time: Utc::now(),
};
let pnl2 = PnL {
realized: Some(dec!(20.0)),
unrealized: Some(dec!(10.0)),
initial_costs: pos_or_panic!(3.0),
initial_income: Positive::TWO,
date_time: Utc::now(),
};
let sum = &pnl1 + &pnl2;
assert_eq!(sum.realized, Some(dec!(30.0)));
assert_eq!(sum.unrealized, Some(dec!(15.0)));
assert_eq!(sum.initial_costs, pos_or_panic!(5.0));
assert_eq!(sum.initial_income, pos_or_panic!(3.0));
}
}
#[cfg(test)]
mod tests_total_pnl {
use super::*;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
#[test]
fn test_total_pnl_both_some() {
let pnl = PnL::new(
Some(dec!(500.0)),
Some(dec!(250.0)),
Positive::HUNDRED,
pos_or_panic!(350.0),
Utc::now(),
);
assert_eq!(pnl.total_pnl(), Some(dec!(750.0)));
}
#[test]
fn test_total_pnl_only_realized() {
let pnl = PnL::new(
Some(dec!(300.0)),
None,
Positive::HUNDRED,
pos_or_panic!(200.0),
Utc::now(),
);
assert_eq!(pnl.total_pnl(), Some(dec!(300.0)));
}
#[test]
fn test_total_pnl_only_unrealized() {
let pnl = PnL::new(
None,
Some(dec!(150.0)),
pos_or_panic!(50.0),
Positive::HUNDRED,
Utc::now(),
);
assert_eq!(pnl.total_pnl(), Some(dec!(150.0)));
}
#[test]
fn test_total_pnl_both_none() {
let pnl = PnL::new(None, None, Positive::ZERO, Positive::ZERO, Utc::now());
assert_eq!(pnl.total_pnl(), None);
}
#[test]
fn test_total_pnl_negative_values() {
let pnl = PnL::new(
Some(dec!(-200.0)),
Some(dec!(-100.0)),
pos_or_panic!(50.0),
pos_or_panic!(25.0),
Utc::now(),
);
assert_eq!(pnl.total_pnl(), Some(dec!(-300.0)));
}
#[test]
fn test_total_pnl_mixed_signs() {
let pnl = PnL::new(
Some(dec!(500.0)),
Some(dec!(-200.0)),
Positive::HUNDRED,
pos_or_panic!(300.0),
Utc::now(),
);
assert_eq!(pnl.total_pnl(), Some(dec!(300.0)));
}
}