Skip to content

Commit

Permalink
TYP: fx directory fix up with refactoring (#537)
Browse files Browse the repository at this point in the history
  • Loading branch information
attack68 authored Dec 6, 2024
1 parent 1cdc83f commit 4957040
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 61 deletions.
6 changes: 6 additions & 0 deletions docs/source/i_whatsnew.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ email contact, see `rateslib <https://rateslib.com>`_.
- :red:`Minor Breaking Change!` :meth:`~rateslib.calendars.get_calendar` has dropped the
``kind`` argument being only useful internally.
(`524 <https://github.com/attack68/rateslib/pull/524>`_)
* - Refactor
- :red:`Minor Breaking Change!` :meth:`FXForwards.rate <rateslib.fx.FXForwards.rate>`
has dropped the ``path`` and ``return_path`` arguments being mainly useful internally.
Replicable functionality is achieved by importing and using the internal method
:meth:`rateslib.fx.FXForwards._rate_with_path`.
(`537 <https://github.com/attack68/rateslib/pull/537>`_)

1.6.0 (30th November 2024)
****************************
Expand Down
6 changes: 3 additions & 3 deletions python/rateslib/curves/curves.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from rateslib.calendars.dcfs import _DCF1d
from rateslib.calendars.rs import Modifier, _get_calendar_with_kind
from rateslib.default import NoInput, _drb, plot
from rateslib.dual import Dual, Dual2, DualTypes, dual_exp, dual_log, set_order_convert
from rateslib.dual import Dual, Dual2, DualTypes, Number, dual_exp, dual_log, set_order_convert
from rateslib.rs import index_left_f64
from rateslib.splines import PPSplineDual, PPSplineDual2, PPSplineF64

Expand Down Expand Up @@ -2813,9 +2813,9 @@ def __init__(
self.calendar = default_curve.calendar
self.node_dates = [self.fx_forwards.immediate, self.terminal]

def __getitem__(self, date: datetime):
def __getitem__(self, date: datetime) -> Number:
return (
self.fx_forwards.rate(self.pair, date, path=self.path)
self.fx_forwards._rate_with_path(self.pair, date, path=self.path)[0]
/ self.fx_forwards.fx_rates_immediate.fx_array[self.cash_idx, self.coll_idx]
* self.fx_forwards.fx_curves[self.coll_pair][date]
)
Expand Down
2 changes: 1 addition & 1 deletion python/rateslib/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,6 @@ def _drb(default: Any, possible_blank: Any | NoInput) -> Any:
return default if possible_blank is NoInput.blank else possible_blank


def _make_py_json(json, class_name):
def _make_py_json(json: str, class_name: str) -> str:
"""Modifies the output JSON output for Rust structs wrapped by Python classes."""
return '{"Py":' + json + "}"
103 changes: 59 additions & 44 deletions python/rateslib/fx/fx_forwards.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@

import numpy as np
from pandas import DataFrame, Series
from pandas.tseries.offsets import CustomBusinessDay

from rateslib.calendars import add_tenor
from rateslib.calendars import CalInput, add_tenor
from rateslib.curves import Curve, LineCurve, MultiCsaCurve, ProxyCurve
from rateslib.default import NoInput, PlotOutput, plot
from rateslib.dual import Dual, DualTypes, Number, gradient
Expand Down Expand Up @@ -305,7 +304,7 @@ def update(
self.currencies,
self.fx_curves,
)
self.base: str = self.fx_rates.base if base is NoInput.blank else base
self.base: str = self.fx_rates.base if isinstance(base, NoInput) else base
self.pairs = self.fx_rates.pairs
self.variables = tuple(f"fx_{pair}" for pair in self.pairs)
self.fx_rates_immediate = self._update_fx_rates_immediate()
Expand All @@ -314,13 +313,13 @@ def update(
def __init__(
self,
fx_rates: FXRates | list[FXRates],
fx_curves: dict,
fx_curves: dict[str, Curve],
base: str | NoInput = NoInput(0),
):
) -> None:
self._ad = 1
self.update(fx_rates, fx_curves, base)

def __repr__(self):
def __repr__(self) -> str:
if len(self.currencies_list) > 5:
return (
f"<rl.FXForwards:[{','.join(self.currencies_list[:2])},"
Expand Down Expand Up @@ -479,16 +478,14 @@ def _update_fx_rates_immediate(self) -> FXRates:
pair = f"{cash_ccy}{coll_ccy}"
fx_rates_immediate.update({pair: self.fx_rates.fx_array[row, col] * v_i / w_i})

fx_rates_immediate = FXRates(fx_rates_immediate, self.immediate, self.base)
return fx_rates_immediate.restate(self.fx_rates.pairs, keep_ad=True)
fx_rates_immediate_ = FXRates(fx_rates_immediate, self.immediate, self.base)
return fx_rates_immediate_.restate(self.fx_rates.pairs, keep_ad=True)

def rate(
self,
pair: str,
settlement: datetime | NoInput = NoInput(0),
path: list[dict[str, int]] | NoInput = NoInput(0),
return_path: bool = False,
) -> Number | tuple[Number, list[dict[str, int]]]:
) -> Number:
"""
Return the fx forward rate for a currency pair.
Expand All @@ -499,18 +496,10 @@ def rate(
settlement : datetime, optional
The settlement date of currency exchange. If not given defaults to
immediate settlement.
path : list of dict, optional
The chain of currency collateral curves to traverse to calculate the rate.
This is calculated automatically and this argument is provided for
internal calculation to avoid repeatedly calculating the same path. Use of
this argument in normal circumstances is not recommended.
return_path : bool
If `True` returns the path in a tuple alongside the rate. Use of this
argument in normal circumstances is not recommended.
Returns
-------
float, Dual, Dual2 or tuple
float, Dual, Dual2
Notes
-----
Expand All @@ -523,22 +512,50 @@ def rate(
where :math:`v` is a local currency discount curve and :math:`w` is a discount
curve collateralised with an alternate currency.
Where curves do not exist in the relevant currencies we chain rates available
given the available curves.
If required curves do not exist in the relevant currencies then forwards rates are chained
using those calculable from available curves. The chain is found using a search algorithm.
.. math::
f_{DOMFOR, i} = f_{DOMALT, i} ... f_{ALTFOR, i}
""" # noqa: E501
return self._rate_with_path(pair, settlement)[0]

def _rate_with_path(
self,
pair: str,
settlement: datetime | NoInput = NoInput(0),
path: list[dict[str, int]] | NoInput = NoInput(0),
) -> tuple[Number, list[dict[str, int]]]:
"""
Return the fx forward rate for a currency pair, including the path taken to traverse ccys.
Parameters
----------
pair : str
The FX pair in usual domestic:foreign convention (6 digit code).
settlement : datetime, optional
The settlement date of currency exchange. If not given defaults to
immediate settlement.
path : list of dict, optional
The chain of currency collateral curves to traverse to calculate the rate.
This is calculated automatically and this argument is provided for
internal calculation to avoid repeatedly calculating the same path. Use of
this argument in normal circumstances is not recommended.
Returns
-------
tuple
"""

def _get_d_f_idx_and_path(
pair, path: list[dict[str, int]] | None
pair: str, path: list[dict[str, int]] | NoInput
) -> tuple[int, int, list[dict[str, int]]]:
domestic, foreign = pair[:3].lower(), pair[3:].lower()
d_idx: int = self.fx_rates_immediate.currencies[domestic]
f_idx: int = self.fx_rates_immediate.currencies[foreign]
if path is NoInput.blank:
if isinstance(path, NoInput):
path = self._get_recursive_chain(self.transform, f_idx, d_idx, [], [])[1]
return d_idx, f_idx, path

Expand All @@ -549,16 +566,13 @@ def _get_d_f_idx_and_path(

if settlement_ == self.fx_rates_immediate.settlement:
rate_ = self.fx_rates_immediate.rate(pair)
if return_path:
_, _, path = _get_d_f_idx_and_path(pair, path)
return rate_, path
return rate_
_, _, path = _get_d_f_idx_and_path(pair, path)
return rate_, path

elif isinstance(self.fx_rates, FXRates) and settlement_ == self.fx_rates.settlement:
rate_ = self.fx_rates.rate(pair)
if return_path:
_, _, path = _get_d_f_idx_and_path(pair, path)
return rate_, path
return rate_
_, _, path = _get_d_f_idx_and_path(pair, path)
return rate_, path

# otherwise must rely on curves and path search which is slower
d_idx, f_idx, path = _get_d_f_idx_and_path(pair, path)
Expand All @@ -581,13 +595,11 @@ def _get_d_f_idx_and_path(
rate_ *= v_i / w_i
current_idx = route["row"]

if return_path:
return rate_, path
return rate_
return rate_, path

def positions(
self, value: Number, base: str | NoInput = NoInput(0), aggregate: bool = False
) -> Series | DataFrame:
) -> Series[float] | DataFrame:
"""
Convert a base value with FX rate sensitivities into an array of cash positions
by settlement date.
Expand Down Expand Up @@ -751,7 +763,10 @@ def convert(

def convert_positions(
self,
array: np.ndarray[tuple[int], np.dtype[np.float64]] | list[float] | DataFrame | Series,
array: np.ndarray[tuple[int], np.dtype[np.float64]]
| list[float]
| DataFrame
| Series[float],
base: str | NoInput = NoInput(0),
) -> DualTypes:
"""
Expand Down Expand Up @@ -825,7 +840,7 @@ def swap(
pair: str,
settlements: list[datetime],
path: list[dict[str, int]] | NoInput = NoInput(0),
) -> DualTypes:
) -> Number:
"""
Return the FXSwap mid-market rate for the given currency pair.
Expand All @@ -845,8 +860,8 @@ def swap(
-------
Dual
"""
fx0: DualTypes = self.rate(pair, settlements[0], path)
fx1: DualTypes = self.rate(pair, settlements[1], path)
fx0, path_ = self._rate_with_path(pair, settlements[0], path)
fx1, _ = self._rate_with_path(pair, settlements[1], path_)
return (fx1 - fx0) * 10000

def _full_curve(self, cashflow: str, collateral: str) -> Curve:
Expand Down Expand Up @@ -882,7 +897,7 @@ def _full_curve(self, cashflow: str, collateral: str) -> Curve:
days = (end - self.immediate).days
nodes = {
k: (
self.rate(f"{cash_ccy}{coll_ccy}", k, path=path)
self._rate_with_path(f"{cash_ccy}{coll_ccy}", k, path=path)[0]
/ self.fx_rates_immediate.fx_array[cash_idx, coll_idx]
* self.fx_curves[f"{coll_ccy}{coll_ccy}"][k]
)
Expand All @@ -900,7 +915,7 @@ def curve(
collateral: str,
convention: str | NoInput = NoInput(0),
modifier: str | bool = False,
calendar: CustomBusinessDay | str | bool = False,
calendar: CalInput | bool = False,
id: str | NoInput = NoInput(0),
) -> Curve:
"""
Expand Down Expand Up @@ -1020,8 +1035,8 @@ def plot(

points: int = (right_ - left_).days
x = [left_ + timedelta(days=i) for i in range(points)]
_, path = self.rate(pair, x[0], return_path=True)
rates: list[DualTypes] = [self.rate(pair, _, path=path) for _ in x]
_, path = self._rate_with_path(pair, x[0])
rates: list[DualTypes] = [self._rate_with_path(pair, _, path=path)[0] for _ in x]
if not fx_swap:
y: list[Number] = [rates]
else:
Expand Down
24 changes: 12 additions & 12 deletions python/rateslib/fx/fx_rates.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from rateslib import defaults
from rateslib.default import NoInput, _drb, _make_py_json
from rateslib.dual import Dual, DualTypes, _get_adorder, gradient
from rateslib.dual import Dual, DualTypes, Number, _get_adorder, gradient
from rateslib.rs import Ccy, FXRate
from rateslib.rs import FXRates as FXRatesObj

Expand Down Expand Up @@ -155,7 +155,7 @@ def __repr__(self) -> str:
return f"<rl.FXRates:[{','.join(self.currencies_list)}] at {hex(id(self))}>"

@cached_property
def fx_array(self) -> np.ndarray:
def fx_array(self) -> np.ndarray[tuple[int, int], np.dtype[np.object_]]:
# caching this prevents repetitive data transformations between Rust/Python
return np.array(self.obj.fx_array)

Expand Down Expand Up @@ -184,7 +184,7 @@ def q(self) -> int:
return len(self.obj.currencies)

@property
def fx_vector(self) -> np.array:
def fx_vector(self) -> np.ndarray[tuple[int], np.dtype[np.object_]]:
return self.fx_array[0, :]

@property
Expand Down Expand Up @@ -351,11 +351,11 @@ class still in memory.

def convert(
self,
value: Dual | float,
value: DualTypes,
domestic: str,
foreign: str | NoInput = NoInput(0),
on_error: str = "ignore",
):
) -> Number | None:
"""
Convert an amount of a domestic currency into a foreign currency.
Expand Down Expand Up @@ -386,7 +386,7 @@ def convert(
fxr.convert(1000000, "nok", "inr") # <- returns None, "inr" not in fxr.
"""
foreign = self.base if foreign is NoInput.blank else foreign.lower()
foreign = self.base if isinstance(foreign, NoInput) else foreign.lower()
domestic = domestic.lower()
for ccy in [domestic, foreign]:
if ccy not in self.currencies:
Expand All @@ -406,9 +406,9 @@ def convert(

def convert_positions(
self,
array: np.ndarray | list[float],
array: np.ndarray[tuple[int], np.dtype[np.float64]] | list[float],
base: str | NoInput = NoInput(0),
) -> DualTypes:
) -> Number:
"""
Convert an array of currency cash positions into a single base currency.
Expand All @@ -434,7 +434,7 @@ def convert_positions(
fxr.currencies
fxr.convert_positions([0, 1000000], "usd")
"""
base = self.base if base is NoInput.blank else base.lower()
base = self.base if isinstance(base, NoInput) else base.lower()
array_ = np.asarray(array)
j = self.currencies[base]
return np.sum(array_ * self.fx_array[:, j])
Expand All @@ -443,7 +443,7 @@ def positions(
self,
value: DualTypes,
base: str | NoInput = NoInput(0),
) -> Series:
) -> Series[float]:
"""
Convert a base value with FX rate sensitivities into an array of cash positions.
Expand Down Expand Up @@ -485,7 +485,7 @@ def _get_positions_from_delta(
b_idx = self.currencies[base]
domestic, foreign = pair[:3], pair[3:]
d_idx, f_idx = self.currencies[domestic], self.currencies[foreign]
_ = np.zeros(self.q, dtype=np.float64)
_: np.ndarray[tuple[int], np.dtype[np.float64]] = np.zeros(self.q, dtype=np.float64)

# f_val = -delta * float(self.fx_array[b_idx, d_idx]) * float(self.fx_array[d_idx,f_idx])**2
# _[f_idx] = f_val
Expand All @@ -510,7 +510,7 @@ def rates_table(self) -> DataFrame:
columns=self.currencies_list,
)

def _set_ad_order(self, order) -> None:
def _set_ad_order(self, order: int) -> None:
"""
Change the node values to float, Dual or Dual2 based on input parameter.
"""
Expand Down
2 changes: 2 additions & 0 deletions python/rateslib/rs.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class FXRate:

class FXRates:
def __init__(self, fx_rates: list[FXRate], base: Ccy | None) -> None: ...
def __copy__(self) -> FXRates: ...
fx_rates: list[FXRate] = ...
currencies: list[Ccy] = ...
ad: int = ...
Expand All @@ -124,3 +125,4 @@ class FXRates:
def rate(self, lhs: Ccy, rhs: Ccy) -> DualTypes | None: ...
def update(self, fx_rates: list[FXRate]) -> None: ...
def set_ad_order(self, ad: ADOrder) -> None: ...
def to_json(self) -> str: ...
2 changes: 1 addition & 1 deletion python/tests/test_fx.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ def test_rate_path_immediate(settlement) -> None:
"nokeur": nokeur,
},
)
_, result = fxf.rate("nokusd", settlement, return_path=True)
_, result = fxf._rate_with_path("nokusd", settlement)
expected = [{"col": 1}, {"col": 2}]
assert result == expected

Expand Down

0 comments on commit 4957040

Please sign in to comment.