An end-to-end, one-factor Black–Karasinski (BK) implementation using Canadian zero-coupon government yield curves. The project improves on independent tenor-by-tenor AR(1) regressions by separating historical parameter estimation from risk-neutral curve calibration and by building a recombining short-rate tree that can price contingent claims.
-
Validates 3,970 daily Canadian zero-coupon curves across 120 maturities from 2000-01-04 to 2015-12-31.
-
Uses the 3-month yield as an observable short-rate proxy and estimates an Ornstein–Uhlenbeck process for its log with the exact transition likelihood. Actual calendar gaps are used instead of assuming every row is exactly
1/252year apart. -
Implements the one-factor BK model
d log(r_t) = [theta(t) - a log(r_t)] dt + sigma dW_t. -
Builds a recombining, moment-matched trinomial tree for the mean-reverting Gaussian state variable.
-
Solves one deterministic log-rate shift per quarter so Arrow–Debreu state prices reproduce the 2015-12-31 zero-coupon curve.
-
Verifies discount-factor and yield-fit errors, positive short rates, transition probabilities, and an illustrative European bond-option price.
The dataset contains yield curves but no cap, floor, or swaption volatility quotes. A yield curve identifies the time-dependent drift/shift needed for an exact term-structure fit, but does not uniquely identify risk-neutral mean reversion and volatility. This repository therefore uses a transparent hybrid:
aandsigmaare historical estimates from a 3-month yield proxy under the physical measure;- the deterministic BK shift is calibrated under the risk-neutral pricing tree to the latest observed curve.
This is a complete BK tree implementation and curve calibration, but it is not
a production market calibration to option-implied volatility. For that, add
swaption or cap/floor quotes and jointly fit a and sigma under the pricing
measure.
.
├── data/CAD2000_2015Yield.csv
├── notebooks/bk_full_calibration.ipynb
├── outputs/ # generated locally; ignored by Git
├── scripts/build_notebook.py
├── src/black_karasinski.py
├── tests/test_black_karasinski.py
├── DATA_SOURCE.md
├── LICENSE
├── README.md
├── requirements.txt
└── run_analysis.py
Python 3.10 or later is recommended.
python -m venv .venv
# Windows
.venv\Scripts\activate
python -m pip install -r requirements.txt
python run_analysis.py
python -m pytest -qTo rebuild and execute the notebook:
python scripts/build_notebook.py
python scripts/execute_notebook.pyGenerated CSV summaries and PNG charts are written to outputs/. The executed
notebook is committed because it is the primary reader-facing project artifact;
bulk generated outputs remain reproducible and are ignored by Git.
Write log(r_t) = alpha(t) + x_t, where
dx_t = -a x_t dt + sigma dW_t.
For each quarterly tree step, the code uses the exact OU conditional mean and
variance. Three transition probabilities match the first two conditional
moments. The node spacing is sqrt(3 * conditional_variance), which keeps the
moment-matched transition probabilities non-negative after centering on the
nearest next node.
At step i, alpha_i is solved by one-dimensional root finding so that
sum_j Q(i,j) * exp(-exp(alpha_i + x_j) * dt) = P_market(0,t_{i+1}),
where Q(i,j) are Arrow–Debreu state prices. The same calibrated tree is then
used for backward-induction pricing.
- Black, F. and Karasinski, P. (1991), “Bond and Option Pricing When Short Rates Are Lognormal,” Financial Analysts Journal, 47(4), 52–59. https://doi.org/10.2469/faj.v47.n4.52
- Bank of Canada, “Yield curves for zero-coupon bonds.” https://www.bankofcanada.ca/rates/interest-rates/bond-yield-curves/
- Bolder, D., Johnson, G., and Metzler, A. (2004), “An Empirical Analysis of the Canadian Term Structure of Zero-Coupon Interest Rates.” https://doi.org/10.34989/swp-2004-48
This is an academic portfolio project, not production trading or risk-system code and not investment advice. The 3-month yield is only a proxy for the instantaneous short rate, and historical parameters need not equal risk-neutral parameters.