Skip to content

Commit b43097c

Browse files
committed
Black-Scholes pit three different ways (by Davis Vaughan and me)
1 parent 4c9379d commit b43097c

File tree

2 files changed

+422
-0
lines changed

2 files changed

+422
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
---
2+
title: "Using RcppArmadillo to price European Put Options"
3+
author: "Davis Vaughan and Dirk Eddelbuettel"
4+
license: GPL (>= 2)
5+
mathjax: true
6+
tags: armadillo basics
7+
summary: "Three different ways for computing Black-Scholes put option prices are discussed."
8+
layout: post
9+
src: 2018-02-28-black-scholes-three-ways.Rmd
10+
---
11+
12+
### Introduction
13+
14+
In the quest for ever faster code, one generally begins exploring ways to integrate C++
15+
with R using [Rcpp](http://www.rcpp.org). This post provides an example of multiple
16+
implementations of a European Put Option pricer. The implementations are done in pure R,
17+
pure [Rcpp](http://www.rcpp.org) using some [Rcpp](http://www.rcpp.org) sugar functions,
18+
and then in [Rcpp](http://www.rcpp.org) using
19+
[RcppArmadillo](http://dirk.eddelbuettel.com/code/rcpp.armadillo.html), which exposes the
20+
incredibly powerful linear algebra library, [Armadillo](http://arma.sourceforge.net/).
21+
22+
Under the [Black-Scholes model](https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model) The value of a European put option has the closed form solution:
23+
24+
$$ V = K e^{-rt} N(-d_2) - S e^{-yt} N(-d_1) $$
25+
26+
where
27+
28+
$$
29+
\begin{equation}
30+
\begin{aligned}
31+
V &= \text{Value of the option} \\
32+
r &= \text{Risk free rate} \\
33+
y &= \text{Dividend yield} \\
34+
t &= \text{Time to expiry} \\
35+
S &= \text{Current stock price} \\
36+
K &= \text{Strike price} \\
37+
N(.) &= \text{Normal CDF}
38+
\end{aligned}
39+
\end{equation}
40+
$$
41+
42+
and
43+
44+
$$
45+
\begin{equation}
46+
\begin{aligned}
47+
d_1 &= \frac{log(\frac{S}{K}) + (r - y + \frac{1}{2} \sigma^2)t}{\sigma \sqrt{t}} \\
48+
d_2 &= d_1 - \sigma \sqrt{t}\\
49+
\end{aligned}
50+
\end{equation}
51+
$$
52+
53+
Armed with the formulas, we can create the pricer using just R.
54+
55+
56+
{% highlight r %}
57+
put_option_pricer <- function(s, k, r, y, t, sigma) {
58+
59+
d1 <- (log(s / k) + (r - y + sigma^2 / 2) * t) / (sigma * sqrt(t))
60+
d2 <- d1 - sigma * sqrt(t)
61+
62+
V <- pnorm(-d2) * k * exp(-r * t) - s * exp(-y * t) * pnorm(-d1)
63+
64+
V
65+
}
66+
67+
# Valuation with 1 stock price
68+
put_option_pricer(s = 55, 60, .01, .02, 1, .05)
69+
{% endhighlight %}
70+
71+
72+
73+
<pre class="output">
74+
[1] 5.52021
75+
</pre>
76+
77+
78+
79+
{% highlight r %}
80+
# Valuation across multiple prices
81+
put_option_pricer(s = 55:60, 60, .01, .02, 1, .05)
82+
{% endhighlight %}
83+
84+
85+
86+
<pre class="output">
87+
[1] 5.52021 4.58142 3.68485 2.85517 2.11883 1.49793
88+
</pre>
89+
90+
Let's see what we can do with [Rcpp](http://www.rcpp.org). Besides explicitely stating the
91+
types of the variables, not much has to change. We can even use the sugar function,
92+
`Rcpp::pnorm()`, to keep the syntax as close to R as possible. Note how we are being
93+
explicit about the symbols we import from the `Rcpp` namespace: the basic vector type, and
94+
well the (vectorized) 'Rcpp Sugar' calls `log()` and `pnorm()` calls. Similarly, we use
95+
`sqrt()` and `exp()` for the calls on an atomic `double` variables from the C++ Standard
96+
Library. With these declarations the code itself is essentially identical to the R code
97+
(apart of course from requiring both static types and trailing semicolons).
98+
99+
100+
{% highlight cpp %}
101+
#include <Rcpp.h>
102+
103+
using Rcpp::NumericVector;
104+
using Rcpp::log;
105+
using Rcpp::pnorm;
106+
using std::sqrt;
107+
using std::log;
108+
109+
// [[Rcpp::export]]
110+
NumericVector put_option_pricer_rcpp(NumericVector s, double k, double r, double y, double t, double sigma) {
111+
112+
NumericVector d1 = (log(s / k) + (r - y + sigma * sigma / 2.0) * t) / (sigma * sqrt(t));
113+
NumericVector d2 = d1 - sigma * sqrt(t);
114+
115+
NumericVector V = pnorm(-d2) * k * exp(-r * t) - s * exp(-y * t) * pnorm(-d1);
116+
return V;
117+
}
118+
{% endhighlight %}
119+
120+
We can call this from R as well:
121+
122+
123+
{% highlight r %}
124+
# Valuation with 1 stock price
125+
put_option_pricer_rcpp(s = 55, 60, .01, .02, 1, .05)
126+
{% endhighlight %}
127+
128+
129+
130+
<pre class="output">
131+
[1] 5.52021
132+
</pre>
133+
134+
135+
136+
{% highlight r %}
137+
# Valuation across multiple prices
138+
put_option_pricer_rcpp(s = 55:60, 60, .01, .02, 1, .05)
139+
{% endhighlight %}
140+
141+
142+
143+
<pre class="output">
144+
[1] 5.52021 4.58142 3.68485 2.85517 2.11883 1.49793
145+
</pre>
146+
147+
Finally, let's look at
148+
[RcppArmadillo](http://dirk.eddelbuettel.com/code/rcpp.armadillo.html). Armadillo has a
149+
number of object types, including `mat`, `colvec`, and `rowvec`. Here, we just use
150+
`colvec` to represent a column vector of prices. By default in Armadillo, `*` represents
151+
matrix multiplication, and `%` is used for element wise multiplication. We need to make
152+
this change to element wise multiplication in 1 place, but otherwise the changes are just
153+
switching out the types and the sugar functions for Armadillo specific functions.
154+
155+
Note that the `arma::normcdf()` function is in the upcoming release of
156+
[RcppArmadillo](http://dirk.eddelbuettel.com/code/rcpp.armadillo.html), which is
157+
`0.8.400.0.0` at the time of writing and still in CRAN's incoming. It also requires the
158+
`C++11` plugin.
159+
160+
161+
{% highlight cpp %}
162+
#include <RcppArmadillo.h>
163+
164+
// [[Rcpp::depends(RcppArmadillo)]]
165+
// [[Rcpp::plugins(cpp11)]]
166+
167+
using arma::colvec;
168+
using arma::log;
169+
using arma::normcdf;
170+
using std::sqrt;
171+
using std::log;
172+
173+
174+
// [[Rcpp::export]]
175+
colvec put_option_pricer_arma(colvec s, double k, double r, double y, double t, double sigma) {
176+
177+
colvec d1 = (log(s / k) + (r - y + sigma * sigma / 2.0) * t) / (sigma * sqrt(t));
178+
colvec d2 = d1 - sigma * sqrt(t);
179+
180+
// Notice the use of % to represent element wise multiplication
181+
colvec V = normcdf(-d2) * k * exp(-r * t) - s * exp(-y * t) % normcdf(-d1);
182+
183+
return V;
184+
}
185+
{% endhighlight %}
186+
187+
Use from R:
188+
189+
190+
{% highlight r %}
191+
# Valuation with 1 stock price
192+
put_option_pricer_arma(s = 55, 60, .01, .02, 1, .05)
193+
{% endhighlight %}
194+
195+
196+
197+
<pre class="output">
198+
[,1]
199+
[1,] 5.52021
200+
</pre>
201+
202+
203+
204+
{% highlight r %}
205+
# Valuation across multiple prices
206+
put_option_pricer_arma(s = 55:60, 60, .01, .02, 1, .05)
207+
{% endhighlight %}
208+
209+
210+
211+
<pre class="output">
212+
[,1]
213+
[1,] 5.52021
214+
[2,] 4.58142
215+
[3,] 3.68485
216+
[4,] 2.85517
217+
[5,] 2.11883
218+
[6,] 1.49793
219+
</pre>
220+
221+
Finally, we can run a speed test to see which comes out on top.
222+
223+
224+
{% highlight r %}
225+
s <- matrix(seq(0, 100, by = .0001), ncol = 1)
226+
227+
rbenchmark::benchmark(R = put_option_pricer(s, 60, .01, .02, 1, .05),
228+
Arma = put_option_pricer_arma(s, 60, .01, .02, 1, .05),
229+
Rcpp = put_option_pricer_rcpp(s, 60, .01, .02, 1, .05),
230+
order = "relative",
231+
replications = 100)[,1:4]
232+
{% endhighlight %}
233+
234+
235+
236+
<pre class="output">
237+
test replications elapsed relative
238+
2 Arma 100 6.409 1.000
239+
3 Rcpp 100 7.917 1.235
240+
1 R 100 9.091 1.418
241+
</pre>
242+
243+
Interestingly, [Armadillo](http://arma.sf.net) comes out on top here on this (multi-core)
244+
machine (as Armadillo uses OpenMP where available in newer versions). But the difference
245+
is slender, and there is certainly variation in repeated runs. And the nicest thing about
246+
all of this is that it shows off the "embarassment of riches" that we have in the R and
247+
C++ ecosystem for multiple ways of solving the same problem.

0 commit comments

Comments
 (0)