Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions python/rateslib/legs/fixed.py
Original file line number Diff line number Diff line change
Expand Up @@ -1066,8 +1066,22 @@ def spread(
settlement: datetime_ = NoInput(0),
forward: datetime_ = NoInput(0),
) -> DualTypes:
disc_curve_ = _disc_required_maybe_from_curve(rate_curve, disc_curve)
# scale target_npv accounting for notional exchanges
_ = self.fixed_rate
self.fixed_rate = 0.0
local_npv = self.local_npv(
rate_curve=rate_curve,
disc_curve=disc_curve,
index_curve=index_curve,
fx=fx,
forward=forward,
settlement=settlement,
)
self.fixed_rate = _
rate_target_npv = target_npv - local_npv

# evaluate settlement relative to ex-div
disc_curve_ = _disc_required_maybe_from_curve(rate_curve, disc_curve)
if not isinstance(settlement, NoInput):
if settlement > self.settlement_params.ex_dividend:
raise ZeroDivisionError(
Expand All @@ -1082,19 +1096,22 @@ def spread(
else:
w_fwd = disc_curve_[forward]

immediate_target_npv = target_npv * w_fwd
immediate_target_npv = rate_target_npv * w_fwd
unindexed_target_npv = immediate_target_npv / self._regular_periods[0].index_up(
1.0, index_curve=index_curve
)
unindexed_reference_target_npv = unindexed_target_npv / self._regular_periods[
0
].convert_deliverable(1.0, fx=fx)
target_cashflow = (
unindexed_reference_target_npv / disc_curve_[self.settlement_params.payment]
)

f = self.schedule.periods_per_annum
d = self._regular_periods[0].dcf
N = self.settlement_params.notional
w = disc_curve_[self.settlement_params.payment]
R = ((-unindexed_reference_target_npv / (N * w) + 1) ** (1 / (d * f)) - 1) * f * 10000.0

R = ((-target_cashflow / N + 1) ** (1 / (d * f)) - 1) * f * 10000.0
return R


Expand Down
46 changes: 42 additions & 4 deletions python/tests/legs/test_legs_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,30 @@ def test_zero_fixed_spread(self, settlement, forward, exp, curve) -> None:
)
assert abs(result / 100 - exp) < 1e-3

@pytest.mark.parametrize("final_exchange", [False, True])
def test_zero_fixed_spread_exchanges(self, curve, final_exchange) -> None:
zfl = ZeroFixedLeg(
schedule=Schedule(
effective=dt(2022, 1, 5),
termination="8m",
payment_lag=0,
frequency="M",
),
notional=-1e8,
convention="ActAct",
final_exchange=final_exchange,
fixed_rate=NoInput(0),
)
result = zfl.spread(
target_npv=50000.0 + 1e8 * curve[dt(2022, 9, 5)] * final_exchange, rate_curve=curve
)
expected = 7.718420018560934 # bps
assert abs(result - expected) < 1e-8

zfl.fixed_rate = expected / 100.0
result = zfl.npv(rate_curve=curve)
assert abs(result - (50000.0 + 1e8 * curve[dt(2022, 9, 5)] * final_exchange)) < 1e-7

def test_zero_fixed_spread_raises_settlement(self, curve) -> None:
zfl = ZeroFixedLeg(
schedule=Schedule(
Expand All @@ -1384,7 +1408,8 @@ def test_zero_fixed_spread_raises_settlement(self, curve) -> None:
forward=NoInput(0),
)

def test_zero_fixed_spread_indexed(self, curve) -> None:
@pytest.mark.parametrize("final_exchange", [False, True])
def test_zero_fixed_spread_indexed(self, curve, final_exchange) -> None:
zfl = ZeroFixedLeg(
schedule=Schedule(
effective=dt(2022, 1, 1),
Expand All @@ -1395,17 +1420,24 @@ def test_zero_fixed_spread_indexed(self, curve) -> None:
notional=-1e8,
convention="ActAct",
fixed_rate=NoInput(0),
final_exchange=final_exchange,
index_base=100.0,
index_fixings=110.0,
)
target_npv = (13140821.29 + 1e8 * 1.1 * final_exchange) * curve[dt(2027, 1, 1)]
result = zfl.spread(
target_npv=13140821.29 * curve[dt(2027, 1, 1)],
target_npv=target_npv,
rate_curve=NoInput(0),
disc_curve=curve,
)
assert abs(result / 100 - 2.2826266057484057) < 1e-3

def test_zero_fixed_spread_non_deliverable(self, curve) -> None:
zfl.fixed_rate = result / 100.0
result = zfl.npv(rate_curve=curve)
assert abs(result - target_npv) < 1e-7

@pytest.mark.parametrize("final_exchange", [False, True])
def test_zero_fixed_spread_non_deliverable(self, curve, final_exchange) -> None:
zfl = ZeroFixedLeg(
schedule=Schedule(
effective=dt(2022, 1, 1),
Expand All @@ -1417,16 +1449,22 @@ def test_zero_fixed_spread_non_deliverable(self, curve) -> None:
convention="ActAct",
fixed_rate=NoInput(0),
currency="usd",
final_exchange=final_exchange,
pair="eurusd",
fx_fixings=2.0,
)
target_npv = (13140821.29 + 1e8 * 2.0 * final_exchange) * curve[dt(2027, 1, 1)]
result = zfl.spread(
target_npv=13140821.29 * curve[dt(2027, 1, 1)],
target_npv=target_npv,
rate_curve=NoInput(0),
disc_curve=curve,
)
assert abs(result / 100 - 1.2808477472765924) < 1e-3

zfl.fixed_rate = result / 100.0
result = zfl.npv(rate_curve=curve)
assert abs(result - target_npv) < 1e-7

def test_amortization_raises(self) -> None:
with pytest.raises(TypeError, match="unexpected keyword argument 'amortization'"):
ZeroFixedLeg(
Expand Down
2 changes: 1 addition & 1 deletion rust/scheduling/frequency/imm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ impl Imm {
let date = NaiveDate::from_ymd_opt(year, month, 1);
match date {
Some(d) => Ok(d.and_hms_opt(0, 0, 0).unwrap()),
None => {return Err(PyValueError::new_err("`year` or `month` out of range."))}
None => return Err(PyValueError::new_err("`year` or `month` out of range.")),
}
}
Imm::Leap => {
Expand Down
Loading