hasquant: Bindings to QuantLib

[ bsd3, ffi, finance, library ] [ Propose Tags ] [ Report a vulnerability ]
Versions [RSS] 0.5.0.2
Change log CHANGELOG.md
Dependencies base (>=4.14 && <5), hasquant, template-haskell (>=2.16 && <2.25), time (>=1.9.3 && <1.16), transformers (>=0.5.6 && <0.7), vector (>=0.12.3 && <0.14) [details]
Tested with ghc ==8.10.6, ghc ==9.10.3, ghc ==9.12.4, ghc ==9.14.1
License BSD-3-Clause
Copyright (c) 2012-2026 Sergei Khorev
Author Sergei Khorev <sergey.khorev@gmail.com>
Maintainer Sergei Khorev <sergey.khorev@gmail.com>
Uploaded by khorser at 2026-08-21T06:03:58Z
Category Finance, FFI, Library
Home page https://github.com/khorser/hasquant#readme
Bug tracker https://github.com/khorser/hasquant/issues
Source repo head: git clone https://github.com/khorser/hasquant
Distributions
Executables hasquant_example
Downloads 2 total (2 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for hasquant-0.5.0.2

[back to package description]

Haskell bindings to QuantLib, the free/open-source C++ library for quantitative finance — rates, bonds, options, swaps, credit, inflation, and equity derivatives, with the associated term structures, indexes, and pricing engines. 1100+ constructors and non-trivial methods are bound so far, covering roughly a tenth of QuantLib's surface.

hasquant gives Haskell direct access to production-grade pricing, curve-building, and risk models from QuantLib. Rather than wrapping it in a new framework, it stays a thin, close-to-1:1 layer over the C++ API, so it composes into whatever architecture you're already building instead of dictating one.

Coverage already spans the parts of QuantLib people actually reach for in practice: yield/credit/inflation/volatility term structures and their bootstrapping helpers, IBOR/overnight/swap/inflation indexes, fixed and floating bonds (including amortizing, callable, and convertible), vanilla and exotic options (barrier, Asian, compound, variance, basket), swaps (vanilla, CMS, OIS, CDS, zero-coupon), and the corresponding pricing engines — analytic, tree, finite-difference, and Monte Carlo — for models from Black-Scholes through SABR and Heston.

Type safety is held to a noticeably higher bar than a typical C++ binding. QuantLib's class hierarchies are mirrored with phantom-typed pointers (GenBond a, GenQuote a, …) rather than one flat handle type, so passing the wrong kind of object is a compile error, not a runtime crash; upcasting is the only implicit conversion, and it's structurally guaranteed safe. Declarations on the C++ and Haskell sides are kept in step by c2hs rather than by hand-written FFI stubs. The C++ shim layer has zero dynamic_cast/dynamic_pointer_cast call sites — classes that need runtime-checked downcasts upstream get a dedicated leaf type instead — and enum-like C++ types are bound with explicit value mirroring rather than an unchecked numeric cast, closing off a whole class of silent-corruption bugs that plain FFI bindings are prone to.

The main departures from a thin wrapper are enums and ADTs standing in for things that are classes on the C++ side (see "On Types" below), and the ownership layer that makes the pointer types safe; individual calls still map close to 1:1 onto the underlying QuantLib call.

This started as a hand-written project in 2012 (see "Project History" below) and has gone through several architecture rewrites since. The core design — the pointer-ownership model, the enum/ADT scheme, the C shim conventions — is hand-designed and predates any AI involvement. More recently I've used AI assistance to extend coverage faster: new classes, methods, day counters, indexes. Every generated binding is still reviewed against the pattern it's supposed to follow, checked against the upstream C++ signature, and covered by a test before it counts as done — see "Testing" below for what that means in practice.

Worked examples live in test/example/QuantLib/Example. They're direct translations of QuantLib's own examples and test suite, not idiomatic Haskell — the goal there is fidelity to a known-correct reference, not style. The test suite proper is test/main/QuantLib/MainTest.hs, a dispatcher over the topic modules in test/hspec/QuantLib/Spec.

Haddock documentation is published at https://khorser.github.io/hasquant

Testing

Bindings aren't just compiled and eyeballed. Where QuantLib's own test-suite/*.cpp covers a class or scenario, the corresponding hasquant test reuses its inputs and cached expected values directly, rather than deriving numbers by hand or relying on self-consistency alone. Enum-dispatched cases (currencies, calendars, day counters, index variants) get a standalone test/smoke/ check that constructs the cases and asserts on the output — this is what caught a real bug where two enum cases silently aliased to the wrong upstream values despite a clean build and a passing test suite.

Coverage is tracked rather than claimed: tools/ql-methods-1.43.txt is a line-by-line dump of every constructor and non-trivial method in QuantLib's headers, and each new binding flips its line as it lands.

Building

Day-to-day development happens on GHC-9.10. GHC-8.10.6 (base >= 4.14) is the supported floor and is verified on every change against the lts-18.8 Docker image below; newer versions should work too, as the public API sticks to widely available language features.

First you need QuantLib version 1.43 or higher, see installation documentation for Linux, MacOS, or cross-platform CMake-based build

Linux and macOS are the primary, well-tested platforms. Windows builds work too, but QuantLib has to be rebuilt with GHC's own bundled Clang first — see WINDOWS.md for the recipe.

Stack

Minimal build: stack build --no-haddock --no-test

Run tests: stack build --test --no-haddock

Build and run examples: stack build --flag hasquant:buildExample --no-haddock && stack exec hasquant_example. The example executable is buildable: False without that flag, so HLS also needs it — add package hasquant / flags: +buildExample to a local cabal.project.local to edit main/exe with HLS.

Build and run examples enabling tracking of memory allocations (log every object as it is created and deleted): stack build --no-haddock --flag hasquant:buildExample --flag hasquant:trackAllocations && stack exec hasquant_example

The trace goes to stderr by default. Set the QLTRACK_ALLOCATIONS environment variable to send it to a file instead, which is usually what you want — redirecting stderr also swallows the program's own output, and a trace is only useful next to the values it explains:

QLTRACK_ALLOCATIONS=/tmp/trace.log stack exec hasquant_example

A raw trace is thousands of interleaved lines. tools/alloc-summary.py /tmp/trace.log pairs allocations with frees by pointer and reports what is still live, grouped by class, listing double frees separately from ordinary leaks; it exits non-zero if anything is unaccounted for, so it can gate a check.

One trap worth knowing: neither cabal nor stack recompiles cxx-sources when only a flag changes, so turning trackAllocations on for an already-built tree reports success and produces a library with no tracing in it — an empty trace and no error. Delete the built C++ objects (the build/cbits directory) first, and confirm with strings <a built .o> | grep -c allocated before trusting an empty result.

Run GHCi: stack ghci --ghci-options $(find .stack-work \( -name "*.so" -o -name "*.dylib" \) -print -quit)

Cabal

Standard build: cabal configure --disable-documentation && cabal build

Build with documentation: cabal configure --enable-documentation && cabal build

Build example: cabal configure -f buildExample --disable-documentation && cabal build

Build example and tests: cabal configure -f buildExample --enable-tests --disable-documentation && cabal build

Docker

The repo contains docker compose files for a custom Linux x86_64 image. You can use it like this to run tests using GHC-8.10.6: docker compose run --rm -it hasquant stack --resolver lts-18.8 test

Drop -it when running without a TTY (CI, or a scripted check) — it fails there.

The config mounts /root/.stack, /root/.ghcup, and /root/.cabal as named volumes so everything installed with stack/ghcup/cabal will persist across runs. /hasquant/.stack-work and /hasquant/dist-newstyle are mounted as anonymous volumes to avoid polluting host filesystem.

On Types

I deliberately kept typeclasses out of public signatures, as the code quickly becomes polluted by typeclass constraints. A few remain as internal plumbing, but you never have to satisfy one yourself.

How to read types

If you see a function accepting CallableBond, you can pass only callable bonds. But if a function accepts GenBond a, you can pass a Bond or any of its derivatives: FixedRateBond, ConvertibleBond, CallableBond. This works thanks to the following definition:

type Bond = GenBond CBond
type FixedRateBond = GenBond CFixedRateBond
type ConvertibleBond = GenBond CConvertibleBond
type CallableBond = GenBond CCallableBond

And if a function accepts GenInstrument a (like npv), you can pass any instrument at all. While this is convenient, it leads to some allocation and deallocation on each call, so you might consider using asBond and asInstrument to get an object of the required type.

TODO

  • EndCriteria/OptimizationMethod are bound as raw, Haskell-GC-finalized (delete-based) pointers rather than shared_ptr-boxed like every other bound class (no QlEndCriteria/QlOptimizationMethod typedef shared_ptr<...> exists anywhere in cbits/). This is why every constructor that needs to store one long-term as a member (FittedBondDiscountCurve's fitting methods, sabrInterpolatedSmileSection, sabrSwaptionVolatilityCube) has to hardcode QuantLib-internal defaults instead of accepting a caller-supplied one — a constructor that only uses them transiently within one call (Gsr::calibrateVolatilitiesIterative, CalibratedModel::calibrate) can safely pass the raw pointer through, but nothing that retains one can. The real fix — re-typedef'ing both as shared_ptr boxes — would also touch qlGsrCalibrateVolatilitiesIterative/qlCalibratedModelCalibrate, the two call sites that currently pass them through transiently.
  • GlobalBootstrap is bound for two trait x interpolator combinations only — DiscountxLogLinear (piecewiseYieldCurveGlobalBootstrap') and SimpleZeroYieldxLinear (piecewiseYieldCurveGlobalBootstrapSimpleZeroLinear') — not the full matrix IterativeBootstrap supports. Deliberate: QuantLib-SWIG itself only ever binds one GlobalBootstrap combination (GlobalLinearSimpleZeroCurve), each combination is a separate template instantiation with its own [temp.inst] trap (see CLAUDE.md), and widening happens per concrete use case, not speculatively.
  • GlobalBootstrap's functor-callback constructors (ql/termstructures/globalbootstrap.hpp): additionalHelpers/additionalDates are bound for SimpleZeroYieldxLinear only, via piecewiseYieldCurveGlobalBootstrapSimpleZeroLinearFull', using upstream QuantLib-SWIG's canned AdditionalErrors/AdditionalDates functors (fixed formulas, not user callbacks — no Haskell-side marshalling needed). additionalDates must have exactly length additionalHelpers - 2 entries (AdditionalErrors' fixed output size); a mismatch raises a clear error. additionalPenalties/additionalVariables remain unbound — real optimizer callbacks, a materially larger feature.
  • (Perpetual) Add more classes and methods. You will need to update cbits/qlaux.h, cbits/qlTypesC2HS.h, and then add some boilerplate to corresponding .h, .cpp, Internal/Type.hs and .chs files. This can be simplified with scripting/LLMs. Refer to CLAUDE.md, .claude/skills, and tools for more detailed information useful even for manual steps.
  • Add more nonempty lists or vectors for some functions where applicable
  • Design a declarative embedded DSL
  • Review interfaces for consistency, add obviously missing features and fix contradictions to the current design
  • See github issues for more formalized tasks