PYBOR

PYBOR is a multi-curve interest rate framework and risk engine based on multivariate optimization techniques, written in Python.

Copyright © 2017 Ondrej Martinsky, All rights reserved

www.github.com/omartinsky/pybor


In [1]:
%pylab
%matplotlib inline
%run jupyter_helpers
%run yc_framework
figure_width = 16


Using matplotlib backend: Qt5Agg
Populating the interactive namespace from numpy and matplotlib

Pricing Curve Map

Generate pricing curvemap using stochastic short-rate model $dr_t=a(b-r_t)dt + \sigma dW_t$ for curves and tenor/cross-currency basis spreads. This will be our starting point, the curves inside this curvemap will be used only to reprice market instruments.


In [2]:
eval_date = create_date('2017-01-03')

In [3]:
def generate_pricing_curvemap(eval_date):
    random.seed(0)
    pricing_curvemap = CurveMap()
    t = linspace(eval_date+0, eval_date+365*80, 7)
    def createCurve(name, r0, speed, mean, sigma):
        return CurveConstructor.FromShortRateModel(name, t, r0, speed, mean, sigma, interpolation=InterpolationMode.CUBIC_LOGDF)
    def createCurveFromSpread(baseCurve, name, r0, speed, mean, sigma):
        out = createCurve(name, r0, speed, mean, sigma)
        out.add_another_curve(baseCurve)
        return out
    u3m = createCurve('USD.LIBOR.3M', 0.02, 0.03, 0.035, 5e-4)
    u6m = createCurveFromSpread(u3m, 'USD.LIBOR.6M', 0.01, 0.03, 0.011, 5e-4)
    u12m = createCurveFromSpread(u6m, 'USD.LIBOR.12M', 0.01, 0.03, 0.011, 5e-4)
    g3m = createCurveFromSpread(u3m, 'GBP.LIBOR.3M', 0.03, 0.03, 0.0, 5e-4)
    u1b = createCurve('USD/USD.OIS', 0.01, 0.03, 0.011, 5e-4)
    g1b = createCurveFromSpread(u1b, 'GBP/GBP.SONIA', 0.005, 0.03, 0.005, 5e-4)
    gu1b = createCurveFromSpread(u1b, 'GBP/USD.OIS', 0.001, 0.03, 0.001, 5e-4)
    pricing_curvemap.add_curve(u3m)
    pricing_curvemap.add_curve(u6m)
    pricing_curvemap.add_curve(u12m)
    pricing_curvemap.add_curve(g3m)
    pricing_curvemap.add_curve(g1b)
    pricing_curvemap.add_curve(u1b)
    pricing_curvemap.add_curve(gu1b)
    return pricing_curvemap

In [4]:
pricing_curvemap = generate_pricing_curvemap(eval_date)
# Display:
figsize(figure_width, 6)
linestyle('solid'), pricing_curvemap.plot(), title('Pricing Curvemap'), legend(), show();


Interpolation Modes

PYBOR supports three different interpolation methods:

  • Linear interpolation of the logarithm of discount factors (aka piecewise-constant in forward-rate space)
  • Linear interpolation of the continuously-compounded zero-rates
  • Cubic interpolation of the logarithm of discount factors

Below is the curve interpolated in three different ways:


In [5]:
cloned_curve = deepcopy(pricing_curvemap['USD.LIBOR.3M'])
figsize(figure_width, 5), linestyle('solid'), title('Curve Interpolation Modes')
for i, interpolation in enumerate(InterpolationMode._member_map_.values()):
    cloned_curve.set_interpolator(interpolation)
    cloned_curve.plot(label=interpolation), legend()


Curve Builder

Create the curve builder. Definitions of curves and market instruments from which these curves are built are loaded from the excel spreadsheet


In [6]:
curve_builder = CurveBuilder('engine_usd_gbp.xlsx', eval_date)

Instrument Repricing

Use the curve builder (specifically instrument definitions which it contains) to reprice instruments from previously created pricing curve map.

Instrument prices are returned in a structure called price ladder


In [7]:
price_ladder = curve_builder.reprice(pricing_curvemap)

Display price ladder for a specific curve


In [8]:
# Display:
figsize(figure_width, 4)
price_ladder.sublist('USD.LIBOR.3M').dataframe()


Out[8]:
Price
USD.LIBOR.3M__Deposit__3M 1.296056
USD.LIBOR.3M__Future__1F_3M 98.678783
USD.LIBOR.3M__Future__2F_3M 98.644672
USD.LIBOR.3M__Future__3F_3M 98.613268
USD.LIBOR.3M__Future__4F_3M 98.581935
USD.LIBOR.3M__Future__5F_3M 98.550090
USD.LIBOR.3M__Future__6F_3M 98.518707
USD.LIBOR.3M__Future__7F_3M 98.487590
USD.LIBOR.3M__Future__8F_3M 98.456544
USD.LIBOR.3M__Future__9F_3M 98.424993
USD.LIBOR.3M__Future__10F_3M 98.393900
USD.LIBOR.3M__Future__11F_3M 98.363071
USD.LIBOR.3M__Future__12F_3M 98.332122
USD.LIBOR.3M__Future__13F_3M 98.301057
USD.LIBOR.3M__Swap__4Y 1.520952
USD.LIBOR.3M__Swap__5Y 1.576216
USD.LIBOR.3M__Swap__6Y 1.629577
USD.LIBOR.3M__Swap__7Y 1.681051
USD.LIBOR.3M__Swap__8Y 1.730786
USD.LIBOR.3M__Swap__9Y 1.778522
USD.LIBOR.3M__Swap__10Y 1.824412
USD.LIBOR.3M__Swap__11Y 1.868467
USD.LIBOR.3M__Swap__12Y 1.910815
USD.LIBOR.3M__Swap__13Y 1.951232
USD.LIBOR.3M__Swap__14Y 1.989850
USD.LIBOR.3M__Swap__15Y 2.026678
USD.LIBOR.3M__Swap__16Y 2.061823
USD.LIBOR.3M__Swap__17Y 2.095099
USD.LIBOR.3M__Swap__18Y 2.126618
USD.LIBOR.3M__Swap__19Y 2.156388
USD.LIBOR.3M__Swap__20Y 2.184495
USD.LIBOR.3M__Swap__25Y 2.298968
USD.LIBOR.3M__Swap__30Y 2.372879
USD.LIBOR.3M__Swap__35Y 2.421239
USD.LIBOR.3M__Swap__40Y 2.458937
USD.LIBOR.3M__Swap__45Y 2.493859
USD.LIBOR.3M__Swap__50Y 2.529655
USD.LIBOR.3M__Swap__60Y 2.607966
USD.LIBOR.3M__Swap__70Y 2.675223

Display instrument par-rates

Every instrument type has a specific relationship between the quoted price $P$ and the par-rate $r$. For instance:

For interest rate swaps, $P = 100 \times r$

For interest rate futures, $P = 10000 \times (1 - r)$

The relationship between interest rate curve in a zero-rate space and instrument par-rates is often a source of confusion for many people. The below is a graph which illustrates the difference between USD.LIBOR.3M pricing curve's zero rates vs. par-rates of instruments (e.g. deposits, futures, swaps), which are repriced using this curve. As we can see, only the par-rates of money market (deposit) instruments correspond to the curve points plotted in a zero-rate space.


In [9]:
figsize(figure_width, 6)
m, r = curve_builder.get_instrument_rates(price_ladder.sublist('USD.LIBOR.3M'))
m = [exceldate_to_pydate(int(i)) for i in m]
title('USD.LIBOR.3M instrument par-rates')
linestyle(' '), plot(m,r,marker='.', label='USD.LIBOR.3M instrument par-rates')
linestyle('-'), pricing_curvemap['USD.LIBOR.3M'].plot()
legend();


Curve Building

Build a brand new collection of curves from the instrument prices. This will take few seconds to complete ...


In [10]:
build_output = curve_builder.build_curves(price_ladder)


Solving stage 1/5 containing curves USD.LIBOR.3M, USD/USD.OIS (72 pillars)
Solving stage 2/5 containing curves USD.LIBOR.6M (30 pillars)
Solving stage 3/5 containing curves USD.LIBOR.12M (30 pillars)
Solving stage 4/5 containing curves GBP.LIBOR.3M, GBP/GBP.SONIA (72 pillars)
Solving stage 5/5 containing curves GBP/USD.OIS (30 pillars)
Done

Below is the comparison of curves which we have just built (solid lines) with pricing curves (dotted lines). These lines should be as close to each other as possible.


In [11]:
# Display:
figsize(figure_width, 6)
title('Curvebuilder output')
linestyle('solid'), build_output.output_curvemap.plot(), legend()
linestyle('dotted'), pricing_curvemap.plot();


Instrument/Pillar Jacobian Matrix

The optimizer is using gradient-descent method to minimize error between instrument par-rates calculated from the curves which are subject to this optimization and the input instrument par-rates. In order to do this, optimizer calculates derivative ${\delta (I-I') / \delta P}$, where $I$ is the actual instrument par-rate, $I'$ is the target instrument par-rate and $P$ is the pillar value from the curve (practically speaking, the discount factor).

Jacobian matrix which is a by-product of the curve building process can be then used for risk calculation purposes and it will be illustrated lated.


In [12]:
jacobian_dPdI = inv(build_output.jacobian_dIdP)
# Display:
figsize(figure_width, 8)
title("Jacobian Matrix"), xlabel('Pillars'), ylabel('Instruments')
imshow(jacobian_dPdI), colorbar();


Risk Calculator

Risk calculator is constructed from the curve builder (which contains curve definitions and market conventions) and build output (which contains curves and the jacobian matrix).


In [13]:
risk_calculator = RiskCalculator(curve_builder, build_output)

Let's define a convenience function which will bump par-rate of a specific instrument by the given amount of basis points and visualise the effect on all curves.


In [14]:
def visualise_bump(instrument_search_string, bumpsize):
    instruments, bumpsize = risk_calculator.find_instruments(instrument_search_string), bumpsize  
    curvemap_bump = risk_calculator.get_bumped_curvemap(instruments, bumpsize, BumpType.JACOBIAN_REBUILD)

    # Display:
    figsize(figure_width, 6)
    linestyle('solid'), build_output.output_curvemap.plot(), legend()
    linestyle('dashed'), curvemap_bump.plot()
    title("Effect of bumping instrument %s" % instrument_search_string)

Bumping Market Instruments

Bumping market instruments (such as those which define USD.LIBOR.3M neutral curve) will cause parallel shift of all other curves which are defined as a basis from this curve


In [15]:
visualise_bump('USD.LIBOR.3M__Swap__20Y', 1e-4)



In [16]:
visualise_bump('USD.LIBOR.3M.*', 15e-4)


Bumping Basis Instruments

Bumping basis instruments (USD.LIBOR.6M) will cause movement in a USD LIBOR 6M basis curve


In [17]:
visualise_bump('USD.LIBOR.6M__BasisSwap__20Y', 1e-4)



In [18]:
visualise_bump('USD.LIBOR.6M.*', 15e-4)



In [19]: