diff --git a/python/rateslib/instruments/stir_future.py b/python/rateslib/instruments/stir_future.py index 77b76b436..aac5e5bff 100644 --- a/python/rateslib/instruments/stir_future.py +++ b/python/rateslib/instruments/stir_future.py @@ -369,6 +369,7 @@ def __init__( leg2_payment_lag=NoInput.inherit, leg2_ex_div=NoInput.inherit, leg2_convention=NoInput.inherit, + leg2_currency=NoInput.inherit, fixed_rate=NoInput(0) if isinstance(price, NoInput) else 100 - price, vol=_Vol(), ) @@ -451,6 +452,7 @@ def _set_pricing_mid( solver=solver, settlement=settlement, forward=forward, + metric="rate", ) self.leg1.fixed_rate = _dual_float(mid_market_rate) @@ -468,28 +470,8 @@ def rate( ) -> DualTypes: c = _parse_curves(self, curves, solver) leg2_rate_curve = _get_curve("leg2_rate_curve", True, True, *c) - leg2_disc_curve = _get_curve("leg2_disc_curve", False, True, *c) - disc_curve = _get_curve("disc_curve", False, True, *c) - metric_ = _drb(self.kwargs.meta["metric"], metric).lower() - - leg2_npv: DualTypes = self.leg2.local_npv( - rate_curve=leg2_rate_curve, - disc_curve=leg2_disc_curve, - settlement=settlement, - forward=forward, - ) - rate = ( - self.leg1.spread( - target_npv=-leg2_npv, - rate_curve=NoInput(0), - disc_curve=disc_curve, - index_curve=NoInput(0), - settlement=settlement, - forward=forward, - ) - / 100 - ) + rate = self.leg2._regular_periods[0].rate(rate_curve=leg2_rate_curve) if metric_ == "price": return 100 - rate elif metric_ == "rate": diff --git a/python/tests/instruments/test_instruments_legacy.py b/python/tests/instruments/test_instruments_legacy.py index b3d59fdd5..d4d2c5da2 100644 --- a/python/tests/instruments/test_instruments_legacy.py +++ b/python/tests/instruments/test_instruments_legacy.py @@ -1068,13 +1068,6 @@ def test_null_priced_delta_round_trip_one_pricing_param_fx_fix(self, inst, param [ CDS(dt(2022, 7, 1), "3M", "Q", notional=1e6 * 25 / 14.91357), IRS(dt(2022, 7, 1), "3M", "A", notional=1e6), - STIRFuture( - dt(2022, 3, 16), - dt(2022, 6, 15), - "Q", - spec="usd_stir", - contracts=-1, - ), FRA(dt(2022, 7, 1), "3M", "A", notional=1e6), SBS( dt(2022, 7, 1), @@ -1247,6 +1240,84 @@ def test_set_pricing_does_not_overwrite_unpriced_status(self, inst): assert rate1 != rate2 assert abs(npv2) < 1e-8 + @pytest.mark.parametrize( + "inst", + [ + STIRFuture( + dt(2022, 3, 16), + dt(2022, 6, 15), + "Q", + spec="usd_stir", + contracts=-1, + ), + # # TODO add a null price test for ZCIS + ], + ) + def test_set_pricing_does_not_overwrite_unpriced_status_single_currency_inst(self, inst): + # unpriced instruments run a `set_pricing_mid` method + # this test ensures that after that run the price is not permanently set and + # will reset when priced from an alternative set of curves. + # test is slightly different to null_priced_delta: uses fx and includes rate call + curve1 = Curve({dt(2022, 1, 1): 1.0, dt(2024, 1, 1): 0.99}, index_base=66) + curve2 = Curve({dt(2022, 1, 1): 1.0, dt(2024, 1, 1): 0.98}, index_base=66) + curve3 = Curve({dt(2022, 1, 1): 1.0, dt(2024, 1, 1): 0.97}) + curve4 = Curve({dt(2022, 1, 1): 1.0, dt(2024, 1, 1): 0.96}, index_base=80) + curve5 = Curve({dt(2022, 1, 1): 1.0, dt(2024, 1, 1): 0.95}, index_base=80) + curve6 = Curve({dt(2022, 1, 1): 1.0, dt(2024, 1, 1): 0.94}) + fxr1 = FXRates({"eurusd": 1.0}, settlement=dt(2022, 1, 1)) + fxr2 = FXRates({"eurusd": 1.5}, settlement=dt(2022, 1, 1)) + fxf1 = FXForwards(fxr1, {"usdusd": curve1, "eureur": curve2, "eurusd": curve3}) + fxf2 = FXForwards(fxr2, {"usdusd": curve4, "eureur": curve5, "eurusd": curve6}) + + rate1 = inst.rate( + curves=dict( + rate_curve=curve1, + disc_curve=curve1, + index_curve=curve1, + leg2_rate_curve=curve2, + leg2_disc_curve=curve1, + leg2_index_curve=curve2, + ), + fx=fxf1, + ) + npv1 = inst.npv( + curves=dict( + rate_curve=curve1, + disc_curve=curve1, + index_curve=curve1, + leg2_rate_curve=curve2, + leg2_disc_curve=curve1, + leg2_index_curve=curve2, + ), + fx=fxf1, + ) + assert abs(npv1) < 1e-8 + + rate2 = inst.rate( + curves=dict( + rate_curve=curve4, + disc_curve=curve4, + index_curve=curve4, + leg2_rate_curve=curve5, + leg2_disc_curve=curve4, + leg2_index_curve=curve5, + ), + fx=fxf2, + ) + npv2 = inst.npv( + curves=dict( + rate_curve=curve4, + disc_curve=curve4, + index_curve=curve4, + leg2_rate_curve=curve5, + leg2_disc_curve=curve4, + leg2_index_curve=curve5, + ), + fx=fxf2, + ) + assert rate1 != rate2 + assert abs(npv2) < 1e-8 + class TestIRS: @pytest.mark.parametrize( @@ -5494,7 +5565,7 @@ def test_stir_npv_currency_bug(self) -> None: spec="usd_stir", contracts=-1, ) - result = stir.npv(curves=[c1, c1, c2, c3], fx=fxf) + result = stir.npv(curves=[None, c1, c2, c1], fx=fxf) assert abs(result) < 1e-7 def test_stir_npv_fx(self) -> None: @@ -5580,8 +5651,9 @@ def test_fixings_table(self, curve): ) def test_3m_spec_contracts(self, spec, expected, curve): stir = STIRFuture(get_imm(3, 2022), get_imm(6, 2022), spec=spec) - result = stir.analytic_delta(curves=curve) - assert abs(result - expected) < 1e-10 + for leg in [1, 2]: + result = stir.analytic_delta(curves=curve, leg=leg) + assert abs(result - expected * (1.5 - leg) * 2) < 1e-10 @pytest.mark.parametrize( ("spec", "expected"), @@ -5592,8 +5664,9 @@ def test_3m_spec_contracts(self, spec, expected, curve): ) def test_1m_spec_contracts(self, spec, expected, curve): stir = STIRFuture(dt(2022, 4, 1), dt(2022, 5, 1), spec=spec) - result = stir.analytic_delta(curves=curve) - assert abs(result - expected) < 1e-10 + for leg in [1, 2]: + result = stir.analytic_delta(curves=curve, leg=leg) + assert abs(result - expected * (1.5 - leg) * 2) < 1e-10 def test_cashflows(self, curve): stir = STIRFuture( @@ -5609,6 +5682,350 @@ def test_cashflows(self, curve): assert result2["Payment"].iloc[0] == dt(2022, 1, 1) assert result2["DF"].iloc[0] == 1.0 + def test_edsp_check(self): + # https://www.fmxfutures.com/wp-content/uploads/2025/09/SOFR-Final-Settlement-Report-9_17_2025.pdf + name = str(hash(os.urandom(3))) + fixings.add( + name + "_1B", + Series( + index=[ + dt.strptime(_, "%m/%d/%Y") + for _ in [ + "06/30/2025", + "06/27/2025", + "06/26/2025", + "06/25/2025", + "06/24/2025", + "06/23/2025", + "06/20/2025", + "06/18/2025", + "06/17/2025", + "06/16/2025", + "06/13/2025", + "06/12/2025", + "06/11/2025", + "06/10/2025", + "06/09/2025", + "06/06/2025", + "06/05/2025", + "06/04/2025", + "06/03/2025", + "06/02/2025", + "05/30/2025", + "05/29/2025", + "05/28/2025", + "05/27/2025", + "05/23/2025", + "05/22/2025", + "05/21/2025", + "05/20/2025", + "05/19/2025", + "05/16/2025", + "05/15/2025", + "05/14/2025", + "05/13/2025", + "05/12/2025", + "05/09/2025", + "05/08/2025", + "05/07/2025", + "05/06/2025", + "05/05/2025", + "05/02/2025", + "05/01/2025", + "04/30/2025", + "04/29/2025", + "04/28/2025", + "04/25/2025", + "04/24/2025", + "04/23/2025", + "04/22/2025", + "04/21/2025", + "04/17/2025", + "04/16/2025", + "04/15/2025", + "04/14/2025", + "04/11/2025", + "04/10/2025", + "04/09/2025", + "04/08/2025", + "04/07/2025", + "04/04/2025", + "04/03/2025", + "04/02/2025", + "04/01/2025", + "03/31/2025", + "03/28/2025", + "03/27/2025", + "03/26/2025", + "03/25/2025", + "03/24/2025", + "03/21/2025", + "03/20/2025", + "03/19/2025", + "03/18/2025", + "03/17/2025", + "03/14/2025", + "03/13/2025", + "03/12/2025", + "03/11/2025", + "03/10/2025", + "03/07/2025", + "03/06/2025", + "03/05/2025", + "03/04/2025", + "03/03/2025", + ] + ], + data=[ + 4.45, + 4.39, + 4.4, + 4.36, + 4.3, + 4.29, + 4.29, + 4.28, + 4.31, + 4.32, + 4.28, + 4.28, + 4.28, + 4.28, + 4.29, + 4.29, + 4.29, + 4.28, + 4.32, + 4.35, + 4.35, + 4.33, + 4.33, + 4.31, + 4.26, + 4.26, + 4.26, + 4.27, + 4.29, + 4.3, + 4.31, + 4.29, + 4.3, + 4.28, + 4.28, + 4.29, + 4.3, + 4.32, + 4.33, + 4.36, + 4.39, + 4.41, + 4.36, + 4.36, + 4.33, + 4.29, + 4.28, + 4.3, + 4.32, + 4.32, + 4.31, + 4.36, + 4.33, + 4.33, + 4.37, + 4.42, + 4.4, + 4.33, + 4.35, + 4.39, + 4.37, + 4.39, + 4.41, + 4.34, + 4.36, + 4.35, + 4.33, + 4.31, + 4.3, + 4.29, + 4.29, + 4.31, + 4.32, + 4.3, + 4.3, + 4.31, + 4.32, + 4.33, + 4.34, + 4.35, + 4.34, + 4.33, + 4.33, + ], + ), + ) + inst = STIRFuture( + get_imm(code="H25"), + get_imm(code="M25"), + spec="usd_stir", + leg2_rate_fixings=name, + ) + result = inst.rate() + edsp = 100 - round(result, 4) + fixings.pop(name + "_1B") + assert edsp == 95.6577 + + def test_edsp_check2(self): + # https://www.cmegroup.com/content/dam/cmegroup/education/files/sofr-futures-settlement-calculation-methodologies.pdf + name = str(hash(os.urandom(3))) + fixings.add( + name + "_1B", + Series( + index=[ + dt.strptime(_, "%Y-%m-%d") + for _ in [ + "2017-06-21", + "2017-06-22", + "2017-06-23", + "2017-06-26", + "2017-06-27", + "2017-06-28", + "2017-06-29", + "2017-06-30", + "2017-07-03", + "2017-07-05", + "2017-07-06", + "2017-07-07", + "2017-07-10", + "2017-07-11", + "2017-07-12", + "2017-07-13", + "2017-07-14", + "2017-07-17", + "2017-07-18", + "2017-07-19", + "2017-07-20", + "2017-07-21", + "2017-07-24", + "2017-07-25", + "2017-07-26", + "2017-07-27", + "2017-07-28", + "2017-07-31", + "2017-08-01", + "2017-08-02", + "2017-08-03", + "2017-08-04", + "2017-08-07", + "2017-08-08", + "2017-08-09", + "2017-08-10", + "2017-08-11", + "2017-08-14", + "2017-08-15", + "2017-08-16", + "2017-08-17", + "2017-08-18", + "2017-08-21", + "2017-08-22", + "2017-08-23", + "2017-08-24", + "2017-08-25", + "2017-08-28", + "2017-08-29", + "2017-08-30", + "2017-08-31", + "2017-09-01", + "2017-09-05", + "2017-09-06", + "2017-09-07", + "2017-09-08", + "2017-09-11", + "2017-09-12", + "2017-09-13", + "2017-09-14", + "2017-09-15", + "2017-09-18", + "2017-09-19", + ] + ], + data=[ + 1.02, + 1.02, + 1.06, + 1.05, + 1.03, + 1.04, + 1.07, + 1.2, + 1.1, + 1.05, + 1.03, + 1.01, + 1.01, + 1.01, + 1.01, + 1.02, + 1.02, + 1.04, + 1.02, + 1.01, + 1.02, + 1.01, + 1.05, + 1.04, + 1.04, + 1.04, + 1.03, + 1.08, + 1.03, + 1.01, + 1.01, + 1.01, + 1.01, + 1.01, + 1.01, + 1.03, + 1.05, + 1.08, + 1.11, + 1.09, + 1.09, + 1.06, + 1.04, + 1.02, + 1.02, + 1.08, + 1.05, + 1.05, + 1.03, + 1.03, + 1.14, + 1.09, + 1.05, + 1.03, + 1.04, + 1.04, + 1.04, + 1.05, + 1.05, + 1.09, + 1.1, + 1.04, + 1.01, + ], + ), + ) + inst = STIRFuture( + get_imm(code="M17"), + get_imm(code="U17"), + spec="usd_stir", + leg2_rate_fixings=name, + ) + result = inst.rate() + edsp = 100 - round(result, 4) + fixings.pop(name + "_1B") + assert edsp == 98.9495 + class TestPricingMechanism: def test_value(self, curve) -> None: