#[derive(Debug, Clone, PartialEq)]
pub enum ForwardType {
Equity,
Fx,
Commodity,
Rate,
}
#[derive(Debug, Clone)]
pub struct EquityForward {
pub spot: f64,
pub strike: f64,
pub r: f64,
pub q: f64,
pub t: f64,
}
impl EquityForward {
pub fn fair_forward(&self) -> f64 {
self.spot * ((self.r - self.q) * self.t).exp()
}
pub fn pnl(&self, spot_at_expiry: f64) -> f64 {
spot_at_expiry - self.strike
}
pub fn delta(&self) -> f64 {
(-self.q * self.t).exp()
}
pub fn carry_cost(&self) -> f64 {
((self.r * self.t).exp() - (self.q * self.t).exp()) * self.spot
}
}
#[derive(Debug, Clone)]
pub struct FxForward {
pub spot_rate: f64,
pub domestic_rate: f64,
pub foreign_rate: f64,
pub t: f64,
pub notional_domestic: f64,
}
impl FxForward {
pub fn forward_rate(&self) -> f64 {
self.spot_rate * ((self.domestic_rate - self.foreign_rate) * self.t).exp()
}
pub fn ndf_settlement(&self, fixing_rate: f64) -> f64 {
(self.forward_rate() - fixing_rate) * self.notional_domestic
}
pub fn basis_points_fwd(&self) -> f64 {
(self.forward_rate() - self.spot_rate) * 10_000.0
}
}
#[derive(Debug, Clone)]
pub struct CommodityForward {
pub spot: f64,
pub storage_cost: f64,
pub convenience_yield: f64,
pub r: f64,
pub t: f64,
}
impl CommodityForward {
pub fn fair_forward(&self) -> f64 {
self.spot * (self.net_carry() * self.t).exp()
}
pub fn net_carry(&self) -> f64 {
self.r + self.storage_cost - self.convenience_yield
}
}
#[derive(Debug, Clone)]
pub struct ForwardCurve {
pub pillars: Vec<(f64, f64)>,
}
impl ForwardCurve {
pub fn from_spot_rates(spot: f64, rates: &[(f64, f64)]) -> Self {
let mut pillars: Vec<(f64, f64)> = rates
.iter()
.map(|&(t, r)| (t, spot * (r * t).exp()))
.collect();
pillars.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
Self { pillars }
}
pub fn interpolate(&self, maturity: f64) -> f64 {
if self.pillars.is_empty() {
return 0.0;
}
if maturity <= self.pillars[0].0 {
return self.pillars[0].1;
}
if maturity >= self.pillars[self.pillars.len() - 1].0 {
return self.pillars[self.pillars.len() - 1].1;
}
for i in 0..self.pillars.len() - 1 {
let (t0, f0) = self.pillars[i];
let (t1, f1) = self.pillars[i + 1];
if maturity >= t0 && maturity <= t1 {
if (t1 - t0).abs() < 1e-12 {
return f0;
}
let log_t0 = t0.ln().max(-30.0);
let log_t1 = t1.ln().max(-30.0);
let log_t = maturity.ln().max(-30.0);
let w = (log_t - log_t0) / (log_t1 - log_t0);
return f0 + w * (f1 - f0);
}
}
self.pillars[self.pillars.len() - 1].1
}
pub fn contango(&self) -> bool {
if self.pillars.len() < 2 {
return false;
}
self.pillars.last().map(|p| p.1).unwrap_or(0.0)
> self.pillars.first().map(|p| p.1).unwrap_or(0.0)
}
pub fn backwardation(&self) -> bool {
if self.pillars.len() < 2 {
return false;
}
self.pillars.last().map(|p| p.1).unwrap_or(0.0)
< self.pillars.first().map(|p| p.1).unwrap_or(0.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_equity_forward_fair_forward() {
let fwd = EquityForward { spot: 100.0, strike: 102.0, r: 0.05, q: 0.02, t: 1.0 };
let f = fwd.fair_forward();
assert!((f - 103.045).abs() < 0.01, "fair_forward={f}");
}
#[test]
fn test_equity_forward_pnl() {
let fwd = EquityForward { spot: 100.0, strike: 100.0, r: 0.05, q: 0.0, t: 1.0 };
assert!((fwd.pnl(110.0) - 10.0).abs() < 1e-9);
}
#[test]
fn test_equity_forward_delta() {
let fwd = EquityForward { spot: 100.0, strike: 100.0, r: 0.05, q: 0.02, t: 1.0 };
let d = fwd.delta();
assert!((d - (-0.02_f64).exp()).abs() < 1e-9);
}
#[test]
fn test_fx_forward_rate() {
let fx = FxForward { spot_rate: 1.25, domestic_rate: 0.03, foreign_rate: 0.01, t: 1.0, notional_domestic: 1_000_000.0 };
let f = fx.forward_rate();
assert!((f - 1.25 * (0.02_f64).exp()).abs() < 1e-6);
}
#[test]
fn test_commodity_forward() {
let cf = CommodityForward { spot: 50.0, storage_cost: 0.02, convenience_yield: 0.01, r: 0.05, t: 1.0 };
let f = cf.fair_forward();
assert!((f - 50.0 * (0.06_f64).exp()).abs() < 0.01);
}
#[test]
fn test_forward_curve_contango() {
let curve = ForwardCurve { pillars: vec![(0.5, 100.0), (1.0, 102.0), (2.0, 105.0)] };
assert!(curve.contango());
assert!(!curve.backwardation());
}
#[test]
fn test_forward_curve_interpolate() {
let curve = ForwardCurve { pillars: vec![(1.0, 100.0), (2.0, 110.0)] };
let mid = curve.interpolate(1.5);
assert!(mid > 100.0 && mid < 110.0);
}
#[test]
fn test_from_spot_rates() {
let curve = ForwardCurve::from_spot_rates(100.0, &[(1.0, 0.05), (2.0, 0.05)]);
assert_eq!(curve.pillars.len(), 2);
assert!((curve.pillars[0].1 - 100.0 * (0.05_f64).exp()).abs() < 0.01);
}
}