2022-02-26 07:16:42 +00:00
|
|
|
import datetime
|
|
|
|
from dataclasses import dataclass
|
2022-04-05 18:13:03 +00:00
|
|
|
from typing import List, Literal, Mapping, Sequence, Tuple
|
2022-02-26 07:16:42 +00:00
|
|
|
|
2022-02-26 15:12:27 +00:00
|
|
|
from .exceptions import DateNotFoundError, DateOutOfRangeError
|
2022-02-26 07:16:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class FincalOptions:
|
|
|
|
date_format: str = "%Y-%m-%d"
|
|
|
|
closest: str = "before" # after
|
2022-03-11 04:11:35 +00:00
|
|
|
traded_days: int = 365
|
2022-03-22 16:00:28 +00:00
|
|
|
get_closest: str = "exact"
|
2022-02-26 07:16:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
def _parse_date(date: str, date_format: str = None):
|
|
|
|
"""Parses date and handles errors"""
|
2022-02-27 10:59:18 +00:00
|
|
|
# print(date, date_format)
|
2022-02-26 07:16:42 +00:00
|
|
|
if isinstance(date, (datetime.datetime, datetime.date)):
|
|
|
|
return datetime.datetime.fromordinal(date.toordinal())
|
|
|
|
|
|
|
|
if date_format is None:
|
|
|
|
date_format = FincalOptions.date_format
|
|
|
|
|
|
|
|
try:
|
|
|
|
date = datetime.datetime.strptime(date, date_format)
|
|
|
|
except TypeError:
|
|
|
|
raise ValueError("Date does not seem to be valid date-like string")
|
|
|
|
except ValueError:
|
|
|
|
raise ValueError("Date could not be parsed. Have you set the correct date format in FincalOptions.date_format?")
|
|
|
|
return date
|
|
|
|
|
|
|
|
|
|
|
|
def _preprocess_timeseries(
|
2022-04-05 18:13:03 +00:00
|
|
|
data: Sequence[Tuple[str | datetime.datetime, float]]
|
2022-04-05 05:13:53 +00:00
|
|
|
| Sequence[Mapping[str | datetime.datetime, float]]
|
|
|
|
| Mapping[str | datetime.datetime, float],
|
2022-02-26 07:16:42 +00:00
|
|
|
date_format: str,
|
|
|
|
) -> List[Tuple[datetime.datetime, float]]:
|
|
|
|
"""Converts any type of list to the correct type"""
|
|
|
|
|
|
|
|
if isinstance(data, Mapping):
|
2022-04-05 05:13:53 +00:00
|
|
|
current_data: List[tuple] = [(k, v) for k, v in data.items()]
|
2022-02-26 07:16:42 +00:00
|
|
|
return _preprocess_timeseries(current_data, date_format)
|
|
|
|
|
|
|
|
if not isinstance(data, Sequence):
|
|
|
|
raise TypeError("Could not parse the data")
|
|
|
|
|
|
|
|
if isinstance(data[0], Sequence):
|
|
|
|
return sorted([(_parse_date(i, date_format), j) for i, j in data])
|
|
|
|
|
|
|
|
if not isinstance(data[0], Mapping):
|
|
|
|
raise TypeError("Could not parse the data")
|
|
|
|
|
|
|
|
if len(data[0]) == 1:
|
2022-04-05 05:13:53 +00:00
|
|
|
current_data: List[tuple] = [tuple(*i.items()) for i in data]
|
2022-02-26 07:16:42 +00:00
|
|
|
elif len(data[0]) == 2:
|
2022-04-05 05:13:53 +00:00
|
|
|
current_data: List[tuple] = [tuple(i.values()) for i in data]
|
2022-02-26 07:16:42 +00:00
|
|
|
else:
|
|
|
|
raise TypeError("Could not parse the data")
|
|
|
|
return _preprocess_timeseries(current_data, date_format)
|
|
|
|
|
|
|
|
|
2022-04-05 05:13:53 +00:00
|
|
|
def _preprocess_match_options(as_on_match: str, prior_match: str, closest: str) -> Tuple[datetime.timedelta]:
|
2022-02-26 07:16:42 +00:00
|
|
|
"""Checks the arguments and returns appropriate timedelta objects"""
|
|
|
|
|
|
|
|
deltas = {"exact": 0, "previous": -1, "next": 1}
|
|
|
|
if closest not in deltas.keys():
|
|
|
|
raise ValueError(f"Invalid argument for closest: {closest}")
|
|
|
|
|
2022-04-05 05:13:53 +00:00
|
|
|
as_on_match: str = closest if as_on_match == "closest" else as_on_match
|
|
|
|
prior_match: str = closest if prior_match == "closest" else prior_match
|
2022-02-26 07:16:42 +00:00
|
|
|
|
|
|
|
if as_on_match in deltas.keys():
|
2022-04-05 05:13:53 +00:00
|
|
|
as_on_delta: datetime.timedelta = datetime.timedelta(days=deltas[as_on_match])
|
2022-02-26 07:16:42 +00:00
|
|
|
else:
|
|
|
|
raise ValueError(f"Invalid as_on_match argument: {as_on_match}")
|
|
|
|
|
|
|
|
if prior_match in deltas.keys():
|
2022-04-05 05:13:53 +00:00
|
|
|
prior_delta: datetime.timedelta = datetime.timedelta(days=deltas[prior_match])
|
2022-02-26 07:16:42 +00:00
|
|
|
else:
|
|
|
|
raise ValueError(f"Invalid prior_match argument: {prior_match}")
|
|
|
|
|
|
|
|
return as_on_delta, prior_delta
|
|
|
|
|
|
|
|
|
2022-03-05 17:53:31 +00:00
|
|
|
def _find_closest_date(
|
|
|
|
data: Mapping[datetime.datetime, float],
|
|
|
|
date: datetime.datetime,
|
|
|
|
limit_days: int,
|
|
|
|
delta: datetime.timedelta,
|
|
|
|
if_not_found: Literal["fail", "nan"],
|
|
|
|
):
|
2022-02-26 07:16:42 +00:00
|
|
|
"""Helper function to find data for the closest available date"""
|
|
|
|
|
2022-02-26 15:12:27 +00:00
|
|
|
if delta.days < 0 and date < min(data):
|
2022-03-05 17:53:31 +00:00
|
|
|
raise DateOutOfRangeError(date, "min")
|
2022-02-26 15:12:27 +00:00
|
|
|
if delta.days > 0 and date > max(data):
|
2022-03-05 17:53:31 +00:00
|
|
|
raise DateOutOfRangeError(date, "max")
|
2022-02-26 15:12:27 +00:00
|
|
|
|
2022-04-05 05:13:53 +00:00
|
|
|
row: tuple = data.get(date, None)
|
2022-02-26 07:16:42 +00:00
|
|
|
if row is not None:
|
|
|
|
return date, row
|
|
|
|
|
2022-02-26 16:48:10 +00:00
|
|
|
if delta and limit_days != 0:
|
2022-03-05 17:53:31 +00:00
|
|
|
return _find_closest_date(data, date + delta, limit_days - 1, delta, if_not_found)
|
2022-02-26 07:16:42 +00:00
|
|
|
|
|
|
|
if if_not_found == "fail":
|
|
|
|
raise DateNotFoundError("Data not found for date", date)
|
|
|
|
if if_not_found == "nan":
|
|
|
|
return date, float("NaN")
|
|
|
|
|
|
|
|
raise ValueError(f"Invalid argument for if_not_found: {if_not_found}")
|
|
|
|
|
|
|
|
|
2022-03-30 17:36:45 +00:00
|
|
|
def _interval_to_years(interval_type: Literal["years", "months", "day"], interval_value: int) -> float:
|
2022-02-26 07:16:42 +00:00
|
|
|
"""Converts any time period to years for use with compounding functions"""
|
|
|
|
|
2022-04-05 05:13:53 +00:00
|
|
|
year_conversion_factor: dict = {"years": 1, "months": 12, "days": 365}
|
|
|
|
years: float = interval_value / year_conversion_factor[interval_type]
|
2022-02-26 07:16:42 +00:00
|
|
|
return years
|