use crate::ExpirationDate;
use crate::error::{PricingError, TransactionError};
use crate::model::Position;
use crate::pnl::transaction::Transaction;
use crate::pnl::utils::PnL;
use crate::strategies::DeltaAdjustment;
use positive::Positive;
pub trait PnLCalculator {
fn calculate_pnl(
&self,
_underlying_price: &Positive,
_expiration_date: ExpirationDate,
_implied_volatility: &Positive,
) -> Result<PnL, PricingError>;
fn calculate_pnl_at_expiration(
&self,
_underlying_price: &Positive,
) -> Result<PnL, PricingError>;
fn adjustments_pnl(&self, _adjustments: &DeltaAdjustment) -> Result<PnL, PricingError> {
Err(PricingError::method_error(
"adjustments_pnl",
&format!("not implemented for {}", std::any::type_name::<Self>()),
))
}
fn diff_position_pnl(&self, _position: &Position) -> Result<PnL, PricingError> {
Err(PricingError::method_error(
"diff_position_pnl",
&format!("not implemented for {}", std::any::type_name::<Self>()),
))
}
}
pub trait TransactionAble {
fn add_transaction(&mut self, transaction: Transaction) -> Result<(), TransactionError>;
fn get_transactions(&self) -> Result<Vec<Transaction>, TransactionError>;
}
#[cfg(test)]
mod tests_pnl_calculator {
use super::*;
use chrono::Utc;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
#[test]
fn test_pnl_new() {
let now = Utc::now();
let pnl = PnL::new(
Some(dec!(100.0)),
Some(dec!(50.0)),
pos_or_panic!(25.0),
pos_or_panic!(75.0),
now,
);
assert_eq!(pnl.realized, Some(dec!(100.0)));
assert_eq!(pnl.unrealized, Some(dec!(50.0)));
assert_eq!(pnl.initial_costs, 25.0);
assert_eq!(pnl.initial_income, 75.0);
assert_eq!(pnl.date_time, now);
}
#[test]
fn test_pnl_with_none_values() {
let now = Utc::now();
let pnl = PnL::new(None, None, pos_or_panic!(10.0), pos_or_panic!(20.0), now);
assert_eq!(pnl.realized, None);
assert_eq!(pnl.unrealized, None);
assert_eq!(pnl.initial_costs, pos_or_panic!(10.0));
assert_eq!(pnl.initial_income, pos_or_panic!(20.0));
assert_eq!(pnl.date_time, now);
}
struct DummyOption;
impl TransactionAble for DummyOption {
fn add_transaction(&mut self, _transaction: Transaction) -> Result<(), TransactionError> {
unimplemented!()
}
fn get_transactions(&self) -> Result<Vec<Transaction>, TransactionError> {
unimplemented!()
}
}
impl PnLCalculator for DummyOption {
fn calculate_pnl(
&self,
market_price: &Positive,
expiration_date: ExpirationDate,
_implied_volatility: &Positive,
) -> Result<PnL, PricingError> {
Ok(PnL::new(
Some(market_price.into()),
None,
pos_or_panic!(10.0),
pos_or_panic!(20.0),
expiration_date.get_date()?,
))
}
fn calculate_pnl_at_expiration(
&self,
underlying_price: &Positive,
) -> Result<PnL, PricingError> {
let underlying_price = underlying_price.to_dec();
Ok(PnL::new(
Some(underlying_price),
None,
pos_or_panic!(10.0),
pos_or_panic!(20.0),
Utc::now(),
))
}
}
#[test]
fn test_pnl_calculator() {
let dummy = DummyOption;
let now = ExpirationDate::Days(pos_or_panic!(3.0));
let pnl = dummy
.calculate_pnl(&Positive::HUNDRED, now, &Positive::HUNDRED)
.unwrap();
assert_eq!(pnl.realized, Some(dec!(100.0)));
assert_eq!(pnl.unrealized, None);
assert_eq!(pnl.initial_costs, pos_or_panic!(10.0));
assert_eq!(pnl.initial_income, pos_or_panic!(20.0));
assert_eq!(
pnl.date_time.format("%Y-%m-%d").to_string(),
now.get_date_string().unwrap()
);
let pnl_at_expiration = dummy
.calculate_pnl_at_expiration(&pos_or_panic!(150.0))
.unwrap();
assert_eq!(pnl_at_expiration.realized, Some(dec!(150.0)));
assert_eq!(pnl_at_expiration.unrealized, None);
assert_eq!(pnl_at_expiration.initial_costs, pos_or_panic!(10.0));
assert_eq!(pnl_at_expiration.initial_income, pos_or_panic!(20.0));
}
#[test]
fn test_default_adjustments_pnl_returns_method_error() {
let dummy = DummyOption;
let adj = DeltaAdjustment::NoAdjustmentNeeded;
match dummy.adjustments_pnl(&adj) {
Err(PricingError::MethodError { method, reason }) => {
assert_eq!(method, "adjustments_pnl");
assert!(reason.contains("not implemented"));
assert!(reason.contains("DummyOption"));
}
other => panic!("expected MethodError, got {other:?}"),
}
}
#[test]
fn test_default_diff_position_pnl_returns_method_error() {
use crate::model::types::{OptionStyle, Side};
use crate::model::utils::create_sample_option_simplest;
let dummy = DummyOption;
let option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
let position = Position::new(
option,
pos_or_panic!(5.25),
Utc::now(),
pos_or_panic!(0.65),
pos_or_panic!(0.65),
None,
None,
);
match dummy.diff_position_pnl(&position) {
Err(PricingError::MethodError { method, reason }) => {
assert_eq!(method, "diff_position_pnl");
assert!(reason.contains("not implemented"));
assert!(reason.contains("DummyOption"));
}
other => panic!("expected MethodError, got {other:?}"),
}
}
}