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.
#include <stdio.h> #include <math.h> #include <stdlib.h> #include <Python.h> // most C compilers define PI, but just in case it doesn't #ifndef PI #define PI 3.141592653589793238462643 #endif #ifndef PI const double PI=3.141592653589793238462643; #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.
// normal distribution function double n(double z) { return (1.0/sqrt(2.0*PI))*exp(-0.5*z*z); } // cumulative normal double N(double z) { if (z > 6.0) { return 1.0; }; // this guards against overflow if (z < -6.0) { return 0.0; }; double b1 = 0.31938153; double b2 = -0.356563782; double b3 = 1.781477937; double b4 = -1.821255978; double b5 = 1.330274429; double p = 0.2316419; double c2 = 0.3989423; double a = fabs(z); double t = 1.0/(1.0+a*p); double b = c2*exp((-z)*(z/2.0)); double n = ((((b5*t+b4)*t+b3)*t+b2)*t+b1)*t; n = 1.0-b*n; if ( z < 0.0 ) n = 1.0 - n; return n; }
Finally, we’ll add the C code for the Black-Scholes call value.
double _bs_call(double S, double K, double r, double t, double sigma) { double time_sqrt = sqrt(t); double d1 = (log(S/K)+r*t)/(sigma*time_sqrt)+0.5*sigma*time_sqrt; double d2 = d1-(sigma*time_sqrt); return S*N(d1) - K*exp(-r*t)*N(d2); }
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.
static PyObject * bs_call(PyObject *self, PyObject *args) { double S, K, r, t, sigma; if (!PyArg_ParseTuple(args, "ddddd", &S, &K, &r, &t, &sigma)) return NULL; return Py_BuildValue("d", _bs_call(S, K, r, t, sigma)); } PyDoc_STRVAR(bs_doc, "Python3 extending.n"); static PyMethodDef methods[] = { {"bs_call", bs_call, METH_VARARGS, ".n"}, {NULL, NULL, 0, NULL} }; static struct PyModuleDef bsmodule = { PyModuleDef_HEAD_INIT, "bs", /* name of module */ bs_doc, /* module documentation, may be NULL */ -1, /* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */ methods }; PyInit_bs(void) { return PyModule_Create(&bsmodule); }
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.
import bs def call(s, k, r, t, sigma): 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.
from setuptools import setup, Extension ext = Extension('bs', sources=['black_scholes/bs.c']) setup( name="black_scholes", version="0.0.1", description="European Options Pricing Library", packages=['black_scholes'], ext_modules=[ext] )
Step 4: Install the Module
In the black-scholes directory type the following to compile and install your new module.
python 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.
In [1]: import black_scholes In [2]: s = 147.30 In [3]: k = 150.0 In [4]: r = 0.001 In [5]: t = 60/365 In [6]: sigma = 0.45 In [7]: black_scholes.call(s, k, r, t, sigma) Out [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.
In [8]: expirations = [30/365, 90/365, 180/365, 270/365, 365/365] In [9]: expirations Out [9]: [0.0821917808219178, 0.2465753424657534, 0.4931506849315068, 0.7397260273972602, 1.0] In [10]: [black_scholes.call(s, k, r, expiration, sigma) for expiration in expirations] Out [10]: [6.376100822041941, 11.933049584157366, 17.37389907520309, 21.529428322438896, 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.
CC=gcc CFLAGS=-c -fPIC SOURCES=src/optlib.c OBJECT=obj/optlib.o INCLUDE=-Iinc EXECUTABLE=bin/optlib all: $(EXECUTABLE) $(EXECUTABLE): $(OBJECT) #mac $(CC) -shared -Wl,-install_name,$(EXECUTABLE).so -o $(EXECUTABLE).so $(OBJECT) #linux #$(CC) -shared -Wl,-soname,$(EXECUTABLE).so -o $(EXECUTABLE).so $(OBJECT) obj/%.o: src/%.c $(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.