Skip to content

Commit

Permalink
PERF: re-organise oaspread for Dual and Dual2 type curves (#441)
Browse files Browse the repository at this point in the history
Co-authored-by: JHM Darbyshire (Win) <[email protected]>
  • Loading branch information
attack68 and attack68 authored Oct 11, 2024
1 parent 5f8094a commit 85544f2
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 53 deletions.
82 changes: 39 additions & 43 deletions python/rateslib/curves/curves.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,47 +758,45 @@ def shift(

else: # use non-composite method, which is faster but does not preserve a dynamic spread.
# Make sure base curve ADorder matches the spread ADorder. Floats are universal
if isinstance(spread, Dual) and self.ad != 1:
base_ = self.copy()
base_._set_ad_order(1)
elif isinstance(spread, Dual2) and self.ad != 2:
base_ = self.copy()
base_._set_ad_order(2)
else:
base_ = self # underlying curve ADorder is unchanged

v1v2 = [1.0] * (base_.n - 1)
n = [0] * (base_.n - 1)
d = 1 / 365 if base_.convention.upper() != "ACT360" else 1 / 360
v_new = [1.0] * (base_.n)
for i, (k, v) in enumerate(base_.nodes.items()):
_ad = self.ad
if isinstance(spread, Dual):
self._set_ad_order(1)
elif isinstance(spread, Dual2):
self._set_ad_order(2)

v1v2 = [1.0] * (self.n - 1)
n = [0] * (self.n - 1)
d = 1 / 365 if self.convention.upper() != "ACT360" else 1 / 360
v_new = [1.0] * (self.n)
for i, (k, v) in enumerate(self.nodes.items()):
if i == 0:
continue
n[i - 1] = (k - base_.node_dates[i - 1]).days
v1v2[i - 1] = (base_.nodes[base_.node_dates[i - 1]] / v) ** (1 / n[i - 1])
n[i - 1] = (k - self.node_dates[i - 1]).days
v1v2[i - 1] = (self.nodes[self.node_dates[i - 1]] / v) ** (1 / n[i - 1])
v_new[i] = v_new[i - 1] / (v1v2[i - 1] + d * spread / 10000) ** n[i - 1]

nodes = base_.nodes.copy()
nodes = self.nodes.copy()
for i, (k, _) in enumerate(nodes.items()):
nodes[k] = v_new[i]

kwargs = {}
if type(base_) is IndexCurve:
kwargs = {"index_base": base_.index_base, "index_lag": base_.index_lag}
_ = type(base_)(
if type(self) is IndexCurve:
kwargs = {"index_base": self.index_base, "index_lag": self.index_lag}
_ = type(self)(
nodes=nodes,
interpolation=base_.interpolation,
t=base_.t,
interpolation=self.interpolation,
t=self.t,
c=NoInput(0),
endpoints=base_.spline_endpoints,
endpoints=self.spline_endpoints,
id=id or uuid4().hex[:5] + "_", # 1 in a million clash,
convention=base_.convention,
modifier=base_.modifier,
calendar=base_.calendar,
ad=base_.ad,
convention=self.convention,
modifier=self.modifier,
calendar=self.calendar,
ad=self.ad,
**kwargs,
)
_.collateral = collateral
self._set_ad_order(_ad)
return _

def _translate_nodes(self, start: datetime):
Expand Down Expand Up @@ -1564,28 +1562,26 @@ def shift(
return super().shift(spread, id, composite, collateral)

# Make sure base curve ADorder matches the spread ADorder. Floats are universal
if isinstance(spread, Dual) and self.ad != 1:
base_ = self.copy()
base_._set_ad_order(1)
elif isinstance(spread, Dual2) and self.ad != 2:
base_ = self.copy()
base_._set_ad_order(2)
else:
base_ = self # underlying curve ADorder is unchanged
_ad = self.ad
if isinstance(spread, Dual):
self._set_ad_order(1)
elif isinstance(spread, Dual2):
self._set_ad_order(2)

_ = LineCurve(
nodes={k: v + spread / 100 for k, v in base_.nodes.items()},
interpolation=base_.interpolation,
t=base_.t,
nodes={k: v + spread / 100 for k, v in self.nodes.items()},
interpolation=self.interpolation,
t=self.t,
c=NoInput(0),
endpoints=base_.spline_endpoints,
endpoints=self.spline_endpoints,
id=id,
convention=base_.convention,
modifier=base_.modifier,
calendar=base_.calendar,
ad=base_.ad,
convention=self.convention,
modifier=self.modifier,
calendar=self.calendar,
ad=self.ad,
)
_.collateral = collateral
self._set_ad_order(_ad)
return _

def _translate_nodes(self, start: datetime):
Expand Down
26 changes: 18 additions & 8 deletions python/rateslib/instruments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,22 +715,31 @@ def oaspread(
base,
self.leg1.currency,
)
ad_ = curves[1].ad
metric = "dirty_price" if dirty else "clean_price"

curves[1]._set_ad_order(1)
# Create a discounting curve with ADOrder:1 exposure to z_spread
disc_curve = curves[1].shift(Dual(0, ["z_spread"], []), composite=False)
npv_price = self.rate(curves=[curves[0], disc_curve], metric=metric)

# Get forecasting curve
if type(self).__name__ in ["FloatRateNote", "IndexFixedRateBond"]:
fore_curve = curves[0].copy()
fore_curve._set_ad_order(1)
elif type(self).__name__ in ["FixedRateBond", "Bill"]:
fore_curve = None
else:
raise TypeError("Method `oaspread` can only be called on Bond type securities.")

npv_price = self.rate(curves=[fore_curve, disc_curve], metric=metric)
# find a first order approximation of z
b = gradient(npv_price, ["z_spread"], 1)[0]
c = float(npv_price) - float(price)
z_hat = -c / b

# shift the curve to the first order approximation and fine tune with 2nd order approxim.
curves[1]._set_ad_order(2)
disc_curve = curves[1].shift(Dual2(z_hat, ["z_spread"], [], []), composite=False)
npv_price = self.rate(curves=[curves[0], disc_curve], metric=metric)
if fore_curve is not None:
fore_curve._set_ad_order(2)
npv_price = self.rate(curves=[fore_curve, disc_curve], metric=metric)
a, b, c = (
0.5 * gradient(npv_price, ["z_spread"], 2)[0][0],
gradient(npv_price, ["z_spread"], 1)[0],
Expand All @@ -739,15 +748,16 @@ def oaspread(
z_hat2 = quadratic_eqn(a, b, c, x0=-c / b)["g"]

# perform one final approximation albeit the additional price calculation slows calc time
curves[1]._set_ad_order(0)
disc_curve = curves[1].shift(z_hat + z_hat2, composite=False)
npv_price = self.rate(curves=[curves[0], disc_curve], metric=metric)
disc_curve._set_ad_order(0)
if fore_curve is not None:
fore_curve._set_ad_order(0)
npv_price = self.rate(curves=[fore_curve, disc_curve], metric=metric)
b = b + 2 * a * z_hat2 # forecast the new gradient
c = float(npv_price) - float(price)
z_hat3 = -c / b

z = z_hat + z_hat2 + z_hat3
curves[1]._set_ad_order(ad_)
return z


Expand Down
4 changes: 2 additions & 2 deletions python/tests/test_instruments_bonds.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,8 +734,8 @@ def test_fixed_rate_bond_duration(self, metric) -> None:
ex_div=7,
fixed_rate=8.0,
)
price0 = gilt.price(4.445, dt(1999, 5, 27))
price1 = gilt.price(4.446, dt(1999, 5, 27))
price0 = gilt.price(4.445, dt(1999, 5, 27), dirty=True)
price1 = gilt.price(4.446, dt(1999, 5, 27), dirty=True)
if metric == "risk":
numeric = price0 - price1
elif metric == "modified":
Expand Down

0 comments on commit 85544f2

Please sign in to comment.