Guides · curves

Discount factor vs zero rate: convert between them

For a continuously compounded zero rate z and a maturity of t years: DF(t) = e−z·t, and inversely z = −ln(DF)/t. A discount factor is the price today of 1 unit paid at t; the zero rate is the same information restated as an annualized rate. One line of math converts either way.

01The two formulas, checked in pure Python

Take a real pillar from a bootstrapped EUR curve (valuation date 2026-07-01): at 1Y the discount factor is 0.967570 and the zero rate 3.2968%. Both directions round-trip:

python · no dependencies
import math

z, t = 0.032968, 1.0          # 1Y zero, continuously compounded
df = math.exp(-z * t)         # 0.9675695  -> DF 0.967570 (6 dp)
z_back = -math.log(df) / t    # 0.0329680  -> recovers z exactly

df_market = 0.967570
print(-math.log(df_market) / t)   # 0.0329675 -> 3.2968% (6 dp)

With the API's full-precision zero, exp(−0.03296776 × 1.0) = 0.9675698 — matching the reported discount factor digit for digit. DF and zero rate are the same fact in two forms: DF for pricing (multiply cashflows), zero rate for intuition (compare levels across maturities).

02One API call returns both columns

You rarely want one conversion; you want the whole curve, consistently. POST /v1/curves/bootstrap builds the curve from market quotes and returns discount factor and zero rate at every pillar, already consistent:

request
curl -s https://quantbox.dev/v1/curves/bootstrap \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"valuation_date": "2026-07-01",
       "deposits": [{"tenor": "3M", "rate": 0.031},
                    {"tenor": "6M", "rate": 0.032}],
       "swaps":    [{"tenor": "2Y",  "rate": 0.034},
                    {"tenor": "5Y",  "rate": 0.036},
                    {"tenor": "10Y", "rate": 0.0375}],
       "output_tenors": ["1Y", "2Y", "5Y", "10Y"]}'
response
{
  "valuation_date": "2026-07-01",
  "points": [
    {"tenor": "1Y",  "date": "2027-07-01", "years": 1.0,
     "discount_factor": 0.967570, "zero_rate": 0.032968},
    {"tenor": "2Y",  "date": "2028-07-03", "years": 2.008219,
     "discount_factor": 0.935148, "zero_rate": 0.033388},
    {"tenor": "5Y",  "date": "2031-07-01", "years": 5.002740,
     "discount_factor": 0.837636, "zero_rate": 0.035415},
    {"tenor": "10Y", "date": "2036-07-01", "years": 10.008219,
     "discount_factor": 0.690776, "zero_rate": 0.036964}
  ],
  "conventions": {
    "deposits": "act/360, ModifiedFollowing, TARGET, 2 fixing days",
    "swaps": "fixed leg annual 30/360 vs Euribor6M, TARGET",
    "zero_rates": "continuously compounded, act/365",
    "interpolation": "log-linear on discount factors"
  }
}

Runs on the free tier — 500 calls/month, no credit card. Get a key. How the bootstrap itself works: bootstrap a yield curve from deposits and swaps.

python · requests
import requests

r = requests.post("https://quantbox.dev/v1/curves/bootstrap",
    headers={"X-API-Key": KEY},
    json={"valuation_date": "2026-07-01",
          "deposits": [{"tenor": "3M", "rate": 0.031},
                       {"tenor": "6M", "rate": 0.032}],
          "swaps":    [{"tenor": "2Y",  "rate": 0.034},
                       {"tenor": "5Y",  "rate": 0.036},
                       {"tenor": "10Y", "rate": 0.0375}],
          "output_tenors": ["1Y", "2Y", "5Y", "10Y"]})
points = r.json()["points"]

03The correspondence, pillar by pillar

Every row satisfies DF = exp(−z·t) to machine precision — t being the act/365 year fraction the API reports, not the nominal tenor:

Tenort (years, act/365)Discount factorZero rateexp(−z·t)
1Y1.0000000.9675703.2968%0.967570 ✓
2Y2.0082190.9351483.3388%0.935148 ✓
5Y5.0027400.8376363.5415%0.837636 ✓
10Y10.0082190.6907763.6964%0.690776 ✓

Why is the 2Y year fraction 2.008219 and not 2.0? The pillar date rolled from Saturday 2028-07-01 to Monday 2028-07-03 (ModifiedFollowing, TARGET), and 733/365 = 2.008219. Use the reported years, never the nominal tenor, when converting by hand.

One caveat on precision: the ✓ checks use the API's full-precision values. Recomputing from the 6-decimal figures printed here, the last digit drifts at long maturities — rounding of z is amplified by t, so at 10Y you get 0.690773 instead of 0.690776. Keep full precision in pipelines; round only for display.

04Compounding is where conversions go wrong

Annual compounding uses a different formula. DF = (1 + r)−t. Treating the 3.2968% continuous zero at 1Y as annually compounded gives DF = 1/1.032968 = 0.968084 instead of 0.967570 — a 5.1 bp price error from a units mistake, and it grows with maturity.

Convert rates before mixing. Annual to continuous: z = ln(1 + rannual); continuous to annual: rannual = ez − 1. A 4% annual yield is a 3.9221% continuous zero (ln 1.04 = 0.039221).

Day count sets t. These zeros are act/365; the same date grid under act/360 gives different year fractions and therefore different rates for identical discount factors. The conventions block in every response states both, so the numbers stay auditable.

05Where you use each form

Discount factors do the pricing: a bond is just Σ cashflow × DF(t). Zero rates feed comparisons, charts and inputs — including the zero_curve field of POST /v1/bonds/price, which expects exactly the continuously compounded act/365 zeros this endpoint outputs. The two endpoints are convention-matched by design; see also bond duration, convexity and BPV for what comes out the other side.

Convert a whole curve in one call. Key in seconds, 500 free calls a month, full request schema in the docs.

Get a free API key

No credit card · schemas and error codes in /docs

Related: Bootstrap a yield curve from deposits and swaps · Price a bond on a bootstrapped zero curve · Bond duration, convexity and BPV over HTTP