use crate::error::GreeksError;
use crate::model::types::Side;
use positive::Positive;
use rust_decimal::Decimal;
pub trait LegAble {
fn get_symbol(&self) -> &str;
fn get_quantity(&self) -> Positive;
fn get_side(&self) -> Side;
fn pnl_at_price(&self, price: Positive) -> Decimal;
fn total_cost(&self) -> Positive;
fn fees(&self) -> Positive;
fn delta(&self) -> Result<Decimal, GreeksError>;
fn gamma(&self) -> Result<Decimal, GreeksError> {
Ok(Decimal::ZERO)
}
fn theta(&self) -> Result<Decimal, GreeksError> {
Ok(Decimal::ZERO)
}
fn vega(&self) -> Result<Decimal, GreeksError> {
Ok(Decimal::ZERO)
}
fn rho(&self) -> Result<Decimal, GreeksError> {
Ok(Decimal::ZERO)
}
#[must_use]
fn is_long(&self) -> bool {
matches!(self.get_side(), Side::Long)
}
#[must_use]
fn is_short(&self) -> bool {
matches!(self.get_side(), Side::Short)
}
fn notional_value(&self, price: Positive) -> Positive {
self.get_quantity() * price
}
}
pub trait Marginable: LegAble {
fn initial_margin(&self) -> Positive;
fn maintenance_margin(&self) -> Positive;
fn leverage(&self) -> Positive;
fn liquidation_price(&self, current_price: Positive) -> Positive;
fn is_liquidation_risk(&self, current_price: Positive, margin_ratio: Decimal) -> bool;
}
pub trait Fundable: LegAble {
fn funding_rate(&self) -> Decimal;
fn funding_interval_hours(&self) -> u32;
fn funding_payment(&self, mark_price: Positive) -> Decimal;
fn annualized_funding(&self, mark_price: Positive) -> Decimal {
let payment = self.funding_payment(mark_price);
let periods_per_year = Decimal::from(24 * 365 / self.funding_interval_hours());
payment * periods_per_year
}
}
pub trait Expirable: LegAble {
fn expiration_timestamp(&self) -> i64;
fn days_to_expiration(&self) -> Positive;
fn is_expired(&self) -> bool;
fn time_to_expiration_years(&self) -> Decimal {
self.days_to_expiration().to_dec() / Decimal::from(365)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockLeg {
symbol: String,
quantity: Positive,
side: Side,
cost_basis: Positive,
fees: Positive,
}
impl LegAble for MockLeg {
fn get_symbol(&self) -> &str {
&self.symbol
}
fn get_quantity(&self) -> Positive {
self.quantity
}
fn get_side(&self) -> Side {
self.side
}
fn pnl_at_price(&self, price: Positive) -> Decimal {
let value_change = (price.to_dec() - self.cost_basis.to_dec()) * self.quantity.to_dec();
match self.side {
Side::Long => value_change,
Side::Short => -value_change,
}
}
fn total_cost(&self) -> Positive {
self.cost_basis * self.quantity + self.fees
}
fn fees(&self) -> Positive {
self.fees
}
fn delta(&self) -> Result<Decimal, GreeksError> {
let delta_per_unit = match self.side {
Side::Long => Decimal::ONE,
Side::Short => -Decimal::ONE,
};
Ok(delta_per_unit * self.quantity.to_dec())
}
}
#[test]
fn test_mock_leg_long_pnl() {
let leg = MockLeg {
symbol: "BTC".to_string(),
quantity: positive::Positive::ONE,
side: Side::Long,
cost_basis: positive::pos_or_panic!(50000.0),
fees: positive::pos_or_panic!(10.0),
};
let pnl = leg.pnl_at_price(positive::pos_or_panic!(55000.0));
assert_eq!(pnl, Decimal::from(5000));
let pnl = leg.pnl_at_price(positive::pos_or_panic!(45000.0));
assert_eq!(pnl, Decimal::from(-5000));
}
#[test]
fn test_mock_leg_short_pnl() {
let leg = MockLeg {
symbol: "BTC".to_string(),
quantity: positive::Positive::ONE,
side: Side::Short,
cost_basis: positive::pos_or_panic!(50000.0),
fees: positive::pos_or_panic!(10.0),
};
let pnl = leg.pnl_at_price(positive::pos_or_panic!(55000.0));
assert_eq!(pnl, Decimal::from(-5000));
let pnl = leg.pnl_at_price(positive::pos_or_panic!(45000.0));
assert_eq!(pnl, Decimal::from(5000));
}
#[test]
fn test_mock_leg_delta() {
let long_leg = MockLeg {
symbol: "BTC".to_string(),
quantity: positive::Positive::TWO,
side: Side::Long,
cost_basis: positive::pos_or_panic!(50000.0),
fees: positive::pos_or_panic!(10.0),
};
let short_leg = MockLeg {
symbol: "BTC".to_string(),
quantity: positive::Positive::TWO,
side: Side::Short,
cost_basis: positive::pos_or_panic!(50000.0),
fees: positive::pos_or_panic!(10.0),
};
assert_eq!(long_leg.delta().unwrap(), Decimal::from(2));
assert_eq!(short_leg.delta().unwrap(), Decimal::from(-2));
}
#[test]
fn test_is_long_short() {
let long_leg = MockLeg {
symbol: "BTC".to_string(),
quantity: positive::Positive::ONE,
side: Side::Long,
cost_basis: positive::pos_or_panic!(50000.0),
fees: positive::pos_or_panic!(10.0),
};
let short_leg = MockLeg {
symbol: "BTC".to_string(),
quantity: Positive::ONE,
side: Side::Short,
cost_basis: positive::pos_or_panic!(50000.0),
fees: positive::pos_or_panic!(10.0),
};
assert!(long_leg.is_long());
assert!(!long_leg.is_short());
assert!(!short_leg.is_long());
assert!(short_leg.is_short());
}
#[test]
fn test_notional_value() {
let leg = MockLeg {
symbol: "BTC".to_string(),
quantity: positive::Positive::TWO,
side: Side::Long,
cost_basis: positive::pos_or_panic!(50000.0),
fees: positive::pos_or_panic!(10.0),
};
let notional = leg.notional_value(positive::pos_or_panic!(55000.0));
assert_eq!(notional, positive::pos_or_panic!(110000.0));
}
}