PyQuant News

PQN #001: How To 45x Python Performance With C

PQN #001: How To 45x Python Performance With C

Read time: 3.5 minutes

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.

Note:

In the case my email breaks the first 4 lines below, they should look like this:

#include <stdio.h>

#include <math.h>

#include <stdlib.h>

#include <Python.h>

#include &ltstdio.h&gt
#include &ltmath.h&gt
#include &ltstdlib.h&gt
#include &ltPython.h&gt

// 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!

Well, that’s it for today. I hope you enjoyed it.

See you again next week.

Join your Fellow Pythonistas on PyQuant News

Follow Us

Recent Posts

Related Posts