Guides · bonds

Price a bond on a bootstrapped zero curve (two API calls)

The desk pattern "price off the swap curve" is two POSTs: /v1/curves/bootstrap turns deposit and swap quotes into zero-rate pillars, and /v1/bonds/price discounts the bond's cashflows on those pillars — returning clean/dirty price, accrued, the curve-implied YTM, duration and BPV.

01Call 1 — bootstrap the curve

Deposits pin the short end, par swaps the long end (valuation date fixed for reproducibility):

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 · pillars (abridged)
"points": [
  {"tenor": "1Y",  "years": 1.0,       "zero_rate": 0.032968, ...},
  {"tenor": "2Y",  "years": 2.008219,  "zero_rate": 0.033388, ...},
  {"tenor": "5Y",  "years": 5.002740,  "zero_rate": 0.035415, ...},
  {"tenor": "10Y", "years": 10.008219, "zero_rate": 0.036964, ...}
]

Runs on the free tier — 500 calls/month, no credit card. Get a key. Full walkthrough of this call: bootstrap a yield curve from deposits and swaps.

02The mapping — pillars in, zero_curve out

The bond endpoint takes zero_curve as a list of {tenor_years, zero_rate}. Turning call 1's pillars into call 2's input is three lines:

python
zero_curve = [
    {"tenor_years": round(p["years"], 4), "zero_rate": round(p["zero_rate"], 6)}
    for p in curve["points"]]

Which, on the pillars above, is exactly:

[{"tenor_years": 1.0,     "zero_rate": 0.032968},
 {"tenor_years": 2.0082,  "zero_rate": 0.033388},
 {"tenor_years": 5.0027,  "zero_rate": 0.035415},
 {"tenor_years": 10.0082, "zero_rate": 0.036964}]

Conventions match by construction: the bootstrap outputs continuously compounded act/365 zeros, and zero_curve expects continuously compounded act/365 zeros. No conversion step to get wrong. (If you ever need the DF↔zero math anyway: discount factor vs zero rate.)

03Call 2 — price the bond on those pillars

A 5% annual bond, issued 2024-07-01, maturing 2029-07-01, settling 2026-10-15, act/act — with zero_curve instead of a flat yield_rate:

request
curl -s https://quantbox.dev/v1/bonds/price \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"coupon_rate": 0.05,
       "issue_date": "2024-07-01",
       "maturity_date": "2029-07-01",
       "settlement_date": "2026-10-15",
       "frequency": "annual",
       "day_count": "act/act",
       "zero_curve": [{"tenor_years": 1.0,     "zero_rate": 0.032968},
                      {"tenor_years": 2.0082,  "zero_rate": 0.033388},
                      {"tenor_years": 5.0027,  "zero_rate": 0.035415},
                      {"tenor_years": 10.0082, "zero_rate": 0.036964}]}'
response
{
  "clean_price": 103.9307,
  "dirty_price": 105.3827,
  "accrued": 1.4521,
  "ytm": 0.034451,
  "modified_duration": 2.4892,
  "convexity": 8.7997,
  "bpv": -0.0262,
  "conventions": {
    "prices": "per 100 of face value (market quote convention)",
    "settlement": "as provided (settlementDays=0, no implicit T+2)",
    "ytm": "compounded annual, act/act",
    "zero_curve": "zero rates continuously compounded, act/365",
    "calendar": "TARGET, unadjusted schedule"
  }
}

04Reading the numbers

FieldValueMeaning
clean_price103.9307Quoted price per 100 face, discounted pillar by pillar on the curve.
dirty_price105.3827What you actually pay: clean + accrued (103.9307 + 1.4521).
accrued1.4521106 days since the 2026-07-01 coupon, act/act: 5 × 106/365 = 1.4521.
ytm0.034451Implied YTM — the single flat yield (annual comp., act/act) that reproduces the curve price. This is the answer to "yield to maturity from a zero curve": 3.4451%.
modified_duration2.4892ΔP ≈ −D·P·Δy at the implied yield.
bpv-0.0262Exact price change for +1 bp. Cross-check: 2.4892 × 105.3827 / 10,000 ≈ 0.0262.

The implied YTM is the bridge between the two pricing worlds: the curve view (four zeros between 3.30% and 3.70%) collapses, for this particular bond's cashflows, into one flat 3.4451%. Duration, convexity and BPV in the response are computed at that implied yield — same machinery as the flat-yield case, explained in bond duration, convexity and BPV over HTTP.

05The whole workflow as one script

python · both calls chained
import requests

BASE = "https://quantbox.dev"
H = {"X-API-Key": KEY}

# 1) bootstrap the curve from market quotes
curve = requests.post(f"{BASE}/v1/curves/bootstrap", headers=H, 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"]}).json()

# 2) map pillars -> zero_curve (the whole "integration")
zero_curve = [
    {"tenor_years": round(p["years"], 4), "zero_rate": round(p["zero_rate"], 6)}
    for p in curve["points"]]

# 3) price the bond off the curve
bond = requests.post(f"{BASE}/v1/bonds/price", headers=H, json={
    "coupon_rate": 0.05, "issue_date": "2024-07-01",
    "maturity_date": "2029-07-01", "settlement_date": "2026-10-15",
    "frequency": "annual", "day_count": "act/act",
    "zero_curve": zero_curve}).json()

print(f'clean {bond["clean_price"]:.4f}  implied YTM {bond["ytm"]:.4%}')
# clean 103.9307  implied YTM 3.4451%

06Curve vs flat yield: 1.44 points apart

The same bond priced with "yield_rate": 0.04 instead of the curve comes back at 102.4892 clean (that call is walked through in price a bond via REST API). The curve prices it 1.44 points higher — no mystery: the bootstrapped zeros (3.30–3.70%) all sit below 4%, so every cashflow discounts less. That spread between "the curve's price" and "the market's flat yield price" is exactly what relative-value screens are built on.

07Details that bite

Pillars are measured from settlement. In the bond call, tenor_years means years from the settlement date (2026-10-15), while call 1 bootstrapped as of 2026-07-01. Passing the pillars unchanged is the standard quick pattern; if you need the curve exactly as of settlement, re-bootstrap with valuation_date = settlement_date.

Interpolation differs between the two endpoints. The bootstrapper interpolates log-linearly on discount factors; the bond pricer rebuilds a curve from your pillars linearly in zero rates (act/365, extrapolation enabled beyond the last pillar). With pillars this close together the difference is beyond the fourth decimal — pass more pillars if you need tighter agreement.

These exact pillars produced this exact response. The numbers above come from the 4-/6-decimal pillars shown, not from hidden full-precision values — rerun the call and you match every digit. Rounding zeros to 6 decimals perturbs each rate by at most 5×10⁻⁷; at a BPV of 0.0262 per bp, that is on the order of a ten-thousandth of a point on this bond.

Run both calls yourself. Key in seconds, 500 free calls a month, full request schemas 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 · Discount factor vs zero rate · Price a bond via REST API · Bond duration, convexity and BPV over HTTP