attack68 / rateslib

A fixed income library for pricing bonds and bond futures, and derivatives such as IRS, cross-currency and FX swaps. Contains tools for full Curveset construction with market standard optimisers and automatic differention (AD) and risk sensitivity calculations including delta and cross-gamma.
https://rateslib.readthedocs.io/en/latest/
Other
131 stars 23 forks source link

DOC: how to create curves from CC zero rates #178

Closed attack68 closed 2 months ago

tinka01 commented 4 months ago

Hi, I tried to price a zcs with frequency Z but I keep getting strange values at the ZeroFixedLeg

As example:

usd = Curve( nodes={ dt(2022, 1, 1): 1.0, dt(2027, 1, 1): 0.85, dt(2032, 1, 1): 0.70, }, id="usd" )

zcs = ZCS( effective=dt(2022, 1, 5), termination="10Y", modifier='mf', frequency="Z", calendar="nyc", payment_lag= 1, currency="usd", fixed_rate=4.22566695954813, convention="Act360", notional=100e6, leg2_payment_lag= 1, leg2_calendar='nyc', leg2_frequency= 'Z', curves=["usd"], )

Am i using ZCS incorrectly? I truly want a ZCS with npv = 0

attack68 commented 4 months ago

If you now run this code on main you will get:

ValueError: `frequency` for a ZeroFixedLeg should not be 'Z'. The Leg is zero frequency by construction. Set the `frequency` equal to the compounding frequency of the expressed fixed rate, e.g. 'S' for semi-annual compounding.

I don't know the convention for US ZCS off hand but I assume it is annual compounding, you should use 'A' for frequency.

With this change, observe that:

usd = Curve(
    nodes={
        dt(2022, 1, 1): 1.0,
        dt(2027, 1, 1): 0.85,
        dt(2032, 1, 8): 0.70,
    },
    id="usd"
)

zcs = ZCS(
    effective=dt(2022, 1, 5),
    termination="10Y",
    modifier='mf',
    frequency="A",
    calendar="nyc",
    payment_lag= 1,
    currency="usd",
    fixed_rate=3.5716248138723428,
    convention="Act360",
    notional=100e6,
    leg2_payment_lag= 1,
    leg2_calendar='nyc',
    leg2_frequency='A',
    curves=[usd],
)
zcs.rate()  # 3.5716248138723428
zcs.npv()  # 0.00

And note that $(1+0.03571)^-10 \approx 0.70$.

tinka01 commented 4 months ago

Thanks, i think i found a workaround for what i need. I wanted to get the delta based off the instruments in the calibrated curve, for a series of "loans" priced off par rates. So....I created a series of dummy IRS, setting 1 side of the notionals to an infinitesimally small number.

here's my un-cleansed work...

#create par rates, tbc
sofr_zc_args = dict(effective=start_date, frequency="Z", convention="Act360")
zc_instruments=[ZeroFloatLeg(termination=_, **sofr_zc_args) for _ in curve_data["Termination"]]
curve_data["Equi_coupon"] = [float(zc_instruments[_].cashflows(sofr).Rate[0]) for _ in curve_data["Termination"].index]

#create dummy irs based on debt schedule to calculate delta of debt schedule to the swap curve,tbc
sofr_zcs_args = dict(effective=start_date,notional=10e6, frequency="Z", convention="Act360",calendar="nyc",leg2_notional=0.000001,curves=["sofr"])
zc_instruments=[IRS(termination=_, fixed_rate=x, **sofr_zcs_args) for _,x in zip(curve_data["Termination"],curve_data["Equi_coupon"])]
zc_instruments = pd.DataFrame(zc_instruments,columns =['inst'])
container = pd.DataFrame(curve_data["Term"])
for _ in zc_instruments.index:
    temp = zc_instruments.loc[_,'inst']
    temp =temp.delta(solver=solver)
    temp = temp.reset_index(level=[0,1,2])
    temp.columns = temp.columns.droplevel(1)
    temp = temp[['label','usd']]
    temp.rename(columns = {'label': 'Term'},inplace = True) 
    temp.rename(columns = {'usd': _},inplace = True)
    container = container.merge(temp, left_on='Term', right_on='Term',suffixes=(False, False))
tinka01 commented 4 months ago

files attached usd_s490_20240530.csv

#%%

from rateslib import *
import pandas as pd
import numpy as np
from datetime import datetime,timedelta

#%%
#variable setup
#swap settlement convention for usd sofr = T+2
#bbg curve id is s490, but we have no direct access, use the export function to download data
#each csv should have Term and Rate as header, see usd_s490_20240530.csv for example

ini_time_for_now = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
curve_date = ini_time_for_now + timedelta(days = -1)
start_date = add_tenor(curve_date, "2b", "F", get_calendar("nyc"))
start_date_string = pd.to_datetime(start_date).strftime('%Y%m%d')
curve_date_string = pd.to_datetime(curve_date).strftime('%Y%m%d')
curve_data_file = 'usd_s490_'+curve_date_string+'.csv'

#%%
#read curve data file
curve_data = pd.read_csv(curve_data_file)

#add the termination date for each swap instrument
curve_data['Termination'] = [add_tenor(start_date, _, "MF", "nyc") for _ in curve_data["Term"]]

sofr = Curve(
    id="sofr",
    convention="Act360",
    calendar="nyc",
    modifier="MF",
    interpolation="log_linear", # equivalent to bbg's Step Forward (cont) interpolation method
    nodes={
        **{curve_date: 1.0},  # <- this is today's DF,
        **{_: 1.0 for _ in curve_data["Termination"]},
    }
)
sofr_args = dict(effective=start_date, spec="usd_irs", curves="sofr")
solver = Solver(
    curves=[sofr],
    instruments=[IRS(termination=_, **sofr_args) for _ in curve_data["Termination"]],
    s=curve_data["Rate"],
    instrument_labels=curve_data["Term"],
    id="us_rates",
)
curve_data["DF"] = [float(sofr[_]) for _ in curve_data["Termination"]]

#create par rates, tbc
sofr_zc_args = dict(effective=start_date, frequency="Z", convention="Act360")
zc_instruments=[ZeroFloatLeg(termination=_, **sofr_zc_args) for _ in curve_data["Termination"]]
curve_data["Equi_coupon"] = [float(zc_instruments[_].cashflows(sofr).Rate[0]) for _ in curve_data["Termination"].index]

#create dummy irs based on debt schedule to calculate delta of debt schedule to the swap curve,tbc
sofr_zcs_args = dict(effective=start_date,notional=10e6, frequency="Z", convention="Act360",calendar="nyc",leg2_notional=0.000001,curves=["sofr"])
zc_instruments=[IRS(termination=_, fixed_rate=x, **sofr_zcs_args) for _,x in zip(curve_data["Termination"],curve_data["Equi_coupon"])]
zc_instruments = pd.DataFrame(zc_instruments,columns =['inst'])
container = pd.DataFrame(curve_data["Term"])
for _ in zc_instruments.index:
    temp = zc_instruments.loc[_,'inst']
    temp =temp.delta(solver=solver)
    temp = temp.reset_index(level=[0,1,2])
    temp.columns = temp.columns.droplevel(1)
    temp = temp[['label','usd']]
    temp.rename(columns = {'label': 'Term'},inplace = True) 
    temp.rename(columns = {'usd': _},inplace = True)
    container = container.merge(temp, left_on='Term', right_on='Term',suffixes=(False, False))
`
attack68 commented 4 months ago

OK, I kind of see what you are doing except at the end where the merge seems to overwrite and not sum the risks in your loop.

Anyway, I note how you replicated your cashflows with a calculated simple rate from a ZeroFloatLeg and altered an IRS to have Zero frequency. Note you can do this with an ZCS:

Screenshot 2024-06-01 at 09 28 48

tinka01 commented 4 months ago

Yeah, I collated the results in a container df for now as I haven’t decided on the final formats.

the problem with the zcs is that Z as frequency is not allowed somehow?

attack68 commented 4 months ago

The frequency of a zero coupon swap is zero in construction. For a ZCS the 'frequency' input only controls the expression of the rate, not the schedule

tinka01 commented 4 months ago

Yeah that was the problem, I didn’t want the compounding… next up, need to try and get rid of the discounting too…

attack68 commented 4 months ago

Oh I see, yes then you should use your hack.

You can hack no discounting on any leg by creating a discount curve with all discount factors at 1.0, then every cashflow is still technically discounted but returns the same value.

This issue has highlighted a need to potentially construct a CustomInstrument where a combination of legs gives, i.e. CustomInstrument([FixedLeg, ZeroFloatLeg]). That would also have made your hacking easier.

attack68 commented 2 months ago

261