PyQuant News is on Substack

Looking for the newest weekly Python deep dives for algorithmic trading, market data analysis, and quant finance? Subscribe to the PyQuant Newsletter on Substack for free.

Subscribe for free 👉

How To 45x Python Performance With C

July 19, 2022
Facebook logo.
Twitter logo.
LinkedIn logo.

How To 45x Python Performance With C

How To 45x Python Performance With  C

In today's issue, I'm going to show you how to call C code from Python to value a call option using Black-Scholes.

Python is based on C, but it's much slower. Python has to figure out the type of data assigned to variables when it runs. C is compiled first so it already knows. This helps C run up to 45x faster than pure Python.

Unfortunately, most people don't take advantage of C's speed.

Here's how you can do it, step by step:

Step 0: Setup the Directory

Go to your working directory and create a new folder called black-scholes.

Inside this folder create another folder called black_scholes.

Navigate to the black_scholes folder.

Step 1: Write the C Code

Open up your favorite editor and start a file called bs.c.

At the top of the file, add the includes and define pi.

1#include <stdio.h>
2#include <math.h>
3#include <stdlib.h>
4#include <Python.h>
5
6// most C compilers define PI, but just in case it doesn't 
7#ifndef PI 
8#define PI 3.141592653589793238462643
9#endif
10 
11#ifndef PI 
12const double PI=3.141592653589793238462643;
13#endif 

To start, we need to create a function that gives us a random sample from a normal distribution and the cumulative normal distribution of a variable.

1// normal distribution function
2double n(double z) {
3    return (1.0/sqrt(2.0*PI))*exp(-0.5*z*z);
4}
5 
6// cumulative normal
7double N(double z) {
8    if (z &gt; 6.0) { return 1.0; }; // this guards against overflow
9    if (z &lt; -6.0) { return 0.0; };
10    
11    double b1 =  0.31938153;
12    double b2 = -0.356563782;
13    double b3 =  1.781477937;
14    double b4 = -1.821255978;
15    double b5 =  1.330274429;
16    double p  =  0.2316419;
17    double c2 =  0.3989423;
18    
19    double a = fabs(z);
20    double t = 1.0/(1.0+a*p);
21    double b = c2*exp((-z)*(z/2.0));
22    double n = ((((b5*t+b4)*t+b3)*t+b2)*t+b1)*t;
23    n = 1.0-b*n;
24    if ( z &lt; 0.0 ) n = 1.0 - n;
25    return n;
26}

Finally, we'll add the C code for the Black-Scholes call value.

1double _bs_call(double S, double K, double r, double t, double sigma) {
2    double time_sqrt = sqrt(t);
3    double d1 = (log(S/K)+r*t)/(sigma*time_sqrt)+0.5*sigma*time_sqrt;
4    double d2 = d1-(sigma*time_sqrt);
5    return S*N(d1) - K*exp(-r*t)*N(d2);
6}

There's one more thing we need to do in this file. We need to let Python know how to call the C code. We do this by using Module Objects.

1static PyObject *
2bs_call(PyObject *self, PyObject *args)
3{
4    double S, K, r, t, sigma;
5    if (!PyArg_ParseTuple(args, "ddddd", &amp;S, &amp;K, &amp;r, &amp;t, &amp;sigma))
6        return NULL;
7    return Py_BuildValue("d", _bs_call(S, K, r, t, sigma));
8}
9
10
11PyDoc_STRVAR(bs_doc, "Python3 extending.n");
12
13static PyMethodDef methods[] = {
14    {"bs_call", bs_call, METH_VARARGS, ".n"},
15    {NULL, NULL, 0, NULL}
16};
17
18static struct PyModuleDef bsmodule = {
19    PyModuleDef_HEAD_INIT,
20    "bs",   /* name of module */
21    bs_doc, /* module documentation, may be NULL */
22    -1,     /* size of per-interpreter state of the module,
23               or -1 if the module keeps state in global variables. */
24    methods
25};
26
27PyInit_bs(void)
28{
29    return PyModule_Create(&amp;bsmodule);
30}

Step 2: Create a Python Wrapper Function

Inside the black_scholes directory, create a new file called __init__.py. Inside this file paste the following.

1import bs
2
3
4def call(s, k, r, t, sigma):
5    return bs.bs_call(s, k, r, t, sigma)

That's it! We're calling C from Python!

Step 3: Create a setup.py file

We'll use Python's setup tools module to build the C code, link it to Python, and install it all as a module.

Go back to the black-scholes directory.

Create a file called setup.py and paste in the contents below.

1from setuptools import setup, Extension
2
3
4ext = Extension('bs', sources=['black_scholes/bs.c'])
5
6setup(
7    name="black_scholes",
8    version="0.0.1",
9    description="European Options Pricing Library",
10    packages=['black_scholes'],
11    ext_modules=[ext]
12)

Step 4: Install the Module

In the black-scholes directory type the following to compile and install your new module.

1python setup.py install

You should see some logs and maybe some warnings (they're ok to ignore).

Step 5: Call the Python Function

Open up your Python terminal and enter the following at the command prompt.

1In  [1]: import black_scholes
2
3In  [2]: s = 147.30
4
5In  [3]: k = 150.0
6
7In  [4]: r = 0.001
8
9In  [5]: t = 60/365
10
11In  [6]: sigma = 0.45
12
13In  [7]: black_scholes.call(s, k, r, t, sigma)
14Out [7]: 9.518562265392418

That's the value of a call option!

As a bonus, let's see how the value changes as the expirations change.

1In  [8]: expirations = [30/365, 90/365, 180/365, 270/365, 365/365]
2
3In  [9]: expirations
4Out [9]: 
5[0.0821917808219178,
6 0.2465753424657534,
7 0.4931506849315068,
8 0.7397260273972602,
9 1.0]
10
11In  [10]: [black_scholes.call(s, k, r, expiration, sigma) for expiration in expirations]
12Out [10]: 
13[6.376100822041941,
14 11.933049584157366,
15 17.37389907520309,
16 21.529428322438896,
17 25.193296648062933]

Congratulations, you just built the Black-Scholes option pricing model in C and called it from Python!

Bonus: Compiling the C code

In case you want to compile the C code directly, you can use a Makefile.

Assuming you have all your C code in a file called optlib.c, the Makefile is run in the same directory, and you have gcc installed, this will work.

1CC=gcc
2CFLAGS=-c -fPIC
3
4SOURCES=src/optlib.c
5OBJECT=obj/optlib.o
6INCLUDE=-Iinc
7EXECUTABLE=bin/optlib
8
9all: $(EXECUTABLE)
10
11$(EXECUTABLE): $(OBJECT)
12    #mac
13    $(CC) -shared -Wl,-install_name,$(EXECUTABLE).so -o $(EXECUTABLE).so $(OBJECT)
14    #linux
15    #$(CC) -shared -Wl,-soname,$(EXECUTABLE).so -o $(EXECUTABLE).so $(OBJECT)
16
17obj/%.o: src/%.c
18    $(CC) $(CFLAGS) $(INCLUDE) -o $@ $<

Note: I cannot help everyone debug their code if it doesn't work. Please consider asking for help on Stack Overflow.

Man with glasses and a wristwatch, wearing a white shirt, looking thoughtfully at a laptop with a data screen in the background.