use super::quoting::CurrencyPair;
use crate::traits::FloatExt;
#[derive(Debug, Clone, Copy)]
pub struct FxForward<T: FloatExt> {
pub pair: CurrencyPair,
pub spot: T,
pub domestic_rate: T,
pub foreign_rate: T,
pub maturity: T,
}
impl<T: FloatExt> std::fmt::Display for FxForward<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"FxForward({}, spot={:.6}, T={:.4})",
self.pair,
self.spot.to_f64().unwrap(),
self.maturity.to_f64().unwrap()
)
}
}
impl<T: FloatExt> FxForward<T> {
pub fn new(pair: CurrencyPair, spot: T, domestic_rate: T, foreign_rate: T, maturity: T) -> Self {
Self {
pair,
spot,
domestic_rate,
foreign_rate,
maturity,
}
}
pub fn forward_rate(&self) -> T {
self.spot * ((self.domestic_rate - self.foreign_rate) * self.maturity).exp()
}
pub fn forward_rate_simple(&self) -> T {
let one = T::one();
self.spot * (one + self.domestic_rate * self.maturity)
/ (one + self.foreign_rate * self.maturity)
}
pub fn forward_points(&self) -> T {
self.forward_rate() - self.spot
}
pub fn forward_points_simple(&self) -> T {
self.forward_rate_simple() - self.spot
}
pub fn premium(&self) -> T {
self.forward_points() / self.spot
}
pub fn annualised_premium(&self) -> T {
self.forward_points() / (self.spot * self.maturity)
}
pub fn implied_domestic_rate(spot: T, forward: T, foreign_rate: T, maturity: T) -> T {
foreign_rate + (forward / spot).ln() / maturity
}
pub fn implied_foreign_rate(spot: T, forward: T, domestic_rate: T, maturity: T) -> T {
domestic_rate - (forward / spot).ln() / maturity
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fx::currency;
fn pair() -> CurrencyPair {
CurrencyPair::new(currency::EUR, currency::USD)
}
#[test]
fn forward_rate_at_zero_maturity_equals_spot() {
let f = FxForward::<f64>::new(pair(), 1.10, 0.05, 0.03, 0.0);
assert!((f.forward_rate() - 1.10).abs() < 1e-12);
}
#[test]
fn covered_interest_parity_at_one_year() {
let f = FxForward::<f64>::new(pair(), 1.10, 0.05, 0.03, 1.0);
let expected = 1.10 * 0.02_f64.exp();
assert!((f.forward_rate() - expected).abs() < 1e-12);
}
#[test]
fn premium_positive_when_domestic_higher() {
let f = FxForward::<f64>::new(pair(), 1.10, 0.05, 0.03, 1.0);
assert!(f.premium() > 0.0);
}
#[test]
fn implied_domestic_round_trip() {
let f = FxForward::<f64>::new(pair(), 1.10, 0.05, 0.03, 1.0);
let fwd = f.forward_rate();
let r_d = FxForward::<f64>::implied_domestic_rate(1.10, fwd, 0.03, 1.0);
assert!((r_d - 0.05).abs() < 1e-12);
}
}