Showing posts with label Piecewise curve construction. Show all posts
Showing posts with label Piecewise curve construction. Show all posts

Sunday, July 14, 2019

QuantLib-Python: flexible construction scheme for piecewise yield term structures

I consider QuantLib to be a fundamental pricing library, which can effectively handle valuations for pretty much any given type of security. If there is no existing implementation for an instrument available, one can create a new implementation for it. What then makes the use of QuantLib library sometimes difficult? It's the amount of work to be done, before anything will happen. Outside of that promised functionality to value security, one has to take full responsibility of all involved janitor work. The code is (usually always) containing endless sections for different variable definitions and creation of different types of helper objects. Even creating realistic pricing scheme for a simple interest rate swap seems to require an army of different variables and objects. A lot of cooking anyway, before the beef will be served.

In this post, one possible scheme for flexible construction of QuantLib piecewise yield term structures will be presented. The program and all involved files can be downloaded from my GitHub repository.

Assume we would like to construct piecewise yield term structure for EUR and USD. Assume also, that we have the following market data for EUR and USD currencies in a specific CSV file.

Ticker,Value
USD.DEPOSIT.1D,0.02359
USD.DEPOSIT.1W,0.0237475
USD.DEPOSIT.1M,0.02325
USD.DEPOSIT.2M,0.0232475
USD.DEPOSIT.3M,0.0230338
USD.FUTURE.2M,97.92
USD.FUTURE.5M,98.005
USD.FUTURE.8M,98.185
USD.FUTURE.11M,98.27
USD.FUTURE.14M,98.33
USD.SWAP.2Y,0.01879
USD.SWAP.3Y,0.01835
USD.SWAP.5Y,0.01862
USD.SWAP.7Y,0.0194
USD.SWAP.10Y,0.02065
USD.SWAP.15Y,0.02204
USD.SWAP.30Y,0.02306
EUR.DEPOSIT.1D,-0.00366
EUR.DEPOSIT.1W,-0.00399
EUR.DEPOSIT.1M,-0.00393
EUR.DEPOSIT.3M,-0.00363
EUR.DEPOSIT.6M,-0.00342
EUR.FUTURE.5M,100.48
EUR.FUTURE.8M,100.505
EUR.FUTURE.11M,100.505
EUR.FUTURE.14M,100.495
EUR.FUTURE.17M,100.47
EUR.SWAP.1Y,-0.0038
EUR.SWAP.2Y,-0.0039
EUR.SWAP.5Y,-0.0019
EUR.SWAP.7Y,-0.0002
EUR.SWAP.10Y,0.0024
EUR.SWAP.15Y,0.0056
EUR.SWAP.30Y,0.008

In essence, we have key-value pairs in this CSV file, where the key is ticker (instrument, such as deposit, future or swap) and the value is rate (or price for a futures contract). Now, all instruments in that file are following some specific market conventions. All these conventions are then stored in a specific JSON file. The content of this file can be easily understood by using some available JSON editor.






















In essence, we are actually storing all required "constant" parameters for all QuantLib instruments we would like to use for constructing piecewise yield curves, into this file. At the moment, there are required conventions available for constructing EUR and USD curves.

Next, we have builder class PiecewiseCurveBuilder for assembling QuantLib piecewise yield term structures, as shown below. In the first stage, we store all conventions and market data into this builder class in its constructor. After this, builder is ready for constructing curves. As client is requesting specific curve by using Build method, the class will then create all bootstrap helpers based on a given market data (for requested currency) and instrument conventions (for instruments in requested currency).

# create piecewise yield term structure
class PiecewiseCurveBuilder(object):
    
    # in constructor, we store all possible instrument conventions and market data
    def __init__(self, settlementDate, conventions, marketData):        
        self.helpers = [] # list containing bootstrap helpers
        self.settlementDate = settlementDate
        self.conventions = conventions
        self.market = marketData
    
    # for a given currency, first assemble bootstrap helpers, 
    # then construct yield term structure handle
    def Build(self, currency, enableExtrapolation = True):

        # clear all existing bootstrap helpers from list
        self.helpers.clear()
        # filter out correct market data set for a given currency
        data = self.market.loc[self.market['Ticker'].str.contains(currency), :]
        
        # loop through market data set
        for i in range(data.shape[0]):            
            # extract ticker and value
            ticker = data.iloc[i]['Ticker']
            value = data.iloc[i]['Value'] 
            
            # add deposit rate helper
            # ticker prototype: 'CCY.DEPOSIT.3M'
            if('DEPOSIT' in ticker):
                # extract correct instrument convention
                convention = self.conventions[currency]['DEPOSIT']
                rate = value
                period = ql.Period(ticker.split('.')[2])
                # extract parameters from instrument convention
                fixingDays = convention['FIXINGDAYS']
                calendar = Convert.to_calendar(convention['CALENDAR'])
                businessDayConvention = Convert.to_businessDayConvention(convention['BUSINESSDAYCONVENTION'])
                endOfMonth = convention['ENDOFMONTH']
                dayCounter = Convert.to_dayCounter(convention['DAYCOUNTER'])
                # create and append deposit helper into helper list
                self.helpers.append(ql.DepositRateHelper(rate, period, fixingDays, 
                    calendar, businessDayConvention, endOfMonth, dayCounter))
        
            # add futures rate helper
            # ticker prototype: 'CCY.FUTURE.10M'
            # note: third ticker field ('10M') is defining starting date
            # for future to be 10 months after defined settlement date
            if('FUTURE' in ticker):
                # extract correct instrument convention
                convention = self.conventions[currency]['FUTURE']
                price = value
                iborStartDate = ql.IMM.nextDate(self.settlementDate + ql.Period(ticker.split('.')[2]))
                # extract parameters from instrument convention
                lengthInMonths = convention['LENGTHINMONTHS']
                calendar = Convert.to_calendar(convention['CALENDAR'])
                businessDayConvention = Convert.to_businessDayConvention(convention['BUSINESSDAYCONVENTION']) 
                endOfMonth = convention['ENDOFMONTH']
                dayCounter = Convert.to_dayCounter(convention['DAYCOUNTER'])
                # create and append futures helper into helper list
                self.helpers.append(ql.FuturesRateHelper(price, iborStartDate, lengthInMonths,
                    calendar, businessDayConvention, endOfMonth, dayCounter))                
            
            # add swap rate helper
            # ticker prototype: 'CCY.SWAP.2Y'
            if('SWAP' in ticker):
                # extract correct instrument convention
                convention = self.conventions[currency]['SWAP']
                rate = value
                periodLength = ql.Period(ticker.split('.')[2])
                # extract parameters from instrument convention
                fixedCalendar = Convert.to_calendar(convention['FIXEDCALENDAR'])
                fixedFrequency = Convert.to_frequency(convention['FIXEDFREQUENCY']) 
                fixedConvention = Convert.to_businessDayConvention(convention['FIXEDCONVENTION'])
                fixedDayCount = Convert.to_dayCounter(convention['FIXEDDAYCOUNTER'])
                floatIndex = Convert.to_iborIndex(convention['FLOATINDEX']) 
                # create and append swap helper into helper list
                self.helpers.append(ql.SwapRateHelper(rate, periodLength, fixedCalendar,
                    fixedFrequency, fixedConvention, fixedDayCount, floatIndex))
        
        # extract day counter for curve from configurations
        dayCounter = Convert.to_dayCounter(self.conventions[currency]['CONFIGURATIONS']['DAYCOUNTER'])
        # construct yield term structure handle
        yieldTermStructure = ql.PiecewiseLinearZero(self.settlementDate, self.helpers, dayCounter)
        if(enableExtrapolation == True): yieldTermStructure.enableExtrapolation()
        return ql.RelinkableYieldTermStructureHandle(yieldTermStructure)

The final component in this scheme is Convert class, which performs conversions from string presentation to specific QuantLib data types. This class is heavily used in builder class, where convention string information is transformed into correct QuantLib data types.

# utility class for different QuantLib type conversions 
class Convert:
    
    # convert date string ('yyyy-mm-dd') to QuantLib Date object
    def to_date(s):
        monthDictionary = {
            '01': ql.January, '02': ql.February, '03': ql.March,
            '04': ql.April, '05': ql.May, '06': ql.June,
            '07': ql.July, '08': ql.August, '09': ql.September,
            '10': ql.October, '11': ql.November, '12': ql.December
        }
        s = s.split('-')
        return ql.Date(int(s[2]), monthDictionary[s[1]], int(s[0]))
    
    # convert string to QuantLib businessdayconvention enumerator
    def to_businessDayConvention(s):
        if (s.upper() == 'FOLLOWING'): return ql.Following
        if (s.upper() == 'MODIFIEDFOLLOWING'): return ql.ModifiedFollowing
        if (s.upper() == 'PRECEDING'): return ql.Preceding
        if (s.upper() == 'MODIFIEDPRECEDING'): return ql.ModifiedPreceding
        if (s.upper() == 'UNADJUSTED'): return ql.Unadjusted
        
    # convert string to QuantLib calendar object
    def to_calendar(s):
        if (s.upper() == 'TARGET'): return ql.TARGET()
        if (s.upper() == 'UNITEDSTATES'): return ql.UnitedStates()
        if (s.upper() == 'UNITEDKINGDOM'): return ql.UnitedKingdom()
        # TODO: add new calendar here
        
    # convert string to QuantLib swap type enumerator
    def to_swapType(s):
        if (s.upper() == 'PAYER'): return ql.VanillaSwap.Payer
        if (s.upper() == 'RECEIVER'): return ql.VanillaSwap.Receiver
        
    # convert string to QuantLib frequency enumerator
    def to_frequency(s):
        if (s.upper() == 'DAILY'): return ql.Daily
        if (s.upper() == 'WEEKLY'): return ql.Weekly
        if (s.upper() == 'MONTHLY'): return ql.Monthly
        if (s.upper() == 'QUARTERLY'): return ql.Quarterly
        if (s.upper() == 'SEMIANNUAL'): return ql.Semiannual
        if (s.upper() == 'ANNUAL'): return ql.Annual

    # convert string to QuantLib date generation rule enumerator
    def to_dateGenerationRule(s):
        if (s.upper() == 'BACKWARD'): return ql.DateGeneration.Backward
        if (s.upper() == 'FORWARD'): return ql.DateGeneration.Forward
        # TODO: add new date generation rule here

    # convert string to QuantLib day counter object
    def to_dayCounter(s):
        if (s.upper() == 'ACTUAL360'): return ql.Actual360()
        if (s.upper() == 'ACTUAL365FIXED'): return ql.Actual365Fixed()
        if (s.upper() == 'ACTUALACTUAL'): return ql.ActualActual()
        if (s.upper() == 'ACTUAL365NOLEAP'): return ql.Actual365NoLeap()
        if (s.upper() == 'BUSINESS252'): return ql.Business252()
        if (s.upper() == 'ONEDAYCOUNTER'): return ql.OneDayCounter()
        if (s.upper() == 'SIMPLEDAYCOUNTER'): return ql.SimpleDayCounter()
        if (s.upper() == 'THIRTY360'): return ql.Thirty360()

    # convert string (ex.'USD.3M') to QuantLib ibor index object
    def to_iborIndex(s):
        s = s.split('.')
        if(s[0].upper() == 'USD'): return ql.USDLibor(ql.Period(s[1]))
        if(s[0].upper() == 'EUR'): return ql.Euribor(ql.Period(s[1]))        

Finally, let us take a look, how easily we can actually construct QuantLib piecewise yield term structures for our two currencies. After this point, these constructed curves can then be used as arguments for pricing engines.

# create instrument conventions and market data
rootDirectory = sys.argv[1] # command line argument: '/home/mikejuniperhill/QuantLib/'
evaluationDate = Convert.to_date(datetime.today().strftime('%Y-%m-%d'))
ql.Settings.instance().evaluationDate = evaluationDate
conventions = Configurations(rootDirectory + 'conventions.json')
marketData = pd.read_csv(rootDirectory + 'marketdata.csv')

# initialize builder, store all conventions and market data
builder = PiecewiseCurveBuilder(evaluationDate, conventions, marketData)
currencies = sys.argv[2] # command line argument: 'USD,EUR'
currencies = currencies.split(',')

# construct curves based on instrument conventions, given market data and currencies
for currency in currencies:    
    curve = builder.Build(currency)
    # print discount factors semiannually up to 30 years
    times = np.linspace(0.0, 30.0, 61)
    df = [round(curve.discount(t), 4) for t in times]
    print('discount factors for', currency)
    print(df)

Address to root directory (containing market data and instrument conventions files) and requested currencies are given as command line arguments. Conventions object and market data (pandas data frame) are being created and fed to curve builder object in its constructor. Finally, curves are being requested for each currency.

It can be seen, that we can actually create piecewise yield term structures for any currency (as long as market data and conventions are available) with a very few lines of code by using this kind of construction scheme. In essence, all the complexity involved is still there, but we have effectively moved all "janitor work" into specific classes (builder, conversions) and files (conventions, market data).

Configuration for a new currency should be straightforward: add instruments into market data file (ticker-value-pairs). Then, add new section for this new currency to conventions file (including configuration sub-sections for all instruments in this new currency). Also, some minor additions might be needed in conversion class.

Program execution is shown below.








Thanks for reading my blog.
-Mike

Wednesday, November 21, 2018

QuantLib-Python: Builder for Piecewise Term Structure

This post is presenting one possible implementation for Python builder class for constructing QuantLib piecewise yield term structure. The purpose is simple: one can assemble piecewise yield curve by adding arbitrary amount of different quote types and finally request handle for the curve. This curve can then be used in other part of the program. Effectively, this class is wrapping some of the required tedious administrative code work away from a client and also offering a nice compact package for all corresponding purposes. Now, it is not my intention to be object-oriented in Python world just for the sake of being object-oriented, but .. this is just a perfect place for Python class implementation. Corresponding C++ version has been presented in here.

Thanks for reading my blog.
-Mike

from QuantLib import *
import numpy as Numpy

# create piecewise yield term structure
class PiecewiseCurveBuilder:
    def __init__(self, settlementDate, dayCounter):
        self.helpers = []
        self.settlementDate = settlementDate
        self.dayCounter = dayCounter

    # 4th constructor: DepositRateHelper(Rate rate, const shared_ptr<IborIndex> &iborIndex)
    def AddDeposit(self, rate, iborIndex):
        helper = DepositRateHelper(rate, iborIndex)
        self.helpers.append(helper)

    # 4th constructor: FraRateHelper(Rate rate, Natural monthsToStart, const shared_ptr<IborIndex> &iborIndex)
    def AddFRA(self, rate, monthsToStart, iborIndex):
        helper = FraRateHelper(rate, monthsToStart, iborIndex)
        self.helpers.append(helper)
    
    # 6th constructor (Real price, const Date &iborStartDate, const ext::shared_ptr<IborIndex> &iborIndex) 
    def AddFuture(self, price, iborStartDate, iborIndex):
        helper = FuturesRateHelper(price, iborStartDate, iborIndex)
        self.helpers.append(helper)
    
    # 4th constructor: SwapRateHelper(Rate rate, const Period &tenor, const Calendar &calendar, 
    # Frequency fixedFrequency, BusinessDayConvention fixedConvention, const DayCounter &fixedDayCount, 
    # const shared_ptr<IborIndex> &iborIndex)
    def AddSwap(self, rate, periodLength, fixedCalendar, fixedFrequency, fixedConvention, fixedDayCount, floatIndex):
        helper = SwapRateHelper(rate, periodLength, fixedCalendar, fixedFrequency, 
            fixedConvention, fixedDayCount, floatIndex)
        self.helpers.append(helper)
    
    # PiecewiseYieldCurve <ZeroYield, Linear>
    def GetCurveHandle(self):  
        yieldTermStructure = PiecewiseLinearZero(self.settlementDate, self.helpers, self.dayCounter)
        return RelinkableYieldTermStructureHandle(yieldTermStructure)


# general parameters    
tradeDate = Date(4, February, 2008)
calendar = TARGET()
dayCounter = Actual360()
convention = ModifiedFollowing
settlementDate = calendar.advance(tradeDate, Period(2, Days), convention)  
swapIndex = USDLibor(Period(3, Months))
frequency = Annual

# create curve builder object
Settings.instance().evaluationDate = tradeDate
builder = PiecewiseCurveBuilder(settlementDate, dayCounter)

# cash deposit
depos = []
depos.append((0.032175, USDLibor(Period(1, Weeks))))
depos.append((0.0318125, USDLibor(Period(1, Months))))
depos.append((0.03145, USDLibor(Period(3, Months))))
[builder.AddDeposit(d[0], d[1]) for d in depos]

# futures
futures = []
futures.append((97.41, IMM.nextDate(settlementDate + Period(3, Months)), swapIndex))
futures.append((97.52, IMM.nextDate(settlementDate + Period(6, Months)), swapIndex))
futures.append((97.495, IMM.nextDate(settlementDate + Period(9, Months)), swapIndex))
futures.append((97.395, IMM.nextDate(settlementDate + Period(12, Months)), swapIndex))
[builder.AddFuture(f[0], f[1], f[2]) for f in futures]

# swaps
swaps = []
swaps.append((0.02795, Period(2, Years), calendar, frequency, convention, dayCounter, swapIndex))
swaps.append((0.03035, Period(3, Years), calendar, frequency, convention, dayCounter, swapIndex))
swaps.append((0.03275, Period(4, Years), calendar, frequency, convention, dayCounter, swapIndex))
swaps.append((0.03505, Period(5, Years), calendar, frequency, convention, dayCounter, swapIndex))
swaps.append((0.03715, Period(6, Years), calendar, frequency, convention, dayCounter, swapIndex))
swaps.append((0.03885, Period(7, Years), calendar, frequency, convention, dayCounter, swapIndex))
swaps.append((0.04025, Period(8, Years), calendar, frequency, convention, dayCounter, swapIndex))
swaps.append((0.04155, Period(9, Years), calendar, frequency, convention, dayCounter, swapIndex))
swaps.append((0.04265, Period(10, Years), calendar, frequency, convention, dayCounter, swapIndex))
swaps.append((0.04435, Period(12, Years), calendar, frequency, convention, dayCounter, swapIndex))
[builder.AddSwap(s[0], s[1], s[2], s[3], s[4], s[5], s[6]) for s in swaps]

# get relinkable curve handle from builder
curve = builder.GetCurveHandle()
curve.enableExtrapolation()

# create and print array of discount factors for every 3M up to 15Y
times = Numpy.linspace(0.0, 15.0, 61)
dfs = Numpy.array([curve.discount(t) for t in times])
print(dfs)

Tuesday, June 5, 2018

QuantLib : Dual-Curve Bootstrapping and Swap Valuation

Implementing OIS curve bootstrapping in QuantLib was presented in my previous post. Story will continue. This post will present, how to implement dual-curve bootstrapping scheme and corresponding valuation for a simple single-currency vanilla swap transaction (collateralization is in transaction currency). Detailed information on how to implement this scheme has been acquired mainly from two sources: StackExchange post on this topic and QuantLib Python Cookbook, which is worth of checking out. The great story does not lose its value, even told in different languages.

The program


In the beginning of this program, two relinkable handles for containing yield term structures are created, one for discounting curve and another for projection curve. The real beauty of these creatures comes from the fact, that we can use these handles as curve "placeholders" within our program and later link (or re-link) these handles with any yield term structure implementation.

After this, Eonia OIS curve will be bootstrapped and discounting curve handle is linked to bootstrapped Eonia curve. Similar bootstrapping procedure will be performed for creating Euribor curve, but with a special twist. In order to implement dual-curve bootstrapping algorithm in QuantLib, discounting curve handle must be delivered as one argument for all swap rate helpers, along with dummy quote handle and dummy zero period. Then, projection curve handle is linked to bootstrapped Euribor curve. After this, our projection curve should return "OIS-adjusted" Euribor forward rates for creating floating leg cash flows.

Finally, seasoned vanilla swap transaction will be created and valued. Effectively, cash flow discounting will be performed by using discounting curve handle (in pricing engine), whereas cash flow projection will be performed by using projection curve handle (in index object, in swap transaction). Just for a final note, I carefully checked all constructors for deposit and FRA rate helpers, but did not find any possibility to deliver discount curve handle for these rate helpers.

Thanks for reading this blog.
-Mike

#include <iostream>
#include <ql\quantlib.hpp>
#include <map>
using namespace QuantLib;

int main() {

 try {

  // create common data 
  Date today(7, Jul, 2017);
  DayCounter dayCounter = Actual360();
  Calendar calendar = TARGET();
  Date settlementDate = calendar.advance(today, Period(2, Days));
  Natural settlementDays = settlementDate - today;
  Settings::instance().evaluationDate() = today;

  // create re-linkable handles for discounting and projection curves
  RelinkableHandle<YieldTermStructure> discountCurve;
  RelinkableHandle<YieldTermStructure> projectionCurve;
  // create container for all rate helpers
  std::vector<boost::shared_ptr<RateHelper>> rateHelpers;

  // create required indices
  auto eoniaIndex = boost::make_shared<Eonia>();
  // forward euribor fixings are requested from dual-curve-bootstrapped projection curve
  auto euriborIndex = boost::make_shared<Euribor6M>(projectionCurve);


  // eonia curve
  // create first cash instrument for eonia curve using deposit rate helper
  rateHelpers.push_back(boost::make_shared<DepositRateHelper>
   (Handle<Quote>(boost::make_shared<SimpleQuote>(-0.0036)), 
   Period(1, Days), eoniaIndex->fixingDays(),
   eoniaIndex->fixingCalendar(), eoniaIndex->businessDayConvention(), 
   eoniaIndex->endOfMonth(), eoniaIndex->dayCounter()));

  // create source data for eonia swaps (period, rate)
  std::map<Period, Real> eoniaSwapData;
  eoniaSwapData.insert(std::make_pair(Period(6, Months), -0.00353));
  eoniaSwapData.insert(std::make_pair(Period(1, Years), -0.00331));
  eoniaSwapData.insert(std::make_pair(Period(2, Years), -0.00248));
  eoniaSwapData.insert(std::make_pair(Period(3, Years), -0.00138));
  eoniaSwapData.insert(std::make_pair(Period(4, Years), -0.0001245));
  eoniaSwapData.insert(std::make_pair(Period(5, Years), 0.0011945));
  eoniaSwapData.insert(std::make_pair(Period(7, Years), 0.00387));
  eoniaSwapData.insert(std::make_pair(Period(10, Years), 0.007634));

  // create other instruments for eonia curve using ois rate helper
  std::for_each(eoniaSwapData.begin(), eoniaSwapData.end(),
   [settlementDays, &rateHelpers, &eoniaIndex](std::pair<Period, Real> p) -> void 
   { rateHelpers.push_back(boost::make_shared<OISRateHelper>(settlementDays,
   p.first, Handle<Quote>(boost::make_shared<SimpleQuote>(p.second)), eoniaIndex)); });
  
  // create eonia curve
  auto eoniaCurve = boost::make_shared<PiecewiseYieldCurve<Discount, LogLinear>>
   (0, eoniaIndex->fixingCalendar(), rateHelpers, eoniaIndex->dayCounter());  
  eoniaCurve->enableExtrapolation(true);
  // link discount curve to eonia curve
  discountCurve.linkTo(eoniaCurve);

  // clear rate helpers container
  rateHelpers.clear();


  // euribor curve
  // cash part
  rateHelpers.push_back(boost::make_shared<DepositRateHelper>(Handle<Quote>
   (boost::make_shared<SimpleQuote>(-0.00273)), Period(6, Months),
   settlementDays, calendar, euriborIndex->businessDayConvention(), 
   euriborIndex->endOfMonth(), euriborIndex->dayCounter()));

  // fra part
  rateHelpers.push_back(boost::make_shared<FraRateHelper>(Handle<Quote>
   (boost::make_shared<SimpleQuote>(-0.00194)), Period(6, Months), euriborIndex));

  // swap part
  rateHelpers.push_back(boost::make_shared<SwapRateHelper>(Handle<Quote>
   (boost::make_shared<SimpleQuote>(-0.00119)), Period(2, Years),
   calendar, Annual, ModifiedFollowing, Actual360(), euriborIndex,
   // in order to use dual-curve bootstrapping, discount curve handle must
   // be given as one argument for swap rate helper (along with dummy handle
   // for quote and dummy zero period for technical reasons)
   Handle<Quote>(), Period(0, Days), discountCurve));

  rateHelpers.push_back(boost::make_shared<SwapRateHelper>(Handle<Quote>
   (boost::make_shared<SimpleQuote>(0.00019)), Period(3, Years),
   calendar, Annual, ModifiedFollowing, Actual360(), euriborIndex,
   Handle<Quote>(), Period(0, Days), discountCurve));

  rateHelpers.push_back(boost::make_shared<SwapRateHelper>(Handle<Quote>
   (boost::make_shared<SimpleQuote>(0.00167)), Period(4, Years),
   calendar, Annual, ModifiedFollowing, Actual360(), euriborIndex,
   Handle<Quote>(), Period(0, Days), discountCurve));

  rateHelpers.push_back(boost::make_shared<SwapRateHelper>(Handle<Quote>
   (boost::make_shared<SimpleQuote>(0.00317)), Period(5, Years),
   calendar, Annual, ModifiedFollowing, Actual360(), euriborIndex,
   Handle<Quote>(), Period(0, Days), discountCurve));

  rateHelpers.push_back(boost::make_shared<SwapRateHelper>(Handle<Quote>
   (boost::make_shared<SimpleQuote>(0.00598)), Period(7, Years),
   calendar, Annual, ModifiedFollowing, Actual360(), euriborIndex,
   Handle<Quote>(), Period(0, Days), discountCurve));

  rateHelpers.push_back(boost::make_shared<SwapRateHelper>(Handle<Quote>
   (boost::make_shared<SimpleQuote>(0.00966)), Period(10, Years),
   calendar, Annual, ModifiedFollowing, Actual360(), euriborIndex,
   Handle<Quote>(), Period(0, Days), discountCurve));
  
  // create euribor curve
  auto euriborCurve = boost::make_shared<PiecewiseYieldCurve<Discount, LogLinear>>
   (0, euriborIndex->fixingCalendar(), rateHelpers, euriborIndex->dayCounter());
  euriborCurve->enableExtrapolation();
  // link projection curve to euribor curve
  projectionCurve.linkTo(euriborCurve);


  // create seasoned vanilla swap
  Date pastSettlementDate(5, Jun, 2015);

  Schedule fixedSchedule(pastSettlementDate, pastSettlementDate + Period(5, Years),
   Period(Annual), calendar, Unadjusted, Unadjusted,
   DateGeneration::Backward, false);

  Schedule floatSchedule(pastSettlementDate, pastSettlementDate + Period(5, Years),
   Period(Semiannual), calendar, Unadjusted, Unadjusted,
   DateGeneration::Backward, false);

  VanillaSwap swap(VanillaSwap::Payer, 10000000.0, fixedSchedule, 0.0285, 
   dayCounter, floatSchedule, euriborIndex, 0.0, dayCounter);

  // add required 6M euribor index fixing for floating leg valuation
  euriborIndex->addFixing(Date(1, Jun, 2017), -0.0025);

  // create pricing engine, request swap pv
  auto pricer = boost::make_shared<DiscountingSwapEngine>(discountCurve);
  swap.setPricingEngine(pricer);
  std::cout << swap.NPV() << std::endl;

 }
 catch (std::exception& e) {
  std::cout << e.what() << std::endl;
 }
 return 0;
}


Wednesday, September 6, 2017

QuantLib : Hull-White one-factor model calibration

Sometimes during the last year I published one post on simulating Hull-White interest rate paths using Quantlib. My conclusion was, that with all the tools provided by this wonderful library, this task should be (relatively) easy thing to do. However, as we know the devil is always in the details - in this case, in the actual process parameters (mean reversion, sigma). Before processing any simulation, we have to get those parameters from somewhere. In this post, we use Quantlib tools for calibrating our model parameters to swaption prices existing in the market.

There are so many variations of this model out there (depending on time-dependencies of different parameters), but the following is the model we are going to use in this example.




Parameters alpha and sigma are constants and theta is time-dependent variable (usually calibrated to current yield curve). Swaption surface is presented in the picture below. Within the table below, time to swaption maturity has been defined in vertical axis, while tenor for underlying swap contract has been defined in horizontal axis. Co-terminal swaptions (used in calibration process) have been specifically marked with yellow colour.























Calibration results


Test cases for three different calibration schemes are included in the example program. More specifically, we :
  1. calibrate the both parameters
  2. calibrate sigma parameter and freeze reversion parameter to 0.05
  3. calibrate reversion parameter and freeze sigma parameter to 0.01
With the given data, we get the following results for these calibration schemes.









The program


As preparatory task, builder class for constructing yield curve should be implemented into a new project from here. After this task, the following header file (ModelCalibrator.h) and tester file (Tester.cpp) should be added to this new project.

First, piecewise yield curve (USD swap curve) and swaption volatilities (co-terminal swaptions) are created by using two free functions in tester. All the data has been hard-coded inside these functions. Needless to say, in any production-level program, this data feed should come from somewhere else. After required market data and ModelCalibrator object has been created, calibration helpers for diagonal swaptions are going to be created and added to ModelCalibrator. Finally, test cases for different calibration schemes are processed.

// ModelCalibrator.h
#pragma once
#include <ql\quantlib.hpp>
#include <algorithm>
//
namespace MJModelCalibratorNamespace
{
 using namespace QuantLib;
 //
 template <typename MODEL, typename OPTIMIZER = LevenbergMarquardt>
 class ModelCalibrator
 {
 public:
  // default implementation (given values) for end criteria class
  ModelCalibrator(const EndCriteria& endCriteria = EndCriteria(1000, 500, 0.0000001, 0.0000001, 0.0000001))
   : endCriteria(endCriteria)
  { }
  void AddCalibrationHelper(boost::shared_ptr<CalibrationHelper>& helper)
  {
   // add any type of calibration helper
   helpers.push_back(helper);
  }
  void Calibrate(boost::shared_ptr<MODEL>& model,
   const boost::shared_ptr<PricingEngine>& pricingEngine,
   const Handle<YieldTermStructure>& curve,
   const std::vector<bool> fixedParameters = std::vector<bool>())
  {
   // assign pricing engine to all calibration helpers
   std::for_each(helpers.begin(), helpers.end(),
    [&pricingEngine](boost::shared_ptr<CalibrationHelper>& helper)
   { helper->setPricingEngine(pricingEngine); });
   //
   // create optimization model for calibrating requested model
   OPTIMIZER solver;
   //
   if (fixedParameters.empty())
   {
    // calibrate all involved parameters
    model->calibrate(helpers, solver, this->endCriteria);
   }
   else
   {
    // calibrate all involved non-fixed parameters
    // hard-coded : vector for weights and constraint type
    model->calibrate(helpers, solver, this->endCriteria, NoConstraint(), std::vector<Real>(), fixedParameters);
   }
  }
 private:
  EndCriteria endCriteria;
  std::vector<boost::shared_ptr<CalibrationHelper>> helpers;
 };
}
//
//
//
//
// Tester.cpp
#include "PiecewiseCurveBuilder.cpp"
#include "ModelCalibrator.h"
#include <iostream>
//
using namespace MJPiecewiseCurveBuilderNamespace;
namespace MJ_Calibrator = MJModelCalibratorNamespace;
//
// declarations for two free functions used to construct required market data
void CreateCoTerminalSwaptions(std::vector<Volatility>& diagonal);
void CreateYieldCurve(RelinkableHandle<YieldTermStructure>& curve, 
 const Date& settlementDate, const Calendar& calendar);
//
//
//
int main()
{
 try
 {
  // dates
  Date tradeDate(4, September, 2017);
  Settings::instance().evaluationDate() = tradeDate;
  Calendar calendar = TARGET();
  Date settlementDate = calendar.advance(tradeDate, Period(2, Days), ModifiedFollowing);
  //
  // market data : create piecewise yield curve
  RelinkableHandle<YieldTermStructure> curve;
  CreateYieldCurve(curve, settlementDate, calendar);
  //
  // market data : create co-terminal swaption volatilities
  std::vector<Volatility> diagonal;
  CreateCoTerminalSwaptions(diagonal);
  //
  // create model calibrator
  MJ_Calibrator::ModelCalibrator<HullWhite> modelCalibrator;
  //
  // create and add calibration helpers to model calibrator
  boost::shared_ptr<IborIndex> floatingIndex(new USDLibor(Period(3, Months), curve));
  for (unsigned int i = 0; i != diagonal.size(); ++i)
  {
   int timeToMaturity = i + 1;
   int underlyingTenor = diagonal.size() - i;
   //
   // using 1st constructor for swaption helper class
   modelCalibrator.AddCalibrationHelper(boost::shared_ptr<CalibrationHelper>(new SwaptionHelper(
     Period(timeToMaturity, Years), // time to swaption maturity
     Period(underlyingTenor, Years), // tenor of the underlying swap
     Handle<Quote>(boost::shared_ptr<Quote>(new SimpleQuote(diagonal[i]))), // swaption volatility
     floatingIndex, // underlying floating index
     Period(1, Years), // tenor for underlying fixed leg
     Actual360(), // day counter for underlying fixed leg
     floatingIndex->dayCounter(), // day counter for underlying floating leg
     curve))); // term structure
  }
  //
  // create model and pricing engine, calibrate model and print calibrated parameters
  // case 1 : calibrate all involved parameters (HW1F : reversion, sigma)
  boost::shared_ptr<HullWhite> model(new HullWhite(curve));
  boost::shared_ptr<PricingEngine> jamshidian(new JamshidianSwaptionEngine(model));
  modelCalibrator.Calibrate(model, jamshidian, curve);
  std::cout << "calibrated reversion = " << model->params()[0] << std::endl;
  std::cout << "calibrated sigma = " << model->params()[1] << std::endl;
  std::cout << std::endl;
  //
  // case 2 : calibrate sigma and fix reversion to famous 0.05
  model = boost::shared_ptr<HullWhite>(new HullWhite(curve, 0.05, 0.0001));
  jamshidian = boost::shared_ptr<PricingEngine>(new JamshidianSwaptionEngine(model));
  std::vector<bool> fixedReversion = { true, false };
  modelCalibrator.Calibrate(model, jamshidian, curve, fixedReversion);
  std::cout << "fixed reversion = " << model->params()[0] << std::endl;
  std::cout << "calibrated sigma = " << model->params()[1] << std::endl;
  std::cout << std::endl;
  //
  // case 3 : calibrate reversion and fix sigma to 0.01
  model = boost::shared_ptr<HullWhite>(new HullWhite(curve, 0.05, 0.01));
  jamshidian = boost::shared_ptr<PricingEngine>(new JamshidianSwaptionEngine(model));
  std::vector<bool> fixedSigma = { false, true };
  modelCalibrator.Calibrate(model, jamshidian, curve, fixedSigma);
  std::cout << "calibrated reversion = " << model->params()[0] << std::endl;
  std::cout << "fixed sigma = " << model->params()[1] << std::endl;
 }
 catch (std::exception& e)
 {
  std::cout << e.what() << std::endl;
 }
 return 0;
}
//
void CreateCoTerminalSwaptions(std::vector<Volatility>& diagonal)
{
 // hard-coded data
 // create co-terminal swaptions 
 diagonal.push_back(0.3133); // 1x10
 diagonal.push_back(0.3209); // 2x9
 diagonal.push_back(0.3326); // 3x8
 diagonal.push_back(0.331); // 4x7
 diagonal.push_back(0.3281); // 5x6
 diagonal.push_back(0.318); // 6x5
 diagonal.push_back(0.3168); // 7x4
 diagonal.push_back(0.3053); // 8x3
 diagonal.push_back(0.2992); // 9x2
 diagonal.push_back(0.3073); // 10x1
}
//
void CreateYieldCurve(RelinkableHandle<YieldTermStructure>& curve,
 const Date& settlementDate, const Calendar& calendar) 
{
 // hard-coded data
 // create piecewise yield curve by using builder class
 DayCounter curveDaycounter = Actual360();
 PiecewiseCurveBuilder<ZeroYield, Linear> builder;
 //
 // cash rates
 pQuote q_1W(new SimpleQuote(0.012832));
 pIndex i_1W(new USDLibor(Period(1, Weeks)));
 builder.AddDeposit(q_1W, i_1W);
 //
 pQuote q_1M(new SimpleQuote(0.012907));
 pIndex i_1M(new USDLibor(Period(1, Months)));
 builder.AddDeposit(q_1M, i_1M);
 //
 pQuote q_3M(new SimpleQuote(0.0131611));
 pIndex i_3M(new USDLibor(Period(3, Months))); 
 builder.AddDeposit(q_3M, i_3M);
 //
 // futures
 Date IMMDate;
 pQuote q_DEC17(new SimpleQuote(98.5825));
 IMMDate = IMM::nextDate(settlementDate + Period(3, Months));
 builder.AddFuture(q_DEC17, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_MAR18(new SimpleQuote(98.5425));
 IMMDate = IMM::nextDate(settlementDate + Period(6, Months));
 builder.AddFuture(q_MAR18, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_JUN18(new SimpleQuote(98.4975));
 IMMDate = IMM::nextDate(settlementDate + Period(9, Months));
 builder.AddFuture(q_JUN18, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_SEP18(new SimpleQuote(98.4475));
 IMMDate = IMM::nextDate(settlementDate + Period(12, Months));
 builder.AddFuture(q_SEP18, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_DEC18(new SimpleQuote(98.375));
 IMMDate = IMM::nextDate(settlementDate + Period(15, Months));
 builder.AddFuture(q_DEC18, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_MAR19(new SimpleQuote(98.3425));
 IMMDate = IMM::nextDate(settlementDate + Period(18, Months));
 builder.AddFuture(q_MAR19, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_JUN19(new SimpleQuote(98.3025));
 IMMDate = IMM::nextDate(settlementDate + Period(21, Months));
 builder.AddFuture(q_JUN19, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_SEP19(new SimpleQuote(98.2675));
 IMMDate = IMM::nextDate(settlementDate + Period(24, Months));
 builder.AddFuture(q_SEP19, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_DEC19(new SimpleQuote(98.2125));
 IMMDate = IMM::nextDate(settlementDate + Period(27, Months));
 builder.AddFuture(q_DEC19, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_MAR20(new SimpleQuote(98.1775));
 IMMDate = IMM::nextDate(settlementDate + Period(30, Months));
 builder.AddFuture(q_MAR20, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 pQuote q_JUN20(new SimpleQuote(98.1425));
 IMMDate = IMM::nextDate(settlementDate + Period(33, Months));
 builder.AddFuture(q_JUN20, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
 //
 // swaps
 pIndex swapFloatIndex(new USDLibor(Period(3, Months))); 
 pQuote q_4Y(new SimpleQuote(0.01706));
 builder.AddSwap(q_4Y, Period(4, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_5Y(new SimpleQuote(0.0176325));
 builder.AddSwap(q_5Y, Period(5, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_6Y(new SimpleQuote(0.01874));
 builder.AddSwap(q_6Y, Period(6, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_7Y(new SimpleQuote(0.0190935));
 builder.AddSwap(q_7Y, Period(7, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_8Y(new SimpleQuote(0.02011));
 builder.AddSwap(q_8Y, Period(8, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_9Y(new SimpleQuote(0.02066));
 builder.AddSwap(q_9Y, Period(9, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_10Y(new SimpleQuote(0.020831));
 builder.AddSwap(q_10Y, Period(10, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_11Y(new SimpleQuote(0.02162));
 builder.AddSwap(q_11Y, Period(11, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_12Y(new SimpleQuote(0.0217435));
 builder.AddSwap(q_12Y, Period(12, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_15Y(new SimpleQuote(0.022659));
 builder.AddSwap(q_15Y, Period(15, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_20Y(new SimpleQuote(0.0238125));
 builder.AddSwap(q_20Y, Period(20, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_25Y(new SimpleQuote(0.0239385));
 builder.AddSwap(q_25Y, Period(25, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 pQuote q_30Y(new SimpleQuote(0.02435));
 builder.AddSwap(q_30Y, Period(30, Years), calendar, Annual, ModifiedFollowing, Actual360(), swapFloatIndex);
 //
 curve = builder.GetCurveHandle(settlementDate, curveDaycounter);
}


I have noticed, that there are some rule-of-thumbs, but the actual calibration for any of these models is not so straightforward as one may think. There are some subtle issues on market data quality and products under pricing, which are leaving us with relatively high degree of freedom. Further step towards the calibration red pill can be taken by checking out this excellent research paper, written by the guys from Mizuho Securities.

Finally, as always, thanks a lot again for reading this blog.
-Mike

Sunday, September 3, 2017

QuantLib : another implementation for piecewise yield curve builder class


Last year I published one possible implementation using Quantlib library for constructing piecewise yield curves. Within this second implementation, I have done a couple of changes in order to increase configurability. For allowing more flexible curve construction algorithm, Traits and Interpolations are now defined as template parameters. Previously, these were hard-coded inside the class. Secondly, all types of quotes for class methods are now wrapped inside shared pointers. Previously, there was an option to give quotes as rates. After some reconsideration, I have decided to give up this option completely. Finally, I have added a class method for allowing futures prices to be used in curve construction process.


Source data and results


The following source data from the book by Richard Flavell has been used for validation.



















Resulting zero-yield curve and discount factors are presented in the graph below. For this data used, the absolute maximum difference between Flavell-constructed and Quantlib-constructed zero yield curves is around one basis point.

















The program


// PiecewiseCurveBuilder.h
#pragma once
#include <ql/quantlib.hpp>
//
namespace MJPiecewiseCurveBuilderNamespace
{
 using namespace QuantLib;
 //
 // type alias definitions
 using pQuote = boost::shared_ptr<Quote>;
 using pIndex = boost::shared_ptr<IborIndex>;
 using pHelper = boost::shared_ptr<RateHelper>;
 //
 // T = traits, I = interpolation
 template<typename T, typename I>
 class PiecewiseCurveBuilder
 {
 public:
  PiecewiseCurveBuilder();
  void AddDeposit(const pQuote& quote, const pIndex& index);
  void AddFRA(const pQuote& quote, const Period& periodLengthToStart, const pIndex& index);
  void AddSwap(const pQuote& quote, const Period& periodLength, const Calendar& fixedCalendar, Frequency fixedFrequency,
   BusinessDayConvention fixedConvention, const DayCounter& fixedDayCount, const pIndex& floatIndex);
  //
  void AddFuture(const pQuote& quote, const Date& IMMDate, int lengthInMonths, const Calendar& calendar,
   BusinessDayConvention convention, const DayCounter& dayCounter, bool endOfMonth = true);
  //
  RelinkableHandle<YieldTermStructure> GetCurveHandle(const Date& settlementDate, const DayCounter& dayCounter);
 private:
  std::vector<pHelper> rateHelpers;

 };
}
//
//
//
//
// PiecewiseCurveBuilder.cpp
#pragma once
#include "PiecewiseCurveBuilder.h"
//
namespace MJPiecewiseCurveBuilderNamespace
{
 template<typename T, typename I>
 PiecewiseCurveBuilder<T, I>::PiecewiseCurveBuilder() { }
 //
 template<typename T, typename I>
 void PiecewiseCurveBuilder<T, I>::AddDeposit(const pQuote& quote, const pIndex& index)
 {
  pHelper rateHelper(new DepositRateHelper(Handle<Quote>(quote), index));
  rateHelpers.push_back(rateHelper);
 }
 //
 template<typename T, typename I>
 void PiecewiseCurveBuilder<T, I>::AddFRA(const pQuote& quote, const Period& periodLengthToStart, const pIndex& index)
 {
  pHelper rateHelper(new FraRateHelper(Handle<Quote>(quote),
   periodLengthToStart, pIndex(index)));
  rateHelpers.push_back(rateHelper);
 }
 //
 template<typename T, typename I>
 void PiecewiseCurveBuilder<T, I>::AddSwap(const pQuote& quote, const Period& periodLength, const Calendar& fixedCalendar,
  Frequency fixedFrequency, BusinessDayConvention fixedConvention, const DayCounter& fixedDayCount,
  const pIndex& floatIndex)
 {
  pHelper rateHelper(new SwapRateHelper(Handle<Quote>(quote), periodLength, fixedCalendar, fixedFrequency,
   fixedConvention, fixedDayCount, floatIndex));
  rateHelpers.push_back(rateHelper);
 }
 //
 template<typename T, typename I>
 void PiecewiseCurveBuilder<T, I>::AddFuture(const pQuote& quote, const Date& IMMDate, int lengthInMonths, const Calendar& calendar,
  BusinessDayConvention convention, const DayCounter& dayCounter, bool endOfMonth)
 {
  pHelper rateHelper(new FuturesRateHelper(Handle<Quote>(quote), IMMDate, lengthInMonths, calendar, convention, endOfMonth, dayCounter));
  rateHelpers.push_back(rateHelper);
 }
 //
 template<typename T, typename I>
 RelinkableHandle<YieldTermStructure> PiecewiseCurveBuilder<T, I>::GetCurveHandle(const Date& settlementDate, const DayCounter& dayCounter)
 {
  // T = traits, I = interpolation
  boost::shared_ptr<YieldTermStructure> yieldTermStructure(new PiecewiseYieldCurve<T, I>(settlementDate, rateHelpers, dayCounter));
  return RelinkableHandle<YieldTermStructure>(yieldTermStructure);
 }
}
//
//
//
//
// Tester.cpp
#include "PiecewiseCurveBuilder.cpp"
#include <iostream>
using namespace MJPiecewiseCurveBuilderNamespace;
//
int main()
{
 try
 {
  Date tradeDate(4, February, 2008);
  Settings::instance().evaluationDate() = tradeDate;
  Calendar calendar = TARGET();
  Date settlementDate = calendar.advance(tradeDate, Period(2, Days), ModifiedFollowing);
  DayCounter curveDaycounter = Actual360();
  PiecewiseCurveBuilder<ZeroYield, Linear> builder;
  //
  //
  // cash part of the curve
  pQuote q_1W(new SimpleQuote(0.032175));
  pIndex i_1W(new USDLibor(Period(1, Weeks)));
  builder.AddDeposit(q_1W, i_1W);
  //
  pQuote q_1M(new SimpleQuote(0.0318125));
  pIndex i_1M(new USDLibor(Period(1, Months)));
  builder.AddDeposit(q_1M, i_1M);
  //
  pQuote q_3M(new SimpleQuote(0.03145));
  pIndex i_3M(new USDLibor(Period(3, Months)));
  builder.AddDeposit(q_3M, i_3M);
  //
  //
  // futures part of the curve
  Date IMMDate;
  pQuote q_JUN08(new SimpleQuote(97.41));
  IMMDate = IMM::nextDate(settlementDate + Period(4, Months));
  builder.AddFuture(q_JUN08, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
  //
  pQuote q_SEP08(new SimpleQuote(97.52));
  IMMDate = IMM::nextDate(settlementDate + Period(7, Months));
  builder.AddFuture(q_SEP08, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
  //
  pQuote q_DEC08(new SimpleQuote(97.495));
  IMMDate = IMM::nextDate(settlementDate + Period(10, Months));
  builder.AddFuture(q_DEC08, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
  //
  pQuote q_MAR09(new SimpleQuote(97.395));
  IMMDate = IMM::nextDate(settlementDate + Period(13, Months));
  builder.AddFuture(q_MAR09, IMMDate, 3, calendar, ModifiedFollowing, Actual360());
  //
  //
  // swap part of the curve
  pIndex swapFloatIndex(new USDLibor(Period(3, Months)));
  pQuote q_2Y(new SimpleQuote(0.02795));
  builder.AddSwap(q_2Y, Period(2, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_3Y(new SimpleQuote(0.03035));
  builder.AddSwap(q_3Y, Period(3, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_4Y(new SimpleQuote(0.03275));
  builder.AddSwap(q_4Y, Period(4, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_5Y(new SimpleQuote(0.03505));
  builder.AddSwap(q_5Y, Period(5, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_6Y(new SimpleQuote(0.03715));
  builder.AddSwap(q_6Y, Period(6, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_7Y(new SimpleQuote(0.03885));
  builder.AddSwap(q_7Y, Period(7, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_8Y(new SimpleQuote(0.04025));
  builder.AddSwap(q_8Y, Period(8, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_9Y(new SimpleQuote(0.04155));
  builder.AddSwap(q_9Y, Period(9, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_10Y(new SimpleQuote(0.04265));
  builder.AddSwap(q_10Y, Period(10, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  pQuote q_12Y(new SimpleQuote(0.04435));
  builder.AddSwap(q_12Y, Period(12, Years), calendar, Annual,
   ModifiedFollowing, Actual360(), swapFloatIndex);
  //
  //
  // get curve handle and print out discount factors
  RelinkableHandle<YieldTermStructure> curve = builder.GetCurveHandle(settlementDate, curveDaycounter);
  std::cout << curve->discount(Date(11, February, 2008)) << std::endl;
  std::cout << curve->discount(Date(4, March, 2008)) << std::endl;
  std::cout << curve->discount(Date(4, May, 2008)) << std::endl;
  std::cout << curve->discount(Date(6, August, 2008)) << std::endl;
  std::cout << curve->discount(Date(6, November, 2008)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2009)) << std::endl;
  std::cout << curve->discount(Date(8, February, 2010)) << std::endl;
  std::cout << curve->discount(Date(7, February, 2011)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2012)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2013)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2014)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2015)) << std::endl;
  std::cout << curve->discount(Date(8, February, 2016)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2017)) << std::endl;
  std::cout << curve->discount(Date(6, February, 2018)) << std::endl;
 }
 catch (std::exception& e)
 {
  std::cout << e.what() << std::endl;
 }
 return 0;
}


Data updating


Since all rates have been wrapped inside quotes (which have been wrapped inside handles), those can be accessed only by using dynamic pointer casting. Below is an example for updating 3M cash quote.

boost::dynamic_pointer_cast<SimpleQuote>(q_3M)->setValue(0.03395);

After this quote updating shown above, a new requested value (zero rate, forward rate or discount factor) from constructed curve object will be re-calculated by using updated quote.

For those readers who are completely unfamiliar with this stuff presented, there are three extremely well-written slides available in Quantlib documentation page, written by Dimitri Reiswich. These slides are offering excellent way to get very practical hands-on overview on QuantLib library. Also, Luigi Ballabio has finally been finishing his book on QuantLib implementation, which can be purchased from Leanpub. This book is offering deep diving experience into the abyss of Quantlib architechture.

Finally, as usual, thanks for spending your precious time here and reading this blog.
-Mike

Saturday, January 23, 2016

QuantLib : Simulating HW1F paths using PathGenerator

Monte Carlo is bread and butter for so many purposes. Calculating payoffs for complex path-dependent products or simulating future exposures for calculating CVA are two excellent examples. The big question is always how to do this efficiently. Designing, implementing and setting up any non-trivial in-house tool to do the job is everything but not a simple afternoon exercise with a cup of coffee and Excel. Fortunately, QuantLib is offering pretty impressive tools for simulating stochastic paths. This time, I wanted to share the results of my woodshedding with QL PathGenerator class. 


Parallel lives


In order to really appreciate the tools offered by QL, let us see the results first. Some simulated paths using Hull-White One-Factor model are shown in the picture below.






























If one really want to start from the scratch, there are a lot of things to do in order to produce these paths on a flexible manner and handling all the complexities of the task at the same time. Thanks for QL, those days are finally over.


Legoland

 

Setting up desired Stochastic Process and Gaussian Sequence Generator are two main components needed in order to get this thing up and running.

Along with required process parameters (reversion speed and rate volatility), HullWhiteProcess needs Handle to YieldTermStructure object, such as PiecewiseYieldCurve, as an input.

 // create Hull-White one-factor stochastic process
 Real reversionSpeed = 0.75;
 Real rateVolatility = 0.015;
 boost::shared_ptr<StochasticProcess1D> HW1F(
  new HullWhiteProcess(curveHandle, reversionSpeed, rateVolatility));

For this example, I have used my own PiecewiseCurveBuilder template class in order to make curve assembling a bit more easier. It should be noted, that the menu of one-dimensional stochastic processes in QL is covering pretty much all standard processes one needs for different asset classes.

Gaussian Sequence Generator (GSG) is assembled by using the following three classes : uniform random generator (MersenneTwisterUniformRng),  distributional transformer (CLGaussianRng) and RandomSequenceGenerator.

 // type definition for complex declaration
 typedef RandomSequenceGenerator<CLGaussianRng<MersenneTwisterUniformRng>> GSG;
 //
 // create mersenne twister uniform random generator
 unsigned long seed = 28749;
 MersenneTwisterUniformRng generator(seed);
 //
 // create gaussian generator by using central limit transformation method
 CLGaussianRng<MersenneTwisterUniformRng> gaussianGenerator(generator);
 //
 // define maturity, number of steps per path and create gaussian sequence generator
 Time maturity = 5.0;
 Size nSteps = 1250;
 GSG gaussianSequenceGenerator(nSteps, gaussianGenerator);
 //
 // create path generator using Hull-White process and gaussian sequence generator
 PathGenerator<GSG> pathGenerator(HW1F, maturity, nSteps, gaussianSequenceGenerator, false);

Finally, PathGenerator object is created by feeding desired process and generator objects in constructor method, along with the other required parameters (maturity, number of steps). After this, PathGenerator object is ready for producing stochastic paths for its client.

The program

 

Example program will first create relinkable handle to PiecewiseYieldCurve object. Remember to include required files into your project from here. After this, the program creates HW1F process object and Gaussian Sequence Generator object, which are feeded into PathGenerator object. Finally, the program creates 20 stochastic paths, which are saved into Matrix object and ultimately being printed into text file for further analysis (Excel chart).

#include "PiecewiseCurveBuilder.cpp"
#include <fstream>
#include <string>
//
// type definition for complex declaration
typedef RandomSequenceGenerator<CLGaussianRng<MersenneTwisterUniformRng>> GSG;
//
// function prototypes
RelinkableHandle<YieldTermStructure> CreateCurveHandle(Date settlementDate);
void PrintMatrix(const Matrix& matrix, std::string filePathName);
//
int main()
{
 // request handle for piecewise USD Libor curve
 Date tradeDate(22, January, 2016);
 Settings::instance().evaluationDate() = tradeDate;
 Date settlementDate = UnitedKingdom().advance(tradeDate, 2, Days);
 RelinkableHandle<YieldTermStructure> curveHandle = CreateCurveHandle(settlementDate);
 //
 // create Hull-White one-factor stochastic process
 Real reversionSpeed = 0.75;
 Real rateVolatility = 0.015;
 boost::shared_ptr<StochasticProcess1D> HW1F(
  new HullWhiteProcess(curveHandle, reversionSpeed, rateVolatility));
 //
 // create mersenne twister uniform random generator
 unsigned long seed = 28749;
 MersenneTwisterUniformRng generator(seed);
 //
 // create gaussian generator by using central limit transformation method
 CLGaussianRng<MersenneTwisterUniformRng> gaussianGenerator(generator);
 //
 // define maturity, number of steps per path and create gaussian sequence generator
 Time maturity = 5.0;
 Size nSteps = 1250;
 GSG gaussianSequenceGenerator(nSteps, gaussianGenerator);
 //
 // create path generator using Hull-White process and gaussian sequence generator
 PathGenerator<GSG> pathGenerator(HW1F, maturity, nSteps, gaussianSequenceGenerator, false);
 //
 // create matrix container for 20 generated paths
 Size nColumns = 20;
 Matrix paths(nSteps + 1, nColumns);
 for(unsigned int i = 0; i != paths.columns(); i++)
 {
  // request a new stochastic path from path generator
  QuantLib::Sample<Path> path = pathGenerator.next();
  //
  // save generated path into container
  for(unsigned int j = 0; j != path.value.length(); j++)
  {
   paths[j][i] = path.value.at(j);
  }
 }
 // finally, print matrix content into text file
 PrintMatrix(paths, "C:\\temp\\HW1F.txt");
 return 0;
}
//
void PrintMatrix(const Matrix& matrix, std::string filePathName)
{
 // open text file for input, loop through matrix rows
 std::ofstream file(filePathName);
 for(unsigned int i = 0; i != matrix.rows(); i++)
 {
  // concatenate column values into string separated by semicolon
  std::string stream;
  for(unsigned int j = 0; j != matrix.columns(); j++)
  {
   stream += (std::to_string(matrix[i][j]) + ";");
  }
  // print string into text file
  file << stream << std::endl;
 }
 // close text file
 file.close();
}
//
RelinkableHandle<YieldTermStructure> CreateCurveHandle(Date settlementDate)
{
 // create curve builder for piecewise USD Libor swap curve
 PiecewiseCurveBuilder<USDLibor> USDCurveBuilder(settlementDate, 
  UnitedKingdom(), Annual, Thirty360());
 //
 // add quotes directly into curve builder
 USDCurveBuilder.AddDeposit(0.0038975, 1 * Weeks);
 USDCurveBuilder.AddDeposit(0.004295, 1 * Months);
 USDCurveBuilder.AddDeposit(0.005149, 2 * Months);
 USDCurveBuilder.AddDeposit(0.006127, 3 * Months);
 USDCurveBuilder.AddFRA(0.008253, 3 * Months, 3 * Months);
 USDCurveBuilder.AddFRA(0.009065, 6 * Months, 3 * Months);
 USDCurveBuilder.AddFRA(0.01059, 9 * Months, 3 * Months);
 USDCurveBuilder.AddSwap(0.011459, 2 * Years);
 USDCurveBuilder.AddSwap(0.013745, 3 * Years);
 USDCurveBuilder.AddSwap(0.015475, 4 * Years);
 USDCurveBuilder.AddSwap(0.016895, 5 * Years);
 USDCurveBuilder.AddSwap(0.01813, 6 * Years);
 USDCurveBuilder.AddSwap(0.019195, 7 * Years);
 USDCurveBuilder.AddSwap(0.020115, 8 * Years);
 USDCurveBuilder.AddSwap(0.020905, 9 * Years);
 USDCurveBuilder.AddSwap(0.021595, 10 * Years);
 USDCurveBuilder.AddSwap(0.0222, 11 * Years);
 USDCurveBuilder.AddSwap(0.022766, 12 * Years);
 USDCurveBuilder.AddSwap(0.0239675, 15 * Years);
 USDCurveBuilder.AddSwap(0.025105, 20 * Years);
 USDCurveBuilder.AddSwap(0.025675, 25 * Years);
 USDCurveBuilder.AddSwap(0.026015, 30 * Years);
 USDCurveBuilder.AddSwap(0.026205, 40 * Years);
 USDCurveBuilder.AddSwap(0.026045, 50 * Years);
 //
 // return relinkable curve handle
 return USDCurveBuilder.GetCurveHandle();
}

Thanks for reading my blog.

-Mike