TRINUG F# Analytics Prep: Part 2
July 1, 2014 1 Comment
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:
- let pow x n = exp(n*log(x))
and then refactored it to make it more readable like this
- 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.
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:
- let cumulativeDistribution (x) =
- let a1 = 0.31938153
- let a2 = -0.356563782
- let a3 = 1.781477937
- let a4 = -1.821255978
- let a5 = 1.330274429
- let pi = 3.141592654
- let l = abs(x)
- let k = 1.0 / (1.0 + 0.2316419 * l)
- let a1' = a1*k
- let a2' = a2*k*k
- let a3' = a3*(power k 3.0)
- let a4' = a4*(power k 4.0)
- let a5' = a5*(power k 5.0)
- let w1 = 1.0/sqrt(2.0*pi)
- let w2 = exp(-l*l/2.0)
- let w3 = a1'+a2'+a3'+a4'+a5'
- let w = 1.0-w1*w2*w3
- if x < 0.0 then 1.0 – w else w
And here is some test values from the REPL:
Finally, the Black Scholes formula. I did create a separate POCO for the input data like this:
- type putCallFlag = Put | Call
- type blackScholesInputData =
- {stockPrice:float;
- strikePrice:float;
- timeToExpiry:float;
- interestRate:float;
- volatility:float}
And I refactored his code to make it more readable like this:
- let blackScholes(inputData:blackScholesInputData, putCallFlag:putCallFlag)=
- let sx = log(inputData.stockPrice / inputData.strikePrice)
- let rv = inputData.interestRate+inputData.volatility*inputData.volatility*0.5
- let rvt = rv*inputData.timeToExpiry
- let vt = (inputData.volatility*sqrt(inputData.timeToExpiry))
- let d1=(sx + rvt)/vt
- let d2=d1-vt
- match putCallFlag with
- | Put ->
- let xrt = inputData.strikePrice*exp(-inputData.interestRate*inputData.timeToExpiry)
- let cdD1 = xrt*cumulativeDistribution(-d2)
- let cdD2 = inputData.stockPrice*cumulativeDistribution(-d1)
- cdD1-cdD2
- | Call ->
- let xrt = inputData.strikePrice*exp(-inputData.interestRate*inputData.timeToExpiry)
- let cdD1 = inputData.stockPrice*cumulativeDistribution(d1)
- let cdD2 = xrt*cumulativeDistribution(d2)
- 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:
- let inputData = {stockPrice=58.60;strikePrice=60.;timeToExpiry=0.5;interestRate=0.01;volatility=0.3}
- let runBSCall = blackScholes(inputData,Call)
- let runBSPut = blackScholes(inputData,Put)
And voila, the results match the book:
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:
- let blackScholesDelta (inputData:blackScholesInputData, putCallFlag:putCallFlag) =
- let sx = log(inputData.stockPrice / inputData.strikePrice)
- let rv = inputData.interestRate+inputData.volatility*inputData.volatility*0.5
- let rvt = rv*inputData.timeToExpiry
- let vt = (inputData.volatility*sqrt(inputData.timeToExpiry))
- let d1=(sx + rvt)/vt
- match putCallFlag with
- | Put -> cumulativeDistribution(d1) – 1.0
- | Call -> cumulativeDistribution(d1)
- let deltaPut = blackScholesDelta(inputData, Put)
- let deltaCall = blackScholesDelta(inputData, Call)
- let blackScholesGamma (inputData:blackScholesInputData) =
- let sx = log(inputData.stockPrice / inputData.strikePrice)
- let rv = inputData.interestRate+inputData.volatility*inputData.volatility*0.5
- let rvt = rv*inputData.timeToExpiry
- let vt = (inputData.volatility*sqrt(inputData.timeToExpiry))
- let d1=(sx + rvt)/vt
- normalDistribution.Density(d1)
- let gamma = blackScholesGamma(inputData)
- let blackScholesVega (inputData:blackScholesInputData) =
- let sx = log(inputData.stockPrice / inputData.strikePrice)
- let rv = inputData.interestRate+inputData.volatility*inputData.volatility*0.5
- let rvt = rv*inputData.timeToExpiry
- let vt = (inputData.volatility*sqrt(inputData.timeToExpiry))
- let d1=(sx + rvt)/vt
- inputData.stockPrice*normalDistribution.Density(d1)*sqrt(inputData.timeToExpiry)
- let vega = blackScholesVega(inputData)
- let blackScholesTheta (inputData:blackScholesInputData, putCallFlag:putCallFlag) =
- let sx = log(inputData.stockPrice / inputData.strikePrice)
- let rv = inputData.interestRate+inputData.volatility*inputData.volatility*0.5
- let rvt = rv*inputData.timeToExpiry
- let vt = (inputData.volatility*sqrt(inputData.timeToExpiry))
- let d1=(sx + rvt)/vt
- let d2=d1-vt
- match putCallFlag with
- | Put ->
- let ndD1 = inputData.stockPrice*normalDistribution.Density(d1)*inputData.volatility
- let ndD1' = ndD1/(2.0*sqrt(inputData.timeToExpiry))
- let rx = inputData.interestRate*inputData.strikePrice
- let rt = exp(-inputData.interestRate*inputData.timeToExpiry)
- let cdD2 = rx*rt*cumulativeDistribution(-d2)
- -(ndD1')+cdD2
- | Call ->
- let ndD1 = inputData.stockPrice*normalDistribution.Density(d1)*inputData.volatility
- let ndD1' = ndD1/(2.0*sqrt(inputData.timeToExpiry))
- let rx = inputData.interestRate*inputData.strikePrice
- let rt = exp(-inputData.interestRate*inputData.timeToExpiry)
- let cdD2 = cumulativeDistribution(d2)
- -(ndD1')-rx*rt*cdD2
- let thetaPut = blackScholesTheta(inputData, Put)
- let thetaCall = blackScholesTheta(inputData, Call)
- let blackScholesRho (inputData:blackScholesInputData, putCallFlag:putCallFlag) =
- let sx = log(inputData.stockPrice / inputData.strikePrice)
- let rv = inputData.interestRate+inputData.volatility*inputData.volatility*0.5
- let rvt = rv*inputData.timeToExpiry
- let vt = (inputData.volatility*sqrt(inputData.timeToExpiry))
- let d1=(sx + rvt)/vt
- let d2=d1-vt
- match putCallFlag with
- | Put ->
- let xt = inputData.strikePrice*inputData.timeToExpiry
- let rt = exp(-inputData.interestRate*inputData.timeToExpiry)
- -xt*rt*cumulativeDistribution(-d2)
- | Call ->
- let xt = inputData.strikePrice*inputData.timeToExpiry
- let rt = exp(-inputData.interestRate*inputData.timeToExpiry)
- xt*rt*cumulativeDistribution(d2)
- let rhoPut = blackScholesRho(inputData, Put)
- let rhoCall = blackScholesRho(inputData, Call)
Finally, I threw in the Monte Carlo, which also used a POCO:
- type monteCarloInputData =
- {stockPrice:float;
- strikePrice:float;
- timeToExpiry:float;
- interestRate:float;
- volatility:float}
- let priceAtMaturity (inputData:monteCarloInputData, randomValue:float) =
- let s = inputData.stockPrice
- let rv = (inputData.interestRate-inputData.volatility*inputData.volatility/2.0)
- let rvt = rv*inputData.timeToExpiry
- let vr = inputData.volatility*randomValue
- let t = sqrt(inputData.timeToExpiry)
- s*exp(rvt+vr*t)
- let maturityPriceInputData = {stockPrice=58.60;strikePrice=60.0;timeToExpiry=0.5;interestRate=0.01;volatility=0.3}
- priceAtMaturity(maturityPriceInputData, 10.0)
- let monteCarlo(inputData: monteCarloInputData, randomValues:seq<float>) =
- randomValues
- |> Seq.map(fun randomValue -> priceAtMaturity(inputData,randomValue) – inputData.strikePrice )
- |> Seq.average
- let random = new System.Random()
- let rnd() = random.NextDouble()
- let data = [for i in 1 .. 1000 -> rnd() * 1.0]
- let monteCarloInputData = {stockPrice=58.60;strikePrice=60.0;timeToExpiry=0.5;interestRate=0.01;volatility=0.3;}
- monteCarlo(monteCarloInputData,data)
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.
- [TestMethod]
- public void PowerUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = 8;
- Double actual = Math.Round(calculations.Power(2.0, 3.0), 0);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void CumulativeDistributionUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = .84134;
- Double actual = Math.Round(calculations.CumulativeDistribution(1.0),5);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void BlackScholesCallUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = 4.4652;
- var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
- Double actual = Math.Round(calculations.BlackScholes(inputData,PutCallFlag.Call), 5);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void BlackScholesPutUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = 5.56595;
- var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
- Double actual = Math.Round(calculations.BlackScholes(inputData, PutCallFlag.Put), 5);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void DaysToYearsUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = .08214;
- Double actual = Math.Round(calculations.DaysToYears(30), 5);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void BlackScholesDeltaCallUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = .50732;
- var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
- Double actual = Math.Round(calculations.BlackScholesDelta(inputData, PutCallFlag.Call), 5);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void BlackScholesDeltaPutUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = -.49268;
- var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
- Double actual = Math.Round(calculations.BlackScholesDelta(inputData, PutCallFlag.Put), 5);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void BlackScholesGammaUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = .39888;
- var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
- Double actual = Math.Round(calculations.BlackScholesGamma(inputData), 5);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void BlackScholesVegaUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = 16.52798;
- var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
- Double actual = Math.Round(calculations.BlackScholesVega(inputData), 5);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void BlackScholesThetaCallUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = -5.21103;
- var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
- Double actual = Math.Round(calculations.BlackScholesTheta(inputData, PutCallFlag.Call), 5);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void BlackScholesThetaPutUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = -4.61402;
- var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
- Double actual = Math.Round(calculations.BlackScholesTheta(inputData, PutCallFlag.Put), 5);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void BlackScholesRhoCallUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = 12.63174;
- var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
- Double actual = Math.Round(calculations.BlackScholesRho(inputData, PutCallFlag.Call), 5);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void BlackScholesRhoPutUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = -17.21863;
- var inputData = new BlackScholesInputData(58.6, 60.0, .5, .01, .3);
- Double actual = Math.Round(calculations.BlackScholesRho(inputData, PutCallFlag.Put), 5);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void PriceAtMaturityUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- Double expected = 480.36923;
- var inputData = new MonteCarloInputData(58.6, 60.0, .5, .01, .3);
- Double actual = Math.Round(calculations.PriceAtMaturity(inputData, 10.0), 5);
- Assert.AreEqual(expected, actual);
- }
- [TestMethod]
- public void MonteCarloUsingValidData_ReturnsExpected()
- {
- var calculations = new Calculations();
- var inputData = new MonteCarloInputData(58.6, 60.0, .5, .01, .3);
- var random = new System.Random();
- List<Double> randomData = new List<double>();
- for (int i = 0; i < 1000; i++)
- {
- randomData.Add(random.NextDouble());
- }
- Double actual = Math.Round(calculations.MonteCarlo(inputData, randomData), 5);
- var greaterThanFour = actual > 4.0;
- var lessThanFive = actual < 5.0;
- Assert.AreEqual(true, greaterThanFour);
- Assert.AreEqual(true, lessThanFive);
- }
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:
- public class GreekData
- {
- public Double StrikePrice { get; set; }
- public Double DeltaCall { get; set; }
- public Double DeltaPut { get; set; }
- public Double Gamma { get; set; }
- public Double Vega { get; set; }
- public Double ThetaCall { get; set; }
- public Double ThetaPut { get; set; }
- public Double RhoCall { get; set; }
- public Double RhoPut { get; set; }
- }
And in the code behind of the MainWindow, I added some calcs based on the prior code that was already in it:
- var theGreeks = new List<GreekData>();
- for (int i = 0; i < 5; i++)
- {
- var greekData = new GreekData();
- greekData.StrikePrice = closestDollar – i;
- theGreeks.Add(greekData);
- greekData = new GreekData();
- greekData.StrikePrice = closestDollar + i;
- theGreeks.Add(greekData);
- }
- theGreeks.Sort((greek1,greek2)=>greek1.StrikePrice.CompareTo(greek2.StrikePrice));
- foreach (var greekData in theGreeks)
- {
- var inputData =
- new BlackScholesInputData(adjustedClose, greekData.StrikePrice, .5, .01, .3);
- greekData.DeltaCall = calculations.BlackScholesDelta(inputData, PutCallFlag.Call);
- greekData.DeltaPut = calculations.BlackScholesDelta(inputData, PutCallFlag.Put);
- greekData.Gamma = calculations.BlackScholesGamma(inputData);
- greekData.RhoCall = calculations.BlackScholesRho(inputData, PutCallFlag.Call);
- greekData.RhoPut = calculations.BlackScholesRho(inputData, PutCallFlag.Put);
- greekData.ThetaCall = calculations.BlackScholesTheta(inputData, PutCallFlag.Call);
- greekData.ThetaPut = calculations.BlackScholesTheta(inputData, PutCallFlag.Put);
- greekData.Vega = calculations.BlackScholesVega(inputData);
- }
- this.TheGreeksDataGrid.ItemsSource = theGreeks;
- var blackScholes = new List<BlackScholesData>();
- for (int i = 0; i < 5; i++)
- {
- var blackScholesData = new BlackScholesData();
- blackScholesData.StrikePrice = closestDollar – i;
- blackScholes.Add(blackScholesData);
- blackScholesData = new BlackScholesData();
- blackScholesData.StrikePrice = closestDollar + i;
- blackScholes.Add(blackScholesData);
- }
- blackScholes.Sort((bsmc1, bsmc2) => bsmc1.StrikePrice.CompareTo(bsmc2.StrikePrice));
- var random = new System.Random();
- List<Double> randomData = new List<double>();
- for (int i = 0; i < 1000; i++)
- {
- randomData.Add(random.NextDouble());
- }
- foreach (var blackScholesMonteCarlo in blackScholes)
- {
- var blackScholesInputData =
- new BlackScholesInputData(adjustedClose, blackScholesMonteCarlo.StrikePrice, .5, .01, .3);
- var monteCarloInputData =
- new MonteCarloInputData(adjustedClose, blackScholesMonteCarlo.StrikePrice, .5, .01, .3);
- blackScholesMonteCarlo.Call = calculations.BlackScholes(blackScholesInputData, PutCallFlag.Call);
- blackScholesMonteCarlo.Put = calculations.BlackScholes(blackScholesInputData, PutCallFlag.Put);
- blackScholesMonteCarlo.MonteCarlo = calculations.MonteCarlo(monteCarloInputData, randomData);
- }
- this.BlackScholesDataGrid.ItemsSource = blackScholes;
And Whammo, the UI.
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.
Pingback: F# Weekly #27, 2014 | Sergey Tihon's Blog