TRINUG F# Analytics Prep: Part 2

I finished up the second part of the F#/Analytics lab scheduled for August.  It is a continuation of going through Astborg’s F# for Quantitative Finance that we started last month.  Here is my fist blog post on it.

In this lab, we are going to tackle the more advanced statistical calculations: the Black-Scholes formula, the Greeks, and Monte Carlo simulation. Using the same solution and projects, I started the script file to figure out the Black Scholes formula.  Astborg uses a couple of supporting functions which I knocked out first: Power and CumulativeDistribution.  I first created his function verbatim like this:

  1. let pow x n = exp(n*log(x))

and then refactored it to make it more readable like this

  1. let power baseNumber exponent = exp(exponent * log(baseNumber))

and then I realized it is the same thing as using pown which is already found in FSharp.Core. 

image

In any event, I then attacked the cumulativeDistribution method.  I downloaded the source from his website and then refactored it so that each step is clearly laid out.  Here is the refactored function:

  1. let cumulativeDistribution (x) =
  2.         let a1 =  0.31938153
  3.         let a2 = -0.356563782
  4.         let a3 =  1.781477937
  5.         let a4 = -1.821255978
  6.         let a5 =  1.330274429
  7.         let pi = 3.141592654
  8.         let l  = abs(x)
  9.         let k  = 1.0 / (1.0 + 0.2316419 * l)
  10.  
  11.         let a1' = a1*k
  12.         let a2' = a2*k*k
  13.         let a3' = a3*(power k 3.0)
  14.         let a4' = a4*(power k 4.0)
  15.         let a5' = a5*(power k 5.0)
  16.         let w1 = 1.0/sqrt(2.0*pi)
  17.         let w2 = exp(-l*l/2.0)
  18.         let w3 = a1'+a2'+a3'+a4'+a5'
  19.         let w  = 1.0-w1*w2*w3
  20.         if x < 0.0 then 1.0 – w else w

And here is some test values from the REPL:

image

Finally, the Black Scholes formula.  I did create a separate POCO for the input data like this:

  1. type putCallFlag = Put | Call
  2.  
  3. type blackScholesInputData =
  4.     {stockPrice:float;
  5.     strikePrice:float;
  6.     timeToExpiry:float;
  7.     interestRate:float;
  8.     volatility:float}

And I refactored his code to make it more readable like this:

  1. let blackScholes(inputData:blackScholesInputData, putCallFlag:putCallFlag)=
  2.    let sx = log(inputData.stockPrice / inputData.strikePrice)
  3.    let rv = inputData.interestRate+inputData.volatility*inputData.volatility*0.5
  4.    let rvt = rv*inputData.timeToExpiry
  5.    let vt = (inputData.volatility*sqrt(inputData.timeToExpiry))
  6.    let d1=(sx + rvt)/vt
  7.    let d2=d1-vt
  8.     
  9.    match putCallFlag with
  10.     | Put ->
  11.         let xrt = inputData.strikePrice*exp(-inputData.interestRate*inputData.timeToExpiry)
  12.         let cdD1 = xrt*cumulativeDistribution(-d2)
  13.         let cdD2 = inputData.stockPrice*cumulativeDistribution(-d1)
  14.         cdD1-cdD2
  15.     | Call ->
  16.         let xrt = inputData.strikePrice*exp(-inputData.interestRate*inputData.timeToExpiry)
  17.         let cdD1 = inputData.stockPrice*cumulativeDistribution(d1)
  18.         let cdD2 = xrt*cumulativeDistribution(d2)
  19.         cdD1-cdD2

And since I was in the script environment, I put in test data that matches the sample that Astborg used in the book:

  1. let inputData = {stockPrice=58.60;strikePrice=60.;timeToExpiry=0.5;interestRate=0.01;volatility=0.3}
  2. let runBSCall = blackScholes(inputData,Call)
  3. let runBSPut = blackScholes(inputData,Put)

And voila, the results match the book:

image

With the Black-Scholes out of the way, I then implemented the Greeks.  Note that I did add helper functions for clarity, and the results match the book:

  1. let blackScholesDelta (inputData:blackScholesInputData, putCallFlag:putCallFlag) =
  2.     let sx = log(inputData.stockPrice / inputData.strikePrice)
  3.     let rv = inputData.interestRate+inputData.volatility*inputData.volatility*0.5
  4.     let rvt = rv*inputData.timeToExpiry
  5.     let vt = (inputData.volatility*sqrt(inputData.timeToExpiry))
  6.     let d1=(sx + rvt)/vt
  7.     match putCallFlag with
  8.     | Put -> cumulativeDistribution(d1) – 1.0
  9.     | Call -> cumulativeDistribution(d1)
  10.  
  11. let deltaPut = blackScholesDelta(inputData, Put)
  12. let deltaCall = blackScholesDelta(inputData, Call)
  13.  
  14. let blackScholesGamma (inputData:blackScholesInputData) =
  15.     let sx = log(inputData.stockPrice / inputData.strikePrice)
  16.     let rv = inputData.interestRate+inputData.volatility*inputData.volatility*0.5
  17.     let rvt = rv*inputData.timeToExpiry
  18.     let vt = (inputData.volatility*sqrt(inputData.timeToExpiry))
  19.     let d1=(sx + rvt)/vt
  20.     normalDistribution.Density(d1)
  21.  
  22. let gamma = blackScholesGamma(inputData)
  23.  
  24. let blackScholesVega (inputData:blackScholesInputData) =
  25.     let sx = log(inputData.stockPrice / inputData.strikePrice)
  26.     let rv = inputData.interestRate+inputData.volatility*inputData.volatility*0.5
  27.     let rvt = rv*inputData.timeToExpiry
  28.     let vt = (inputData.volatility*sqrt(inputData.timeToExpiry))
  29.     let d1=(sx + rvt)/vt   
  30.     inputData.stockPrice*normalDistribution.Density(d1)*sqrt(inputData.timeToExpiry)
  31.  
  32. let vega = blackScholesVega(inputData)
  33.  
  34. let blackScholesTheta (inputData:blackScholesInputData, putCallFlag:putCallFlag) =
  35.     let sx = log(inputData.stockPrice / inputData.strikePrice)
  36.     let rv = inputData.interestRate+inputData.volatility*inputData.volatility*0.5
  37.     let rvt = rv*inputData.timeToExpiry
  38.     let vt = (inputData.volatility*sqrt(inputData.timeToExpiry))
  39.     let d1=(sx + rvt)/vt   
  40.     let d2=d1-vt
  41.     match putCallFlag with
  42.     | Put ->
  43.         let ndD1 = inputData.stockPrice*normalDistribution.Density(d1)*inputData.volatility
  44.         let ndD1' = ndD1/(2.0*sqrt(inputData.timeToExpiry))
  45.         let rx = inputData.interestRate*inputData.strikePrice
  46.         let rt = exp(-inputData.interestRate*inputData.timeToExpiry)
  47.         let cdD2 = rx*rt*cumulativeDistribution(-d2)
  48.         -(ndD1')+cdD2
  49.     | Call ->
  50.         let ndD1 = inputData.stockPrice*normalDistribution.Density(d1)*inputData.volatility
  51.         let ndD1' = ndD1/(2.0*sqrt(inputData.timeToExpiry))
  52.         let rx = inputData.interestRate*inputData.strikePrice
  53.         let rt = exp(-inputData.interestRate*inputData.timeToExpiry)
  54.         let cdD2 = cumulativeDistribution(d2)
  55.         -(ndD1')-rx*rt*cdD2
  56.  
  57. let thetaPut = blackScholesTheta(inputData, Put)
  58. let thetaCall = blackScholesTheta(inputData, Call)
  59.  
  60. let blackScholesRho (inputData:blackScholesInputData, putCallFlag:putCallFlag) =
  61.     let sx = log(inputData.stockPrice / inputData.strikePrice)
  62.     let rv = inputData.interestRate+inputData.volatility*inputData.volatility*0.5
  63.     let rvt = rv*inputData.timeToExpiry
  64.     let vt = (inputData.volatility*sqrt(inputData.timeToExpiry))
  65.     let d1=(sx + rvt)/vt   
  66.     let d2=d1-vt
  67.     match putCallFlag with
  68.     | Put ->
  69.         let xt = inputData.strikePrice*inputData.timeToExpiry
  70.         let rt = exp(-inputData.interestRate*inputData.timeToExpiry)  
  71.         -xt*rt*cumulativeDistribution(-d2)
  72.     | Call ->
  73.         let xt = inputData.strikePrice*inputData.timeToExpiry
  74.         let rt = exp(-inputData.interestRate*inputData.timeToExpiry)          
  75.         xt*rt*cumulativeDistribution(d2)
  76.  
  77. let rhoPut = blackScholesRho(inputData, Put)
  78. let rhoCall = blackScholesRho(inputData, Call)

 

image

Finally, I threw in the Monte Carlo, which also used a POCO:

  1. type monteCarloInputData =
  2.     {stockPrice:float;
  3.     strikePrice:float;
  4.     timeToExpiry:float;
  5.     interestRate:float;
  6.     volatility:float}
  7.  
  8. let priceAtMaturity (inputData:monteCarloInputData, randomValue:float) =
  9.     let s = inputData.stockPrice
  10.     let rv = (inputData.interestRate-inputData.volatility*inputData.volatility/2.0)
  11.     let rvt = rv*inputData.timeToExpiry
  12.     let vr = inputData.volatility*randomValue
  13.     let t = sqrt(inputData.timeToExpiry)
  14.     s*exp(rvt+vr*t)
  15.     
  16. let maturityPriceInputData = {stockPrice=58.60;strikePrice=60.0;timeToExpiry=0.5;interestRate=0.01;volatility=0.3}
  17. priceAtMaturity(maturityPriceInputData, 10.0)
  18.  
  19. let monteCarlo(inputData: monteCarloInputData, randomValues:seq<float>) =
  20.     randomValues
  21.         |> Seq.map(fun randomValue -> priceAtMaturity(inputData,randomValue) – inputData.strikePrice )
  22.         |> Seq.average
  23.  
  24.  
  25. let random = new System.Random()
  26. let rnd() = random.NextDouble()
  27. let data = [for i in 1 .. 1000 -> rnd() * 1.0]
  28.  
  29. let monteCarloInputData = {stockPrice=58.60;strikePrice=60.0;timeToExpiry=0.5;interestRate=0.01;volatility=0.3;}
  30. monteCarlo(monteCarloInputData,data)

image

One thing I really like about Astborg is that the Monte Carlo function does not new up the array of random numbers, rather they are passed in.  This makes the function much more testable and is the right way to right it (IMHO).  In fact, I think that seeing “new Random” or “DateTime.Now” hard-coded into functions is an anti-pattern that is all too common.

With the last of the functions done in the script file, I moved them into the .fs file and created covering unit tests based on the sample data that I did in the REPL.

  1. [TestMethod]
  2. public void PowerUsingValidData_ReturnsExpected()
  3. {
  4.     var calculations = new Calculations();
  5.     Double expected = 8;
  6.     Double actual = Math.Round(calculations.Power(2.0, 3.0), 0);
  7.     Assert.AreEqual(expected, actual);
  8. }
  9.  
  10. [TestMethod]
  11. public void CumulativeDistributionUsingValidData_ReturnsExpected()
  12. {
  13.     var calculations = new Calculations();
  14.     Double expected = .84134;
  15.     Double actual = Math.Round(calculations.CumulativeDistribution(1.0),5);
  16.     Assert.AreEqual(expected, actual);
  17. }
  18.  
  19. [TestMethod]
  20. public void BlackScholesCallUsingValidData_ReturnsExpected()
  21. {
  22.     var calculations = new Calculations();
  23.     Double expected = 4.4652;
  24.     var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
  25.     Double actual = Math.Round(calculations.BlackScholes(inputData,PutCallFlag.Call), 5);
  26.     Assert.AreEqual(expected, actual);
  27. }
  28.  
  29. [TestMethod]
  30. public void BlackScholesPutUsingValidData_ReturnsExpected()
  31. {
  32.     var calculations = new Calculations();
  33.     Double expected = 5.56595;
  34.     var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
  35.     Double actual = Math.Round(calculations.BlackScholes(inputData, PutCallFlag.Put), 5);
  36.     Assert.AreEqual(expected, actual);
  37. }
  38.  
  39. [TestMethod]
  40. public void DaysToYearsUsingValidData_ReturnsExpected()
  41. {
  42.     var calculations = new Calculations();
  43.     Double expected = .08214;
  44.     Double actual = Math.Round(calculations.DaysToYears(30), 5);
  45.     Assert.AreEqual(expected, actual);
  46. }
  47.  
  48. [TestMethod]
  49. public void BlackScholesDeltaCallUsingValidData_ReturnsExpected()
  50. {
  51.     var calculations = new Calculations();
  52.     Double expected = .50732;
  53.     var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
  54.     Double actual = Math.Round(calculations.BlackScholesDelta(inputData, PutCallFlag.Call), 5);
  55.     Assert.AreEqual(expected, actual);
  56. }
  57.  
  58. [TestMethod]
  59. public void BlackScholesDeltaPutUsingValidData_ReturnsExpected()
  60. {
  61.     var calculations = new Calculations();
  62.     Double expected = -.49268;
  63.     var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
  64.     Double actual = Math.Round(calculations.BlackScholesDelta(inputData, PutCallFlag.Put), 5);
  65.     Assert.AreEqual(expected, actual);
  66. }
  67.  
  68. [TestMethod]
  69. public void BlackScholesGammaUsingValidData_ReturnsExpected()
  70. {
  71.     var calculations = new Calculations();
  72.     Double expected = .39888;
  73.     var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
  74.     Double actual = Math.Round(calculations.BlackScholesGamma(inputData), 5);
  75.     Assert.AreEqual(expected, actual);
  76. }
  77.  
  78. [TestMethod]
  79. public void BlackScholesVegaUsingValidData_ReturnsExpected()
  80. {
  81.     var calculations = new Calculations();
  82.     Double expected = 16.52798;
  83.     var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
  84.     Double actual = Math.Round(calculations.BlackScholesVega(inputData), 5);
  85.     Assert.AreEqual(expected, actual);
  86. }
  87.  
  88. [TestMethod]
  89. public void BlackScholesThetaCallUsingValidData_ReturnsExpected()
  90. {
  91.     var calculations = new Calculations();
  92.     Double expected = -5.21103;
  93.     var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
  94.     Double actual = Math.Round(calculations.BlackScholesTheta(inputData, PutCallFlag.Call), 5);
  95.     Assert.AreEqual(expected, actual);
  96. }
  97.  
  98. [TestMethod]
  99. public void BlackScholesThetaPutUsingValidData_ReturnsExpected()
  100. {
  101.     var calculations = new Calculations();
  102.     Double expected = -4.61402;
  103.     var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
  104.     Double actual = Math.Round(calculations.BlackScholesTheta(inputData, PutCallFlag.Put), 5);
  105.     Assert.AreEqual(expected, actual);
  106. }
  107.  
  108. [TestMethod]
  109. public void BlackScholesRhoCallUsingValidData_ReturnsExpected()
  110. {
  111.     var calculations = new Calculations();
  112.     Double expected = 12.63174;
  113.     var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
  114.     Double actual = Math.Round(calculations.BlackScholesRho(inputData, PutCallFlag.Call), 5);
  115.     Assert.AreEqual(expected, actual);
  116. }
  117.  
  118. [TestMethod]
  119. public void BlackScholesRhoPutUsingValidData_ReturnsExpected()
  120. {
  121.     var calculations = new Calculations();
  122.     Double expected = -17.21863;
  123.     var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
  124.     Double actual = Math.Round(calculations.BlackScholesRho(inputData, PutCallFlag.Put), 5);
  125.     Assert.AreEqual(expected, actual);
  126. }
  127.  
  128.  
  129. [TestMethod]
  130. public void PriceAtMaturityUsingValidData_ReturnsExpected()
  131. {
  132.     var calculations = new Calculations();
  133.     Double expected = 480.36923;
  134.     var inputData = new MonteCarloInputData(58.6, 60.0, .5, .01, .3);
  135.     Double actual = Math.Round(calculations.PriceAtMaturity(inputData, 10.0), 5);
  136.     Assert.AreEqual(expected, actual);
  137. }
  138.  
  139. [TestMethod]
  140. public void MonteCarloUsingValidData_ReturnsExpected()
  141. {
  142.     var calculations = new Calculations();
  143.     var inputData = new MonteCarloInputData(58.6, 60.0, .5, .01, .3);
  144.     var random = new System.Random();
  145.     List<Double> randomData = new List<double>();
  146.     for (int i = 0; i < 1000; i++)
  147.     {
  148.         randomData.Add(random.NextDouble());
  149.     }
  150.  
  151.     Double actual = Math.Round(calculations.MonteCarlo(inputData, randomData), 5);
  152.     var greaterThanFour = actual > 4.0;
  153.     var lessThanFive = actual < 5.0;
  154.  
  155.     Assert.AreEqual(true, greaterThanFour);
  156.     Assert.AreEqual(true, lessThanFive);
  157. }

 

With all of the tests running green, I then turned my attention to the UI.  I created more real state on the MainWindow  and added some additional data structures to the results of the analytics that lend themselves to charting and graphing.  For example:

  1. public class GreekData
  2. {
  3.     public Double StrikePrice { get; set; }
  4.     public Double DeltaCall { get; set; }
  5.     public Double DeltaPut { get; set; }
  6.     public Double Gamma { get; set; }
  7.     public Double Vega { get; set; }
  8.     public Double ThetaCall { get; set; }
  9.     public Double ThetaPut { get; set; }
  10.     public Double RhoCall { get; set; }
  11.     public Double RhoPut { get; set; }
  12.  
  13. }

And in the code behind of the MainWindow, I added some calcs based on the prior code that was already in it:

  1. var theGreeks = new List<GreekData>();
  2. for (int i = 0; i < 5; i++)
  3. {
  4.     var greekData = new GreekData();
  5.     greekData.StrikePrice = closestDollar – i;
  6.     theGreeks.Add(greekData);
  7.     greekData = new GreekData();
  8.     greekData.StrikePrice = closestDollar + i;
  9.     theGreeks.Add(greekData);
  10. }
  11. theGreeks.Sort((greek1,greek2)=>greek1.StrikePrice.CompareTo(greek2.StrikePrice));
  12.  
  13. foreach (var greekData in theGreeks)
  14. {
  15.     var inputData =
  16.         new BlackScholesInputData(adjustedClose, greekData.StrikePrice, .5, .01, .3);
  17.     greekData.DeltaCall = calculations.BlackScholesDelta(inputData, PutCallFlag.Call);
  18.     greekData.DeltaPut = calculations.BlackScholesDelta(inputData, PutCallFlag.Put);
  19.     greekData.Gamma = calculations.BlackScholesGamma(inputData);
  20.     greekData.RhoCall = calculations.BlackScholesRho(inputData, PutCallFlag.Call);
  21.     greekData.RhoPut = calculations.BlackScholesRho(inputData, PutCallFlag.Put);
  22.     greekData.ThetaCall = calculations.BlackScholesTheta(inputData, PutCallFlag.Call);
  23.     greekData.ThetaPut = calculations.BlackScholesTheta(inputData, PutCallFlag.Put);
  24.     greekData.Vega = calculations.BlackScholesVega(inputData);
  25.  
  26. }
  27.  
  28. this.TheGreeksDataGrid.ItemsSource = theGreeks;
  29.  
  30.  
  31. var blackScholes = new List<BlackScholesData>();
  32. for (int i = 0; i < 5; i++)
  33. {
  34.     var blackScholesData = new BlackScholesData();
  35.     blackScholesData.StrikePrice = closestDollar – i;
  36.     blackScholes.Add(blackScholesData);
  37.     blackScholesData = new BlackScholesData();
  38.     blackScholesData.StrikePrice = closestDollar + i;
  39.     blackScholes.Add(blackScholesData);
  40. }
  41. blackScholes.Sort((bsmc1, bsmc2) => bsmc1.StrikePrice.CompareTo(bsmc2.StrikePrice));
  42.  
  43. var random = new System.Random();
  44. List<Double> randomData = new List<double>();
  45. for (int i = 0; i < 1000; i++)
  46. {
  47.     randomData.Add(random.NextDouble());
  48. }
  49.  
  50. foreach (var blackScholesMonteCarlo in blackScholes)
  51. {
  52.     var blackScholesInputData =
  53.         new BlackScholesInputData(adjustedClose, blackScholesMonteCarlo.StrikePrice, .5, .01, .3);
  54.     var monteCarloInputData =
  55.         new MonteCarloInputData(adjustedClose, blackScholesMonteCarlo.StrikePrice, .5, .01, .3);
  56.  
  57.     blackScholesMonteCarlo.Call = calculations.BlackScholes(blackScholesInputData, PutCallFlag.Call);
  58.     blackScholesMonteCarlo.Put = calculations.BlackScholes(blackScholesInputData, PutCallFlag.Put);
  59.     blackScholesMonteCarlo.MonteCarlo = calculations.MonteCarlo(monteCarloInputData, randomData);
  60. }
  61.  
  62. this.BlackScholesDataGrid.ItemsSource = blackScholes;

And Whammo, the UI.

 

image

Fortunately Conrad D’Cruz is a member of TRINUG and an options trader and is going to explain what the heck we are looking at when the SIG gets together again.

 

One Response to TRINUG F# Analytics Prep: Part 2

  1. Pingback: F# Weekly #27, 2014 | Sergey Tihon's Blog

Leave a comment