Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions financepy/market/curves/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
from .bond_bootstrap_discount_curve import *
from .bond_parametric_discount_curve import *
from .bond_parametric_yield_curve import *
from .fx_discount_curve import *
87 changes: 87 additions & 0 deletions financepy/market/curves/fx_discount_curve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Copyright (C) 2018, 2019, 2020 Dominic O'Kane

from typing import Union

import numpy as np

from ...utils.date import Date
from ...utils.day_count import DayCountTypes
from ...utils.error import FinError
from ...utils.helpers import check_argument_types, label_to_string
from .discount_curve import DiscountCurve
from .interpolator import InterpTypes


########################################################################################
class FXDiscountCurve(DiscountCurve):
"""Discount curve implied from FX spot, FX forwards, and a domestic curve."""

####################################################################################
def __init__(
self,
value_dt: Date,
spot_fx_rate: float,
forward_dts: list,
forward_fx_rates: Union[list, np.ndarray],
domestic_curve: DiscountCurve,
interp_type: InterpTypes = InterpTypes.FLAT_FWD_RATES,
time_dc_type: DayCountTypes = DayCountTypes.ACT_365F,
):
"""Create an implied foreign discount curve from FX forward quotes.

The FX rate convention is domestic currency per unit of foreign currency.
Given spot S, forward F(T), and domestic discount factor DF_dom(T), the
implied foreign discount factor is:

DF_for(T) = F(T) / S * DF_dom(T)
"""
check_argument_types(self.__init__, locals())

if spot_fx_rate <= 0.0:
raise FinError("Spot FX rate must be positive.")

if len(forward_dts) == 0:
raise FinError("At least one FX forward date is required.")

if len(forward_dts) != len(forward_fx_rates):
raise FinError("Forward dates and FX forward rates must have same length.")

if domestic_curve.value_dt != value_dt:
raise FinError("Domestic curve value date must match curve value date.")

for dt in forward_dts:
if dt <= value_dt:
raise FinError("Forward dates must be after the value date.")

forward_fx_rates = np.asarray(forward_fx_rates, dtype=float)

if np.any(~np.isfinite(forward_fx_rates)) or np.any(forward_fx_rates <= 0.0):
raise FinError("FX forward rates must be finite and positive.")

domestic_dfs = domestic_curve.df(forward_dts)
foreign_dfs = forward_fx_rates / spot_fx_rate * domestic_dfs

self.spot_fx_rate = spot_fx_rate
self.forward_fx_rates = forward_fx_rates
self.domestic_curve = domestic_curve

super().__init__(
value_dt,
forward_dts,
foreign_dfs,
interp_type,
time_dc_type,
)

####################################################################################
def __repr__(self):
s = label_to_string("OBJECT TYPE", type(self).__name__)
s += label_to_string("SPOT FX RATE", self.spot_fx_rate)
s += "\n"
s += super().__repr__()
return s

####################################################################################
def _print(self):
"""Simple print function for backward compatibility."""
print(self)
56 changes: 56 additions & 0 deletions regression_tests/TestFinFXDiscountCurve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright (C) 2018, 2019, 2020 Dominic O'Kane

import add_fp_to_path

from financepy.utils.date import Date
from financepy.utils.frequency import FrequencyTypes
from financepy.market.curves.discount_curve_flat import DiscountCurveFlat
from financepy.market.curves.fx_discount_curve import FXDiscountCurve
from FinTestCases import FinTestCases, global_test_case_mode

test_cases = FinTestCases(__file__, global_test_case_mode)


########################################################################################
def test_fin_fx_discount_curve():
value_dt = Date(1, 1, 2024)

spot_fx_rate = 150.0
forward_tenors = ["1M", "3M", "6M"]
forward_dts = value_dt.add_months([1, 3, 6])
forward_fx_rates = [149.8, 149.2, 148.5]

domestic_curve = DiscountCurveFlat(
value_dt,
0.05,
FrequencyTypes.CONTINUOUS,
)

foreign_curve = FXDiscountCurve(
value_dt,
spot_fx_rate,
forward_dts,
forward_fx_rates,
domestic_curve,
)

test_cases.header("TENOR", "INPUT_FWD", "IMPLIED_FWD")

for tenor, forward_dt, forward_fx_rate in zip(
forward_tenors,
forward_dts,
forward_fx_rates,
):
implied_forward = (
spot_fx_rate
* foreign_curve.df(forward_dt)
/ domestic_curve.df(forward_dt)
)

test_cases.print(tenor, forward_fx_rate, implied_forward)


########################################################################################

test_fin_fx_discount_curve()
test_cases.compare_test_cases()
5 changes: 5 additions & 0 deletions regression_tests/golden/TestFinFXDiscountCurve_GOLDEN.testLog
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
File Created on:20260711_000000
HEADER,TENOR,INPUT_FWD,IMPLIED_FWD,
RESULTS,1M,149.80000000,149.80000000,
RESULTS,3M,149.20000000,149.20000000,
RESULTS,6M,148.50000000,148.50000000,
Loading