Skip to content

Commit 8c76b47

Browse files
authored
REF: FXFixings only derived via majors (#209) (#1124)
1 parent 6f09ed7 commit 8c76b47

File tree

8 files changed

+235
-71
lines changed

8 files changed

+235
-71
lines changed

python/rateslib/data/fixings.py

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ class FXFixing(_BaseFixing):
267267
The initial value for the fixing to adopt. Most commonly this is not given and it is
268268
determined from a timeseries of published FX rates.
269269
identifier: str, optional
270-
The string name of the timeseries to be loaded by the *Fixings* object.
270+
The string name of the series to be loaded by the *Fixings* object. Will be
271+
appended with "_{pair}" to derive the full timeseries key
271272
272273
Examples
273274
--------
@@ -281,14 +282,16 @@ class FXFixing(_BaseFixing):
281282
282283
.. ipython:: python
283284
284-
fixings.add("EURGBP-x89", Series(index=[dt(2000, 1, 1)], data=[0.905]))
285-
fxfix = FXFixing(date=dt(2000, 1, 1), pair="eurusd", identifier="EURGBP-x89")
286-
fxfix.value
285+
fixings.add("WMR_10AM_TYO_T+2_USDJPY", Series(index=[dt(2000, 1, 1)], data=[155.00]))
286+
fixings.add("WMR_10AM_TYO_T+2_AUDUSD", Series(index=[dt(2000, 1, 1)], data=[1.260]))
287+
fxfix = FXFixing(date=dt(2000, 1, 1), pair="audjpy", identifier="WMR_10AM_TYO_T+2")
288+
fxfix.value # <-- should be 1.26 * 155 = 202.5
287289
288290
.. ipython:: python
289291
:suppress:
290292
291-
fixings.pop("EURGBP-x89")
293+
fixings.pop("WMR_10AM_TYO_T+2_USDJPY")
294+
fixings.pop("WMR_10AM_TYO_T+2_AUDUSD")
292295
293296
"""
294297

@@ -301,6 +304,87 @@ def __init__(
301304
) -> None:
302305
super().__init__(date=date, value=value, identifier=identifier)
303306
self._pair = pair.lower()
307+
self._is_cross = "usd" not in self._pair
308+
309+
@property
310+
def is_cross(self) -> bool:
311+
"""Whether the fixing is a cross rate derived from other USD dominated fixings."""
312+
return self._is_cross
313+
314+
def _value_from_possible_inversion(self) -> DualTypes_:
315+
direct, inverted = self.pair, f"{self.pair[3:6]}{self.pair[0:3]}"
316+
try:
317+
state, timeseries, bounds = fixings.__getitem__(self._identifier + "_" + direct)
318+
exponent = 1.0
319+
except ValueError as e:
320+
try:
321+
state, timeseries, bounds = fixings.__getitem__(self._identifier + "_" + inverted)
322+
exponent = -1.0
323+
except ValueError:
324+
raise e
325+
326+
if state == self._state:
327+
return NoInput(0)
328+
else:
329+
self._state = state
330+
v = self._lookup_and_calculate(timeseries, bounds)
331+
if isinstance(v, NoInput):
332+
return NoInput(0)
333+
self._value = v**exponent
334+
return self._value
335+
336+
def _value_from_cross(self) -> DualTypes_:
337+
lhs1, lhs2 = "usd" + self.pair[:3], self.pair[:3] + "usd"
338+
try:
339+
state_l, timeseries_l, bounds_l = fixings.__getitem__(self._identifier + "_" + lhs1)
340+
exponent_l = -1.0
341+
except ValueError:
342+
try:
343+
state_l, timeseries_l, bounds_l = fixings.__getitem__(self._identifier + "_" + lhs2)
344+
exponent_l = 1.0
345+
except ValueError:
346+
raise ValueError(
347+
"The LHS cross currency has no available fixing series, either "
348+
f"{self._identifier + +'_' + lhs1} or {self._identifier + '_' + lhs2}"
349+
)
350+
351+
rhs1, rhs2 = "usd" + self.pair[3:], self.pair[3:] + "usd"
352+
try:
353+
state_r, timeseries_r, bounds_r = fixings.__getitem__(self._identifier + "_" + rhs1)
354+
exponent_r = 1.0
355+
except ValueError:
356+
try:
357+
state_r, timeseries_r, bounds_r = fixings.__getitem__(self._identifier + "_" + rhs2)
358+
exponent_r = -1.0
359+
except ValueError:
360+
raise ValueError(
361+
"The RHS cross currency has no available fixing series, either "
362+
f"{self._identifier + '_' + lhs1} or {self._identifier + '_' + lhs2}"
363+
)
364+
365+
if hash(state_l + state_r) == self._state:
366+
return NoInput(0)
367+
else:
368+
self._state = hash(state_l + state_r)
369+
v_l = self._lookup_and_calculate(timeseries_l, bounds_l)
370+
v_r = self._lookup_and_calculate(timeseries_r, bounds_r)
371+
if isinstance(v_l, NoInput) or isinstance(v_r, NoInput):
372+
return NoInput(0)
373+
self._value = v_l**exponent_l * v_r**exponent_r
374+
return self._value
375+
376+
@property
377+
def value(self) -> DualTypes_:
378+
if not isinstance(self._value, NoInput):
379+
return self._value
380+
else:
381+
if isinstance(self._identifier, NoInput):
382+
return NoInput(0)
383+
else:
384+
if self.is_cross:
385+
return self._value_from_cross()
386+
else:
387+
return self._value_from_possible_inversion()
304388

305389
def _lookup_and_calculate(
306390
self, timeseries: Series, bounds: tuple[datetime, datetime] | None

python/rateslib/instruments/irs.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,14 +253,14 @@ class IRS(_BaseInstrument):
253253
254254
.. ipython:: python
255255
256-
fixings.add("USDTHB", Series(index=[dt(2000, 7, 3), dt(2001, 1, 3)], data=[35.25, 37.0]))
256+
fixings.add("WMR_10AM_TYO_T+2_USDTHB", Series(index=[dt(2000, 7, 3), dt(2001, 1, 3)], data=[35.25, 37.0]))
257257
irs = IRS(
258258
effective=dt(2000, 1, 1),
259259
termination="2y",
260260
frequency="S",
261261
currency="usd", # <- USD set as the settlement currency
262262
pair="usdthb", # <- THB inferred as the reference currency
263-
fx_fixings="USDTHB",
263+
fx_fixings="WMR_10AM_TYO_T+2",
264264
fixed_rate=2.0,
265265
# all other arguments set as normal IRS
266266
)
@@ -269,7 +269,7 @@ class IRS(_BaseInstrument):
269269
.. ipython:: python
270270
:suppress:
271271
272-
fixings.pop("USDTHB")
272+
fixings.pop("WMR_10AM_TYO_T+2_USDTHB")
273273
274274
Further information is available in the documentation for a :class:`~rateslib.legs.FixedLeg`.
275275

python/rateslib/instruments/ndxcs.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class NDXCS(_BaseInstrument):
6262
6363
.. ipython:: python
6464
65-
fixings.add("USDINR", Series(index=[dt(2025, 1, 8), dt(2025, 7, 8)], data=[92.0, 92.5]))
65+
fixings.add("WMR_10AM_TY0_T+2_USDINR", Series(index=[dt(2025, 1, 8), dt(2025, 7, 8)], data=[92.0, 92.5]))
6666
ndxcs = NDXCS(
6767
effective=dt(2025, 1, 8),
6868
termination="1y",
@@ -71,7 +71,7 @@ class NDXCS(_BaseInstrument):
7171
pair="usdinr",
7272
notional=5e6, # <- INR Leg
7373
fixed=True,
74-
fx_fixings="USDINR",
74+
fx_fixings="WMR_10AM_TY0_T+2",
7575
leg2_fx_fixings=91.55, # <- USD Notional at execution
7676
payment_lag=0,
7777
)
@@ -323,11 +323,11 @@ class NDXCS(_BaseInstrument):
323323
effective=dt(2026, 1, 1),
324324
termination="18M",
325325
frequency="S",
326-
currency="usd", # <- USD settlement currency
327-
pair="usdinr", # <- INR reference currency implied
328-
notional=500e6, # <- Leg1 is based on the reference currency
329-
fx_fixings="USDINR", # <- Data series tag for FXFixings
330-
leg2_fx_fixings=92.0, # <- The USD Leg notional is implied as 5.43mm
326+
currency="usd", # <- USD settlement currency
327+
pair="usdinr", # <- INR reference currency implied
328+
notional=500e6, # <- Leg1 is based on the reference currency
329+
fx_fixings="WMR_10AM_TY0_T+2",
330+
leg2_fx_fixings=92.0, # <- The USD Leg notional is implied as 5.43mm
331331
)
332332
ndxcs.cashflows()
333333
@@ -356,18 +356,18 @@ class NDXCS(_BaseInstrument):
356356
357357
.. ipython:: python
358358
359-
fixings.add("USDCHF", Series(index=[dt(2025, 1, 8)], data=[0.9]))
359+
fixings.add("WMR_10AM_TY0_T+2_USDCHF", Series(index=[dt(2025, 1, 8)], data=[0.9]))
360360
ndxcs = NDXCS(
361361
effective=dt(2026, 1, 1),
362362
termination="18M",
363363
frequency="S",
364-
currency="usd", # <- USD settlement currency
365-
pair="usdinr", # <- INR reference currency 1 implied
366-
leg2_pair="usdchf", # <- CHF reference currency 2 implied
367-
notional=500e6, # <- Leg1 is based on the reference currency 1
368-
leg2_notional=500e6/125.0, # <- Leg2 entered directly in ref currency 2 units
369-
fx_fixings="USDINR", # <- Data series tag for FXFixings on Leg1
370-
leg2_fx_fixings="USDCHF", # <- Data series tag for FXFixings on Leg2
364+
currency="usd", # <- USD settlement currency
365+
pair="usdinr", # <- INR reference currency 1 implied
366+
leg2_pair="usdchf", # <- CHF reference currency 2 implied
367+
notional=500e6, # <- Leg1 is based on the reference currency 1
368+
leg2_notional=500e6/125.0, # <- Leg2 entered directly in ref currency 2 units
369+
fx_fixings="WMR_10AM_TY0_T+2", # <- Data series tag for FXFixings on Leg1
370+
leg2_fx_fixings="WMR_10AM_TY0_T+2", # <- Data series tag for FXFixings on Leg2
371371
)
372372
ndxcs.cashflows()
373373
@@ -376,8 +376,8 @@ class NDXCS(_BaseInstrument):
376376
.. ipython:: python
377377
:suppress:
378378
379-
fixings.pop("USDINR")
380-
fixings.pop("USDCHF")
379+
fixings.pop("WMR_10AM_TY0_T+2_USDINR")
380+
fixings.pop("WMR_10AM_TY0_T+2_USDCHF")
381381
382382
""" # noqa: E501
383383

python/rateslib/instruments/xcs.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,21 @@ class XCS(_BaseInstrument):
6161
6262
.. ipython:: python
6363
64-
fixings.add("EURUSD_1600_GMT", Series(index=[dt(2025, 4, 8)], data=[1.175]))
64+
fixings.add("WMR_10AM_TY0_T+2_EURUSD", Series(index=[dt(2025, 4, 8)], data=[1.175]))
6565
xcs = XCS(
6666
effective=dt(2025, 1, 8),
6767
termination="6m",
6868
spec="eurusd_xcs",
6969
notional=5e6,
70-
leg2_fx_fixings=(1.15, "EURUSD_1600_GMT"),
70+
leg2_fx_fixings=(1.15, "WMR_10AM_TY0_T+2"),
7171
leg2_mtm=True,
7272
)
7373
xcs.cashflows()
7474
7575
.. ipython:: python
7676
:suppress:
7777
78-
fixings.pop("EURUSD_1600_GMT")
78+
fixings.pop("WMR_10AM_TY0_T+2_EURUSD")
7979
8080
.. rubric:: Pricing
8181

python/rateslib/legs/fixed.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ class FixedLeg(_BaseLeg, _WithExDiv):
308308
309309
.. ipython:: python
310310
311-
fixings.add("EURUSD_1700", Series(
311+
fixings.add("WMR_10AM_TY0_T+2_EURUSD", Series(
312312
index=[dt(2000, 1, 1), dt(2000, 4, 2), dt(2000, 7, 1), dt(2000, 7, 2)],
313313
data=[1.26, 1.27, 1.29, 1.295])
314314
)
@@ -325,8 +325,8 @@ class FixedLeg(_BaseLeg, _WithExDiv):
325325
mtm="payment",
326326
currency="usd",
327327
pair="eurusd",
328-
notional=10e6, # <- Leg settles in USD leg but reference cashflows in EUR
329-
fx_fixings=(1.25, "EURUSD_1700"), # <- Initial exchange rate and future fixings
328+
notional=10e6, # <- Leg settles in USD leg but reference cashflows in EUR
329+
fx_fixings=(1.25, "WMR_10AM_TY0_T+2"), # <- Initial exchange rate and future fixings
330330
)
331331
print(leg.cashflows())
332332
@@ -345,7 +345,7 @@ class FixedLeg(_BaseLeg, _WithExDiv):
345345
346346
.. ipython:: python
347347
348-
fixings.add("EURUSD_1600", Series(
348+
fixings.add("WMR_4PM_GMT_T+2_EURUSD", Series(
349349
index=[dt(2000, 4, 1), dt(2000, 4, 2), dt(2000, 7, 2)],
350350
data=[1.265, 1.27, 1.29])
351351
)
@@ -363,7 +363,7 @@ class FixedLeg(_BaseLeg, _WithExDiv):
363363
pair="eurusd",
364364
mtm="xcs",
365365
notional=10e6,
366-
fx_fixings=(1.25, "EURUSD_1600"),
366+
fx_fixings=(1.25, "WMR_4PM_GMT_T+2"),
367367
)
368368
print(leg.cashflows())
369369
@@ -420,7 +420,7 @@ class FixedLeg(_BaseLeg, _WithExDiv):
420420
pair="eurusd",
421421
notional=10e6, # <- Leg settles in USD leg but reference cashflows in EUR
422422
amortization=4e6,
423-
fx_fixings=(1.25, "EURUSD_1700"), # <- Initial exchange rate and future fixings
423+
fx_fixings=(1.25, "WMR_10AM_TY0_T+2"), # <- Initial exchange rate and future fixings
424424
)
425425
print(leg.cashflows())
426426
@@ -445,7 +445,7 @@ class FixedLeg(_BaseLeg, _WithExDiv):
445445
mtm="xcs",
446446
notional=10e6,
447447
amortization=4e6,
448-
fx_fixings=(1.25, "EURUSD_1600"),
448+
fx_fixings=(1.25, "WMR_4PM_GMT_T+2"),
449449
)
450450
print(leg.cashflows())
451451
@@ -472,7 +472,7 @@ class FixedLeg(_BaseLeg, _WithExDiv):
472472
notional=5e6,
473473
amortization=1000000,
474474
mtm="xcs",
475-
fx_fixings=(1.25, "EURUSD_1600"),
475+
fx_fixings=(1.25, "WMR_10AM_TY0_T+2"),
476476
index_lag=0,
477477
index_fixings="MY_RPI",
478478
index_method="monthly",
@@ -482,11 +482,11 @@ class FixedLeg(_BaseLeg, _WithExDiv):
482482
.. ipython:: python
483483
:suppress:
484484
485-
fixings.pop("EURUSD_1600")
486-
fixings.pop("EURUSD_1700")
485+
fixings.pop("WMR_10AM_TY0_T+2_EURUSD")
486+
fixings.pop("WMR_4PM_GMT_T+2_EURUSD")
487487
fixings.pop("MY_RPI")
488488
489-
""" # noqa: E501
489+
""" # noqa: E501
490490

491491
@property
492492
def settlement_params(self) -> _SettlementParams:

0 commit comments

Comments
 (0)