Fixings#
Published financial fixings form a key part of the valuation of Instruments and for those used in Curve or Surface calibration. Rateslib cannot be distributed with financial data, therefore data management must be handled externally by the user.
But, some easy-to-use classes exist to provide the bridge between external data and the
objects constructed in rateslib. The global fixings object coordinates
this interaction and is a singleton-instance of the Fixings class.
The key attribute of this class is its loader which is, by default, an instance of the
DefaultFixingsLoader, but a user can replace this with their
own implementation to interact with their own data sources, if required.
DefaultFixingsLoader#
The DefaultFixingsLoader provides a way for a user to load
fixings directly into rateslib. Data can be populated in one of two ways. Either,
Manually, with Python objects, using the
add()method. This is often used for simple examples.From a datafile stored as a CSV. This must have a particular template format.
Let’s demonstrate the CSV method. Create a CSV in the required date-string format in the current working directory.
In [1]: df = DataFrame(
...: data=[1.21, 2.75],
...: index=Index(["02-01-2023", "03-01-2023"], name="reference_date"),
...: columns=["rate"]
...: )
...:
In [2]: print(df)
rate
reference_date
02-01-2023 1.21
03-01-2023 2.75
In [3]: df.to_csv("SEK_IBOR_3M.csv") # <--- Save a CSV file to disk
Now we set the directory of the DefaultFixingsLoader to point
to this folder and load the fixings directly.
In [4]: fixings.loader.directory = os.getcwd()
In [5]: fixings["SEK_IBOR_3M"]
Out[5]:
(-2254238477608731968,
reference_date
2023-01-02 1.21
2023-01-03 2.75
Name: rate, dtype: float64,
(Timestamp('2023-01-02 00:00:00'), Timestamp('2023-01-03 00:00:00')))
Note this __getitem__ mechanism loads; a data state id, the timeseries itself, and the range of the index. When an attempt is made for an unavailable dataset a ValueError is raised.
In [6]: try:
...: fixings["unavailable_data"]
...: except ValueError as e:
...: print(e)
...:
Fixing data for the index 'unavailable_data' has been attempted, but there is no file:
'unavailable_data.csv' located in the search directory.
For further info see the documentation section regarding `Fixings`.
It is also possible to add a dataseries created in Python directly to the fixings object. If
the state is not expressly given then a random state is set upon this insertion.
In [7]: ts = Series(index=[dt(2000, 1, 1), dt(2000, 2, 1)], data=[666., 667.])
In [8]: fixings.add("my_series", ts)
In [9]: fixings["MY_SERIES"]
Out[9]:
(-8929434576318580492,
reference_date
2000-01-01 666.00
2000-02-01 667.00
Name: rate, dtype: float64,
(Timestamp('2000-01-01 00:00:00'), Timestamp('2000-02-01 00:00:00')))
Any data, regardless of the loading method, can be directly removed:
In [10]: fixings.pop("My_Series")
Out[10]:
reference_date
2000-01-01 666.00
2000-02-01 667.00
Name: rate, dtype: float64
Lowercase and uppercase are ignored.
Relevant dates and objects#
To date, rateslib makes use of 3 classifications of fixing;
index, and the object
IndexFixing,fx, and the object
FXFixing,rate, and the objects
IBORFixing,IBORStubFixing,RFRFixing.
The use case for an IndexFixing is for inflation
related Instruments.
The relevant date for an IndexFixing is its reference value date.
Determining the value of an IndexFixing depends upon other
information such as the index_lag, and index_method.
Important
Inflation data, and other monthly data, must be indexed in the Series according to the start of the month relating to the data publication.
As an example the following data in 2025 for the UK RPI series was released:
Publication |
Month |
Value |
|---|---|---|
18th June |
May |
402.9 |
16th July |
June |
404.5 |
20th Aug |
July |
406.2 |
This must be entered into rateslib as the following dates:
In [11]: uk_rpi = Series(
....: index=[dt(2025, 5, 1), dt(2025, 6, 1), dt(2025, 7, 1)],
....: data=[402.9, 404.5, 406.2]
....: )
....:
In [12]: fixings.add("UK_RPI", uk_rpi)
This data is then sufficient to populate some IndexFixing
values. The below fixing has a reference value date of 12-Sep, with a 3-month lag and
therefore is linearly interpolated between the Jun and July values and should be
approximately 405.
In [13]: index_fixing = IndexFixing(
....: index_lag=3,
....: index_method="daily",
....: date=dt(2025, 9, 12),
....: identifier="UK_RPI"
....: )
....:
In [14]: index_fixing.value
Out[14]: np.float64(405.12333333333333)
The use case for an FXFixing is multi-currency Instruments,
and non-deliverability.
Important
The relevant date for an FX Fixing is its publication date. Any identifier
will have the currency pair appended to it. Series should be added according to the
following format and must contain this suffix: ‘{identifier}_{ccy1ccy2}’.
Rateslib makes every effort to accurately implement the methodology as described for
the WMR FX Benchmarks and the ISDA MTM Matrix, whilst abstracting many of the
conventions. This is why an FXFixing can be instantiated
with a delivery and/or a publication date. Each can, in general under the appropriate
conventions, be implied from the other.
Suppose the following values are published at 11am London time in following base majors (noting the 6th January is a Stockholm holiday).
Publication |
EURUSD Spot |
Value |
EURSEK Spot |
Value |
USDSEK Cross |
|---|---|---|---|---|---|
2nd Jan |
6th Jan |
1.165 |
7th Jan |
10.79 |
9.26180 |
5th Jan |
7th Jan |
1.168 |
8th Jan |
10.81 |
9.25514 |
6th Jan |
8th Jan |
1.148 |
8th Jan |
10.72 |
9.33798 |
This data must be keyed by the format: “{series_name}_{pair}” and is thus entered into rateslib as the following dates:
In [15]: fixings.add("WMR_11AMLDN_EURUSD", Series(
....: index=[dt(2026, 1, 2), dt(2026, 1, 5), dt(2026, 1, 6)],
....: data=[1.165, 1.168, 1.148]
....: ))
....:
In [16]: fixings.add("WMR_11AMLDN_EURSEK", Series(
....: index=[dt(2026, 1, 2), dt(2026, 1, 5), dt(2026, 1, 6)],
....: data=[10.79, 10.81, 10.72]
....: ))
....:
This data is sufficient to populate a ‘USDSEK’ crossed
FXFixing value.
In [17]: fx_fixing = FXFixing(
....: publication=dt(2026, 1, 5),
....: identifier="WMR_11AMLDN",
....: fx_index="usdsek" # <-- this genererates an `FXIndex` from `defaults`
....: )
....:
In [18]: fx_fixing.value
Out[18]: np.float64(9.255136986301371)
Prohibiting crosses
In a pricing environment, in order to price these USDSEK fixings a EUR market is required becuase the EURUSD and EURSEK rates are both needed separately to forecast the cross.
This may not be desired. In this instance the FXIndex may
be specifically defined to not allow_cross. In this case only the USDSEK rate is
required and any fixing series must be populated directly.
In [19]: fixings.add("WMR_11AMLDN_USDSEK", Series(
....: index=[dt(2026, 1, 2), dt(2026, 1, 5), dt(2026, 1, 6)],
....: data=[9.2618, 9.999, 9.33798]
....: ))
....:
In [20]: fx_fixing = FXFixing(
....: publication=dt(2026, 1, 5),
....: identifier="WMR_11AMLDN",
....: fx_index=FXIndex(
....: pair="usdsek",
....: calendar="stk|fed",
....: settle=2,
....: allow_cross=False
....: )
....: )
....:
In [21]: fx_fixing.value # <-- data is taken directly from the above series
Out[21]: np.float64(9.999)
Important
The relevant date for an IBORFixing or
IBORStubFixing is its publication date, whilst the
relevant date for an RFRFixing is its reference value
date. These difference generally reflect market conventions for handling this data.
Rates fixings are handled slightly differently to other fixings types since there is the
notion of a FloatRateSeries which is a particular set of
conventions for multiple, different tenor FloatRateIndex.
Important
When rates fixings are populated, their identifier must contain a suffix which matches the frequency of the index. Internally rateslib uses these identifiers to match the right fixing frequency to the right dataset.
IBOR
Suppose the following data is observed for EURIBOR in 2025
Publication |
Value Date |
EURIBOR 3M |
EURIBOR 6M |
|---|---|---|---|
11th Sep |
15th Sep |
2.014 |
2.119 |
12th Sep |
16th Sep |
2.000 |
2.108 |
15th Sep |
17th Sep |
2.033 |
2.101 |
This is populated to rateslib by publication date as follows,
In [22]: euribor_3m = Series(
....: index=[dt(2025, 9, 11), dt(2025, 9, 12), dt(2025, 9, 15)],
....: data=[2.014, 2.000, 2.033]
....: )
....:
In [23]: euribor_6m = Series(
....: index=[dt(2025, 9, 11), dt(2025, 9, 12), dt(2025, 9, 15)],
....: data=[2.119, 2.108, 2.101]
....: )
....:
In [24]: fixings.add("EURIBOR_3M", euribor_3m)
In [25]: fixings.add("EURIBOR_6M", euribor_6m)
These can populate the the following fixing values.
In [26]: ibor_fixing=IBORFixing(
....: accrual_start=dt(2025, 9, 16),
....: identifier="EURIBOR_3M",
....: rate_index=FloatRateIndex(
....: frequency="Q",
....: series="eur_ibor"
....: )
....: )
....:
In [27]: ibor_fixing.value
Out[27]: np.float64(2.0)
In [28]: ibor_stub_fixing=IBORStubFixing(
....: accrual_start=dt(2025, 9, 16),
....: accrual_end=dt(2026, 1, 22),
....: identifier="EURIBOR",
....: rate_series="eur_ibor",
....: )
....:
In [29]: ibor_stub_fixing.value
Out[29]: np.float64(2.0444)
Note that these individual objects also store attributes that might be useful to inspect (see the individual class documentation). For example
In [30]: ibor_stub_fixing.fixing1.value
Out[30]: np.float64(2.0)
In [31]: ibor_stub_fixing.fixing2.value
Out[31]: np.float64(2.108)
In [32]: ibor_stub_fixing.weights
Out[32]: (0.5888888888888889, 0.4111111111111111)
RFR
An RFRFixing is a determined rate fixing for an entire
Period, not just one single RFR publication. This means that it must account for the
fixing_method, spread_compound_method and float_spread. If there are not
sufficient publications to determine a full Period fixing it will not be assigned a value.
Suppose the following data is observed for ESTR in 2025
Publication |
Value Date |
ESTR 1B |
|---|---|---|
15th Sep |
12th Sep |
1.91 |
16th Sep |
15th Sep |
1.92 |
17th Sep |
16th Sep |
1.93 |
This is populated to rateslib by reference value date as follows,
In [33]: estr_1b = Series(
....: index=[dt(2025, 9, 12), dt(2025, 9, 15), dt(2025, 9, 16)],
....: data=[1.91, 1.92, 1.93]
....: )
....:
In [34]: fixings.add("ESTR_1B", estr_1b)
The following Period is fully specified and will determine a value.
In [35]: rfr_fixing = RFRFixing(
....: accrual_start=dt(2025, 9, 12),
....: accrual_end=dt(2025, 9, 19),
....: identifier="ESTR_1B",
....: spread_compound_method="NoneSimple",
....: fixing_method="RFRLockout",
....: method_param=2,
....: float_spread=100.0,
....: rate_index=FloatRateIndex(frequency="1B", series="eur_rfr")
....: )
....:
In [36]: rfr_fixing.value
Out[36]: np.float64(2.9202637862854033)
Again some attributes may be useful to inspect.
In [37]: rfr_fixing.populated
Out[37]:
2025-09-12 1.91
2025-09-15 1.92
2025-09-16 1.93
2025-09-17 1.93
2025-09-18 1.93
dtype: object
A Period which is not fully specified will not return a value, but may still contain information regarding some of the individual fixings related to the period.
In [38]: rfr_fixing = RFRFixing(
....: accrual_start=dt(2025, 9, 12),
....: accrual_end=dt(2025, 9, 19),
....: identifier="ESTR_1B",
....: spread_compound_method="NoneSimple",
....: fixing_method="RFRPaymentDelay",
....: method_param=0,
....: float_spread=100.0,
....: rate_index=FloatRateIndex(frequency="1B", series="eur_rfr")
....: )
....:
In [39]: rfr_fixing.value
Out[39]: <NoInput.blank: 0>
In [40]: rfr_fixing.populated
Out[40]:
2025-09-12 1.91
2025-09-15 1.92
2025-09-16 1.93
dtype: object
Custom Data Loading#
The _BaseFixingsLoader is the necessary subclass with the required
abstract base classes to design a customised user data loader.
However, the easiest way to do this is to inherit the methods of
the DefaultFixingsLoader and simply overload them.
Suppose you have an SQL database with fixings data and you want to fetch timeseries from there:
# this represents the SQL database
In [41]: MY_DATABASE = {
....: "SELECT S1 FROM DB": Series(index=[dt(2000, 1, 1)], data=[100.0]),
....: "SELECT S2 FROM DB": Series(index=[dt(2000, 1, 1)], data=[200.0]),
....: }
....:
Then you can ignore the CSV file loading system (i.e. have a folder with no CSV files in it)
and implement your own class. Make sure to set it as the loader.
In [42]: from rateslib.data.loader import DefaultFixingsLoader
In [43]: class MyFixingsLoader(DefaultFixingsLoader):
....: def __getitem__(self, query: str):
....: try:
....: return super().__getitem__(query) # <- let DFL handle data caching
....: except ValueError:
....: if query not in MY_DATABASE:
....: raise ValueError(f"'{query}' is not in MY_DATABASE")
....: result = MY_DATABASE[query]
....: data = (hash(os.urandom(8)), result, (result.index[0], result.index[-1]))
....: self.loaded[query.upper()] = data
....: return data
....:
In [44]: fixings.loader = MyFixingsLoader()
In [45]: fixings["SELECT S1 FROM DB"]
Out[45]:
(-4819701883534548653,
2000-01-01 100.00
dtype: float64,
(Timestamp('2000-01-01 00:00:00'), Timestamp('2000-01-01 00:00:00')))
This loaded state will persist until it is cleared and reloaded.
In [46]: fixings.loader._loaded = {}
In [47]: fixings["SELECT S1 FROM DB"]
Out[47]:
(-794224826908797044,
2000-01-01 100.00
dtype: float64,
(Timestamp('2000-01-01 00:00:00'), Timestamp('2000-01-01 00:00:00')))