使用 Python 使用 QuantLib 对债券进行每日定价

Daily Pricing of a Bond with QuantLib using Python

我想在 python 中使用 QuantLib,主要是在投资组合环境中为利率工具(衍生品)定价。主要要求是将每日收益率曲线传递给系统以在连续几天进行定价(现在让我们忽略系统性能问题)。我的问题是,我是否正确地构建了下面的示例来做到这一点?我的理解是我每天至少需要一个曲线对象和必要的链接等。我已经使用 pandas 来尝试这一点。对此的指导将不胜感激。

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))

感谢 Luigi Ballabio,我重新编写了上面的示例,将设计原则纳入 QuantLib 中,以避免不必要的调用。 现在静态数据是真正静态的,只有市场数据在变化(我希望如此)。 我现在更好地理解活动对象如何监听链接变量的变化。

静态数据如下:

市场数据将是唯一变化的组成部分

改造后的例子如下:

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))

您的解决方案可行,但每天建立联系有点违背图书馆的宗旨。您只需创建一次债券和JIBAR指数,只需更改评估日期和相应的曲线即可;债券将检测到变化并重新计算。

在一般情况下,这将类似于:

# 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')

现在,即使在最一般的情况下,也可以优化上述内容:您每个日期调用 price_floater 两次,所以您做的工作是原来的两倍。我不熟悉 pandas,但我猜你可以进行一次调用并通过一次赋值设置 df_ad['all_in_price']df_ad['clean_price']

此外,根据您的用例,可能还有进一步简化代码的方法。折扣曲线可能被实例化一次并且价差在定价期间发生变化:

# 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()]

在不同的部分,您将只有一组信用利差而不是一组贴现曲线。

如果曲线都是平坦的,您可以利用另一个功能来实现同样的效果:如果您使用天数和日历而不是日期来初始化曲线,其参考日期将随着评估而移动日期(天数为0为评估日,为1为下一个工作日,依此类推)。

# 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()]

在不同的部分,您将用简单的比率数组替换 jibar 曲线数组。

上面的代码应该会给出与您的代码相同的结果,但实例化的对象会少很多,因此可能会节省内存并提高性能。

最后一个警告:如果 pandas' map 并行计算结果,我的代码和你的代码都不会工作;您最终会尝试同时将全局评估日期设置为多个值,这不会很顺利。