import%20marimo%0A%0A__generated_with%20%3D%20%220.17.6%22%0Aapp%20%3D%20marimo.App(width%3D%22medium%22)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_()%3A%0A%20%20%20%20import%20warnings%0A%0A%20%20%20%20import%20marimo%20as%20mo%0A%0A%20%20%20%20%23%20Suppress%20RuntimeWarnings%20for%20division%20operations%20globally%0A%20%20%20%20warnings.filterwarnings(%22ignore%22%2C%20category%3DRuntimeWarning%2C%20message%3D%22.*invalid%20value%20encountered.*%22)%0A%20%20%20%20return%20(mo%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20Example%3A%20Using%20Loman%20to%20price%20Interest%20Rate%20Swaps%0A%0A%20%20%20%20In%20this%20example%2C%20we'll%20look%20at%20calibrating%20interest%20rate%20curves%20to%20market%20prices%2C%20and%20then%20using%20them%20to%20price%20portfolios%20of%20swaps.%0A%0A%20%20%20%20%23%23%20Curve%20Classes%0A%0A%20%20%20%20Because%20the%20focus%20will%20be%20on%20using%20Loman%2C%20we'll%20adopt%20deliberately%20simplifying%20assumptions%3B%20time%20is%20a%20float%2C%20day%20count%20and%20business%20day%20conventions%20are%20ignored%2C%20quarters%20are%20exactly%200.25%20years%20long%20and%20so%20on.%0A%0A%20%20%20%20Our%20interest%20rate%20curves%20can%20be%20used%20for%20two%20things%3A%20discounting%20and%20projecting%20rates.%20To%20do%20this%2C%20we%20define%20a%20continuously-compounded%20forward%20rate%20%24r(t)%24%2C%20so%20that%20the%20discount%20rate%20from%20a%20payment%20at%20time%20%24t%24%20to%20a%20time%20%24s%24%20is%0A%0A%20%20%20%20%24%24df(s%2Ct)%20%3D%20%5Cexp%5Cleft%5B-%5Cint_s%5Et%20r(%5Ctau)%20d%5Ctau%5Cright%5D%24%24%0A%0A%20%20%20%20and%20zero%2FFRA%20rates%20are%20defined%20by%0A%0A%20%20%20%20%24%24df(s%2Ct)%20%3D%20%5Cfrac%7B1%7D%7B1%2B%5Ctext%7BFRA%7D(s%2Ct)(t-s)%7D.%24%24%0A%0A%20%20%20%20Our%20BaseIRCurve%20class%20leaves%20the%20definition%20of%20%24r%24%20blank%2C%20but%20otherwise%20fleshes%20out%20the%20methods%20we'll%20need%2C%20including%20methods%20to%20PV%20a%20set%20of%20cashflows%2C%20and%20also%20to%20plot%20the%20continuously-compounded%20forward%20rate%2C%203M%20FRA%20rates%2C%20and%20spot%20swap%20rates%20(once%20we%20define%20swap_rate%2C%20further%20below).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20matplotlib.pyplot%20as%20plt%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20from%20scipy.integrate%20import%20quad%0A%0A%20%20%20%20class%20BaseIRCurve%3A%0A%20%20%20%20%20%20%20%20def%20r_quad(self%2C%20s%2C%20t)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20b%20%3D%20np.broadcast(s%2C%20t)%0A%20%20%20%20%20%20%20%20%20%20%20%20out%20%3D%20np.empty(b.shape)%0A%20%20%20%20%20%20%20%20%20%20%20%20out.flat%20%3D%20%5Bquad(self.r%2C%20s0%2C%20t0)%5B0%5D%20for%20s0%2C%20t0%20in%20b%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20out%0A%0A%20%20%20%20%20%20%20%20def%20r(self%2C%20t)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20pass%0A%0A%20%20%20%20%20%20%20%20def%20df(self%2C%20s%2C%20t)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20np.exp(-self.r_quad(s%2C%20t))%0A%0A%20%20%20%20%20%20%20%20def%20fra(self%2C%20s%2C%20t)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20(1%20%2F%20self.df(s%2C%20t)%20-%201.0)%20%2F%20(t%20-%20s)%0A%0A%20%20%20%20%20%20%20%20def%20pv(self%2C%20amts%2C%20ts%2C%20s%3D0)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20dfs%20%3D%20self.df(s%2C%20ts)%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20np.sum(amts%20*%20dfs)%0A%0A%20%20%20%20%20%20%20%20def%20plot_basic(self)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%22%22%22Plot%20r%20and%203M%20forward%20rates%20(swap%20rate%20plotting%20done%20separately%20to%20avoid%20cycles).%22%22%22%0A%20%20%20%20%20%20%20%20%20%20%20%20ts%20%3D%20np.linspace(0.0%2C%2029.75%2C%20360)%0A%20%20%20%20%20%20%20%20%20%20%20%20plt.plot(ts%2C%20self.r(ts)%2C%20label%3D%22r%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20ts%20%3D%20np.linspace(0.0%2C%2029.75%2C%20120)%0A%20%20%20%20%20%20%20%20%20%20%20%20plt.plot(ts%2C%20self.fra(ts%2C%20ts%20%2B%200.25)%2C%20label%3D%223M%20fwd%22)%0A%20%20%20%20%20%20%20%20%20%20%20%20plt.legend()%0A%20%20%20%20return%20BaseIRCurve%2C%20np%2C%20plt%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20FlatIRCurve%20class%20fills%20out%20the%20definitions%20of%20r%20and%20r_quad.%20%24r(t)%24%20is%20piece-wise%20flat%2C%20and%20hence%20its%20integral%20is%20piecewise%20linear.%20Together%20with%20the%20methods%20defined%20by%20BaseIRCurve%20we%20will%20now%20have%20a%20basic%20but%20functional%20interest%20rate%20curve.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(BaseIRCurve%2C%20np)%3A%0A%20%20%20%20class%20FlatIRCurve(BaseIRCurve)%3A%0A%20%20%20%20%20%20%20%20def%20__init__(self%2C%20ts%2C%20rates)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.ts%20%3D%20ts%0A%20%20%20%20%20%20%20%20%20%20%20%20self.rates%20%3D%20rates%0A%20%20%20%20%20%20%20%20%20%20%20%20self.ts0%20%3D%20np.zeros(len(self.ts)%20%2B%201)%0A%20%20%20%20%20%20%20%20%20%20%20%20self.ts0%5B1%3A%5D%20%3D%20self.ts%0A%20%20%20%20%20%20%20%20%20%20%20%20self.rquads%20%3D%20np.zeros_like(self.ts0)%0A%20%20%20%20%20%20%20%20%20%20%20%20self.rquads%5B1%3A%5D%20%3D%20np.cumsum(np.diff(self.ts0)%20*%20self.rates)%0A%0A%20%20%20%20%20%20%20%20def%20r(self%2C%20t)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20idx%20%3D%20np.minimum(np.searchsorted(self.ts%2C%20t%2C%20%22right%22)%2C%20len(self.ts)%20-%201)%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20self.rates%5Bidx%5D%0A%0A%20%20%20%20%20%20%20%20def%20r_quad(self%2C%20s%2C%20t)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20np.interp(t%2C%20self.ts0%2C%20self.rquads)%20-%20np.interp(s%2C%20self.ts0%2C%20self.rquads)%0A%20%20%20%20return%20(FlatIRCurve%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Finally%2C%20we'll%20need%20to%20set%20up%20some%20functions%20to%20calculate%20quarterly%20FRAs%20and%20discount%20them%20to%20calculate%20PVs%20of%20the%20fixed%20and%20floating%20legs%20of%20swaps%2C%20as%20well%20as%20the%20swap%20rate.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(np)%3A%0A%20%20%20%20def%20sched(a%2C%20b%2C%20p)%3A%0A%20%20%20%20%20%20%20%20n%20%3D%20int((b%20-%20a)%20%2F%20p)%0A%20%20%20%20%20%20%20%20if%20b%20-%20n%20*%20p%20%3E%20a%20%2B%20p%20%2F%202.0%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20n%20%3D%20n%20%2B%201%0A%20%20%20%20%20%20%20%20ts%20%3D%20np.linspace(b%20-%20n%20*%20p%2C%20b%2C%20n%20%2B%201)%0A%20%20%20%20%20%20%20%20ts%5B0%5D%20%3D%20a%0A%20%20%20%20%20%20%20%20return%20ts%0A%0A%20%20%20%20def%20swap_leg_pvs(a%2C%20b%2C%20p%2C%20projection_curve%2C%20discount_curve)%3A%0A%20%20%20%20%20%20%20%20ts%20%3D%20sched(a%2C%20b%2C%20p)%0A%20%20%20%20%20%20%20%20pers%20%3D%20np.diff(ts)%0A%20%20%20%20%20%20%20%20fixed_pv%20%3D%20discount_curve.pv(pers%2C%20ts%5B1%3A%5D)%0A%20%20%20%20%20%20%20%20fras%20%3D%20projection_curve.fra(ts%5B%3A-1%5D%2C%20ts%5B1%3A%5D)%0A%20%20%20%20%20%20%20%20float_pv%20%3D%20discount_curve.pv(pers%20*%20fras%2C%20ts%5B1%3A%5D)%0A%20%20%20%20%20%20%20%20return%20fixed_pv%2C%20float_pv%0A%0A%20%20%20%20def%20swap_rate(a%2C%20b%2C%20p%2C%20projection_curve%2C%20discount_curve)%3A%0A%20%20%20%20%20%20%20%20fixed_pv%2C%20float_pv%20%3D%20swap_leg_pvs(a%2C%20b%2C%20p%2C%20projection_curve%2C%20discount_curve)%0A%20%20%20%20%20%20%20%20return%20float_pv%20%2F%20fixed_pv%0A%20%20%20%20return%20swap_leg_pvs%2C%20swap_rate%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Calibrating%20a%20LIBOR%20curve%0A%0A%20%20%20%20Up%20until%202008%2C%20it%20was%20standard%20to%20use%20the%20LIBOR%20curve%20for%20both%20projection%20of%20LIBOR%20swap%20cashflows%20*and*%20for%20discounting%20those%20cashflows.%20We%20set%20up%20a%20Loman%20computation%20with%20inputs%20**usd_libor_ts**%2C%20a%20set%20of%20swap%20maturities%2C%20and%20**usd_libor_c_rate**%20a%20set%20of%20continuous%20compounding%20rates%20for%20each%20period.%20From%20those%2C%20we%20create%20a%20curve%20object%20**usd_libor_curve**%2C%20and%20then%20calculate%20swap%20rates%20using%20that%20curve%20for%20both%20projection%20and%20discounting%2C%20in%20**usd_libor_swap_rates**.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(FlatIRCurve%2C%20np%2C%20swap_rate)%3A%0A%20%20%20%20import%20loman%0A%0A%20%20%20%20comp%20%3D%20loman.Computation()%0A%20%20%20%20comp.add_node(%22usd_libor_ts%22%2C%20value%3Dnp.array(%5B1.0%2C%202.0%2C%203.0%2C%205.0%2C%207.0%2C%2010.0%2C%2015.0%2C%2020.0%2C%2030.0%5D))%0A%20%20%20%20comp.add_node(%22usd_libor_c_rates%22%2C%20value%3D0.03%20*%20np.ones(9))%0A%20%20%20%20comp.add_node(%0A%20%20%20%20%20%20%20%20%22usd_libor_curve%22%2C%20lambda%20usd_libor_ts%2C%20usd_libor_c_rates%3A%20FlatIRCurve(usd_libor_ts%2C%20usd_libor_c_rates)%0A%20%20%20%20)%0A%20%20%20%20comp.add_node(%0A%20%20%20%20%20%20%20%20%22usd_libor_swap_rates%22%2C%0A%20%20%20%20%20%20%20%20lambda%20usd_libor_curve%2C%20usd_libor_ts%3A%20np.vectorize(swap_rate)(%0A%20%20%20%20%20%20%20%20%20%20%20%200%2C%20usd_libor_ts%2C%200.25%2C%20usd_libor_curve%2C%20usd_libor_curve%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20)%0A%20%20%20%20comp.draw(graph_attr%3D%7B%22size%22%3A%20%2212%22%7D)%0A%20%20%20%20return%20(comp%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20If%20we%20calculate%20all%20the%20nodes%20in%20Loman%2C%20we%20can%20plot%20the%20curve.%20Because%20we%20set%20all%20the%20continuously%20compounding%20rates%20to%203%25%2C%20it's%20not%20a%20very%20interesting%20curve%20yet.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20np%2C%20plt%2C%20swap_rate)%3A%0A%20%20%20%20comp.compute_all()%0A%20%20%20%20curve%20%3D%20comp.value(%22usd_libor_curve%22)%0A%20%20%20%20curve.plot_basic()%0A%20%20%20%20%23%20Add%20swap%20rate%20to%20plot%0A%20%20%20%20ts_plot%20%3D%20np.linspace(0.0%2C%2029.75%2C%20120)%0A%20%20%20%20plt.plot(ts_plot%2C%20np.vectorize(swap_rate)(0%2C%20ts_plot%2C%200.25%2C%20curve%2C%20curve)%2C%20label%3D%22Swap%20Rate%22)%0A%20%20%20%20plt.legend()%0A%20%20%20%20plt.gca()%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Now%2C%20to%20help%20calibrate%20our%20curve%2C%20we%20add%20a%20node%20that%20is%20market%20swap%20rates%20(**usd_libor_mkt_swap_rates**)%2C%20and%20calculate%20the%20difference%20between%20the%20rates%20our%20curve%20is%20producing%2C%20and%20the%20market%20rates%20we%20are%20trying%20to%20fit%20to%2C%20in%20**usd_libor_fitting_error**.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20np)%3A%0A%20%20%20%20comp.add_node(%0A%20%20%20%20%20%20%20%20%22usd_libor_mkt_swap_rates%22%2C%0A%20%20%20%20%20%20%20%20value%3Dnp.array(%5B0.01364%2C%200.01593%2C%200.01776%2C%200.02023%2C%200.02181%2C%200.02343%2C%200.02499%2C%200.02566%2C%200.02593%5D)%2C%0A%20%20%20%20)%0A%20%20%20%20comp.add_node(%0A%20%20%20%20%20%20%20%20%22usd_libor_fitting_error%22%2C%0A%20%20%20%20%20%20%20%20lambda%20usd_libor_swap_rates%2C%20usd_libor_mkt_swap_rates%3A%20usd_libor_swap_rates%20-%20usd_libor_mkt_swap_rates%2C%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20To%20calibrate%20our%20curve%2C%20we%20use%20a%20minimizer%20from%20scipy.%20Our%20objective%20function%20is%20to%20insert%20a%20trial%20set%20of%20continuously-compounded%20rates%20into%20the%20input%20**usd_libor_c_rates**%2C%20calculate%20the%20error%20vector%20**usd_libor_fitting_error**%20using%20Loman%2C%20and%20then%20return%20the%20sum%20of%20the%20squares%20(scaled%20appropriately%20for%20the%20solver).%20Our%20initial%20guess%20is%20taken%20from%20the%20current%20set%20of%20rates%20in%20**usd_libor_c_rates**.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20np)%3A%0A%20%20%20%20from%20scipy.optimize%20import%20minimize%0A%0A%20%20%20%20def%20error(xs)%3A%0A%20%20%20%20%20%20%20%20comp.insert(%22usd_libor_c_rates%22%2C%20xs)%0A%20%20%20%20%20%20%20%20comp.compute(%22usd_libor_fitting_error%22)%0A%20%20%20%20%20%20%20%20return%2010000.0%20*%20np.sum(comp.value(%22usd_libor_fitting_error%22)%20**%202)%0A%0A%20%20%20%20res%20%3D%20minimize(error%2C%20comp.value(%22usd_libor_c_rates%22))%0A%20%20%20%20return%20minimize%2C%20res%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20solver%20indicates%20that%20it%20rans%20successfully%20with%20275%20evaluations%2C%20so%20we%20insert%20its%20solution%20set%20of%20input%20rates%20into%20the%20computation%20(solvers%20aren't%20required%20that%20their%20last%20evaluation%20be%20the%20solution%20value)%2C%20re-compute%20everything%2C%20and%20plot%20the%20resulting%20curve.%20We%20also%20show%20the%20market%20swap%20rates%20so%20we%20can%20see%20our%20calibration%20was%20successful.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20np%2C%20plt%2C%20res%2C%20swap_rate)%3A%0A%20%20%20%20comp.insert(%22usd_libor_c_rates%22%2C%20res.x)%0A%20%20%20%20comp.compute_all()%0A%20%20%20%20curve_calibrated%20%3D%20comp.value(%22usd_libor_curve%22)%0A%20%20%20%20curve_calibrated.plot_basic()%0A%20%20%20%20%23%20Add%20swap%20rate%20to%20plot%0A%20%20%20%20ts_calib%20%3D%20np.linspace(0.0%2C%2029.75%2C%20120)%0A%20%20%20%20plt.plot(%0A%20%20%20%20%20%20%20%20ts_calib%2C%20np.vectorize(swap_rate)(0%2C%20ts_calib%2C%200.25%2C%20curve_calibrated%2C%20curve_calibrated)%2C%20label%3D%22Swap%20Rate%22%0A%20%20%20%20)%0A%20%20%20%20plt.scatter(comp.value(%22usd_libor_ts%22)%2C%20comp.value(%22usd_libor_mkt_swap_rates%22)%2C%20label%3D%22Mkt%20Swap%20Rates%22)%0A%20%20%20%20plt.legend()%0A%20%20%20%20plt.gca()%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Valuing%20a%20Portfolio%20of%20Interest%20Rate%20Swaps%0A%0A%20%20%20%20Now%20that%20we%20have%20a%20valid%20interest%20rate%20curve%2C%20we%20can%20use%20it%20to%20value%20interest%20rate%20swaps.%20For%20now%2C%20our%20swaps%20will%20all%20be%20valued%20with%20using%20the%20same%20USD%20LIBOR%20curve.%0A%0A%20%20%20%20We%20define%20a%20swap%20as%20a%20collection%20of%20named%20parameters%2C%20create%20a%20function%20to%20value%20them%2C%20and%20add%20a%20node%20**portfolio**%20to%20our%20computation%2C%20which%20is%20a%20set%20of%20swaps%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20swap_leg_pvs)%3A%0A%20%20%20%20from%20collections%20import%20namedtuple%0A%0A%20%20%20%20_Swap%20%3D%20namedtuple(%22Swap%22%2C%20%5B%22notional%22%2C%20%22start%22%2C%20%22end%22%2C%20%22rate%22%2C%20%22freq%22%5D)%0A%0A%20%20%20%20def%20swap_pv(swap%2C%20projection_curve%2C%20discount_curve)%3A%0A%20%20%20%20%20%20%20%20fixed_pv%2C%20float_pv%20%3D%20swap_leg_pvs(swap.start%2C%20swap.end%2C%20swap.freq%2C%20projection_curve%2C%20discount_curve)%0A%20%20%20%20%20%20%20%20return%20swap.notional%20*%20(float_pv%20-%20swap.rate%20*%20fixed_pv)%0A%0A%20%20%20%20comp.add_node(%22portfolio%22%2C%20value%3D%5B_Swap(10000000%2C%205%2C%2010%2C%200.025%2C%200.25)%2C%20_Swap(-5000000%2C%202.5%2C%2012.5%2C%200.02%2C%200.25)%5D)%0A%20%20%20%20return%20namedtuple%2C%20swap_pv%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20To%20value%20our%20portfolio%2C%20we%20simply%20apply%20our%20valuation%20function%20to%20each%20position%20in%20our%20portfolio%2C%20and%20Loman%20gives%20us%20an%20array%20back%2C%20with%20the%20value%20of%20each%20position%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20swap_pv)%3A%0A%20%20%20%20comp.add_node(%0A%20%20%20%20%20%20%20%20%22portfolio_val%22%2C%0A%20%20%20%20%20%20%20%20lambda%20portfolio%2C%20usd_libor_curve%3A%20%5Bswap_pv(swap%2C%20usd_libor_curve%2C%20usd_libor_curve)%20for%20swap%20in%20portfolio%5D%2C%0A%20%20%20%20)%0A%20%20%20%20comp.draw(graph_attr%3D%7B%22size%22%3A%20%2212%22%7D)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp)%3A%0A%20%20%20%20comp.compute_all()%0A%20%20%20%20comp.value(%22portfolio_val%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Dual%20Bootstrap%20Curves%0A%0A%20%20%20%20Since%202008%2C%20the%20spread%20between%20LIBOR%20and%20OIS%20has%20been%20material%2C%20and%20it%20is%20standard%20practice%20to%20calibrate%20curves%20to%20LIBOR%20swaps%20and%20LIBOR-OIS%20basis%20swaps%2C%20say.%20To%20start%2C%20we'll%20need%20some%20inputs%2C%20a%20curve%2C%20and%20the%20market%20rates.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(FlatIRCurve%2C%20comp%2C%20np)%3A%0A%20%20%20%20comp.add_node(%22usd_ois_ts%22%2C%20value%3Dnp.array(%5B1.0%2C%202.0%2C%203.0%2C%205.0%2C%207.0%2C%2010.0%2C%2015.0%2C%2020.0%2C%2030.0%5D))%0A%20%20%20%20comp.add_node(%22usd_ois_c_rates%22%2C%20value%3D0.03%20*%20np.ones(9))%0A%20%20%20%20comp.add_node(%22usd_ois_curve%22%2C%20lambda%20usd_ois_ts%2C%20usd_ois_c_rates%3A%20FlatIRCurve(usd_ois_ts%2C%20usd_ois_c_rates))%0A%20%20%20%20comp.add_node(%0A%20%20%20%20%20%20%20%20%22usd_libor_ois_mkt_spreads%22%2C%20value%3Dnp.array(%5B24.2%2C%2026%2C%2027.2%2C%2029%2C%2030.9%2C%2033.4%2C%2036.4%2C%2038%2C%2039.8%5D)%20%2F%2010000.0%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Now%20we%20need%20a%20function%20to%20calculate%20LIBOR-OIS%20spreads%20from%20our%20two%20curves%20using%0A%0A%20%20%20%20%24%24PV_%5Ctext%7BLIBOR%20float%20leg%7D%20%3D%20PV_%5Ctext%7BOIS%20float%20leg%7D%20%2B%20s%20%5Ctimes%20PV_%5Ctext%7BOIS%201bp%20fixed%20leg%7D%24%24%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(swap_leg_pvs)%3A%0A%20%20%20%20def%20swap_spread(a%2C%20b%2C%20p%2C%20projection_curve1%2C%20projection_curve2%2C%20discount_curve)%3A%0A%20%20%20%20%20%20%20%20fixed_pv1%2C%20float_pv1%20%3D%20swap_leg_pvs(a%2C%20b%2C%20p%2C%20projection_curve1%2C%20discount_curve)%0A%20%20%20%20%20%20%20%20fixed_pv2%2C%20float_pv2%20%3D%20swap_leg_pvs(a%2C%20b%2C%20p%2C%20projection_curve2%2C%20discount_curve)%0A%20%20%20%20%20%20%20%20return%20(float_pv1%20-%20float_pv2)%20%2F%20fixed_pv2%0A%20%20%20%20return%20(swap_spread%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20We%20use%20that%20function%20to%20calculate%20LIBOR-OIS%20spreads%20using%20our%20two%20curves%2C%20as%20well%20as%20another%20fitting%20error%20vector%20that%20we%20will%20use%20in%20calibration.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20np%2C%20swap_spread)%3A%0A%20%20%20%20comp.add_node(%0A%20%20%20%20%20%20%20%20%22usd_libor_ois_spreads%22%2C%0A%20%20%20%20%20%20%20%20lambda%20usd_libor_curve%2C%20usd_ois_curve%2C%20usd_ois_ts%3A%20np.vectorize(swap_spread)(%0A%20%20%20%20%20%20%20%20%20%20%20%200%2C%20usd_ois_ts%2C%200.25%2C%20usd_libor_curve%2C%20usd_ois_curve%2C%20usd_ois_curve%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20)%0A%0A%20%20%20%20comp.add_node(%0A%20%20%20%20%20%20%20%20%22usd_libor_ois_fitting_error%22%2C%0A%20%20%20%20%20%20%20%20lambda%20usd_libor_ois_spreads%2C%20usd_libor_ois_mkt_spreads%3A%20usd_libor_ois_spreads%20-%20usd_libor_ois_mkt_spreads%2C%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20We%20should%20also%20update%20our%20LIBOR%20swap%20rate%20calculation%20to%20use%20our%20OIS%20curve%20for%20discounting%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20np%2C%20swap_rate)%3A%0A%20%20%20%20comp.add_node(%0A%20%20%20%20%20%20%20%20%22usd_libor_swap_rates%22%2C%0A%20%20%20%20%20%20%20%20lambda%20usd_libor_curve%2C%20usd_ois_curve%2C%20usd_libor_ts%3A%20np.vectorize(swap_rate)(%0A%20%20%20%20%20%20%20%20%20%20%20%200%2C%20usd_libor_ts%2C%200.25%2C%20usd_libor_curve%2C%20usd_ois_curve%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Finally%2C%20for%20our%20calibration%2C%20it%20would%20be%20more%20convenient%20if%20we%20could%20just%20insert%20one%20vector%2C%20containing%20both%20LIBOR%20and%20OIS%20inputs%2C%20and%20have%20those%20feed%20through%20into%20**usd_libor_c_rates**%20and%20**usd_ois_c_rates**.%20Thanks%20to%20Loman%2C%20we%20can%20do%20this%20by%20giving%20them%20a%20common%20parent%20node.%20If%20we%20later%20want%20to%20insert%20values%20directly%20into%20**usd_libor_c_rates**%20and%20**usd_ois_c_rates**%2C%20then%20we%20can%20do%20that%20too%2C%20even%20though%20they%20are%20calculation%20nodes.%20We%20also%20define%20a%20node%20%20**usd_fitting_error**%20to%20collect%20the%20fitting%20error%20vectors.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20np)%3A%0A%20%20%20%20comp.add_node(%22usd_c_rates%22%2C%20value%3D0.03%20*%20np.ones(18))%0A%20%20%20%20comp.add_node(%22usd_libor_c_rates%22%2C%20lambda%20usd_c_rates%3A%20usd_c_rates%5B0%3A9%5D)%0A%20%20%20%20comp.add_node(%22usd_ois_c_rates%22%2C%20lambda%20usd_c_rates%3A%20usd_c_rates%5B9%3A18%5D)%0A%20%20%20%20comp.add_node(%0A%20%20%20%20%20%20%20%20%22usd_fitting_error%22%2C%0A%20%20%20%20%20%20%20%20lambda%20usd_libor_fitting_error%2C%20usd_libor_ois_fitting_error%3A%20np.concatenate(%0A%20%20%20%20%20%20%20%20%20%20%20%20%5Busd_libor_fitting_error%2C%20usd_libor_ois_fitting_error%5D%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20)%0A%20%20%20%20comp.draw(graph_attr%3D%7B%22size%22%3A%20%2212%22%7D)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Complicated.%0A%0A%20%20%20%20But%20it's%20ok.%20Loman%20can%20take%20care%20of%20this.%0A%0A%20%20%20%20We%20can%20just%20apply%20the%20same%20method%20as%20before.%20Having%20the%20solver%20insert%20sets%20of%20inputs%2C%20and%20iterate%20until%20it%20has%20minimized%20the%20error.%20This%20time%2C%20the%20set%20of%20inputs%20that%20it%20is%20adjusting%20flow%20into%20the%20construction%20of%20two%20separate%20curves%2C%20**usd_ois_curve**%2C%20and%20**usd_libor_curve**.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20minimize%2C%20np)%3A%0A%20%20%20%20def%20error_dual(xs)%3A%0A%20%20%20%20%20%20%20%20comp.insert(%22usd_c_rates%22%2C%20xs)%0A%20%20%20%20%20%20%20%20comp.compute(%22usd_fitting_error%22)%0A%20%20%20%20%20%20%20%20return%2010000.0%20*%20np.sum(comp.value(%22usd_fitting_error%22)%20**%202)%0A%0A%20%20%20%20res_dual%20%3D%20minimize(error_dual%2C%20comp.value(%22usd_c_rates%22))%0A%20%20%20%20res_dual.success%2C%20res_dual.nfev%0A%20%20%20%20return%20(res_dual%2C)%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20res_dual)%3A%0A%20%20%20%20comp.insert(%22usd_c_rates%22%2C%20res_dual.x)%0A%20%20%20%20comp.compute_all()%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Finally%2C%20we%20can%20plot%20LIBOR-OIS%20spreads%20from%20our%20dual%20boot-strapped%20curve%2C%20as%20well%20as%20LIBOR%20and%20OIS%20swap%20rates%2C%20to%20check%20the%20calibration.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20np%2C%20plt%2C%20swap_spread)%3A%0A%20%20%20%20ts%20%3D%20np.linspace(0.0%2C%2029.75%2C%20360)%0A%20%20%20%20spreads%20%3D%20np.vectorize(swap_spread)(%0A%20%20%20%20%20%20%20%200.0%2C%20ts%2C%200.25%2C%20comp.value(%22usd_libor_curve%22)%2C%20comp.value(%22usd_ois_curve%22)%2C%20comp.value(%22usd_ois_curve%22)%0A%20%20%20%20)%0A%20%20%20%20plt.plot(ts%2C%20spreads%2C%20label%3D%22Model%20LIBOR-OIS%20Spreads%22)%0A%20%20%20%20plt.scatter(comp.value(%22usd_ois_ts%22)%2C%20comp.value(%22usd_libor_ois_mkt_spreads%22)%2C%20label%3D%22Market%20LIBOR-OIS%20Spreads%22)%0A%20%20%20%20plt.legend()%0A%20%20%20%20plt.gca()%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20np%2C%20plt%2C%20swap_rate)%3A%0A%20%20%20%20libor_curve_dual%20%3D%20comp.value(%22usd_libor_curve%22)%0A%20%20%20%20ois_curve_dual%20%3D%20comp.value(%22usd_ois_curve%22)%0A%20%20%20%20libor_curve_dual.plot_basic()%0A%20%20%20%20%23%20Add%20swap%20rate%20to%20plot%0A%20%20%20%20ts_dual%20%3D%20np.linspace(0.0%2C%2029.75%2C%20120)%0A%20%20%20%20plt.plot(ts_dual%2C%20np.vectorize(swap_rate)(0%2C%20ts_dual%2C%200.25%2C%20libor_curve_dual%2C%20ois_curve_dual)%2C%20label%3D%22Swap%20Rate%22)%0A%20%20%20%20plt.scatter(comp.value(%22usd_libor_ts%22)%2C%20comp.value(%22usd_libor_mkt_swap_rates%22)%2C%20label%3D%22Mkt%20Swap%20Rates%22)%0A%20%20%20%20plt.legend()%0A%20%20%20%20plt.gca()%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20np%2C%20plt%2C%20swap_rate)%3A%0A%20%20%20%20ois_curve_plot%20%3D%20comp.value(%22usd_ois_curve%22)%0A%20%20%20%20ois_curve_plot.plot_basic()%0A%20%20%20%20%23%20Add%20swap%20rate%20to%20plot%20for%20OIS%0A%20%20%20%20ts_ois%20%3D%20np.linspace(0.0%2C%2029.75%2C%20120)%0A%20%20%20%20plt.plot(ts_ois%2C%20np.vectorize(swap_rate)(0%2C%20ts_ois%2C%200.25%2C%20ois_curve_plot%2C%20ois_curve_plot)%2C%20label%3D%22Swap%20Rate%22)%0A%20%20%20%20plt.legend()%0A%20%20%20%20plt.gca()%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Revisiting%20Portfolio%20Valuation%0A%0A%20%20%20%20Now%20that%20we%20have%20two%20curves%2C%20and%20our%20swaps%20could%20potentially%20be%20projected%20and%20discounted%20with%20different%20curves%2C%20it's%20appropriate%20to%20introduce%20a%20new%20node%2C%20**curveset**%20and%20have%20the%20valuation%20of%20our%20portfolio%20depend%20on%20that%20curveset%2C%20rather%20than%20pointing%20directly%20at%20specific%20curves.%20When%20we%20create%20**curveset**%2C%20we%20can%20also%20put%20in%20a%20check%20that%20our%20fit%20was%20good.%0A%0A%20%20%20%20We%20update%20the%20design%20of%20our%20Swap%20instrument%2C%20and%20recreate%20the%20portfolio%20with%20the%20additional%20information.%0A%0A%20%20%20%20By%20design%2C%20Loman%20lets%20us%20redefine%20the%20function%20that%20calculates%20portfolio_val%20and%20have%20it%20depend%20on%20**curveset**%20rather%20than%20**usd_libor_curve**%2C%20and%20the%20Swap%20instruments%20themselves%20direct%20curves%20to%20take%20from%20**curveset**%20to%20perform%20valuation.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20namedtuple%2C%20np%2C%20swap_pv)%3A%0A%20%20%20%20Swap%20%3D%20namedtuple(%22Swap%22%2C%20%5B%22notional%22%2C%20%22start%22%2C%20%22end%22%2C%20%22rate%22%2C%20%22freq%22%2C%20%22projection_curve%22%2C%20%22discount_curve%22%5D)%0A%0A%20%20%20%20def%20create_curveset(usd_libor_curve%2C%20usd_ois_curve%2C%20usd_fitting_error)%3A%0A%20%20%20%20%20%20%20%20if%20np.max(np.abs(usd_fitting_error)%20%3E%200.00001)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20Exception(%22Fitting%20error%20%3E%200.1bps%22)%0A%20%20%20%20%20%20%20%20return%20%7B%22USD-LIBOR-3M%22%3A%20usd_libor_curve%2C%20%22USD-OIS%22%3A%20usd_ois_curve%7D%0A%0A%20%20%20%20comp.add_node(%22curveset%22%2C%20create_curveset)%0A%0A%20%20%20%20comp.insert(%0A%20%20%20%20%20%20%20%20%22portfolio%22%2C%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20Swap(10000000%2C%205%2C%2010%2C%200.025%2C%200.25%2C%20%22USD-LIBOR-3M%22%2C%20%22USD-OIS%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20Swap(-5000000%2C%202.5%2C%2012.5%2C%200.02%2C%200.25%2C%20%22USD-LIBOR-3M%22%2C%20%22USD-OIS%22)%2C%0A%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20)%0A%0A%20%20%20%20comp.add_node(%0A%20%20%20%20%20%20%20%20%22portfolio_val%22%2C%0A%20%20%20%20%20%20%20%20lambda%20portfolio%2C%20curveset%3A%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20swap_pv(swap%2C%20curveset%5Bswap.projection_curve%5D%2C%20curveset%5Bswap.discount_curve%5D)%20for%20swap%20in%20portfolio%0A%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20)%0A%20%20%20%20comp.draw(graph_attr%3D%7B%22size%22%3A%20%2212%22%7D)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Finally%2C%20we%20can%20see%20that%20our%20valuation%20is%20close%20but%20materially%20different%20to%20the%20previous%20LIBOR-only%20curve's%20result%20(86%2C395%20and%20-274%2C086)%2C%20exactly%20as%20we%20expect.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp)%3A%0A%20%20%20%20comp.compute(%22portfolio_val%22)%0A%20%20%20%20comp.value(%22portfolio_val%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20Risks%0A%0A%20%20%20%20There%20are%20a%20few%20ways%20to%20calculate%20risk%20for%20a%20book%20of%20interest%20rate%20derivatives%3A%0A%0A%20%20%20%20*%20Spot%20risks%20directly.%20We%20can%20directly%20perturb%20the%20inputs%20**usd_libor_mkt_swap_rates**%20or%20**usd_libor_ois_mkt_spreads**%20one%20element%20at%20a%20time%2C%20recalibrating%2C%20and%20repricing%20the%20portfolio%20each%20time.%0A%20%20%20%20*%20Spot%20risks%2C%20using%20Jacobians.%20We%20can%20perturb%20the%20inputs%20**usd_libor_c_rates**%20and%20**usd_ois_c_rates**%2C%20using%20the%20changes%20in%20**usd_libor_swap_rates**%20and%20**usd_libor_ois_spreads**%20to%20calculate%20a%20Jacobian.%20Applying%20the%20inverse%20of%20this%20Jacobian%20matrix%20on%20the%20matrix%20of%20portfolio%20valuation%20changes%20gives%20us%20the%20sensitivities%20to%20market%20instruments%20as%20above%2C%20without%20the%20need%20to%20recalibrate.%0A%20%20%20%20*%20Forward%20risk.%20We%20can%20directly%20perturb%20the%20curve%20using%20%24r'(t%3B%20a%2Cb)%20%3D%20r(t)%20%2B%20%5Cdelta%20I_%7Ba%20%5Cle%20t%20%3C%20b%7D%24%20to%20create%20a%20new%20curve%20where%20the%20continuously-compounded%20rate%20is%20elevated%20in%20the%20region%20%24%5Ba%2Cb)%24.%20This%20will%20cause%20the%20forward%20swap%20rate%20to%20change%20by%20some%20amount%2C%20and%20by%20scaling%20%24%5Cdelta%24%2C%20we%20can%20make%20that%20amount%201bp%20(or%20we%20can%20scale%20the%20PV%20changes%20to%20the%20same%20effect)%2C%20so%20we%20can%20get%20the%20risk%20for%20our%20portfolio%20to%20a%20set%20of%20forwards%2C%20potentially%20with%20much%20higher%20granularity%20than%20our%20initial%20set%20of%20calibration%20instruments.%20Additionally%20this%20method%20is%20more%20numerically%20stable%20when%20using%20non-local%20interpolations%20for%20curve%20construction.%0A%0A%20%20%20%20For%20this%20exposition%2C%20we%20will%20go%20with%20the%20latter%20method%2C%20and%20to%20keep%20things%20moving%20along%2C%20we%20will%20only%20produce%20risks%20to%20the%20LIBOR%20curve.%0A%0A%20%20%20%20To%20accommodate%20this%20change%2C%20we'll%20create%20a%20new%20node%20called%20**usd_libor_curve_perturbed**%20which%20will%20be%20fed%20into%20**curveset**%20for%20valuation%2C%20and%20will%20itself%20be%20created%20from%20**usd_libor_curve**%20and%20a%20control%20input%20called%20**usd_libor_curve_perturbation**.%0A%0A%20%20%20%20First%20things%20first%2C%20we%20need%20to%20define%20how%20to%20create%20a%20perturbed%20curve%2C%20using%20the%20recipe%20above.%20Note%20that%20once%20we've%20defined%20r%20and%20r_quad%2C%20everything%20else%20will%20flow%20through%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(BaseIRCurve%2C%20FlatIRCurve%2C%20np)%3A%0A%20%20%20%20class%20SumCurve(BaseIRCurve)%3A%0A%20%20%20%20%20%20%20%20def%20__init__(self%2C%20*curves)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.curves%20%3D%20curves%0A%0A%20%20%20%20%20%20%20%20def%20r_quad(self%2C%20s%2C%20t)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20sum(curve.r_quad(s%2C%20t)%20for%20curve%20in%20self.curves)%0A%0A%20%20%20%20%20%20%20%20def%20r(self%2C%20t)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20sum(curve.r(t)%20for%20curve%20in%20self.curves)%0A%0A%20%20%20%20def%20create_perturbed_curve(curve%2C%20start%2C%20end%2C%20amount)%3A%0A%20%20%20%20%20%20%20%20pert_curv%20%3D%20FlatIRCurve(np.array(%5Bstart%2C%20end%2C%20np.max(curve.ts)%5D)%2C%20np.array(%5B0.0%2C%20amount%2C%200.0%5D))%0A%20%20%20%20%20%20%20%20return%20SumCurve(curve%2C%20pert_curv)%0A%20%20%20%20return%20(create_perturbed_curve%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20To%20test%20this%20is%20working%2C%20we%20can%20see%20what%20happens%20when%20we%20bump%20the%20continuously-compounded%20rate%20by%205%25%20between%20years%205%20and%2010%20for%20example.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20create_perturbed_curve%2C%20np%2C%20plt%2C%20swap_rate)%3A%0A%20%20%20%20perturbed_test%20%3D%20create_perturbed_curve(comp.value(%22usd_libor_curve%22)%2C%205.0%2C%2010.0%2C%200.05)%0A%20%20%20%20perturbed_test.plot_basic()%0A%20%20%20%20%23%20Add%20swap%20rate%20to%20plot%0A%20%20%20%20ts_pert%20%3D%20np.linspace(0.0%2C%2029.75%2C%20120)%0A%20%20%20%20plt.plot(ts_pert%2C%20np.vectorize(swap_rate)(0%2C%20ts_pert%2C%200.25%2C%20perturbed_test%2C%20perturbed_test)%2C%20label%3D%22Swap%20Rate%22)%0A%20%20%20%20plt.legend()%0A%20%20%20%20plt.gca()%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20That%20looks%20as%20we%20expect%2C%20so%20we%20can%20go%20ahead%20and%20add%20our%20new%20nodes%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20create_perturbed_curve%2C%20np)%3A%0A%20%20%20%20comp.add_node(%22usd_libor_curve_perturbation%22%2C%20value%3D(0%2C%201%2C%200.0))%0A%20%20%20%20comp.add_node(%0A%20%20%20%20%20%20%20%20%22usd_libor_curve_perturbed%22%2C%0A%20%20%20%20%20%20%20%20lambda%20usd_libor_curve%2C%20usd_libor_curve_perturbation%3A%20create_perturbed_curve(%0A%20%20%20%20%20%20%20%20%20%20%20%20usd_libor_curve%2C%20*usd_libor_curve_perturbation%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20)%0A%0A%20%20%20%20def%20create_curveset_perturbed(usd_libor_curve_perturbed%2C%20usd_ois_curve%2C%20usd_fitting_error)%3A%0A%20%20%20%20%20%20%20%20if%20np.max(np.abs(usd_fitting_error)%20%3E%200.00001)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20raise%20Exception(%22Fitting%20error%20%3E%200.1bps%22)%0A%20%20%20%20%20%20%20%20return%20%7B%22USD-LIBOR-3M%22%3A%20usd_libor_curve_perturbed%2C%20%22USD-OIS%22%3A%20usd_ois_curve%7D%0A%0A%20%20%20%20comp.add_node(%22curveset%22%2C%20create_curveset_perturbed)%0A%0A%20%20%20%20comp.draw(graph_attr%3D%7B%22size%22%3A%20%2212%22%7D)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20We%20can%20iterate%20over%20each%20year%2C%20and%20see%20the%20sensitivity%20of%20the%20portfolio%20to%20a%20change%20of%201bp%20in%20the%20forward%20swap%20rate%20for%20that%20year%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20np%2C%20plt%2C%20swap_rate)%3A%0A%20%20%20%20comp.compute(%22portfolio_val%22)%0A%20%20%20%20comp.insert(%22usd_libor_curve_perturbation%22%2C%20value%3D(0%2C%201%2C%200.0))%0A%20%20%20%20base_value%20%3D%20np.array(comp.value(%22portfolio_val%22))%0A%20%20%20%20pert_values%20%3D%20np.empty((30%2C%20base_value.shape%5B0%5D))%0A%20%20%20%20delta_value%20%3D%20np.empty((30%2C%20base_value.shape%5B0%5D))%0A%20%20%20%20ts_risk%20%3D%20np.arange(30)%0A%20%20%20%20for%20i%20in%20ts_risk%3A%0A%20%20%20%20%20%20%20%20comp.insert(%22usd_libor_curve_perturbation%22%2C%20value%3D(i%2C%20i%20%2B%201%2C%200.0001))%0A%20%20%20%20%20%20%20%20comp.compute(%22portfolio_val%22)%0A%20%20%20%20%20%20%20%20pert_values%5Bi%5D%20%3D%20comp.value(%22portfolio_val%22)%0A%20%20%20%20%20%20%20%20swap_rate_base%20%3D%20swap_rate(i%2C%20i%20%2B%201%2C%200.25%2C%20comp.value(%22usd_libor_curve%22)%2C%20comp.value(%22usd_ois_curve%22))%0A%20%20%20%20%20%20%20%20swap_rate_pert%20%3D%20swap_rate(i%2C%20i%20%2B%201%2C%200.25%2C%20comp.value(%22usd_libor_curve_perturbed%22)%2C%20comp.value(%22usd_ois_curve%22))%0A%20%20%20%20%20%20%20%20swap_rate_delta%20%3D%20swap_rate_pert%20-%20swap_rate_base%0A%20%20%20%20%20%20%20%20delta_value%5Bi%5D%20%3D%200.0001%20%2F%20swap_rate_delta%20*%20(pert_values%5Bi%5D%20-%20base_value)%0A%0A%20%20%20%20plt.bar(ts_risk%2C%20delta_value.sum(axis%3D1)%2C%20np.diff(np.concatenate(%5B%5B0%5D%2C%20ts_risk%5D)))%0A%20%20%20%20plt.gca()%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Conclusion%0A%0A%20%20%20%20This%20concludes%20our%20interest%20example.%20We%20have%20shown%20that%20Loman%20makes%20it%20easy%20to%20simultaneously%20calibrate%20interest%20rate%20curves%20to%20market%20quotes%2C%20to%20value%20portfolios%20of%20interest%20rate%20derivatives%2C%20and%20to%20produce%20bucketed%20sensitivities.%0A%0A%20%20%20%20The%20portfolio%20valuation%20also%20naturally%20extends%20to%20cover%20other%20interest%20rate%20derivatives.%0A%0A%20%20%20%20The%20calibration%20approach%20-%20an%20external%20routine%20driving%20Loman%20to%20produce%20calibrated%20inputs%20for%20the%20computation%20-%20easily%20and%20naturally%20extends%20to%20calibrating%20to%20a%20global%20set%20of%20interest%20rate%20markets%2C%20with%20curveset%20drawing%20from%20many%20separate%20independent%20and%20interdependent%20calibrations.%20It%20can%20also%20extend%20to%20other%20markets%2C%20such%20as%20FX%20derivatives%2C%20listed%20options%2C%20credit%20derivatives.%0A%0A%20%20%20%20Loman's%20ability%20to%20serialize%20computations%20allows%20the%20calibration%20to%20happen%20once%2C%20centrally%2C%20and%20be%20broadcast%20to%20user%20desktops%20for%20firm-wide%20consistent%20live%20valuation.%0A%0A%20%20%20%20In%20short%2C%20we%20have%20only%20scratched%20the%20surface%20of%20what%20is%20possible%20with%20Loman%20with%20this%20example%2C%20and%20we%20look%20forward%20to%20further%20exploration%20in%20the%20future.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
11ba9406513d3e2cf057384a75a7d1db0ae7f31afa2817a60afd7a147ee31529