Discount/Zero Curve Construction in F# – Part 3 (Bootstrapping)
In Part 1, the first lines of code we saw formed the final step in the construction of the discount curve:
let discs = [ (curveDate, 1.0) ] |> bootstrap spotPoints |> bootstrapCash spotDate cashPoints |> bootstrapFutures futuresStartDate futuresPoints |> bootstrapSwaps spotDate USD swapPoints |> Seq.sortBy (fun (qDate, _) -> qDate)
The curve begins with today’s discount factor of 1.0 in a one-element list, which is passed to the bootstrap function along with the spot quotes.
let rec bootstrap quotes discountCurve = match quotes with quote :: tail -> let newDf = computeDf (List.hd discountCurve) quote bootstrap tail (newDf :: discountCurve) | [] -> discountCurve
This tail recursion works through the given quotes, computing the discount factor for each based on the previous discount factor in the curve (the details of this we’ll cover in Part 4), and places that new discount factor at the head of a new curve, that is used to continue the recursion. When there are no more quotes, the discount curve is returned, ending the recursion. After bootstrapping the spot quotes, the curve looks like this:
Cash quotes are bootstrapped with respect to the spot discount factor, rather than their previous cash point:
let rec bootstrapCash spotDate quotes discountCurve = match quotes with quote :: tail -> let spotDf = (spotDate, findDf logarithmic spotDate discountCurve) let newDf = computeDf spotDf quote bootstrapCash spotDate tail (newDf :: discountCurve) | [] -> discountCurve
Once again, the futures offer a twist, albeit a minor one this time:
let bootstrapFutures futuresStartDate quotes discountCurve = match futuresStartDate with | Some d -> bootstrap (Seq.to_list quotes) ((d, findDf logarithmic d discountCurve) :: discountCurve) | None -> discountCurve
Note that these aren’t in date order at this point, since we bootstrapped from the futures start date of Jun 17th, but that’s OK for our purposes – it won’t affect the accuracy of the final curve once we sort it chronologically.
Now to the swaps -- this time it’s the swaps that complicate the procedure. With the assumption that the swap quotes are fair market rates (present value zero), we use a root solver (in this case Newton’s method) to find the discount factor of each swap, pricing them using the curve we’ve built thus far:
let rec bootstrapSwaps spotDate calendar swapQuotes discountCurve = match swapQuotes with (qDate, qQuote) :: tail -> // build the schedule for this swap let swapDates = schedule semiAnnual { startDate = spotDate; endDate = qDate } let rolledSwapDates = Seq.map (fun (d:Date) -> roll RollRule.Following calendar d) swapDates let swapPeriods = Seq.to_list (Seq.map (fun (s, e) -> { startDate = s; endDate = e }) (Seq.pairwise rolledSwapDates)) // solve let accuracy = 1e-12 let spotFactor = findDf logarithmic spotDate discountCurve let f = computeSwapDf dayCount spotDate (qDate, qQuote) discountCurve swapPeriods let newDf = solveNewton f accuracy spotFactor bootstrapSwaps spotDate calendar tail ((qDate, newDf) :: discountCurve) | [] -> discountCurve
For each swap we build a semi-annual schedule from spot to swap maturity using the schedule function, which recursively builds a sequence of dates six months apart:
let rec schedule frequency period = seq { yield period.startDate let next = frequency period.startDate if (next <= period.endDate) then yield! schedule frequency { startDate = next; endDate = period.endDate } }
let semiAnnual (from:Date) = from.AddMonths(6)
In Part 4, we’ll dive into to (relatively simple) math functions that underpin the bootstrapping calculations.
Reader Comments