@@ -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
0 commit comments