1. How we price a bond every day
Every night the engine walks each active bond through five steps: generate the cashflow schedule from your coupon and dates, solve YTM from your purchase price, look up the matching central-bank benchmark yield at the trade-date tenor, calibrate the implied spread, and discount tomorrow's cashflows at the current benchmark plus that stored spread to produce a theoretical price.
The two formulas that drive everything
Worked example: a 5-year corporate at 102.50
4.05% - 3.65% = 0.40%, stored as implied_spread = 40 bps.3.78% + 0.40% = 4.18%. Output: ~101.95.Under the hood: cashflows and the YTM solver
Cashflow generation walks from issue_date to maturity_date applying the frequency and day-count convention. Each coupon period accrues at coupon_rate / frequency on the face value, weighted by the day-count rule. The final period repays principal. Stub periods live at one end (Upfront for most US corporates, Backend for some European issuers). Zero-coupon bonds get a single terminal cashflow. Perpetuals are handled as a separate class rather than being integrated to infinity.
The YTM solver uses Newton-Raphson as the primary method, falling back to brentq (bracket-and-bisect) when Newton stalls or returns a yield outside an economic acceptance band of roughly minus 50% to plus 50%. Distressed and deeply premium bonds that would defeat a single-method solver still resolve through the bracket-and-bisect path.
Clean and Dirty round-trip symmetry
For a Clean-quoted bond, the YTM solver adds accrued interest before solving; for a Dirty-quoted bond it uses the input price directly. The reprice path mirrors the symmetry, subtracting accrued for Clean and leaving it in for Dirty. Either way, calibrating against the input price and immediately repricing against the same curve returns the input price.
2. Yield curves and the shifted-curve mechanism
Stored as (curve, tenor_months, date, yield_value) and interpolated on lookup. Sources by currency:
| Currency | Source | Tenors stored | Cadence |
|---|---|---|---|
| USD | U.S. Treasury Daily Yield Curve | 13 pillars: 1/2/3/4/6 month + 1/2/3/5/7/10/20/30 year | Daily |
| EUR | ECB AAA par curve + 7 country shifts | 3M-30Y AAA daily; per-country 10Y daily | Daily |
| GBP | Bank of England gilt yields | 5Y, 10Y, 20Y | Daily |
| CAD | Bank of Canada | Overnight CORRA, 1M/2M/3M/6M T-bills, 1Y/2Y/3Y/5Y/7Y/10Y T-bonds, long bond | Daily |
| AUD | RBA F2 (primary) plus OECD STES (fallback) | RBA: 2Y/3Y/5Y/10Y; OECD fallback: 1M cash, 3M interbank, 10Y govt | Daily / monthly |
For curve fundamentals see the Yield Curve Calculator.
The shifted-curve mechanism for EU sovereigns
The ECB publishes the AAA par curve at full tenor but only the 10Y point per country. Italian, Spanish, French, Portuguese, Irish, Greek, and Belgian government bonds trade at persistent positive premiums to AAA, so pricing them off pure AAA would systematically undervalue. The shifted curve anchors at the 10-year point:
Country-shifted curve
All seven country 10Y points come from a single batched HTTP call to the ECB statistical data warehouse, keyed by ISO country code. When a date is missing for a country, the shifted-curve lookup falls back to plain AAA at the requested tenor rather than refusing to price.
Sovereign detection is a four-layer hybrid: ISIN prefix fast-path when issuer is empty, generic sovereign tokens ("republic of", "treasury", "kingdom of"), quasi-sovereign tokens (KfW, EIB, German Pfandbriefe) that route sovereign-like, and per-country tokens that disambiguate within EUR. The token list is intentionally conservative: a misclassified corporate routed to a sovereign curve would understate spread.
AUD: govt-only primary with documented basis risk on the fallback
RBA Table F2 (four government tenors, daily) is primary. When F2 is unreachable, the engine falls back to OECD STES, which publishes three Australian series of mixed character:
- 1-month proxy: RBA cash rate target (monetary policy rate)
- 3-month proxy: BBSW interbank rate (bank credit, not government)
- 10-year: government bond yield
Basis risk on the AUD fallback
The 1-month and 3-month points are not government bond yields. Mixing them with the 10Y govt point introduces a small basis at the short end: typically 5-30 basis points in calm regimes, materially wider in funding stress. The source is stamped on every stored row, so fallback-priced AUD bonds can be identified after the fact. For the broader framing of where basis risk hits a bond portfolio, see Basis Risk for Bond Investors.
Storage and self-healing
Each (curve, tenor, date) row is unique; re-runs do not duplicate. When the bond report needs a curve for a date that is not cached, the lookup calls the registered fetcher and writes the data in line, so a stale curve self-heals on the next read.
Bad data is rejected at the fetch boundary: yields outside [-2%, +25%] are dropped, and upstream schema changes abort the batch rather than write rows under a wrong schema.
3. The implied spread: what it captures
The spread number we calibrate is an implied I-spread: a single-point benchmark match between your bond's YTM at purchase and the matching central-bank curve point on that date. Bond Spread Calculator covers how I-spread compares to Z-spread and OAS.
Freezing it at trade date separates two risks:
- Rate risk: how the benchmark curve moves day to day. Captured automatically by the nightly reprice.
- Credit risk: the premium you locked in for taking on the issuer's specific creditworthiness. Captured in the stored spread.
The engine measures your credit premium at the moment of purchase, not today's market credit premium. The two diverge as the issuer's situation evolves: downgrades, distress, sector repricing. Edit Spread lets you re-anchor explicitly, or back-solve from a current market price.
The stored spread persists, with three documented exceptions:
- You override it via Edit Spread. The override is flagged so subsequent re-anchoring migrations skip your bond.
- An operator runs a country-curve migration that re-anchors non-user-overridden EUR sovereign bonds onto the country-shifted curve. The prior spread is snapshotted for audit.
- The stored value drifts to an unreasonable range on the next reprice. The engine recalibrates against the current curve and writes the corrected value, self-healing a previously poisoned spread.
4. Daily repricing
A scheduled job runs nightly, iterating active bonds (type = Bond, maturity in the future, valid metadata). For each user it pre-warms the set of curves needed by that user's bonds (including the AAA parent for any country-shifted curve), then walks the bonds one at a time. Each call lazy-fetches the curve if stale, reprices the bond, and saves the result so downstream caches refresh.
For multi-lot positions (same ISIN held across several trade lots), one lot is designated the anchor. The anchor's reprice runs through the full pipeline; the resulting theoretical price propagates to the other lots in bulk.
If a curve source has a transient outage, the bond's theoretical price blanks for the night with a tooltip explaining the source is temporarily unavailable, and the engine retries automatically on the next run. YTM and duration continue to display from the bond's own cashflows in the meantime — the rate-sensitivity numbers stay on the page even when the rate environment hasn't refreshed yet.
5. What you control
Edit Spread
The Edit Spread modal on every bond row lets you set the implied spread directly in basis points or paste a target market price so the engine back-solves the spread for you. The override is flagged so subsequent re-anchoring migrations skip your bond and your value persists.
One caveat for currencies with single-tenor curves: if the bond's remaining tenor falls outside the safe window (AUD: 24-180 months), the by-price back-solve will decline because the resulting spread would be calibrated against a flat-extrapolated benchmark.
Price priority in your bond table
The Price column for each row picks the freshest credible source in this order:
- Broker market price: live price reported by your synced broker, when fresher than 7 days.
- Market price: for bonds we cover in our market data, the latest daily quote when fresher than 7 days. Covers users without a broker sync.
- Theoretical price: our nightly-computed value, when calibration succeeded.
- Purchase price: your original input, always available as a floor.
YTM and duration are computed against whichever price is current. Each number is labelled with its source. Daily change is suppressed when a non-theoretical price is in use to avoid phantom moves from the source switch.
Freshness honesty on the market quote
The freshness date we use to gate the market quote is the date our data feed refreshed successfully, not the date of the underlying trade. For an illiquid bond that did not trade today, the feed can still carry the latest available quote and stamp it with today's date. Liquid bonds reflect today's market; illiquid bonds may carry a quote from earlier in the week with a current freshness stamp.
Manual reprice
The Reprice button forces a curve refresh and per-bond recalculation without waiting for the nightly. Useful after editing a coupon or maturity date, or after a curve source has recovered from an outage.
6. What we don't model
- Callable and putable bonds: priced as if straight to maturity. No option-adjusted spread, no short-rate model (Hull-White, Black-Karasinski) to value the embedded option. The theoretical price will systematically overvalue callable bonds when they are trading near or above the call price.
- Convertible bonds: not modeled. The equity-option leg requires Black-Scholes or a tree model; not yet supported.
- Floating-rate notes (FRNs): treated as fixed-rate at the current coupon. Forward-curve projection of future coupon resets is not implemented.
- Real-time credit spread feeds: not implemented. The implied spread is what you calibrated at purchase, not today's market spread.
- Borrowed-shape curves for low-coverage currencies: CHF, JPY, NZD, NOK, SEK, DKK, MXN and similar currencies are deferred pending user demand. Bonds in those currencies show YTM and duration from cashflows but no live curve-based theoretical price.
- SGD: no usable public curve. Singapore is not in OECD STES and MAS does not publish a comparable daily feed. Holders see YTM and duration from cashflows; the Spread cell stays blank with a tooltip naming the currency.
The callable case
If you hold a US corporate that is callable in 2 years and trades at 105, our theoretical will use the stated maturity (say 10 years) and overstate value relative to a yield-to-worst calculation a desk would run. Edit Spread by market price is the workaround until OAS support is added.
7. Reference: full inputs and outputs schema
Inputs (per bond)
| Field | Used by |
|---|---|
isin | Issuer / sovereign detection, curve routing |
issuer, name | Sovereign token matching, label rendering |
coupon_rate | Cashflow schedule |
issue_date, maturity_date | Tenor, accrual periods |
frequency (Annual, Semi, Quarterly) | Coupon period length |
rate_convention (30/360, ACT/365, ACT/ACT) | Day-count weighting |
stub_period (Upfront, Backend) | Stub coupon placement |
quotation (Clean, Dirty) | YTM solver input handling |
face_value, lot_size | Cashflow notional scaling, position value |
currency | Curve routing |
price (purchase), trade_date | YTM at purchase, spread calibration |
Outputs (per bond, recomputed nightly)
| Field | How |
|---|---|
ytm | Newton-Raphson with brentq fallback over the cashflows |
duration, convexity | Modified duration and dollar convexity from the same cashflows |
current_yield, accrued_interest | Coupon over price; accrual since last coupon |
implied_spread (bps) | YTM at trade-date minus benchmark yield at the matching tenor |
theoretical_price | Present value of cashflows discounted at (today's benchmark + stored spread) |
benchmark_curve | The curve identifier (e.g. USD_UST, EUR_IT) |
Scale note: the YTM solver works in price-as-percent-of-face, so face values of 100 (typical sovereign), 1,000 (typical US corporate), or 1.0 (a wrapped-fund unit) all produce the same per-100 theoretical. lot_size applies downstream at the position-value layer, not inside the solver.
8. Frequently asked questions
See the engine on your own bonds
Import your fixed-income positions and watch the daily theoretical price, YTM, spread, and duration update against live central-bank yield curves.
Start Free View Bond Report