pythonquantlibquantlib-swig

Daily Pricing of a Bond with QuantLib using Python


I would like to use QuantLib within python mainly to price interest rate instruments (derivatives down the track) within a portfolio context. The main requirement would be to pass daily yield curves to the system to price on successive days (let's ignore system performance issues for now). My question is, have I structured the example below correctly to do this? My understanding is that I would need at least one curve object per day with the necessary linking etc. I have made use of pandas to attempt this. Guidance on this would be appreciated.

import QuantLib as ql
import math
import pandas as pd
import datetime as dt


# MARKET PARAMETRES
calendar = ql.SouthAfrica()
bussiness_convention = ql.Unadjusted
day_count = ql.Actual365Fixed()
interpolation = ql.Linear()
compounding = ql.Compounded
compoundingFrequency = ql.Quarterly


def perdelta(start, end, delta):
    date_list=[]
    curr = start
    while curr < end:
        date_list.append(curr)
        curr += delta
    return date_list


def to_datetime(d):
    return dt.datetime(d.year(),d.month(), d.dayOfMonth())

def format_rate(r):
    return '{0:.4f}'.format(r.rate()*100.00)


#QuantLib must have dates in its date objects
dicPeriod={'DAY':ql.Days,'WEEK':ql.Weeks,'MONTH':ql.Months,'YEAR':ql.Years}


issueDate = ql.Date(19,8,2014)
maturityDate = ql.Date(19,8,2016)

#Bond Schedule
schedule = ql.Schedule (issueDate, maturityDate,
                     ql.Period(ql.Quarterly),ql.TARGET(),ql.Following, ql.Following,
                    ql.DateGeneration.Forward,False)


fixing_days = 0 
face_amount = 100.0


def price_floater(myqlvalDate,jindex,jibarTermStructure,discount_curve):

    bond = ql.FloatingRateBond(settlementDays = 0,
                            faceAmount = 100,
                            schedule = schedule,
                            index = jindex,
                            paymentDayCounter = ql.Actual365Fixed(),
                            spreads=[0.02])   

    bondengine = ql.DiscountingBondEngine(ql.YieldTermStructureHandle(discount_curve))
    bond.setPricingEngine(bondengine)
    ql.Settings.instance().evaluationDate = myqlvalDate
    return [bond.NPV() ,bond.cleanPrice()]


start_date=dt.datetime(2014,8,19)
end_date=dt.datetime(2015,8,19)
all_dates=perdelta(start_date,end_date,dt.timedelta(days=1))
dtes=[];fixings=[]
for d in all_dates:
    if calendar.isBusinessDay(ql.QuantLib.Date(d.day,d.month,d.year)):
        dtes.append(ql.QuantLib.Date(d.day,d.month,d.year))
        fixings.append(0.1)


df_ad=pd.DataFrame(all_dates,columns=['valDate'])
df_ad['qlvalDate']=df_ad.valDate.map(lambda x:ql.DateParser.parseISO(x.strftime('%Y-%m-%d')))
df_ad['jibarTermStructure'] = df_ad.qlvalDate.map(lambda x:ql.RelinkableYieldTermStructureHandle())
df_ad['discountStructure'] = df_ad.qlvalDate.map(lambda x:ql.RelinkableYieldTermStructureHandle())
df_ad['jindex'] = df_ad.jibarTermStructure.map(lambda x: ql.Jibar(ql.Period(3,ql.Months),x))
df_ad.jindex.map(lambda x:x.addFixings(dtes, fixings))
df_ad['flatCurve'] = df_ad.apply(lambda r: ql.FlatForward(r['qlvalDate'],0.1,ql.Actual365Fixed(),compounding,compoundingFrequency),axis=1)
df_ad.apply(lambda x:x['jibarTermStructure'].linkTo(x['flatCurve']),axis=1)
df_ad.apply(lambda x:x['discountStructure'].linkTo(x['flatCurve']),axis=1)
df_ad['discount_curve']= df_ad.apply(lambda x:ql.ZeroSpreadedTermStructure(x['discountStructure'],ql.QuoteHandle(ql.SimpleQuote(math.log(1+0.02)))),axis=1)
df_ad['all_in_price']=df_ad.apply(lambda r:price_floater(r['qlvalDate'],r['jindex'],r['jibarTermStructure'],r['discount_curve'])[0],axis=1)
df_ad['clean_price']=df_ad.apply(lambda r:price_floater(r['qlvalDate'],r['jindex'],r['jibarTermStructure'],r['discount_curve'])[1],axis=1)
df_plt=df_ad[['valDate','all_in_price','clean_price']]
df_plt=df_plt.set_index('valDate')


from matplotlib import ticker

def func(x, pos): 
    s = str(x)
    ind = s.index('.')
    return s[:ind] + '.' + s[ind+1:]  

ax=df_plt.plot()
ax.yaxis.set_major_formatter(ticker.FuncFormatter(func))

Thanks to Luigi Ballabio I have reworked the example above to incorporate the design principles within QuantLib so as to avoid unnecessary calling. Now the static data is truly static and only the market data varies (I hope). I now understand better how the live objects listen for changes in linked variables.

Static data is the following:

Market data will be the only varying component

The reworked example is below:

import QuantLib as ql
import math
import pandas as pd
import datetime as dt
import numpy as np


# MARKET PARAMETRES
calendar = ql.SouthAfrica()
bussiness_convention = ql.Unadjusted
day_count = ql.Actual365Fixed()
interpolation = ql.Linear()
compounding = ql.Compounded
compoundingFrequency = ql.Quarterly


def perdelta(start, end, delta):
    date_list=[]
    curr = start
    while curr < end:
        date_list.append(curr)
        curr += delta
    return date_list


def to_datetime(d):
    return dt.datetime(d.year(),d.month(), d.dayOfMonth())

def format_rate(r):
    return '{0:.4f}'.format(r.rate()*100.00)


#QuantLib must have dates in its date objects
dicPeriod={'DAY':ql.Days,'WEEK':ql.Weeks,'MONTH':ql.Months,'YEAR':ql.Years}


issueDate = ql.Date(19,8,2014)
maturityDate = ql.Date(19,8,2016)

#Bond Schedule
schedule = ql.Schedule (issueDate, maturityDate,
                     ql.Period(ql.Quarterly),ql.TARGET(),ql.Following, ql.Following,
                    ql.DateGeneration.Forward,False)

fixing_days = 0 
face_amount = 100.0

start_date=dt.datetime(2014,8,19)
end_date=dt.datetime(2015,8,19)
all_dates=perdelta(start_date,end_date,dt.timedelta(days=1))
dtes=[];fixings=[]
for d in all_dates:
    if calendar.isBusinessDay(ql.QuantLib.Date(d.day,d.month,d.year)):
        dtes.append(ql.QuantLib.Date(d.day,d.month,d.year))
        fixings.append(0.1)

jibarTermStructure = ql.RelinkableYieldTermStructureHandle()
jindex = ql.Jibar(ql.Period(3,ql.Months), jibarTermStructure)
jindex.addFixings(dtes, fixings)
discountStructure = ql.RelinkableYieldTermStructureHandle()

bond = ql.FloatingRateBond(settlementDays = 0,
                          faceAmount = 100,
                          schedule = schedule,
                          index = jindex,
                          paymentDayCounter = ql.Actual365Fixed(),
                          spreads=[0.02])   

bondengine = ql.DiscountingBondEngine(discountStructure)
bond.setPricingEngine(bondengine)

spread = ql.SimpleQuote(0.0)
discount_curve = ql.ZeroSpreadedTermStructure(jibarTermStructure,ql.QuoteHandle(spread))
discountStructure.linkTo(discount_curve)

# ...here is the pricing function...

# pricing:
def price_floater(myqlvalDate,jibar_curve,credit_spread):
    credit_spread = math.log(1.0+credit_spread)
    ql.Settings.instance().evaluationDate = myqlvalDate
    jibarTermStructure.linkTo(jibar_curve)
    spread.setValue(credit_spread)
    ql.Settings.instance().evaluationDate = myqlvalDate
    return pd.Series({'NPV': bond.NPV(), 'cleanPrice': bond.cleanPrice()})


# ...and here are the remaining varying parts:

df_ad=pd.DataFrame(all_dates,columns=['valDate'])
df_ad['qlvalDate']=df_ad.valDate.map(lambda x:ql.DateParser.parseISO(x.strftime('%Y-%m-%d')))
df_ad['jibar_curve'] = df_ad.apply(lambda r: ql.FlatForward(r['qlvalDate'],0.1,ql.Actual365Fixed(),compounding,compoundingFrequency),axis=1)
df_ad['spread']=np.random.uniform(0.015, 0.025, size=len(df_ad)) # market spread
df_ad['all_in_price'], df_ad["clean_price"]=zip(*df_ad.apply(lambda r:price_floater(r['qlvalDate'],r['jibar_curve'],r['spread']),axis=1).to_records())[1:]


# plot result

df_plt=df_ad[['valDate','all_in_price','clean_price']]
df_plt=df_plt.set_index('valDate')

from matplotlib import ticker


def func(x, pos):  # formatter function takes tick label and tick position
    s = str(x)
    ind = s.index('.')
    return s[:ind] + '.' + s[ind+1:]   # change dot to comma

ax=df_plt.plot()
ax.yaxis.set_major_formatter(ticker.FuncFormatter(func))

Solution

  • Your solution would work, but creating a bond per day kind of goes against the grain of the library. You can create the bond and the JIBAR index just once, and just change the evaluation date and the corresponding curves; the bond will detect the changes and recalculate.

    In the general case, this would be something like:

    # here are the parts that stay the same...
    
    jibarTermStructure = ql.RelinkableYieldTermStructureHandle()
    jindex = ql.Jibar(ql.Period(3,ql.Months), jibarTermStructure)
    jindex.addFixings(dtes, fixings)
    discountStructure = ql.RelinkableYieldTermStructureHandle()
    
    bond = ql.FloatingRateBond(settlementDays = 0,
                              faceAmount = 100,
                              schedule = schedule,
                              index = jindex,
                              paymentDayCounter = ql.Actual365Fixed(),
                              spreads=[0.02])   
    
    bondengine = ql.DiscountingBondEngine(discountStructure)
    bond.setPricingEngine(bondengine)
    
    # ...here is the pricing function...
    
    def price_floater(myqlvalDate,jibar_curve,discount_curve):
        ql.Settings.instance().evaluationDate = myqlvalDate
        jibarTermStructure.linkTo(jibar_curve)
        discountStructure.linkTo(discount_curve)
        return [bond.NPV() ,bond.cleanPrice()]
    
    # ...and here are the remaining varying parts:
    
    df_ad=pd.DataFrame(all_dates,columns=['valDate'])
    df_ad['qlvalDate']=df_ad.valDate.map(lambda x:ql.DateParser.parseISO(x.strftime('%Y-%m-%d')))
    df_ad['flatCurve'] = df_ad.apply(lambda r: ql.FlatForward(r['qlvalDate'],0.1,ql.Actual365Fixed(),compounding,compoundingFrequency),axis=1)
    df_ad['discount_curve']= df_ad.apply(lambda x:ql.ZeroSpreadedTermStructure(jibarTermStructure,ql.QuoteHandle(ql.SimpleQuote(math.log(1+0.02)))),axis=1)
    df_ad['all_in_price']=df_ad.apply(lambda r:price_floater(r['qlvalDate'],r['flatCurve'],r['discount_curve'])[0],axis=1)
    df_ad['clean_price']=df_ad.apply(lambda r:price_floater(r['qlvalDate'],r['flatCurve'],r['discount_curve'])[0],axis=1)
    df_plt=df_ad[['valDate','all_in_price','clean_price']]
    df_plt=df_plt.set_index('valDate')
    

    Now, even in the most general case, the above can be optimized: you're calling price_floater twice per date, so you're doing twice the work. I'm not familiar with pandas, but I'd guess you can make a single call and set df_ad['all_in_price'] and df_ad['clean_price'] with a single assignment.

    Moreover, there might be ways to simplify the code even further depending on your use cases. The discount curve might be instantiated once and the spread changed during pricing:

    # in the "only once" part:
    spread = ql.SimpleQuote()
    discount_curve = ql.ZeroSpreadedTermStructure(jibarTermStructure,ql.QuoteHandle(spread))
    discountStructure.linkTo(discount_curve)
    
    # pricing:
    def price_floater(myqlvalDate,jibar_curve,credit_spread):
        ql.Settings.instance().evaluationDate = myqlvalDate
        jibarTermStructure.linkTo(jibar_curve)
        spread.setValue(credit_spread)
        return [bond.NPV() ,bond.cleanPrice()]
    

    and in the varying part, you'll just have an array of credit spreads intead of an array of discount curves.

    If the curves are all flat, you can do the same by taking advantage of another feature: if you initialize a curve with a number of days and a calendar instead of a date, its reference date will move with the evaluation date (if the number of days is 0, it will be the evaluation date; if it's 1, it will be the next business day, and so on).

    # only once:
    risk_free = ql.SimpleQuote()
    jibar_curve = ql.FlatForward(0,calendar,ql.QuoteHandle(risk_free),ql.Actual365Fixed(),compounding,compoundingFrequency)
    jibarTermStructure.linkTo(jibar_curve)
    
    # pricing:
    def price_floater(myqlvalDate,risk_free_rate,credit_spread):
        ql.Settings.instance().evaluationDate = myqlvalDate
        risk_free.linkTo(risk_free_rate)
        spread.setValue(credit_spread)
        return [bond.NPV() ,bond.cleanPrice()]
    

    and in the varying part, you'll replace the array of jibar curves with a simple array of rates.

    The above should give you the same result as your code, but will instantiate a lot less objects and thus probably save memory and increase performance.

    One final warning: neither my code nor yours will work if pandas' map evaluates the results in parallel; you'd end up trying to set up the global evaluation date to several values simultaneously, and that wouldn't go well.