Quantitative Development Pricing a European option in C++ from Python wi...

Pricing a European option in C++ from Python with SWIG

Pricing a European option in C++ from Python with SWIG

Suppose you have an option pricer written in C++, but the people testing it work in Python notebooks. Rewriting the formula in Python would leave you with two implementations to maintain. Instead, we can keep the calculation in C++ and expose a small interface to Python. SWIG generates the glue for that interface.

I’ll use the Derivatives example in my quantitative finance repository. Its src folder contains Options.h, Options.cpp, Options.i, and setup.py; the Python notebook calls the resulting module. The goal is to understand both the financial calculation and what happens between a C++ class and import Options.

The option we are pricing

The C++ class quantero::European takes the spot price $S$, strike $K$, continuously compounded risk-free rate $r$, volatility $\sigma$, time to expiry $T$ in years, and a sign $\psi$. Set $\psi=1$ for a call and $\psi=-1$ for a put. This example assumes a European option with no dividends or foreign interest rate.

\[d_1 = \frac{\ln(S/K) + (r + \sigma^2/2)T}{\sigma\sqrt{T}}, \qquad d_2 = d_1 - \sigma\sqrt{T}\]

The valuation() method implements the call and put in one expression:

\[V_{\psi} = \psi S\,N(\psi d_1) - \psi K e^{-rT}N(\psi d_2), \qquad \psi \in \{1,-1\}.\]

Here $N(x)$ is the standard normal cumulative distribution function (CDF). The implementation in Options.cpp uses the error function rather than Boost:

\[F(x;\mu,\sigma) = \frac{1}{2}\left[1 + \operatorname{erf}\left(\frac{x-\mu}{\sigma\sqrt{2}}\right)\right], \qquad N(x)=F(x;0,1).\]

This distinction matters when building the example: the current option pricler does not require Boost for the nor CDF. Older installation notes in the repository describe a Boost-based version of the CDF; installing Boost is unnecessary for the code linked above.

From source code to a Python module

A C++ build usually involves preprocessing headers, compiling source into object files, assembling machine code, and linking objects into a program or library. See this compilation overview for a longer explanation. From the repository’s Derivatives/src directory, you can inspect an intermediate object without creating the Python extension:

g++ -c Options.cpp -o Options.o
nm -C Options.o
objdump -d Options.o

nm -C displays demangled C++ symbols, including methods of quantero::European; objdump -d shows machine instructions. An object file is not a Python module. Nor can we link this particular source directly into an executable with g++ Options.cpp -o options: there is no main() function. For Python, we need a wrapper and a shared extension library instead.

Tell SWIG what to expose

The repository already provides Options.i:

%module Options

%{
#include "Options.h"
%}

%include "Options.h"

The %{ ... %} block inserts the header into the generated C++ wrapper so it can compile. %include "Options.h" tells SWIG which declarations to expose to Python. The module name determines the Python import name. SWIG presents this C++ namespace’s European class as Options.European, as you can see in the notebook.

Hint: expose only what Python needs. Put %ignore directives before %include "Options.h" to exclude individual functions or whole classes. For example, adding %ignore quantero::N; there keeps the CDF helper in C++ while exposing European and its public valuation() method. In a larger header, %ignore quantero::SomeInternalClass; would similarly hide that class. This changes the Python API, not the C++ implementation; rebuild the wrapper after editing the interface.

On Linux, install a C++ compiler, Python development headers, SWIG, and Python build tooling for your chosen interpreter. For example, on Debian or Ubuntu:

sudo apt-get update
sudo apt-get install build-essential python3-dev python3-venv swig
git clone https://github.com/craquinterogo/quantitative_finance.git
cd quantitative_finance/Derivatives/src
python3 -m venv .venv
source .venv/bin/activate
python -m pip install setuptools
swig -c++ -python Options.i

The last command generates Options_wrap.cxx, which bridges Python calls to C++, and Options.py, the Python-facing proxy. It does not compile the extension yet.

Compile the extension

The repository’s setup.py specifies the two C++ sources, Options_wrap.cxx and Options.cpp, and names the native module _Options. Its older distutils.core import predates the removal of distutils from the Python standard library. If you are updating that file for a current Python environment, use setuptools with the same module and sources:

from setuptools import Extension, setup

setup(
    name="Options",
    version="0.1",
    ext_modules=[
        Extension("_Options", sources=["Options_wrap.cxx", "Options.cpp"])
    ],
    py_modules=["Options"],
)

Run this from Derivatives/src with the virtual environment active:

python setup.py build_ext --inplace
python -c "import Options; print(Options.European(100, 100, 0.05, 0.20, 1, 1).valuation())"

The printed call premium should be about 10.4506. build_ext --inplace puts the compiled _Options extension next to Options.py, so Python can import both from the current directory. On Linux the extension has a platform-tagged .so suffix; on Windows it is normally .pyd, not a generic .dll. A prebuilt extension is tied to its operating system, architecture, and Python ABI, so distribute or rebuild it for the environment where it will run.

Check the result from Python

In a notebook whose working directory can import Options.py and its matching _Options extension, try both signs:

import Options

call = Options.European(100, 100, 0.05, 0.20, 1, 1)
put = Options.European(100, 100, 0.05, 0.20, 1, -1)

print(f"Call: {call.valuation():.4f}")  # 10.4506
print(f"Put:  {put.valuation():.4f}")   # 5.5735

These inputs also give a useful financial check: call-put parity implies $C-P=S-Ke^{-rT}$, about $4.8771$ here. This helps catch a pricing mistake independently of whether SWIG built successfully.

For this small project, setup.py handles the extension build; CMake is optional, not another required step. The repository also has a CMakeLists.txt that declares an executable from Options.cpp, but that target needs a main() before it can link. If you later add a C++ command-line driver, CMake becomes useful for managing that separate executable. The important boundary today is simpler: C++ owns the formula, SWIG exposes its declarations, and the compiled extension lets Python call the same implementation.