You remember when we used to fly? Well, I do! The big bottleneck in flying was not always the flight if you were going short haul. Maybe it took an hour or two. However, what took time was getting the airport, checking in, queuing, delays in boarding etc. One way to try to reduce such delays is to cough up for a business class ticket (or go on a private jet..). Would it really be worth it? Well, it depends how much your time is worth (and whether you can make use of the waiting time to do work on a laptop etc.)
When it comes to finance, time is money too! When you’re analysing financial markets, you’re often looking for an edge to make your computations faster. One of the complaints of Python is that it really isn’t that fast. In many cases, this is a valid criticism, because it’s not as fast as language like C and Java. However, there are ways to speed up Python so it can comparable in speed in C, which we’ll talk about here, with a particular finance use case.
Just like with any programming language, to speed up Python, we need to find the bottleneck in your calculation. A code profiler can be a good way to do this. In particular, we want to avoid spending ages speeding up code, which doesn’t need to be speeded up (and resulting in code that is more difficult to maintain).
If you are interested in speeding up your Python code, I run a workshop, which I can teach at your firm on Python, alternative data, natural language processing and large datasets, part of which is about using Numba and other tools to speed up numerical calculations. A Jupyter notebook with all the code below is available on my GitHub teaching site here and you can run it interactively in Binder here.
Speeding up CDF
If we take something like the Black-Scholes formula, one of the slowest arithmetic operations is calculating the normal CDF N(.). If you’re just pricing one option this probably doesn’t really matter. However, if your pricing many thousands options or using the Black-Scholes formula within some optimization, then it can make a big difference.
I’ll look at several ways of calculating the CDF. Initially we’ll look at SciPy’s implementation, then rewriting it in Python and subsequently using Numba to speed it up. Numba is a LLVM which can speed up Python code (but you’ll sometimes need to rewrite the code so it can be jitted by Numba).
Typically when calculating the CDF, it is common to use scipy.norm.cdf and this can be our initial benchmark. Underneath however, scipy actually calls scipy.special.ndtr which is actually C code, and has been Cythonized. We find that doing the lower level call speeds up the CDF by 100x!! This can happen quite often in Python, doing lower level calls can be much quicker. For example, when doing arithmetic calculations in Pandas, very often underneath it is doing NumPy calls. Using the NumPy calls directly can speed things up.
SciPy versions of CDF
63200 ns – scipy.norm.cdf
629 ns – scipy.special.ndtr
If we take the C code for scipy.special.ndtr, it is sufficiently simple that it can be rewritten in Python (careful with brackets). This results in slower execution though! This probably isn’t surprising given that Python tends to be slower than the equivalent C code. However, if we then jit this code, but a Numba decorator, our code is now 3x faster than scipy.special.ndtr.
So far we have used exactly the same algorithm, but simply tried to speed it up by rewriting it. FinancePy is a great option pricing library, which I’ve been using recently (and have been calling it via my own finmarketpy library). FinancePy also has the CDF implemented, but the algorithm is a fast approximation (good to around 6 decimal places). A fully Python version of this is slower than our jitted/Numba version of scipy.special.ndtr. However, the actual CDF implemented in FinancePy uses Numba, and this is actually slightly faster than the Numba version of SciPy’s CDF above.
Python doesn’t execute as quickly as C in most cases. However, we’ve illustrated how with a bit of thought we can really speed up Python with the use case of the CDF. In particular, we’ve seen how Numba can massively speed up the CDF compared to using a high level CDF call within scipy.
Once you’ve identified the bottleneck (often using a code profiler) in your numerical calculation, here is a basic checklist for speeding it up:
- can we use a lower level call
- can we rewrite the function as a simple Python function (with NumPy functions, but for example no calls to Pandas/SciPy etc.)
- njit or jit function with a Numba decorator (experiment with flags)
- is there an alternative algorithm for our numerical calculation (obviously requires more work)?
We should note that Numba isn’t the only way to speed up code. There’s also Cython. If you are doing matrix operations/optimization, you might be able to use TensorFlow or JAX too.
It isn’t necessarily very easy to optimize code, so we shouldn’t simply rewrite all our Python code for Numba, in particular, given that it can make code less maintainable. However, for those parts which are truly bottlenecks it can be worth our while. It’s also fun too! Python’s days might not be Numba-ed.