Introduction

When a central bank raises interest rates, bond prices fall. When rates fall, prices rise. That much is common knowledge. What is less obvious is how much they move — and why the relationship isn't a straight line.

Two numbers capture that sensitivity. Duration is the first-order answer: for every 1% rise in yield, a bond with duration DD years loses approximately DD% of its value. A 10-year Treasury with a modified duration of 8 will drop roughly 8% if yields jump by a full percentage point.

But "approximately" hides something important. The true price-yield relationship is a curve, not a line. Convexity measures that curvature — the second-order term. Because the curve bows toward the investor, convexity is almost always a gift: it means the bond falls less when rates rise than duration alone predicts, and gains more when rates fall.

Together, duration and convexity are the two derivatives of the price-yield function — the same Taylor-series logic that appears in dynamic programming and numerical optimization, applied to the oldest financial instrument in the world.

Try It

Adjust the bond parameters and drag the yield slider. The chart shows three lines: the exact price (blue curve), the duration-only linear approximation (dashed), and the duration + convexity quadratic correction (dotted).

<!-- Bond price vs. yield chart: exact price (blue), duration approx (red dashed), duration+convexity approx (green dotted) -->
<div class="controls">
  <label>Coupon: <span id="couponVal">5.0</span>%
    <input type="range" id="coupon" min="0" max="12" step="0.5" value="5">
  </label>
  <label>Maturity: <span id="maturityVal">10</span> yrs
    <input type="range" id="maturity" min="1" max="30" step="1" value="10">
  </label>
  <label>Yield: <span id="yieldVal">5.0</span>%
    <input type="range" id="yield" min="0.5" max="15" step="0.25" value="5">
  </label>
</div>
<canvas id="chart" width="560" height="220"></canvas>
<div class="metrics" id="metrics"></div>
<div class="legend">
  <span class="leg-exact">Exact price</span>
  <span class="leg-dur">Duration only</span>
  <span class="leg-conv">Duration + Convexity</span>
</div>
/* base reset */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; padding: .5rem; }
.controls { display: flex; flex-direction: column; gap: .45rem; margin-bottom: .6rem; }
label { font-size: .85rem; color: #444; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
label span { font-weight: 700; color: #1d3557; min-width: 2.4rem; text-align: right; }
input[type=range] { flex: 1; min-width: 120px; accent-color: #1d3557; }
canvas { width: 100%; height: auto; display: block; border: 1px solid #dde3ea; border-radius: 8px; }
.metrics { font-size: .82rem; color: #333; margin: .45rem 0 .3rem;
           display: flex; gap: 1rem; flex-wrap: wrap; }
.metrics span { background: #eef2f6; border-radius: 6px; padding: .2rem .55rem; }
.metrics b { color: #1d3557; }
.legend { font-size: .78rem; color: #555; display: flex; gap: 1rem; flex-wrap: wrap; }
.leg-exact::before { content: ""; display: inline-block; width: 22px; height: 3px;
                      background: #1d6fb5; border-radius: 2px; vertical-align: middle; margin-right: 4px; }
.leg-dur::before { content: ""; display: inline-block; width: 22px; height: 2px;
                    border-top: 2px dashed #c0392b; vertical-align: middle; margin-right: 4px; }
.leg-conv::before { content: ""; display: inline-block; width: 22px; height: 2px;
                     border-top: 2px dotted #27ae60; vertical-align: middle; margin-right: 4px; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Compute full bond price: semiannual coupons, face value 100
function bondPrice(coupon, n, y) {
  // semiannual coupon rate and discount rate
  const c = coupon / 2 / 100;
  const r = y / 2 / 100;
  const face = 100;
  if (r === 0) return face + c * face * n * 2;
  let pv = 0;
  for (let t = 1; t <= n * 2; t++) pv += (c * face) / Math.pow(1 + r, t);
  pv += face / Math.pow(1 + r, n * 2);
  return pv;
}

// Macaulay duration: weighted-average time to cash flows (in years)
function macaulayDuration(coupon, n, y) {
  const c = coupon / 2 / 100;
  const r = y / 2 / 100;
  const face = 100;
  const price = bondPrice(coupon, n, y);
  let num = 0;
  for (let t = 1; t <= n * 2; t++) {
    const cf = c * face;
    num += (t / 2) * cf / Math.pow(1 + r, t);
  }
  num += n * face / Math.pow(1 + r, n * 2);
  return num / price;
}

// Convexity: normalized second derivative of price w.r.t. yield
function convexity(coupon, n, y) {
  const c = coupon / 2 / 100;
  const r = y / 2 / 100;
  const face = 100;
  const price = bondPrice(coupon, n, y);
  let num = 0;
  for (let t = 1; t <= n * 2; t++) {
    const cf = c * face;
    num += t * (t + 1) * cf / Math.pow(1 + r, t);
  }
  num += (n * 2) * (n * 2 + 1) * face / Math.pow(1 + r, n * 2);
  return num / (price * Math.pow(1 + r, 2) * 4);
}

// Main draw function: reads sliders, computes metrics, draws chart
function draw() {
  const coupon = parseFloat(document.getElementById('coupon').value);
  const maturity = parseInt(document.getElementById('maturity').value);
  const yieldPct = parseFloat(document.getElementById('yield').value);

  document.getElementById('couponVal').textContent = coupon.toFixed(1);
  document.getElementById('maturityVal').textContent = maturity;
  document.getElementById('yieldVal').textContent = yieldPct.toFixed(2);

  const canvas = document.getElementById('chart');
  const ctx = canvas.getContext('2d');
  const W = canvas.width, H = canvas.height;
  ctx.clearRect(0, 0, W, H);

  // yield range for x-axis: current yield +/- 6 percentage points
  const yMin = Math.max(0.25, yieldPct - 6);
  const yMax = yieldPct + 6;
  const steps = 200;

  const prices = [];
  for (let i = 0; i <= steps; i++) {
    const y = yMin + (yMax - yMin) * i / steps;
    prices.push({ y, p: bondPrice(coupon, maturity, y) });
  }

  const pMin = Math.min(...prices.map(o => o.p)) * 0.97;
  const pMax = Math.max(...prices.map(o => o.p)) * 1.02;

  const px = (y) => 36 + (y - yMin) / (yMax - yMin) * (W - 46);
  const py = (p) => H - 22 - (p - pMin) / (pMax - pMin) * (H - 38);

  // draw axis grid lines and labels
  ctx.strokeStyle = '#e0e7ef'; ctx.lineWidth = 1;
  for (let i = 0; i <= 4; i++) {
    const y = yMin + (yMax - yMin) * i / 4;
    const x = px(y);
    ctx.beginPath(); ctx.moveTo(x, 12); ctx.lineTo(x, H - 22); ctx.stroke();
    ctx.fillStyle = '#888'; ctx.font = '10px system-ui';
    ctx.textAlign = 'center';
    ctx.fillText(y.toFixed(1) + '%', x, H - 8);
  }
  for (let i = 0; i <= 3; i++) {
    const p = pMin + (pMax - pMin) * i / 3;
    const y2 = py(p);
    ctx.beginPath(); ctx.moveTo(36, y2); ctx.lineTo(W - 10, y2); ctx.stroke();
    ctx.textAlign = 'right';
    ctx.fillText(p.toFixed(0), 32, y2 + 3);
  }

  const P0 = bondPrice(coupon, maturity, yieldPct);
  const D = macaulayDuration(coupon, maturity, yieldPct);
  const Dmod = D / (1 + yieldPct / 2 / 100);
  const Cvx = convexity(coupon, maturity, yieldPct);

  // first-order (duration) and second-order (convexity) approximation functions
  function durLine(y) {
    const dy = (y - yieldPct) / 100;
    return P0 * (1 - Dmod * dy);
  }
  function convLine(y) {
    const dy = (y - yieldPct) / 100;
    return P0 * (1 - Dmod * dy + 0.5 * Cvx * dy * dy);
  }

  // draw the duration-only linear approximation (dashed red)
  ctx.beginPath(); ctx.setLineDash([6, 4]); ctx.strokeStyle = '#c0392b'; ctx.lineWidth = 1.8;
  prices.forEach(({ y }, i) => {
    const x = px(y), yp = py(durLine(y));
    if (i === 0) ctx.moveTo(x, yp); else ctx.lineTo(x, yp);
  });
  ctx.stroke();

  // draw the duration+convexity quadratic approximation (dotted green)
  ctx.beginPath(); ctx.setLineDash([2, 3]); ctx.strokeStyle = '#27ae60'; ctx.lineWidth = 1.8;
  prices.forEach(({ y }, i) => {
    const x = px(y), yp = py(convLine(y));
    if (i === 0) ctx.moveTo(x, yp); else ctx.lineTo(x, yp);
  });
  ctx.stroke();

  // draw the exact price curve (solid blue)
  ctx.beginPath(); ctx.setLineDash([]); ctx.strokeStyle = '#1d6fb5'; ctx.lineWidth = 2.4;
  prices.forEach(({ y, p }, i) => {
    const x = px(y), yp = py(p);
    if (i === 0) ctx.moveTo(x, yp); else ctx.lineTo(x, yp);
  });
  ctx.stroke();

  // dot at current yield
  const cx2 = px(yieldPct), cy2 = py(P0);
  ctx.beginPath(); ctx.setLineDash([]);
  ctx.arc(cx2, cy2, 5, 0, 2 * Math.PI);
  ctx.fillStyle = '#1d3557'; ctx.fill();

  // update the metrics strip below the chart
  document.getElementById('metrics').innerHTML =
    '<span><b>Price:</b> ' + P0.toFixed(2) + '</span>' +
    '<span><b>Mac. Duration:</b> ' + D.toFixed(2) + ' yrs</span>' +
    '<span><b>Mod. Duration:</b> ' + Dmod.toFixed(2) + '</span>' +
    '<span><b>Convexity:</b> ' + Cvx.toFixed(2) + '</span>';
}

document.getElementById('coupon').addEventListener('input', draw);
document.getElementById('maturity').addEventListener('input', draw);
document.getElementById('yield').addEventListener('input', draw);
draw();
Result
Console

Notice that the duration line is tangent to the price curve at the current yield. As you move the yield away, the gap grows — that gap is exactly what convexity corrects. The correction is always positive: convexity bends the approximation back toward the true price, in both directions.

The Real Complexity

The price of a plain coupon bond paying coupon CC each period, with face value FF, nn periods to maturity, and yield yy per period is:

P(y)=∑t=1nC(1+y)t+F(1+y)nP(y) = \sum_{t=1}^{n} \frac{C}{(1+y)^{t}} + \frac{F}{(1+y)^{n}}

Macaulay duration is the weighted average time to receive each cash flow, where the weight is the present-value fraction of that flow:

DMac=1P∑t=1nt⋅CFt(1+y)tD_{Mac} = \frac{1}{P} \sum_{t=1}^{n} t \cdot \frac{CF_t}{(1+y)^{t}}

Modified duration converts this into a price-sensitivity measure — it is literally −dPdy/P-\frac{dP}{dy} / P:

Dmod=DMac1+yD_{mod} = \frac{D_{Mac}}{1+y}

So the first-order approximation is ΔP≈−Dmod⋅P⋅Δy\Delta P \approx -D_{mod} \cdot P \cdot \Delta y.

Convexity is the normalized second derivative 1Pd2Pdy2\frac{1}{P} \frac{d^2P}{dy^2}:

Cvx=1P(1+y)2∑t=1nt(t+1)⋅CFt(1+y)tC_{vx} = \frac{1}{P(1+y)^{2}} \sum_{t=1}^{n} t(t+1) \cdot \frac{CF_t}{(1+y)^{t}}

The full second-order approximation (the Taylor expansion to two terms) is:

ΔP≈−Dmod⋅P⋅Δy+12⋅Cvx⋅P⋅(Δy)2\Delta P \approx -D_{mod} \cdot P \cdot \Delta y + \tfrac{1}{2} \cdot C_{vx} \cdot P \cdot (\Delta y)^{2}

Three facts that fall out of this algebra:

  • Zero-coupon bonds have Macaulay duration exactly equal to their maturity — every cent arrives at the end, so the weighted average time is the time.
  • Convexity is always positive for plain bonds (every cash-flow weight is positive and t(t+1)>0t(t+1) > 0), which is why bondholders love it: the second-order term always helps.
  • Longer maturity and lower coupon both raise duration and convexity, making the bond more sensitive to rates in both directions.

Where It Matters

Duration and convexity are not theoretical curiosities — they drive decisions worth trillions of dollars every year:

  • Immunization: a pension fund matches the duration of its bond portfolio to the duration of its liabilities. If rates move, assets and liabilities shift by the same amount, leaving the funding ratio intact.
  • Hedging: a trader who owns a bond with duration 7 can sell Treasury futures with an equivalent dollar duration (DV01) to neutralize rate risk while keeping other exposures.
  • Convexity trading: two portfolios can have the same duration but different convexity. The higher-convexity portfolio outperforms in either direction of rate moves — investors pay a premium for it, compressing yields.
  • Mortgage-backed securities: homeowners refinance when rates fall, shortening the MBS duration just when investors want it to be long. This negative convexity is the defining risk of the asset class and the reason MBS spreads are wider than comparable Treasuries.
  • Central banks: the Fed's quantitative easing removed enormous duration from the market, compressing the term premium — the extra yield investors demand for holding long bonds. Measuring that effect requires exactly the tools above.

Understanding duration and convexity also connects to optimization: immunization is a linear-programming problem over duration constraints, and convexity turns it into a quadratic one.

Conclusion

Bond pricing looks like arithmetic — discount each cash flow, add them up. But sensitivity to interest rates is calculus. Duration is the slope of the price-yield curve at a given point; convexity is the curvature that keeps the slope from being the whole story.

The practical punchline is the same as the mathematical one: the price-yield relationship always bows toward the bondholder. Rates rise — you lose less than duration says. Rates fall — you gain more. That asymmetry, measured by convexity, is one of the few genuine free lunches in fixed income, and it is why long-dated, low-coupon bonds command a convexity premium in every deep bond market in the world.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/bond-duration-convexity/Content licensed under CC BY-NC 4.0.