Skip to content

Fixed Income Risk Analytics

Risk managers and quantitative traders rely on sensitivity measures to hedge interest rate risk across yield curves.

Risk Metrics Overview

1. Macaulay Duration (DmacD_{\text{mac}})

The weighted-average time until cash flows are received:

Dmac=∑i=1nti Ai d(y,ti)PdirtyD_{\text{mac}} = \frac{\sum_{i=1}^n t_i \, A_i \, d(y, t_i)}{P_{\text{dirty}}}

2. Modified Duration (DmodD_{\text{mod}})

Measures the percentage price sensitivity of a bond to a small change in yield yy:

Dmod=−1PdirtydPdy=Dmac1+ymD_{\text{mod}} = -\frac{1}{P_{\text{dirty}}} \frac{dP}{dy} = \frac{D_{\text{mac}}}{1 + \frac{y}{m}}

3. Convexity (CC)

The second-order curvature of the price-yield relationship:

C=1Pdirtyd2Pdy2=1Pdirty∑i=1nti(ti+1m)Ai(1+ym)−(m⋅ti+2)C = \frac{1}{P_{\text{dirty}}} \frac{d^2P}{dy^2} = \frac{1}{P_{\text{dirty}}} \sum_{i=1}^n t_i \left(t_i + \frac{1}{m}\right) A_i \left(1 + \frac{y}{m}\right)^{-(m \cdot t_i + 2)}

4. Dollar Value of a Basis Point (DV01)

The dollar price change for a 1 basis point (0.01%=10−40.01\% = 10^{-4}) parallel shift in yield:

DV01=−dPdy×0.0001=Pdirty×Dmod×0.0001\text{DV01} = -\frac{dP}{dy} \times 0.0001 = P_{\text{dirty}} \times D_{\text{mod}} \times 0.0001

5. Second-Order Price Shift Approximation

Using a second-order Taylor series expansion:

ΔPP≈−Dmod Δy+12C (Δy)2\frac{\Delta P}{P} \approx -D_{\text{mod}} \, \Delta y + \frac{1}{2} C \, (\Delta y)^2

ΔP≈−DV01×104 Δy+12Pdirty C (Δy)2\Delta P \approx -\text{DV01} \times 10^4 \, \Delta y + \frac{1}{2} P_{\text{dirty}} \, C \, (\Delta y)^2


Analytical Rust Example

use sabiquant::bonds::BondPricingPlan;
use sabiquant::core::{Date, Frequency};
use sabiquant::core::calendars::TargetCalendar;
use sabiquant::core::daycount::ActualActualICMA;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let settlement = Date::from_ymd(2025, 1, 15)?;
let maturity = Date::from_ymd(2035, 1, 15)?;
let plan = BondPricingPlan::builder()
.settlement(settlement)
.maturity(maturity)
.coupon_rate(0.04) // 4.0%
.frequency(Frequency::Annual)
.calendar(TargetCalendar::default())
.day_counter(ActualActualICMA::default())
.face_value(100.0)
.build()?;
let base_yield = 0.045; // 4.5%
let metrics = plan.price_from_yield(base_yield)?;
println!("Base Dirty Price: ${:.4}", metrics.dirty_price);
println!("Modified Duration: {:.4}", metrics.modified_duration);
println!("Convexity: {:.4}", metrics.convexity);
// Calculate DV01 per 100 face value
let dv01 = -metrics.dp_dy * 0.0001;
println!("DV01: ${:.5}", dv01);
// Approximate price impact for a +50 bps (+0.005) rate shock
let delta_y = 0.005;
let duration_effect = -metrics.modified_duration * delta_y;
let convexity_effect = 0.5 * metrics.convexity * delta_y.powi(2);
let approx_pct_change = duration_effect + convexity_effect;
println!("Duration Effect: {:.3}%", duration_effect * 100.0);
println!("Convexity Effect: +{:.3}%", convexity_effect * 100.0);
println!("Predicted Total ΔP%: {:.3}%", approx_pct_change * 100.0);
// Compare with exact repricing
let shocked_metrics = plan.price_from_yield(base_yield + delta_y)?;
let exact_pct_change = (shocked_metrics.dirty_price - metrics.dirty_price) / metrics.dirty_price;
println!("Exact Repriced ΔP%: {:.3}%", exact_pct_change * 100.0);
Ok(())
}