Strasmore Research
Learn Matt ConnorBy Matt Connor · data as of August 12, 2026 · refreshed weekly

QuantLib in Rust: libitofin, a Pre-1.0 Port

libitofin is a QuantLib port in Rust, reachable from Python through itofin. What a pricing library adds over a spreadsheet, and why to pin a tagged release.

QuantLib in Rust is the one-line description of libitofin: a port of QuantLib, the C++ library that has been the open source reference for derivatives pricing since the early 2000s, into Rust, with a Python package named itofin on top. As of August 2026 the project describes itself as pre-1.0, and that label governs everything practical about using it. A pricing library earns its place through the machinery wrapped around the formula, and that machinery is the same job in any language.

What a pricing library gives you that a spreadsheet formula does not

A Black-Scholes cell in a spreadsheet takes five inputs and returns a price. The formula is the easy part. Four layers sit around it, and those layers are the library.

Term structures. A discount rate is a curve across maturities, with an interpolation rule for the gaps between the points anyone actually quotes. The panel below is the raw material: quoted Treasury tenors on a single date.

QueryThe Treasury curve on its most recent quoted date
The exact SQL behind every number
SELECT
    arrayElement(tenors, i)                  AS tenor,
    round(arrayElement(rates, i), 2)         AS yield_pct,
    formatDateTime(curve_date, '%b %e, %Y')  AS as_of
FROM
(
    SELECT
        date AS curve_date,
        ['1 month', '3 months', '6 months', '1 year', '2 years', '3 years',
         '5 years', '7 years', '10 years', '20 years', '30 years']         AS tenors,
        [toFloat64(yield_1_month), toFloat64(yield_3_month), toFloat64(yield_6_month),
         toFloat64(yield_1_year),  toFloat64(yield_2_year),  toFloat64(yield_3_year),
         toFloat64(yield_5_year),  toFloat64(yield_7_year),  toFloat64(yield_10_year),
         toFloat64(yield_20_year), toFloat64(yield_30_year)]                AS rates,
        arrayJoin(range(1, 12))                                            AS i
    FROM global_markets.treasury_yields
    WHERE date = (SELECT max(date) FROM global_markets.treasury_yields)
)
WHERE yield_pct > 0
ORDER BY i
Run this yourself

As of Aug 10, 2026 the quoted curve carried 7 tenors, running from 3.79% at 1 month to 5.25% at 30 years. A spreadsheet meets this with a lookup and a hardcoded 4%. A library meets it with a curve object that every instrument prices off, under a stated interpolation (linear on zero rates, log-linear on discount factors, monotone splines) and a stated extrapolation policy past the last point. Shift that one object by a basis point and every sensitivity in the book moves consistently with it.

Day-count conventions. Interest accrues over a fraction of a year, and the definition of that fraction is a convention attached to the instrument. Actual/360 divides elapsed days by 360. Actual/365 divides by 365. The 30/360 family pretends every month has 30 days. Business/252 counts trading sessions against a 252-day year, which requires a real exchange calendar with the holidays already loaded. Take a hypothetical million dollars borrowed at 5% for 90 days: actual/360 accrues $12,500 and actual/365 accrues $12,329 on the same trade. The panel below shows why the business-day basis needs a calendar rather than a divisor.

QueryTrading sessions against calendar days, by month
The exact SQL behind every number
SELECT
    formatDateTime(toStartOfMonth(date), '%Y-%m')      AS month,
    countDistinct(date)                                AS trading_days,
    toUInt8(toDayOfMonth(toLastDayOfMonth(max(date)))) AS calendar_days
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
  AND date >= toStartOfMonth(today() - 365)
  AND date <  toStartOfMonth(today())
GROUP BY month
ORDER BY month
Run this yourself

Over the 12 months in view, 2026-07 held 22 sessions against 31 calendar days. No arithmetic rule produces that first number. It comes from a holiday calendar, and every month has its own answer. A library ships those calendars per venue and per country; a spreadsheet asks you to maintain them.

Calibration machinery. Model parameters are not quoted anywhere. You pick them by fitting model prices to observed market prices across a whole surface of quotes, then refitting as the surface moves. That is a bounded least-squares problem, and the library supplies the loop around it: a Levenberg-Marquardt optimiser, parameter transforms that keep a variance positive without hard constraints, a cost function over the quote set, and convergence criteria that fail loudly rather than quietly returning the starting guess.

A numerics layer. Underneath everything sits linear algebra and integration. QR and SVD decompositions solve the systems a fit produces, quadrature and Fourier integration price the models whose formulas are integrals, and root finders back out implied volatility. This layer is dull, and it is exactly the layer that gets reimplemented badly. The classic version is a hand-rolled Newton solver that converges on liquid at-the-money quotes and wanders off on deep out-of-the-money ones. A close second is a matrix inverse that loses precision on a near-singular fit and hands back parameters that look entirely plausible. If you are assembling the mental model from scratch, an open source quant trading book is a better starting point than any library's API reference.

What is libitofin, and is it QuantLib in Rust?

Yes, in the sense that matters. It is a port of QuantLib's design into Rust, and the project states that it is tested against QuantLib's own test suite. That is the honest way to port a numerics library: results are checked against the reference implementation rather than against a hand-written expectation of what the answer ought to be. The Python path is a package named itofin, so a Python process can reach the Rust engine without a C++ toolchain in the loop.

The label that matters more is pre-1.0. Under Rust's versioning convention a 0.x release carries no compatibility promise between minor versions: 0.4 to 0.5 is allowed to rename, move, or delete anything it likes. Treat the API as a moving target, and a few habits follow.

  • Pin an exact tagged release in your lockfile and upgrade on purpose, with your own test suite as the gate.
  • Keep a thin wrapper of your own around the library's types, so a rename lands in one file instead of forty.
  • Record the library version next to the numbers it produced, so a rerun that disagrees points at the upgrade rather than at the market.
  • Keep production margin, collateral, and regulatory risk numbers on something that carries a compatibility promise until a 1.0 arrives.

None of that is a criticism of the project. Pre-1.0 is an accurate self-description and the right place for a young port of a very large library to sit. The failure mode is a reader treating it as a drop-in QuantLib swap and meeting a changed signature in a minor release, halfway through a quarter. Install instructions also move with the version, and both cargo and pip need network access to resolve anything, so read the project's own README at the tag you intend to pin rather than a snippet copied out of a blog post, this one included.

Python, a compiled engine, or Rust?

Two questions settle this, and neither is about taste. How many times a second do you reprice, and do your numbers have to match across separate processes? Chain size sets the scale of the first question.

QueryDistinct option contracts that traded in one session
The exact SQL behind every number
WITH (SELECT max(date) FROM global_markets.options_greeks) AS last_session
SELECT
    underlying_symbol                      AS symbol,
    countDistinct(ticker)                  AS contracts_priced,
    formatDateTime(max(date), '%b %e, %Y') AS as_of
FROM global_markets.options_greeks
WHERE date = last_session
  AND underlying_symbol IN ('SPY', 'AAPL', 'NVDA', 'MSFT', 'KO')
  AND volume > 0
GROUP BY symbol
ORDER BY contracts_priced DESC
Run this yourself

On Aug 11, 2026, SPY had 5336 distinct contracts trade in a single session, against 360 for KO. Pricing the wide chain once is nothing at all. Pricing it with five sensitivities per contract, on every quote update, across a book of underlyings, is a different program with different constraints.

Stay in Python with an established library when the loop is measured in thousands of valuations a minute and the surrounding work is research, analysis, or end-of-day marks. QuantLib's own Python bindings are the mature choice: the same C++ engine, the widest instrument coverage, and years of production mileage. Speed is rarely the binding constraint in research code; coverage and correctness are.

Call a compiled engine from Python when the loop is hot and the code around it is not. The cost to watch is the boundary itself. A per-contract call from Python pays overhead on every crossing, and the fix is to hand the engine an array and take an array back. This is the seat itofin is aiming at, alongside QuantLib-Python.

Write Rust when the pricing loop is the product: a pricer inside a quoting service, a risk run on a schedule you cannot miss, or a binary shipped to a box with no Python on it. The second question lands here as well. Floating point results depend on the order of operations, so the same model implemented twice can disagree in the last digits, and a research notebook that disagrees with a production service is a week of forensics. One engine used from both sides removes that whole category of discrepancy, which is the durable argument for a compiled core with bindings whatever language it is written in. The same instinct applies to strategy work, as a reproducible backtest shows.

Where the calibration target actually lives

Calibration needs something to fit against, and that something is a surface of market-implied volatilities. The root-finding side of that is covered in how implied volatility is calculated. The shape below is what a model has to match.

QueryAAPL near-the-money implied volatility by time to expiry
The exact SQL behind every number
SELECT
    multiIf(days_to_expiry <=   7, '0 to 7 days',
            days_to_expiry <=  30, '8 to 30 days',
            days_to_expiry <=  60, '31 to 60 days',
            days_to_expiry <= 120, '61 to 120 days',
            days_to_expiry <= 240, '121 to 240 days',
                                   '241 days or more') AS dte_bucket,
    round(avg(implied_volatility) * 100, 1)            AS iv_pct,
    countDistinct(ticker)                              AS contracts
FROM global_markets.options_greeks
WHERE underlying_symbol = 'AAPL'
  AND date >= today() - 10
  AND days_to_expiry >= 0
  AND iv_converged = 1
  AND volume > 0
  AND abs(toFloat64(strike_price) / toFloat64(underlying_close) - 1) < 0.05
GROUP BY dte_bucket
ORDER BY min(days_to_expiry)
Run this yourself

Near the money, AAPL contracts in the 0 to 7 days bucket averaged 29.6% implied volatility across the sessions in view, against 29.3% at 241 days or more. A model carrying one volatility parameter cannot sit on both points at once, which is the whole reason models with a volatility term structure exist. Fitting one to a surface like this is the calibration step, and the sensitivities that fall out of the fitted model are the greeks, covered in the option greeks explained.

How these panels were built

The curve panel reads the single most recent quoted date in the Treasury series and unrolls its eleven tenor columns into rows, dropping any tenor with no quote that day. The session panel counts distinct dates on the SPY tape per month, which is a clean proxy for a full exchange session count. The chain panel counts distinct contract codes with non-zero volume on the latest available session, grouped by underlying. The volatility panel keeps only converged solves with non-zero volume and strikes within 5% of the underlying close, which is the standard near-the-money band; the front bucket includes same-day expiries.

FAQ

Is libitofin a drop-in replacement for QuantLib?

No. As of August 2026 it is a pre-1.0 port that covers part of QuantLib's surface and validates its results against QuantLib's own test suite. QuantLib itself, reached through its Python bindings, remains the broader and more stable option for production work.

What does pre-1.0 mean for a pricing library?

Under Rust's versioning convention, a 0.x release makes no compatibility promise: the next minor version is free to rename or remove anything. In practice that means pinning an exact tagged release and rerunning your own test suite on every upgrade.

Do I need to write Rust to use libitofin?

No. The project publishes a Python package named itofin, so the engine is callable from a normal Python process. Writing Rust becomes relevant when the pricing loop itself is the thing you are shipping.

What does a pricing library give me that a spreadsheet formula does not?

Curves instead of single rates, day-count conventions with real exchange calendars behind them, a calibration loop that fits model parameters to quoted prices, and a tested numerics layer underneath all three. The closed-form formula is the small part of the job.

Does the programming language change the option price?

Not mathematically. It changes reproducibility: floating point results depend on the order of operations, so two implementations of one model can disagree in the final digits. Running research and production off the same engine removes that gap.


Every panel here carries its exact SQL underneath, so expand one to see how the count was taken. The same curve, calendar, and chain questions can be asked in plain English on the Strasmore terminal.