In [1]:
%pylab
%matplotlib inline
%run jupyter_helpers
%run yc_framework
figure_width = 16
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();
PYBOR supports three different interpolation methods:
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()
In [6]:
curve_builder = CurveBuilder('engine_usd_gbp.xlsx', eval_date)
In [7]:
price_ladder = curve_builder.reprice(pricing_curvemap)
In [8]:
# Display:
figsize(figure_width, 4)
price_ladder.sublist('USD.LIBOR.3M').dataframe()
Out[8]:
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();
In [10]:
build_output = curve_builder.build_curves(price_ladder)
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();
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();
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 (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 (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]: