use crate::error::{GreeksError, PricingError};
use crate::greeks::Greeks;
use optionstratlib_core::model::Side;
use optionstratlib_core::model::decimal::{d_div, d_mul};
use optionstratlib_core::model::leg::traits::{Expirable, Fundable};
use optionstratlib_core::model::leg::{FuturePosition, Leg, PerpetualPosition, SpotPosition};
use rust_decimal::Decimal;
pub trait LegGreeks {
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)
}
}
impl LegGreeks for SpotPosition {
fn delta(&self) -> Result<Decimal, GreeksError> {
let delta_per_unit = match self.side {
Side::Long => Decimal::ONE,
Side::Short => -Decimal::ONE,
};
Ok(d_mul(
delta_per_unit,
self.quantity.to_dec(),
"greeks::legs::spot::delta",
)?)
}
}
impl LegGreeks for FuturePosition {
fn delta(&self) -> Result<Decimal, GreeksError> {
let delta_per_contract = match self.side {
Side::Long => self.contract_size.to_dec(),
Side::Short => -self.contract_size.to_dec(),
};
Ok(d_mul(
delta_per_contract,
self.quantity.to_dec(),
"greeks::legs::future::delta",
)?)
}
fn rho(&self) -> Result<Decimal, GreeksError> {
let time_to_exp = self
.time_to_expiration_years()
.map_err(PricingError::from)?;
let notional = self
.notional_value_at_entry()
.map_err(PricingError::from)?
.to_dec();
let signed_notional = match self.side {
Side::Long => notional,
Side::Short => -notional,
};
let rho_value = d_mul(signed_notional, time_to_exp, "greeks::legs::future::rho")?;
Ok(d_div(
rho_value,
Decimal::ONE_HUNDRED,
"greeks::legs::future::rho_per_point",
)?)
}
}
impl LegGreeks for PerpetualPosition {
fn delta(&self) -> Result<Decimal, GreeksError> {
let delta_per_unit = match self.side {
Side::Long => Decimal::ONE,
Side::Short => -Decimal::ONE,
};
Ok(d_mul(
delta_per_unit,
self.quantity.to_dec(),
"greeks::legs::perpetual::delta",
)?)
}
fn theta(&self) -> Result<Decimal, GreeksError> {
Ok(-self
.funding_payment(self.entry_price)
.map_err(PricingError::from)?)
}
}
impl LegGreeks for Leg {
fn delta(&self) -> Result<Decimal, GreeksError> {
match self {
Self::Option(pos) => Greeks::delta(pos.as_ref()),
Self::Spot(pos) => pos.delta(),
Self::Future(pos) => pos.delta(),
Self::Perpetual(pos) => pos.delta(),
}
}
fn gamma(&self) -> Result<Decimal, GreeksError> {
match self {
Self::Option(pos) => Greeks::gamma(pos.as_ref()),
Self::Spot(_) | Self::Future(_) | Self::Perpetual(_) => Ok(Decimal::ZERO),
}
}
fn theta(&self) -> Result<Decimal, GreeksError> {
match self {
Self::Option(pos) => Greeks::theta(pos.as_ref()),
Self::Spot(pos) => pos.theta(),
Self::Future(pos) => pos.theta(),
Self::Perpetual(pos) => pos.theta(),
}
}
fn vega(&self) -> Result<Decimal, GreeksError> {
match self {
Self::Option(pos) => Greeks::vega(pos.as_ref()),
Self::Spot(_) | Self::Future(_) | Self::Perpetual(_) => Ok(Decimal::ZERO),
}
}
fn rho(&self) -> Result<Decimal, GreeksError> {
match self {
Self::Option(pos) => Greeks::rho(pos.as_ref()),
Self::Spot(pos) => pos.rho(),
Self::Future(pos) => pos.rho(),
Self::Perpetual(pos) => pos.rho(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use optionstratlib_core::model::ExpirationDate;
use optionstratlib_core::{model::Positive, pos_or_panic};
use rust_decimal_macros::dec;
fn spot_long() -> SpotPosition {
SpotPosition::long("AAPL".to_string(), Positive::HUNDRED, pos_or_panic!(150.0))
}
fn future(long: bool) -> FuturePosition {
let build = if long {
FuturePosition::long
} else {
FuturePosition::short
};
build(
"ES".to_string(),
Positive::TWO,
pos_or_panic!(4500.0),
ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(50.0),
pos_or_panic!(15000.0),
)
}
fn perpetual(long: bool) -> PerpetualPosition {
let build = if long {
PerpetualPosition::long
} else {
PerpetualPosition::short
};
build(
"BTC-USDT-PERP".to_string(),
Positive::TWO,
pos_or_panic!(50000.0),
pos_or_panic!(10.0),
pos_or_panic!(10000.0),
)
}
fn ok(result: Result<Decimal, GreeksError>) -> Decimal {
match result {
Ok(value) => value,
Err(error) => panic!("greek must compute: {error}"),
}
}
#[test]
fn test_spot_delta_is_one_per_unit_signed_by_side() {
assert_eq!(ok(spot_long().delta()), Decimal::from(100));
let short =
SpotPosition::short("AAPL".to_string(), Positive::HUNDRED, pos_or_panic!(150.0));
assert_eq!(ok(short.delta()), Decimal::from(-100));
}
#[test]
fn test_spot_has_no_other_greek() {
let spot = spot_long();
assert_eq!(ok(spot.gamma()), Decimal::ZERO);
assert_eq!(ok(spot.theta()), Decimal::ZERO);
assert_eq!(ok(spot.vega()), Decimal::ZERO);
assert_eq!(ok(spot.rho()), Decimal::ZERO);
}
#[test]
fn test_future_delta_scales_with_contract_size() {
assert_eq!(ok(future(true).delta()), Decimal::from(100));
assert_eq!(ok(future(false).delta()), Decimal::from(-100));
}
#[test]
fn test_future_rho_is_signed_by_side() {
let long = ok(future(true).rho());
let short = ok(future(false).rho());
assert!(
long > Decimal::ZERO,
"a long future gains with rates: {long}"
);
assert_eq!(long, -short);
}
#[test]
fn test_future_rho_with_an_uncomputable_life_is_an_error_not_zero() {
let mut future = future(true);
future.expiration_date = ExpirationDate::Days(Positive::MAX);
assert!(matches!(future.rho(), Err(GreeksError::Pricing(_))));
}
#[test]
fn test_perpetual_delta_is_one_per_unit_signed_by_side() {
assert_eq!(ok(perpetual(true).delta()), dec!(2));
assert_eq!(ok(perpetual(false).delta()), dec!(-2));
}
#[test]
fn test_perpetual_theta_is_the_negated_funding_payment()
-> Result<(), Box<dyn std::error::Error>> {
let perp = perpetual(true);
let expected = -perp.funding_payment(perp.entry_price)?;
assert_eq!(ok(perp.theta()), expected);
Ok(())
}
#[test]
fn test_leg_greeks_overflow_is_error() {
let mut wide_future = future(true);
wide_future.quantity = Positive::MAX;
assert!(wide_future.rho().is_err());
assert!(wide_future.delta().is_err());
let mut wide_perp = perpetual(true);
wide_perp.quantity = Positive::MAX;
wide_perp.funding_rate = dec!(2);
assert!(wide_perp.theta().is_err());
}
#[test]
fn test_leg_forwards_to_the_wrapped_position() {
assert_eq!(ok(Leg::spot(spot_long()).delta()), Decimal::from(100));
let short =
SpotPosition::short("AAPL".to_string(), Positive::HUNDRED, pos_or_panic!(150.0));
assert_eq!(ok(Leg::spot(short).delta()), Decimal::from(-100));
}
#[test]
fn test_leg_gamma_is_zero_for_every_linear_leg() {
assert_eq!(ok(Leg::spot(spot_long()).gamma()), Decimal::ZERO);
assert_eq!(ok(Leg::future(future(true)).gamma()), Decimal::ZERO);
assert_eq!(ok(Leg::perpetual(perpetual(true)).gamma()), Decimal::ZERO);
}
}