How To 45x Python Performance With C
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 > 6.0) { return 1.0; }; // this guards against overflow
9 if (z < -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 < 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", &S, &K, &r, &t, &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(&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.