#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct FuturesContract {
pub underlying_price: f64,
pub risk_free_rate: f64,
pub convenience_yield: f64,
pub storage_cost: f64,
pub time_to_expiry: f64,
pub spot_price: f64,
}
pub fn fair_value(contract: &FuturesContract) -> f64 {
let carry = contract.risk_free_rate - contract.convenience_yield + contract.storage_cost;
contract.spot_price * (carry * contract.time_to_expiry).exp()
}
pub fn basis(contract: &FuturesContract, futures_price: f64) -> f64 {
contract.spot_price - futures_price
}
pub fn annualized_basis(contract: &FuturesContract, futures_price: f64) -> f64 {
let b = basis(contract, futures_price);
if contract.spot_price == 0.0 || contract.time_to_expiry == 0.0 {
return 0.0;
}
b / contract.spot_price / contract.time_to_expiry * 365.0
}
pub fn cost_of_carry(contract: &FuturesContract) -> f64 {
(contract.risk_free_rate + contract.storage_cost - contract.convenience_yield)
* contract.time_to_expiry
}
pub fn implied_repo_rate(
spot: f64,
futures: f64,
t: f64,
convenience_yield: f64,
storage: f64,
) -> f64 {
if spot <= 0.0 || t == 0.0 {
return 0.0;
}
(futures / spot).ln() / t - storage + convenience_yield
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct FuturesCalendarSpread {
pub near_price: f64,
pub far_price: f64,
pub near_expiry: f64,
pub far_expiry: f64,
}
pub fn spread_value(cs: &FuturesCalendarSpread) -> f64 {
cs.far_price - cs.near_price
}
pub fn theoretical_spread(
spot: f64,
r: f64,
q: f64,
u: f64,
cs: &FuturesCalendarSpread,
) -> f64 {
let make = |t: f64| {
let c = FuturesContract {
underlying_price: spot,
risk_free_rate: r,
convenience_yield: q,
storage_cost: u,
time_to_expiry: t,
spot_price: spot,
};
fair_value(&c)
};
make(cs.far_expiry) - make(cs.near_expiry)
}
pub fn carry_trade_pnl(entry_spread: f64, exit_spread: f64, notional: f64) -> f64 {
(exit_spread - entry_spread) * notional
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct RollYield {
pub near_price: f64,
pub far_price: f64,
pub days_between: f64,
}
pub fn roll_yield(ry: &RollYield) -> f64 {
if ry.far_price == 0.0 || ry.days_between == 0.0 {
return 0.0;
}
(ry.near_price - ry.far_price) / ry.far_price * 365.0 / ry.days_between
}
pub fn contango_or_backwardation(ry: &RollYield) -> &'static str {
if ry.far_price > ry.near_price {
"contango"
} else {
"backwardation"
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_contract(r: f64, q: f64, u: f64, t: f64, spot: f64) -> FuturesContract {
FuturesContract {
underlying_price: spot,
risk_free_rate: r,
convenience_yield: q,
storage_cost: u,
time_to_expiry: t,
spot_price: spot,
}
}
#[test]
fn fair_value_zero_rates_equals_spot() {
let c = make_contract(0.0, 0.0, 0.0, 0.5, 100.0);
let fv = fair_value(&c);
assert!((fv - 100.0).abs() < 1e-10, "expected 100.0 got {fv}");
}
#[test]
fn fair_value_positive_carry() {
let c = make_contract(0.05, 0.02, 0.01, 1.0, 100.0);
let fv = fair_value(&c);
let expected = 100.0_f64 * 0.04_f64.exp();
assert!((fv - expected).abs() < 1e-8);
}
#[test]
fn basis_calculation() {
let c = make_contract(0.05, 0.0, 0.0, 1.0, 100.0);
let b = basis(&c, 102.0);
assert!((b - (-2.0)).abs() < 1e-10);
}
#[test]
fn annualized_basis_calculation() {
let c = make_contract(0.05, 0.0, 0.0, 0.5, 100.0);
let ab = annualized_basis(&c, 102.0);
let expected = -2.0 / 100.0 / 0.5 * 365.0;
assert!((ab - expected).abs() < 1e-8);
}
#[test]
fn cost_of_carry_calculation() {
let c = make_contract(0.05, 0.02, 0.01, 2.0, 100.0);
let coc = cost_of_carry(&c);
assert!((coc - 0.08).abs() < 1e-10);
}
#[test]
fn implied_repo_rate_round_trip() {
let spot: f64 = 100.0;
let r: f64 = 0.05;
let q = 0.02;
let u = 0.01;
let t = 1.0;
let f = spot * ((r - q + u) * t).exp();
let r2 = implied_repo_rate(spot, f, t, q, u);
assert!((r2 - r).abs() < 1e-10, "r={r} r2={r2}");
}
#[test]
fn calendar_spread_value() {
let cs = FuturesCalendarSpread {
near_price: 100.0,
far_price: 105.0,
near_expiry: 0.25,
far_expiry: 0.5,
};
assert!((spread_value(&cs) - 5.0).abs() < 1e-10);
}
#[test]
fn theoretical_spread_zero_carry() {
let cs = FuturesCalendarSpread {
near_price: 100.0,
far_price: 100.0,
near_expiry: 0.25,
far_expiry: 0.5,
};
let ts = theoretical_spread(100.0, 0.0, 0.0, 0.0, &cs);
assert!(ts.abs() < 1e-10);
}
#[test]
fn carry_trade_pnl_calculation() {
let pnl = carry_trade_pnl(5.0, 7.0, 1_000_000.0);
assert!((pnl - 2_000_000.0).abs() < 1e-6);
}
#[test]
fn roll_yield_sign_contango() {
let ry = RollYield { near_price: 100.0, far_price: 105.0, days_between: 30.0 };
let ry_val = roll_yield(&ry);
assert!(ry_val < 0.0, "contango => negative roll yield, got {ry_val}");
assert_eq!(contango_or_backwardation(&ry), "contango");
}
#[test]
fn roll_yield_sign_backwardation() {
let ry = RollYield { near_price: 105.0, far_price: 100.0, days_between: 30.0 };
let ry_val = roll_yield(&ry);
assert!(ry_val > 0.0, "backwardation => positive roll yield, got {ry_val}");
assert_eq!(contango_or_backwardation(&ry), "backwardation");
}
}