How To 45x Python Performance With C

July 19, 2022
Facebook logo.
Twitter logo.
LinkedIn logo.
Get this code in Google Colab

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.