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