This article introduces two ways to perform parallel programming with a pricing library such as QLNet.
First we get an idea of the time required to do sequentially a series of time consuming operations, like a monte carlo simulation.
Second we present two C#.Net alternatives of multithreaded computations: Threadpool and TPL.
We measure time using a Timespan since the framework is 4.0 (StopWatch is not available with this version). This is a basic console project and QLNet is attached to the references with Nuget.
The calculation class
Here is a class that takes care of arranging the QLNet objects and exposing a Premium double property.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
public class OptionPricing { private VanillaOption europeanOption; private double premium; public double Premium { get { return Math.Round(premium,4); } } public OptionPricing(Date today, Date expiry, double spot, double strike, double rf, double yield, double volatility, int nbsim, int nbstep, DayCounter dc, Calendar calendar) { Handle<Quote> underlyingH = new Handle<Quote>(new SimpleQuote(spot)); YieldTermStructure yts = new FlatForward(today, rf, new ActualActual()); Handle<YieldTermStructure> flatTermStructure = new Handle<YieldTermStructure>(yts); Handle<YieldTermStructure> flatDividendTs = new Handle<YieldTermStructure>(( new FlatForward(today, yield, dc))); Handle<BlackVolTermStructure> flatVolTs = new Handle<BlackVolTermStructure>( new BlackConstantVol(today, calendar, volatility, dc)); BlackScholesMertonProcess bsmProcess = new BlackScholesMertonProcess(underlyingH, flatDividendTs, flatTermStructure, flatVolTs); StrikedTypePayoff payoff = new PlainVanillaPayoff(Option.Type.Call, strike); europeanOption = new VanillaOption(payoff, new EuropeanExercise(expiry)); IPricingEngine mcengine2 = new MakeMCEuropeanEngine<LowDiscrepancy>(bsmProcess) .withSteps(nbstep) .withSamples(nbsim) .value(); europeanOption.setPricingEngine(mcengine2); } public void Calculate() { premium = europeanOption.NPV(); } } |
Sequential calculation
We will call the previous class sequentially in order to get an idea of the time needed to price this option through Monte Carlo engine. Let’s do it four times:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
const int nbstrikes = 4; // Parameters var today = new Date(5, 5, 2015); Settings.setEvaluationDate(today); var expiry = new Date(5, 5, 2016); var volatility = 0.2; var dividendYield = 0.01; var rf = 0.02; var nbsim = 10000; var nbsteps = 5; DayCounter dc = new ActualActual(); Calendar calendar = new TARGET(); var underlying = 36.0; var strike = 36.0; Console.WriteLine("Sequential"); #region Sequential var now0 = DateTime.Now; for (var i = 0; i < nbstrikes; i++) { var nowi = DateTime.Now; var pricing = new OptionPricing(today, expiry, underlying, strike * (1 + i / 100.0), rf, dividendYield, volatility, nbsim, nbsteps, dc, calendar); pricing.Calculate(); var now2 = DateTime.Now; var spani = now2 - nowi; Console.WriteLine("Iter: {0}, Strike: {1}, Premium:{2}, in {3} ms", i, strike * (1 + i / 100.0), pricing.Premium, spani.TotalMilliseconds); } var nowT = DateTime.Now; var spanT = nowT - now0; Console.WriteLine("Total sequential execution in {0} ms", spanT.TotalMilliseconds); |
On my old 1 CPU, dual core machine, the total time is around 9 seconds.
Threadpool
Now let’s rely on System.Threading ThreadPool class which will help us to parallelise the calls to OptionPricing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#region Threadpool Console.WriteLine("Threadpool"); now0 = DateTime.Now; var events = new ManualResetEvent[nbstrikes]; for (int i = 0; i < nbstrikes; i++) { events[i] = new ManualResetEvent(false); } for (int i = 0; i < nbstrikes; i++) { int p = i; ThreadPool.QueueUserWorkItem(x => { var nowi = DateTime.Now; var pricing = new OptionPricing(today, expiry, underlying, strike * (1 + p / 100.0), rf, dividendYield, volatility, nbsim, nbsteps, dc, calendar); pricing.Calculate(); var now2 = DateTime.Now; var spani = now2 - nowi; Console.WriteLine("Iter: {0}, Strike: {1}, Premium:{2}, in {3} ms", p, strike * (1 + p / 100.0), pricing.Premium, spani.TotalMilliseconds); events[p].Set(); }); } WaitHandle.WaitAll(events); nowT = DateTime.Now; spanT = nowT - now0; Console.WriteLine("Total sequential execution in {0} ms", spanT.TotalMilliseconds); #endregion |
Now it takes 2.7 seconds.
TPL
Let’s see if TPL (Task Parallel Library) Parallel.For helps to improve that multitasking work:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#region TPL Console.WriteLine("TPL"); now0 = DateTime.Now; Parallel.For(0, nbstrikes, i => { var nowi = DateTime.Now; var pricing = new OptionPricing(today, expiry, underlying, strike * (1 + i / 100.0), rf, dividendYield, volatility, nbsim, nbsteps, dc, calendar); pricing.Calculate(); var now2 = DateTime.Now; var spani = now2 - nowi; Console.WriteLine("Iter: {0}, Strike: {1}, Premium:{2}, in {3} ms", i, strike * (1 + i / 100.0), pricing.Premium, spani.TotalMilliseconds); }); nowT = DateTime.Now; spanT = nowT - now0; Console.WriteLine("Total sequential execution in {0} ms", spanT.TotalMilliseconds); #endregion Console.Read(); |
Now the same operation takes around 2.3 seconds.
Below is the overall output:
Leave a Reply