Convexity and Bond Portfolio Management
Introduction
Bond portfolio management depends critically on understanding duration and convexity. While duration measures a bond's interest rate sensitivity linearly, convexity captures the curvature of this relationship. For quantitative traders managing fixed-income portfolios, exploiting the nonlinear relationship between bond prices and yields creates alpha opportunities. This guide explores the mathematical foundations, Python implementations, and practical strategies for managing convexity risk.
Understanding Duration and Convexity
Duration: The Basics
Modified duration measures bond price sensitivity to yield changes:
Modified_Duration = Macaulay_Duration / (1 + YTM)
Price_Change ≈ -Modified_Duration × Yield_Change × Bond_Price
For example, a bond with 5-year modified duration loses 5% in price value for each 1% increase in yield.
Limitation: Duration assumes linear price-yield relationships, which breaks down for large yield changes.Convexity: Capturing the Curve
Convexity adjusts for the curvature of the price-yield relationship:
Bond_Price_Change = (-Duration × ΔYield × Price) + (0.5 × Convexity × ΔYield² × Price)
Convexity is always positive for regular bonds, meaning prices rise faster when yields fall than they fall when yields rise by the same amount. This asymmetry creates profitable opportunities.
Mathematical Formula:Convexity = (1 / Bond_Price) × Σ[t(t+1) × CF_t / (1+y)^(t+2)] / (1+y)²
Where:
- CF_t = Cash flows at time t
- y = Yield to maturity
- t = Time period
The Bond Pricing Model
Exact Price Calculation
Bond_Price = Σ[CF_t / (1 + y)^t] for t = 1 to n
Where CF includes coupon payments plus principal repayment at maturity.
Duration-Convexity Approximation
For yield changes up to 2-3%, the second-order approximation provides excellent accuracy:
ΔP/P = -Duration × Δy + (1/2) × Convexity × (Δy)²
For larger yield shocks, incorporate higher-order terms or use direct numerical calculation.
Python Implementation of Duration and Convexity Calculations
Basic Bond Pricing
import numpy as np
import pandas as pd
from scipy.optimize import fsolve
class BondPricer:
def __init__(self, face_value=100, coupon_rate=0.05, years_to_maturity=5):
self.face_value = face_value
self.coupon_rate = coupon_rate
self.ytm = years_to_maturity
self.coupon_payment = face_value * coupon_rate
def bond_price(self, yield_to_maturity):
"""Calculate bond price given YTM"""
price = 0
for t in range(1, int(self.ytm) + 1):
price += self.coupon_payment / (1 + yield_to_maturity) ** t
price += self.face_value / (1 + yield_to_maturity) ** self.ytm
return price
def macaulay_duration(self, yield_to_maturity):
"""Calculate Macaulay duration (weighted average time to cash flows)"""
numerator = 0
denominator = 0
for t in range(1, int(self.ytm) + 1):
cf = self.coupon_payment
denominator += cf / (1 + yield_to_maturity) ** t
numerator += t cf / (1 + yield_to_maturity) * t
# Add principal repayment
pv_principal = self.face_value / (1 + yield_to_maturity) ** self.ytm
denominator += pv_principal
numerator += self.ytm * pv_principal
return numerator / denominator
def modified_duration(self, yield_to_maturity):
"""Calculate modified duration"""
mac_duration = self.macaulay_duration(yield_to_maturity)
return mac_duration / (1 + yield_to_maturity)
def convexity(self, yield_to_maturity):
"""Calculate bond convexity"""
bond_price = self.bond_price(yield_to_maturity)
numerator = 0
for t in range(1, int(self.ytm) + 1):
cf = self.coupon_payment
numerator += t (t + 1) cf / (1 + yield_to_maturity) ** (t + 2)
# Add principal repayment contribution
t = self.ytm
pv_principal = self.face_value / (1 + yield_to_maturity) ** (t + 2)
numerator += t (t + 1) pv_principal
convexity = numerator / bond_price
return convexity / ((1 + yield_to_maturity) ** 2)
def price_change_approximation(self, current_yield, new_yield):
"""Estimate price change using duration and convexity"""
ytm_change = new_yield - current_yield
duration = self.modified_duration(current_yield)
conv = self.convexity(current_yield)
current_price = self.bond_price(current_yield)
# Duration effect
duration_effect = -duration ytm_change current_price
# Convexity effect
convexity_effect = 0.5 conv (ytm_change * 2) current_price
return duration_effect + convexity_effect, duration_effect, convexity_effect
Example: Analyze a 10-year corporate bond
bond = BondPricer(face_value=100, coupon_rate=0.05, years_to_maturity=10)
current_ytm = 0.04
print(f"Current Price: ${bond.bond_price(current_ytm):.2f}")
print(f"Modified Duration: {bond.modified_duration(current_ytm):.3f}")
print(f"Convexity: {bond.convexity(current_ytm):.3f}")
Estimate price change if yields rise 100 bps
new_ytm = current_ytm + 0.01
total_change, duration_effect, convexity_effect = bond.price_change_approximation(
current_ytm, new_ytm
)
actual_price = bond.bond_price(new_ytm)
actual_change = actual_price - bond.bond_price(current_ytm)
print(f"\nYield increase: +100 bps")
print(f"Estimated price change: ${total_change:.2f}")
print(f" Duration effect: ${duration_effect:.2f}")
print(f" Convexity effect: ${convexity_effect:.2f}")
print(f"Actual price change: ${actual_change:.2f}")
Portfolio Duration and Convexity
class BondPortfolio:
def __init__(self, bonds_data):
"""
bonds_data: list of dicts with 'price', 'modified_duration', 'convexity', 'weight'
"""
self.bonds = bonds_data
self.total_value = sum(b['price'] * b['weight'] for b in bonds_data)
def portfolio_duration(self):
"""Calculate weighted average duration"""
weighted_duration = 0
for bond in self.bonds:
weight = (bond['price'] * bond['weight']) / self.total_value
weighted_duration += weight * bond['modified_duration']
return weighted_duration
def portfolio_convexity(self):
"""Calculate weighted average convexity"""
weighted_convexity = 0
for bond in self.bonds:
weight = (bond['price'] * bond['weight']) / self.total_value
weighted_convexity += weight * bond['convexity']
return weighted_convexity
def price_sensitivity(self, yield_change):
"""Estimate portfolio price change for given yield change"""
portfolio_duration = self.portfolio_duration()
portfolio_convexity = self.portfolio_convexity()
duration_effect = -portfolio_duration yield_change self.total_value
convexity_effect = 0.5 portfolio_convexity (yield_change * 2) self.total_value
return {
'total_change': duration_effect + convexity_effect,
'duration_effect': duration_effect,
'convexity_effect': convexity_effect,
'portfolio_duration': portfolio_duration,
'portfolio_convexity': portfolio_convexity
}
Example portfolio
portfolio_data = [
{'price': 98.5, 'weight': 0.4, 'modified_duration': 5.2, 'convexity': 0.032},
{'price': 102.0, 'weight': 0.3, 'modified_duration': 8.1, 'convexity': 0.085},
{'price': 101.2, 'weight': 0.3, 'modified_duration': 10.5, 'convexity': 0.145}
]
portfolio = BondPortfolio(portfolio_data)
sensitivity = portfolio.price_sensitivity(yield_change=0.01)
print(f"Portfolio Duration: {sensitivity['portfolio_duration']:.2f}")
print(f"Portfolio Convexity: {sensitivity['portfolio_convexity']:.3f}")
print(f"Estimated value change for +100 bps: ${sensitivity['total_change']:,.2f}")
Convexity-Based Trading Strategies
Positive Convexity Capture
Long bonds with high positive convexity to profit from yield volatility:
def identify_high_convexity_bonds(bonds_df, min_convexity=0.08, ytm_spread=0.015):
"""Find bonds with attractive convexity-to-yield spread trade-off"""
bonds_df['convexity_to_spread'] = bonds_df['convexity'] / bonds_df['ytm_spread']
high_convexity = bonds_df[
(bonds_df['convexity'] > min_convexity) &
(bonds_df['ytm_spread'] > 0.01)
].sort_values('convexity_to_spread', ascending=False)
return high_convexity
Duration Neutral Curve Flattening Trade
Long short-duration bonds, short long-duration bonds to profit from curve changes while maintaining neutral duration:
def construct_duration_neutral_trade(short_bond, long_bond):
"""Create duration-neutral curve flattening strategy"""
# Calculate hedge ratio to maintain zero net duration
hedge_ratio = short_bond['modified_duration'] / long_bond['modified_duration']
return {
'long_position': short_bond,
'long_quantity': 1,
'short_position': long_bond,
'short_quantity': hedge_ratio,
'net_duration': 0,
'expected_gain': 'Curve flattening (long-duration underperformance)'
}
Barbell vs. Bullet Strategies
Barbell: Own short and long-duration bonds, avoid intermediate. Higher convexity. Bullet: Concentrate in intermediate-duration. Lower convexity, lower volatility.Choose barbell when volatility is low/expected to rise (positive convexity value increases).
Frequently Asked Questions
Q1: Why does convexity matter more for bonds with longer duration? A: Longer-duration bonds experience larger percentage price moves for given yield changes. The nonlinear (squared) term in the convexity adjustment becomes more significant. A 10-year bond's price can move 50% or more for large yield swings, where convexity effects are substantial. Q2: Can a bond have negative convexity? A: Yes. Bonds with embedded call options (callable bonds) have negative convexity. When yields fall and prices rise, the issuer's call option becomes more valuable, capping the bond's upside. This makes callable bonds less attractive in declining-yield scenarios. Q3: How does convexity affect portfolio immunization? A: Duration immunization assumes flat yield curves and small yield changes. Negative convexity creates systematic losses as yield changes grow larger. Convexity-aware immunization strategies require holding portfolios with positive convexity to benefit from large yield movements. Q4: What's the difference between effective duration and modified duration? A: Modified duration assumes static yield curves (parallel shifts). Effective duration accounts for changing yield curve shapes and embedded options. For non-standard bonds with options, effective duration provides more accurate sensitivity. Q5: How should I monitor convexity risk in a bond portfolio? A: Track portfolio convexity daily alongside duration. Stress-test the portfolio assuming ±200 bps yield moves and verify that convexity effects are properly captured. Monitor changes in the yield curve shape to anticipate convexity revaluation.Best Practices
- Combine Metrics: Use both duration and convexity—duration alone is insufficient for large yield moves
- Monitor Curve Positioning: Barbell strategies benefit from curve flattening; bullet strategies from bull flattening
- Rebalance Dynamically: As yields change, duration and convexity drift; rebalance quarterly or when duration exceeds thresholds
- Test Curve Scenarios: Backtest against historical curve shifts (parallel, twist, butterfly) not just yield level changes
- Account for Embedded Options: Callable bonds require option-adjusted convexity analysis
Conclusion
Convexity transforms bond portfolio management from linear approximations to sophisticated nonlinear strategies. By understanding how bond prices curve relative to yields and implementing Python frameworks to measure and exploit convexity, algorithmic traders can construct more robust fixed-income portfolios that profit across varying yield scenarios. The techniques outlined here form the foundation for professional bond portfolio management.
Last updated: 2026-03-16