Skip to content

STIRFuture with leg2_rate_fixings=Series silently drops fixings for RFR instruments #1288

@conighion

Description

@conighion

Hi team — following up on #1189 where I noted that passing a Series to leg2_rate_fixings no longer worked. I've since traced the root cause and believe this is a bug in the Rust layer rather than intended behavior

Description:

When passing a pd.Series of daily fixings to leg2_rate_fixings on a STIRFuture with an RFR spec, the Series is silently dropped by the Rust layer and the instrument behaves as if no fixings were provided.

Reproduction:

import rateslib as rl
import pandas as pd
from pandas import Series

# Register a dummy fixings series in the global fixings object
fixings_series = Series(
    index=rl.get_calendar('nyc').bus_date_range(rl.dt(2025, 12, 17), rl.dt(2026, 2, 9)),
    data=4.15,
)
rl.fixings.add("SOFR_1B", fixings_series)

# Build a curve starting at 2026-02-09
curve = rl.Curve(
    nodes={rl.dt(2026, 2, 9): 1.0, rl.dt(2026, 6, 17): 0.99},
    convention="Act360",
)

# Case 1 - string identifier: WORKS
fut_str = rl.STIRFuture(
    effective=rl.dt(2025, 12, 17),
    termination=rl.dt(2026, 3, 18),
    spec="usd_stir",
    curves=curve,
    leg2_rate_fixings="sofr",
)
print(fut_str.rate(curves=curve))  # works

# Case 2 - Series: FAILS (silently dropped by Rust layer)
fut_srs = rl.STIRFuture(
    effective=rl.dt(2025, 12, 17),
    termination=rl.dt(2026, 3, 18),
    spec="usd_stir",
    curves=curve,
    leg2_rate_fixings=fixings_series,
)
print(fut_srs.rate(curves=curve))  # ValueError

The second case fails with:

ValueError: `effective` date for rate period is before the initial node date of the Curve.
If you are trying to calculate a rate for an historical FloatPeriod have you neglected to supply appropriate `fixings`?

Root cause:

The Rust function _init_FloatRateParams creates an RFRFixing object with .identifier (string) and .value (scalar) slots. A pd.Series doesn't fit either slot, so it's silently converted to NoInput. This causes _is_rfr_efficient (fixings.py:3332) to return True -> pure DF path -> fails for dates before the curve's initial node.

The Python code downstream is ready to handle this case. In _push_rate_fixings_as_series_to_fixing_rates (fixings.py:3179-3182):

if isinstance(rate_fixings, str):
    fixing_series = fixings[rate_fixings][1]  # global lookup
else:
    fixing_series = rate_fixings              # Series used directly

Both str and Series converge into the same fixing_rates.update(fixing_series) call, but the Series never reaches this code because the Rust intermediary drops it.

Use case:

Historical curve fitting where global fixings extend past the curve date. Passing a truncated Series would allow the Solver to use fixings only up to the curve date, keeping forward rates curve-implied.

Versions: rateslib 2.5.x, Python 3.14, Windows 11

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions