A faithful pure-Python reference implementation of the fixed-rate bond
subset of QuantLib v1.43 (tag v1.43, commit
6b57206e04598f092efee66e3b367efc84771995).
It reproduces the behaviour of QuantLib 1.43 for fixed-rate bonds, and it is validated against QuantLib 1.41 through a differential suite. It is not part of QuantLib and is not endorsed by the QuantLib Group or its contributors.
It is read, and its test suite is run, so that a separate model can be developed from it elsewhere (decision D-11). It is not imported by any model, it is not published (decision D-12), and it makes no API-shape promise to any caller. Fidelity to upstream, validation against the oracle, readability and traceability are the product.
Start with docs/READING_THE_PORT.md - how a
ported module is laid out, what the # ql: anchors and the FORMULA CORE: /
STATE READ: markers mean, which five modules to open first, and one worked
reading of a formula shown as C++ beside the port.
No install, no build, no wheel. Everything runs from the working tree:
C:/Users/fumito/anaconda3/envs/py313/python.exe -m pytest
C:/Users/fumito/anaconda3/envs/py313/python.exe tools/gate.py
pythonpath = ["src"] in pyproject.toml is the whole of the setup. Python
3.13 or later; the port itself imports nothing outside the standard
library - no numpy, no scipy, nothing to install.
The differential tests additionally need the QuantLib SWIG binding (the
ORACLE, pinned at 1.41 in the py313 environment). It is a TEST dependency
and never a runtime one: src/ may not import QuantLib, and
tools/check_layers.py fails the gate if it ever does.
tools/gate.py is the single definition of green - ruff, mypy --strict, the
suite, the oracle suite, coverage floors, the header and unit checks, the
import-layer rules and the lint rules. --wp <id> scopes it to a work
package; --nightly adds the slow suite, the benchmarks and golden drift.
Seventy-five upstream units. Everything needed to build and price a fixed-rate bond, end to end, on a discount curve or at a yield.
| area | what is here |
|---|---|
| instruments | Bond, FixedRateBond, ZeroCouponBond, AmortizingFixedRateBond (with sinkingSchedule / sinkingNotionals) |
| pricing | DiscountingBondEngine, BondFunctions (clean/dirty price, yield, accrued, duration, convexity, bps, atm rate, z-spread), CashFlows |
| cash flows | CashFlow, Coupon, FixedRateCoupon, FixedRateLeg, SimpleCashFlow, Redemption, AmortizingPayment |
| curves | FlatForward, ZeroCurve, ZeroSpreadedTermStructure, ForwardSpreadedTermStructure, InterpolatedCurve, YieldTermStructure, InterestRate |
| time | Date, Period, Schedule / MakeSchedule, Calendar (TARGET, UnitedStates, UnitedKingdom, Brazil, Australia, Canada, SouthAfrica, WeekendsOnly, JointCalendar, NullCalendar), DayCounter (ActualActual, Actual365Fixed, Actual360, Thirty360, Business252), IMM |
| numerics | Brent, NewtonSafe, Solver1D, LinearInterpolation, Rounding, close / close_enough |
| machinery | Settings, Observable / Observer, LazyObject, Handle / RelinkableHandle, Quote / SimpleQuote, Instrument, PricingEngine, Error |
How good is it? Measured on 2026-09-24. A 10-year semiannual
FixedRateBond - 4% coupon, TARGET / Thirty360(BondBasis), three
settlement days - priced on a flat 3% Actual365Fixed curve at 16-Sep-2015,
on pureql and on QuantLib side by side:
- bit for bit identical: NPV
108.36776235612268, clean price108.34563953853349, dirty price, accrued amount, settlement value, and all 21 cash flows, date and amount; - bit for bit identical when both libraries are asked at the SAME yield: clean price, modified and Macaulay duration, convexity, bps, basis-point value, the yield value of a basis point, bps on the curve, the atm rate and the accrued days;
- the one quantity that differs is the SOLVED yield, by 3.2e-11
(
0.030250400779423203against0.030250400810980599). That is the oracle skew OS-10, and it is the oracle that is less accurate, not the port: QuantLib 1.41 predates theIrrFinderderivative fix of #2589, which the port carries. Round-tripping both yields through the oracle's own pricer leaves the port's root with a residual of 1.6e-13 against the oracle's 2.8e-08.
The PORT half of those figures is frozen: tests/meta/test_documentation_set.py::test_the_readme_headline_is_what_the_port_computes_today
rebuilds the same bond with no oracle and asserts the NPV, the clean price,
the solved yield and the 21 flows against the literals above, bit for bit, so
this paragraph cannot outlive the code it describes. The oracle half is the
differential sweep's job, below.
Everything else is the differential suite: 41 registered scenarios - dates,
periods, four calendars, seven day counters, five schedules, eight interest
rates, both curves, three legs and ten priced bonds over a flat AND a non-flat
curve - run on both libraries in one pinned state and compared key by key,
with zero mismatches. The breakdown is the registry's own
(tests/helpers/scenario.py::CASES), and
tests/meta/test_documentation_set.py derives it from there so the sentence
cannot drift from the set it describes.
A missing class is a roadmap item, not a defect. Release 1 is deliberately
the fixed-rate subset. Floating-rate, CMS, inflation, callable, convertible,
cat and Italian-government instruments - FloatingRateBond, IborCoupon,
CallableFixedRateBond, CPIBond, BTP, ConvertibleBond and the 151
units behind them - are registered as deferred, each with the family it
returns with, in docs/migration/units.toml and Appendix E of the plan.
Twelve machine-checked guards (Appendix F, FG-1..FG-12) keep the kept files
shaped so that a family can be added later without editing them; all twelve
were re-verified against the code as merged. So the answer to "why is there no
FloatingRateBond?" is a plan entry with an order and a guard, not an
oversight.
Two more categories of "missing" that are also deliberate, and are written up where you will meet them:
pureql.TARGETraisesAttributeErrorwhilepureql.FixedRateBondworks. Twenty ported modules are outside the flat namespace by the tier rule of 3.1; each is one import deeper.docs/READING_THE_PORT.mdsection 2.5 lists all twenty with the import line for each.- Parts of a ported unit that upstream compiles out or that the subset does
not need are listed, unit by unit, in the module's own docstring under
"Not ported", and
tools/check_units.pycompares that list with the ledger on every gate run. Nothing is dropped silently (convention C-17).
docs/migration/DEVIATIONS.md- every deliberate difference from upstream, with its reason and its test. The rule that governs the registry: a difference that changes a value C++ produces deterministically on valid input is not a deviation, it is a bug in the port, and the port is fixed.docs/migration/QUIRKS.md- upstream oddities the port reproduces on purpose, each markedQUIRK(Q-nn)at the site and pinned by a characterisation test. Read Q-09 and Q-04 before you transcribe a duration or a price.- The oracle skews - four behaviours where the installed 1.41 oracle
disagrees with the v1.43 target on purpose (OS-02, OS-05, OS-09, OS-10).
They are a closed registry, never a widened tolerance: a comparison that
cannot be made on 1.41 is SKIPPED through its registry entry and reported.
One of the four was under review for SCOPE and has been ruled on.
tests/differential/test_bond_vs_oracle.py::test_the_bracketing_failure_message_equals_the_oraclesis skipped under@skew_oracle('OS-10')although it compares a DERIVATIVE-FREE Brent message and so never reaches theIrrFinderderivative OS-10 was first written about; what it meets is the OTHER half of PR #2589, the sign ofIrrFinder::operator()(cashflows.cpp:754-760). First raised with a field-by-field amendment indocs/migration/reviews/WP-19.mdand carried forward in WP-23 and WP-24; the reviewer ruled on 2026-09-25 that #2589 is one upstream change and widened OS-10 to cover both halves - the objective's sign atcashflows.cpp:754-760and the derivative at:762-769. The entry now says exactly which comparisons it excuses (a Newton-family oracle solve's last ~1e-10, and text that interpolates the objective) and which it does not (every root, every error/no-error outcome, every price). The mismatch is closed, andtests/meta/test_release_review.pyENFORCES the widened scope against the syntax tree of every marked test instead of exempting one.
BSD 3-Clause, the same as upstream. See LICENSE for the port's own terms,
LICENSES/QuantLib-LICENSE.TXT for the verbatim upstream licence file (the
full copyright-holder list, the BSD-3 text and three further attributions),
and NOTICE for what this derives from.
All three BSD-3-Clause conditions are redistribution conditions, so they
attach the instant any part of this port is copied out - into a model's
formulas, into another project, into a blog post. docs/ATTRIBUTION_TEMPLATE.md
holds what must travel with such a copy, and its checklist is the one part of
the practice that can be forgotten.
| what | where |
|---|---|
| how to read the port | docs/READING_THE_PORT.md |
| what changed since you last read this | CHANGELOG.md |
| the migration plan (binding) | docs/migration/MIGRATION_PLAN.md |
| conventions, decisions, deviations, quirks | docs/migration/ |
| what is deferred and how it comes back | plan Appendix E; docs/migration/units.toml |
| where every unit and work package stands | docs/migration/units.toml, STATUS.md |
| per-package reviews | docs/migration/reviews/ |
| the upstream checkout (read-only, not committed) | reference/QuantLib-1.43/ |
This page is document 2 of the reference documentation set
(MIGRATION_PLAN.md 3.7), written by work package WP-24. pureql is a
port of QuantLib and is not part of it; see NOTICE.