use super::{define_non_negative_value_type, Error, Leverage, ParamKind, Price, Quantity, Volume};
define_non_negative_value_type!(
Notional,
ParamKind::Notional
);
impl Notional {
pub fn from_price_quantity(price: Price, quantity: Quantity) -> Result<Self, Error> {
let notional = price
.to_decimal()
.abs()
.checked_mul(quantity.to_decimal())
.ok_or(Error::Overflow {
param: ParamKind::Price,
})?;
Ok(Self::new_unchecked(notional))
}
pub fn from_volume(volume: Volume) -> Self {
Self::new_unchecked(volume.to_decimal())
}
pub fn to_volume(self) -> Volume {
Volume::new_unchecked(self.to_decimal())
}
pub fn calculate_margin_required(self, leverage: Leverage) -> Result<Self, Error> {
leverage.calculate_margin_required(self)
}
}
#[cfg(test)]
mod tests {
use super::Notional;
use crate::param::{Error, Leverage, ParamKind, Price, Quantity, Volume};
use rust_decimal::Decimal;
fn d(value: &str) -> Decimal {
value
.parse()
.expect("decimal literal in tests must be valid")
}
#[test]
fn from_price_quantity_computes_absolute_notional() {
let price = Price::from_str("42350.75").expect("must be valid");
let qty = Quantity::from_str("0.15").expect("must be valid");
let notional = Notional::from_price_quantity(price, qty).expect("must be valid");
assert_eq!(notional.to_decimal(), d("6352.6125"));
}
#[test]
fn from_price_quantity_uses_absolute_price() {
let price = Price::from_str("-100.0").expect("must be valid");
let qty = Quantity::from_str("2").expect("must be valid");
let notional = Notional::from_price_quantity(price, qty).expect("must be valid");
assert_eq!(notional.to_decimal(), d("200"));
}
#[test]
fn from_price_quantity_reports_overflow() {
let price = Price::new(Decimal::MAX);
let qty = Quantity::from_str("2").expect("must be valid");
assert_eq!(
Notional::from_price_quantity(price, qty),
Err(Error::Overflow {
param: ParamKind::Price
})
);
}
#[test]
fn from_volume_and_to_volume_roundtrip() {
let vol = Volume::from_str("1234.56").expect("must be valid");
let notional = Notional::from_volume(vol);
let back = notional.to_volume();
assert_eq!(back.to_decimal(), vol.to_decimal());
}
#[test]
fn calculate_margin_required_divides_by_leverage() {
let notional = Notional::from_str("10000").expect("must be valid");
let leverage = Leverage::from_u16(100).expect("must be valid");
let margin = notional
.calculate_margin_required(leverage)
.expect("must be valid");
assert_eq!(margin.to_decimal(), d("100"));
}
#[test]
fn calculate_margin_required_with_fractional_leverage() {
let notional = Notional::from_str("1050").expect("must be valid");
let leverage = Leverage::from_f64(10.5).expect("must be valid");
let margin = notional
.calculate_margin_required(leverage)
.expect("must be valid");
assert_eq!(margin.to_decimal(), d("100"));
}
#[test]
fn calculate_margin_required_with_min_leverage() {
let notional = Notional::from_str("5000").expect("must be valid");
let leverage = Leverage::from_u16(1).expect("must be valid");
let margin = notional
.calculate_margin_required(leverage)
.expect("must be valid");
assert_eq!(margin.to_decimal(), d("5000"));
}
#[test]
fn from_price_quantity_zero_quantity_yields_zero() {
let price = Price::from_str("50000").expect("must be valid");
let qty = Quantity::ZERO;
let notional = Notional::from_price_quantity(price, qty).expect("must be valid");
assert!(notional.is_zero());
}
}