Skip to content
Open
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
52 changes: 49 additions & 3 deletions financepy/market/curves/discount_curve.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def __init__(
self._times = [0.0]
self._dfs = [1.0]
self._df_dates = df_dates
self._dates = [value_dt]

num_points = len(df_dates)

Expand All @@ -62,13 +63,18 @@ def __init__(
if df_dates[0] == value_dt:
self._dfs[0] = df_values[0]
start_index = 1

self._dfs[0] = df_values[0]
start_index = 1

for i in range(start_index, num_points):
t = (df_dates[i] - value_dt) / G_DAYS_IN_YEARS
self._times.append(t)
self._dfs.append(df_values[i])
self._dates.append(df_dates[i])

self._times = np.array(self._times)
self._dates = np.array(self._dates)

if test_monotonicity(self._times) is False:
print(self._times)
Expand Down Expand Up @@ -345,6 +351,46 @@ def df(self, dt: Union[list, Date], day_count=DayCountTypes.ACT_ACT_ISDA):
return dfs

return np.array(dfs)

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

def dates_from_df(self, dfs: Union[list, float], day_count=DayCountTypes.ACT_ACT_ISDA):
"""Function to calculate dates corresponding to given discount factors.
This is the inverse of df(): it maps discount factors -> dates.
The day count determines how times get converted back to dates."""

# Ensure discount factors are in list/array form
if isinstance(dfs, (float, int)):
dfs = [dfs]

# Step 1: Invert the discount factor function
times = []
for df_value in dfs:
# We need to find t such that df_t(t) = df_value
# Use a numerical root-finding method since df_t is typically monotonic decreasing
def func(t):
return self.df_t(t) - df_value

# Bounds: assume discount factors decrease from 1 at t=0
# Expand upper bound dynamically until df_t < df_value
t_low, t_high = 0.0, 1.0
while self.df_t(t_high) > df_value and t_high < 100:
t_high *= 2

# Solve for t
from scipy.optimize import brentq
t_sol = brentq(func, t_low, t_high)
times.append(t_sol)

# Step 2: Convert times -> dates
from numpy import array
dates = dates_from_times(array(times), self.value_dt, day_count)

if len(dates) == 1:
return dates[0]

return dates


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

Expand Down Expand Up @@ -428,13 +474,14 @@ def bump(self, bump_size: float):

times = self._times.copy()
values = self._dfs.copy()
dates = self._dates.copy()

n = len(self._times)
for i in range(0, n):
t = times[i]
values[i] = values[i] * np.exp(-bump_size * t)

disc_curve = DiscountCurve(self.value_dt, times, values, self._interp_type)
disc_curve = DiscountCurve(self.value_dt, list(dates), values, self._interp_type)

return disc_curve

Expand Down Expand Up @@ -494,8 +541,7 @@ def __repr__(self):
num_points = len(self._df_dates)
s += label_to_string("DATES", "DISCOUNT FACTORS")
for i in range(0, num_points):
s += label_to_string(f"{self._df_dates[i]:>12}", f"{self._dfs[i]:12.8f}")

s += label_to_string(f"{self._times[i]:10.6f}", f"{self._dfs[i]:12.10f}")
return s

####################################################################################
Expand Down
50 changes: 50 additions & 0 deletions financepy/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,56 @@ def times_from_dates(
########################################################################################


def dates_from_times(
t: Union[float, list, np.ndarray],
value_dt: Date,
dc_type: DayCountTypes = None
):
"""If a single time (in years) is passed in then return the date from the
valuation date. If a whole vector of times is passed in then convert to a
vector of dates from the valuation date. The output is always a numpy vector
of Date objects (with one element if the input is only one time)."""

if isinstance(value_dt, Date) is False:
raise FinError("Valuation date is not a Date")

if dc_type is None:
dc_counter = None
else:
dc_counter = DayCount(dc_type)

# Helper for one value
def single_date_from_time(time_value):
if dc_counter is None:
# Assume simple day count = 365
delta_days = int(round(time_value * G_DAYS_IN_YEARS))
return value_dt.add_days(delta_days)
else:
# Use inverse of year_frac logic — approximate by solving for date
# This assumes the DayCount class has add_year_frac or similar method
try:
return dc_counter.add_year_frac(value_dt, time_value)
except AttributeError:
# Fallback if no such method exists — use simple approximation
delta_days = int(round(time_value * G_DAYS_IN_YEARS))
return value_dt.add_days(delta_days)

# Case 1: single float
if isinstance(t, (float, int)):
return single_date_from_time(t)

# Case 2: list or numpy array
elif isinstance(t, (list, np.ndarray)):
dates = [single_date_from_time(time_value) for time_value in t]
return np.array(dates)

else:
raise FinError("Time input must be a float, list, or numpy array.")

return None
########################################################################################


def check_vector_differences(x: np.ndarray, y: np.ndarray, tol: float = 1e-6):
"""Compare two vectors elementwise to see if they are more different than
tolerance."""
Expand Down
Loading