Wednesday, November 28, 2018

QuantLib-Python: Transaction Builder

Even the fanciest restaurant needs to have dedicated staff to visit fresh market and peel potatoes, in order to have everything ready for head chef to prepare delicious late night dinners for us. Similarly, having amazing analytics library (such as QuantLib) available for calculations is still only "halfway home", since all required data inputs (ex. transactions) need to be constructed beforehand. It is highly preferred for this part of the process to be performed in a way, that maximum re-configurability would be maintained, while manual labour would be completely avoided.

In a nutshell, this post will present, how to go from having several XML configuration files for specific QuantLib transaction in a directory (shown on the left side), to have all these transactions constructed and processed through QuantLib (shown on the right side).




















Implement the following Python program. First, it will create QuantLib flat yield term structure and discounting bond pricing engine, request a batch of constructed QuantLib transactions from TransactionManager method (located in a separate QuantLibTransactionBuilder), then assign pricing engine for each transaction and finally, print calculated NPV along with some other transaction-related information.

%config IPCompleter.greedy = True
from QuantLib import *
from QuantLibTransactionBuilder import *

# create valuation curve and pricing engine
tradeDate = Date(28, November, 2018)
settlementDate = TARGET().advance(tradeDate, Period(2, Days))
curveHandle = YieldTermStructureHandle(FlatForward(settlementDate, 0.01, Actual360()))
engine = DiscountingBondEngine(curveHandle)

# create QuantLib transactions from repository XML files
transactionsFolder = '/home/TransactionRepository/'
transactions = TransactionManager(transactionsFolder)

# set pricing engine for all transactions, request PV
for t in range(len(transactions)):
    transactions[t].setPricingEngine(engine)
    print('Maturity: ' + str(transactions[t].maturityDate()))
    print('Notional: ' + str(transactions[t].notional()))
    print('NPV: ' + str(transactions[t].NPV()))
    print()

Next, implement the following new module (QuantLibTransactionBuilder.py), which contains specific transaction builder for QuantLib ZeroCouponBond instrument, static class methods for QuantLib-related type conversions and one method for handling a set of given XML files and their conversions into QuantLib objects. Given comments in this module should give crystal clear picture what is happening here.

import os
import importlib
import re as Regex
from bs4 import BeautifulSoup # sudo apt-get install python3-bs4
from QuantLib import *

# static class for conversions from string to QuantLib objects/enumerators
class QL():
    @staticmethod
    def to_date(s):
        monthDictionary = {
            '01': January, '02': February, '03': March,
            '04': April, '05': May, '06': June,
            '07': July, '08': August, '09': September,
            '1': January, '2': February, '3': March,
            '4': April, '5': May, '6': June,
            '7': July, '8': August, '9': September,
            '10': October, '11': November, '12': December
        }
        arr = Regex.findall(r"[\w']+", s)
        day = int(arr[2])
        month = monthDictionary[arr[1]]
        year = int(arr[0])
        return Date(day, month, year)
    @staticmethod
    def to_businessDayConvention(s):
        if (s.upper() == 'MODIFIEDFOLLOWING'): return ModifiedFollowing
        # add new businessdayconvention here
    @staticmethod
    def to_calendar(s):
        if (s.upper() == 'TARGET'): return TARGET()
        # add new calendar here


# loop through XML files in a given folder
# for each transaction, read 'transactionBuilder' attribute from XML data
# call configured transaction builder, build transaction and add it to list
# finally, return list of all built QuantLib transactions to client 
def TransactionManager(folder):
    transactions = []
    files = os.listdir(folder)
    for f in range(len(files)):
        soup = BeautifulSoup(open(folder + files[f]).read(), 'xml')
        transactionBuilder = soup.find('transactionBuilder').get_text()
        builder = getattr(importlib.import_module('QuantLibTransactionBuilder'), transactionBuilder)(folder + files[f])
        transaction = builder
        transactions.append(transaction)
    return transactions


# method for constructing QuantLib 'ZeroCouponBond' object from a given XML data 
def ZeroCouponBondBuilder(filePathName):
    # assign transaction data from XML to variables using soup
    # use conversion class methods for all QuantLib-related conversion 
    soup = BeautifulSoup(open(filePathName).read(), 'xml')
    tradeDate = QL.to_date(soup.find('tradeDate').get_text())
    settlementDate = QL.to_date(soup.find('settlementDate').get_text())
    calendar = QL.to_calendar(soup.find('calendar').get_text())
    faceAmount = float(soup.find('faceAmount').get_text())
    maturityDate = QL.to_date(soup.find('maturityDate').get_text())
    paymentConvention = QL.to_businessDayConvention(soup.find('paymentConvention').get_text())
    settlementDays = settlementDate - tradeDate 
    # create QuantLib object by calling appropriate constructor
    return ZeroCouponBond(settlementDays, calendar, faceAmount, maturityDate, paymentConvention)

All desired new transaction builders should be implemented into previous module by adding another builder method. Also, corresponding builder method name should be also set into transaction XML file as one input parameter.

<ZeroCouponBond>
  <transactionBuilder>ZeroCouponBondBuilder</transactionBuilder>
  <transactionID>CITI.0207</transactionID>
  <tradeDate>2008-07-02</tradeDate>
  <settlementDate>2008-07-05</settlementDate>
  <calendar>target</calendar>
  <faceAmount>1000000</faceAmount>
  <maturityDate>2032-07-05</maturityDate>
  <paymentConvention>modifiedfollowing</paymentConvention>
</ZeroCouponBond>

Needless to say, one should also add all required new conversion methods from their string presentation to QuantLib objects into previous module. The original C# implementation can be found in here. Thanks for reading my blog.
-Mike

Sunday, November 25, 2018

QuantLib-Python: Hull-White one-factor model calibration

This Python program is presenting the process of calibrating Hull-White One-factor interest rate model to a given set of Swaption volatilities. In the example program, I have used exactly the same data as used in the book QuantLib Python Cookbook by G. Balaraman and L. Ballabio in the Chapter 14 (Short Interest Rate Model Calibration) and corresponding results are perfectly replicated here.











Thanks for reading my blog.
-Mike

from QuantLib import *
import numpy as Numpy

# hard-coded data
def CreateSwaptionVolatilityList():
    vol = []
    vol.append(0.1148)
    vol.append(0.1108)
    vol.append(0.1070)
    vol.append(0.1021)
    vol.append(0.1000)
    return vol

class ModelCalibrator:
    def __init__(self, endCriteria):        
        self.endCriteria = endCriteria
        self.helpers = []

    def AddCalibrationHelper(self, helper):
        self.helpers.append(helper)

    def Calibrate(self, model, engine, curve, fixedParameters):
        # assign pricing engine to all calibration helpers
        for i in range(len(self.helpers)):
            self.helpers[i].setPricingEngine(engine)
        method = LevenbergMarquardt()
        if(len(fixedParameters) == 0):
            model.calibrate(self.helpers, method, self.endCriteria)
        else:
            model.calibrate(self.helpers, method, self.endCriteria,
                NoConstraint(), [], fixedParameters)

# general parameters
tradeDate = Date(15, February, 2002)
Settings.instance().evaluationDate = tradeDate
settlementDate = Date(19, February, 2002)
calendar = TARGET()
dayCounter = Actual360()

# create market data: term structure and diagonal volatilities
curveHandle = YieldTermStructureHandle(FlatForward(settlementDate, 0.04875825, Actual365Fixed()))
vol = CreateSwaptionVolatilityList()

# create calibrator object
endCriteria = EndCriteria(10000, 100, 0.000001, 0.00000001, 0.00000001)
calibrator = ModelCalibrator(endCriteria)

# add swaption helpers to calibrator
for i in range(len(vol)):
    t = i + 1; tenor = len(vol) - i    
    helper = SwaptionHelper(
        Period(t, Years), 
        Period(tenor, Years), 
        QuoteHandle(SimpleQuote(vol[i])), 
        USDLibor(Period(3, Months), curveHandle), 
        Period(1, Years), 
        dayCounter, 
        dayCounter, 
        curveHandle)    
    calibrator.AddCalibrationHelper(helper)

# create model and pricing engine, calibrate model and print calibrated parameters
print('case 1 : calibrate all involved parameters (HW1F : reversion, sigma)')
model = HullWhite(curveHandle)
engine = JamshidianSwaptionEngine(model)
fixedParameters = []
calibrator.Calibrate(model, engine, curveHandle, fixedParameters)
print('calibrated reversion: ' + str(round(model.params()[0], 5)))
print('calibrated sigma: ' + str(round(model.params()[1], 5)))
print()

print('case 2 : calibrate sigma and fix reversion to 0.05')
model = HullWhite(curveHandle, 0.05, 0.0001)
engine = JamshidianSwaptionEngine(model)
fixedParameters = [True, False]
calibrator.Calibrate(model, engine, curveHandle, fixedParameters)
print('fixed reversion: ' + str(round(model.params()[0], 5)))
print('calibrated sigma: ' + str(round(model.params()[1], 5)))
print()

print('case 3 : calibrate reversion and fix sigma to 0.01')
model = HullWhite(curveHandle, 0.05, 0.01)
engine = JamshidianSwaptionEngine(model)
fixedParameters = [False, True]
calibrator.Calibrate(model, engine, curveHandle, fixedParameters)
print('calibrated reversion: ' + str(round(model.params()[0], 5)))
print('fixed sigma: ' + str(round(model.params()[1], 5)))
print()

QuantLib-Python: Simulating Paths for Correlated 1-D Stochastic Processes

This program, which is just an extension to my previous post, will create two correlated Geometric Brownian Motion processes, then request simulated paths from dedicated generator function and finally, plots all simulated paths to charts. For the two processes in this example program, correlation has been set to minus one and total of 20 paths has been requested for the both processes. Each path has maturity of one year, which has been divided into 90 time steps. This example can be generalized for higher dimensions by adjusting size of a given correlation matrix and corresponding lists of other parameters (nProcesses, names, spot, mue, sigma).

Thanks for reading my blog.
-Mike 

%config IPCompleter.greedy = True
from QuantLib import *
import numpy as Numpy
import matplotlib.pyplot as Matplotlib

# processArray = StochasticProcessArray (Array of correlated 1-D stochastic processes)
# timeGrid = TimeGrid object
def GenerateCorrelatedPaths(processArray, timeGrid, nPaths):
    times = []; [times.append(timeGrid[t]) for t in range(len(timeGrid))]
    generator = UniformRandomGenerator()
    nProcesses = processArray.size()
    nGridSteps = len(times) - 1 # deduct initial time (0.0)
    nSteps = nGridSteps * nProcesses
    sequenceGenerator = UniformRandomSequenceGenerator(nSteps, generator)
    gaussianSequenceGenerator = GaussianRandomSequenceGenerator(sequenceGenerator)
    multiPathGenerator = GaussianMultiPathGenerator(processArray, times, gaussianSequenceGenerator)
    paths = Numpy.zeros(shape = (nPaths, nProcesses, len(timeGrid)))

    # loop through number of paths
    for i in range(nPaths):
        # request multiPath, which contains the list of paths for each process
        multiPath = multiPathGenerator.next().value()
        # loop through number of processes
        for j in range(multiPath.assetNumber()):
            # request path, which contains the list of simulated prices for a process
            path = multiPath[j]
            # push prices to array
            paths[i, j, :] = Numpy.array([path[k] for k in range(len(path))])
    return paths

# create two 1-D stochastic processes
process = []
nProcesses = 2
correlation = -1.0
names = ['equity_1', 'equity_2']
spot = [100.0, 100.0]
mue = [0.01, 0.01]
sigma = [0.10, 0.10]
[process.append(GeometricBrownianMotionProcess(spot[i], mue[i], sigma[i])) for i in range(nProcesses)]
matrix = [[1.0, correlation], [correlation, 1.0]]

# create timegrid object and define number of paths
maturity = 1.0
nSteps = 90
timeGrid = TimeGrid(maturity, nSteps)
nPaths = 20

# create StochasticProcessArray object
# (array of correlated 1-D stochastic processes)
processArray = StochasticProcessArray(process, matrix)
# request simulated correlated paths for all processes
# result array dimensions: nPaths, nProcesses, len(timeGrid)
paths = GenerateCorrelatedPaths(processArray, timeGrid, nPaths)

# plot paths
f, subPlots = Matplotlib.subplots(nProcesses, sharex = True)
f.suptitle('Path simulations rho=' + str(correlation) + ', n=' + str(nPaths))

for i in range(nPaths):
    for j in range(nProcesses):
        subPlots[j].set_title(names[j])
        path = paths[i, j, :]
        subPlots[j].plot(timeGrid, path)


Saturday, November 24, 2018

QuantLib-Python: Simulating Paths for 1-D Stochastic Processes

This simple Python program will create two 1-dimensional stochastic process objects (Hull-White 1-Factor and Geometric Brownian Motion), then request simulated paths from dedicated generator function and finally, plots all simulated paths to charts. The original C++ implementation can be found in here.

Thanks for reading my blog.
-Mike























%config IPCompleter.greedy = True
from QuantLib import *
import numpy as Numpy
import matplotlib.pyplot as Matplotlib

# process = QuantLib 1-dimensional stochastic process object
def GeneratePaths(process, maturity, nPaths, nSteps):
    generator = UniformRandomGenerator()
    sequenceGenerator = UniformRandomSequenceGenerator(nSteps, generator)
    gaussianSequenceGenerator = GaussianRandomSequenceGenerator(sequenceGenerator)
    paths = Numpy.zeros(shape = ((nPaths), nSteps + 1))
    pathGenerator = GaussianPathGenerator(process, maturity, nSteps, gaussianSequenceGenerator, False)
    for i in range(nPaths):
        path = pathGenerator.next().value()
        paths[i, :] = Numpy.array([path[j] for j in range(nSteps + 1)])
    return paths


# general parameters and objects
tradeDate = Date(23, November, 2018)
Settings_instance().evaluationDate = tradeDate
dayCounter = Actual360()
calendar = UnitedStates()
settlementDate = calendar.advance(tradeDate, 2, Days)

# common simulation-related parameters for all processes
maturity = 3.0
nPaths = 50
nSteps = int(maturity * 365)
timeGrid = Numpy.linspace(0.0, maturity, nSteps + 1)

# create HW1F model, request paths from generator
reversionSpeed = 0.05
rateVolatility = 0.0099255
r = QuoteHandle(SimpleQuote(0.01))
curve = RelinkableYieldTermStructureHandle(FlatForward(settlementDate, r, dayCounter))
HW1F = HullWhiteProcess(curve, reversionSpeed, rateVolatility)
hw1f_paths = GeneratePaths(HW1F, maturity, nPaths, nSteps)

# create GBM model, request paths from generator
initialValue = 0.01
mue = 0.01
sigma = 0.0099255
GBM = GeometricBrownianMotionProcess(initialValue, mue, sigma)
gbm_paths = GeneratePaths(GBM, maturity, nPaths, nSteps)

# plot all paths for the both processes
f, subPlots = Matplotlib.subplots(2, sharex = True)
Matplotlib.rcParams['figure.figsize'] = [16.0, 10.0]
f.suptitle('Path simulations n=' + str(nPaths))
subPlots[0].set_title('Hull-White 1-Factor')
subPlots[1].set_title('Geometric Brownian Motion')

for i in range(hw1f_paths.shape[0]):
    path = hw1f_paths[i, :] 
    subPlots[0].plot(timeGrid, path)

for i in range(gbm_paths.shape[0]):
    path = gbm_paths[i, :] 
    subPlots[1].plot(timeGrid, path)

Friday, November 23, 2018

Synthetic Basis Spread Calculation for Short-term Cross-currency Swaps

For cross-currency basis spread curves, there are usually no quotes available below 1Y time point (for 3M and 6M) and there might be a need to implement the appropriate calculation methodology for the missing data. This post will explain one possible calculation methodology based on Covered Interest Parity and presents the most relevant test results for this applied methodology.

Methodology


We will use adjusted version of traditional Covered Interest Rate Parity (CIP) for calculating synthetic quotes for short-term cross-currency basis spreads. First, in the absence of any arbitrage opportunities, CIP is assuming that the following parity will hold:



Assuming EUR/USD currency pair, we may consider the following two separate investment strategies:

1. Converting EUR amount to USD by using FX spot rate, then investing received USD amount by using USD interest rate for a given period and finally, converting resulting USD amount back to EUR by using FX forward.

2. Investing directly into EUR currency by using EUR interest rate for a given period.

As empirical evidence is strongly suggesting, CIP does not hold, since there is always some basis spread for a given currency pair for a given period. In order to take this basis spread properly into account, adjusted CIP is assuming the following parity to hold:



Where S is cross-currency basis spread for a given period. Finally, we can solve this cross-currency basis spread S for a given period by using the following analytical formula:




Test I: snapshot


As a first test for evaluating this methodology, we used formula for calculating short-term basis spread for a few liquid/major currency pairs. We used comparison data for this specific date from Bloomberg and/or ICE. We present the results in the following tables. Note, that even the curves are up to 5Y, only 3M and 6M points have been estimated by using Adjusted CIP.
























For these specific liquid/major currency pairs, applied calculation methodology is able to re-produce snapshot market quotes for the short-end relatively accurately. General hypothesis is, that the more liquid/major the currency pair, the more accurate results will be produced by Adjusted CIP.

Test II: time-series


At this point in our analysis, we are interested about the consistency of the proposed methodology. We want to know, whether this methodology produces consistent results as market changes over time. As a second test, we calculated short-term basis spread time-series for EUR/USD pair since the beginning of the year of 2014. We have used Bloomberg market quotes for this period as benchmark. The following table shows time-series data for 3M cross-currency basis spread.






















While there are clearly some periods with larger difference between market quote and estimated spread, applied calculation methodology is still able to re-produce market quotes consistently and relatively accurately over time.

Sensitivity


Generally, calculated basis spread estimate is highly sensitive for the interaction of specific variables (∆t, FX spot and FX forward). More specifically, as tenor ∆t will approach to zero, the relative change in spread estimate will generally increase. The following analysis was made for EUR/USD currency pair. All the other parameters were held constant, while FX spot was stressed to both directions in order to see the effect. In practice this means, that basis spread estimate might be heavily biased if, for example, market variables used in parity formula are not captured exactly at the same time.























Practical implementation notes


Assume the following parameters and market data. As we estimate spread for 3M tenor, we can use adjusted CIP formula as follows:






























Note, that when estimating spread for 6M tenor, the formula above can still be used, but domestic and foreign interest rate quotes must be adjusted accordingly by using 3v6 tenor basis spreads. Since there is no explicit basis spread quotes available for 3M and 6M, 1Y quote can be used as relatively close proxy.

































Finally, the following Excel/VBA function implements the calculation presented above. Thanks for reading my blog.
-Mike

Public Function GetXCCYBasisSpread( _
    ByVal FX_spot As Double, _
    ByVal FX_forward As Double, _
    ByVal BaseDepositRate As Double, _
    ByVal TermDepositRate As Double, _
    ByVal Tenor As String, _
    ByVal BasisSign As Integer, _
    ByVal ScaleRates As Boolean, _
    ByVal UseForwardPoints As Boolean, _
    ByVal ConvertToBasisPoints As Boolean) As Double
    
    ' tenor conversion from string ("3M") to double (0.25)
    ' handling for FX spot and forward, rate scaling
    Dim tenorInYears As Double
    If (VBA.UCase(Tenor) = "3M") Then tenorInYears = (3 / 12)
    If (VBA.UCase(Tenor) = "6M") Then tenorInYears = (6 / 12)
    If (UseForwardPoints) Then FX_forward = FX_spot + FX_forward
    If (ScaleRates) Then
        BaseDepositRate = BaseDepositRate / VBA.CDbl(100)
        TermDepositRate = TermDepositRate / VBA.CDbl(100)
    End If

    ' calculate cross-currency basis spread from given market data
    ' by using modified FX-IR-parity :
    ' (FX_spot / FX_forward) * (1 + R_domestic * dt) = (1 + [R_foreign + spread] * dt)
    
    ' solve the previous parity formula for spread
    Dim spread As Double
    spread = (((FX_spot / FX_forward) * (1 + TermDepositRate * tenorInYears) - 1) * _
            (1 / tenorInYears)) - BaseDepositRate
    
    If (ConvertToBasisPoints) Then spread = spread * 10000
    GetXCCYBasisSpread = spread * VBA.CDbl(BasisSign)
End Function

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)