diff --git a/fincal/core.py b/fincal/core.py index 79f053a..8f04abf 100644 --- a/fincal/core.py +++ b/fincal/core.py @@ -18,6 +18,25 @@ class Frequency: def date_parser(*pos): + """Decorator to parse dates in any function + + Accepts the 0-indexed position of the parameter for which date parsing needs to be done. + Works even if function is used with keyword arguments while not maintaining parameter order. + + Example: + -------- + >>> @date_parser(2, 3) + >>> def calculate_difference(diff_units='days', return_type='int', date1, date2): + ... diff = date2 - date1 + ... if return_type == 'int': + ... return diff.days + ... return diff + ... + >>> calculate_difference(date1='2019-01-01'm date2='2020-01-01') + datetime.timedelta(365) + + Each of the dates is automatically parsed into a datetime.datetime object from string. + """ def parse_dates(func): def wrapper_func(*args, **kwargs): date_format = kwargs.get("date_format", None) diff --git a/fincal/fincal.py b/fincal/fincal.py index b0fe12a..5a67c3d 100644 --- a/fincal/fincal.py +++ b/fincal/fincal.py @@ -6,14 +6,10 @@ from typing import Iterable, List, Literal, Mapping, Union from dateutil.relativedelta import relativedelta from .core import AllFrequencies, TimeSeriesCore, date_parser -from .utils import ( - _find_closest_date, - _interval_to_years, - _parse_date, - _preprocess_match_options, -) +from .utils import _find_closest_date, _interval_to_years, _preprocess_match_options +@date_parser(0, 1) def create_date_series( start_date: Union[str, datetime.datetime], end_date: Union[str, datetime.datetime], @@ -55,8 +51,8 @@ def create_date_series( if eomonth and frequency.days < AllFrequencies.M.days: raise ValueError(f"eomonth cannot be set to True if frequency is higher than {AllFrequencies.M.name}") - start_date = _parse_date(start_date) - end_date = _parse_date(end_date) + # start_date = _parse_date(start_date) + # end_date = _parse_date(end_date) datediff = (end_date - start_date).days / frequency.days + 1 dates = [] @@ -261,7 +257,6 @@ class TimeSeries(TimeSeriesCore): (datetime.datetime(2020, 1, 1, 0, 0), .0567) """ - # as_on = _parse_date(as_on, date_format) as_on_delta, prior_delta = _preprocess_match_options(as_on_match, prior_match, closest) prev_date = as_on - relativedelta(**{interval_type: interval_value}) @@ -359,9 +354,6 @@ class TimeSeries(TimeSeriesCore): TimeSeries.calculate_returns """ - # from_date = _parse_date(from_date, date_format) - # to_date = _parse_date(to_date, date_format) - if frequency is None: frequency = self.frequency else: diff --git a/fincal/utils.py b/fincal/utils.py index e0cb531..38643fc 100644 --- a/fincal/utils.py +++ b/fincal/utils.py @@ -85,20 +85,26 @@ def _preprocess_match_options(as_on_match: str, prior_match: str, closest: str) return as_on_delta, prior_delta -def _find_closest_date(data, date, limit_days, delta, if_not_found): +def _find_closest_date( + data: Mapping[datetime.datetime, float], + date: datetime.datetime, + limit_days: int, + delta: datetime.timedelta, + if_not_found: Literal["fail", "nan"], +): """Helper function to find data for the closest available date""" if delta.days < 0 and date < min(data): - raise DateOutOfRangeError(date, 'min') + raise DateOutOfRangeError(date, "min") if delta.days > 0 and date > max(data): - raise DateOutOfRangeError(date, 'max') + raise DateOutOfRangeError(date, "max") row = data.get(date, None) if row is not None: return date, row if delta and limit_days != 0: - return _find_closest_date(data, date + delta, limit_days-1, delta, if_not_found) + return _find_closest_date(data, date + delta, limit_days - 1, delta, if_not_found) if if_not_found == "fail": raise DateNotFoundError("Data not found for date", date) diff --git a/tests/test_fincal.py b/tests/test_fincal.py index 1b9489e..a3e2e96 100644 --- a/tests/test_fincal.py +++ b/tests/test_fincal.py @@ -182,59 +182,60 @@ class TestFincalBasic: class TestReturns: data = [ - ('2020-01-01', 10), - ('2020-02-01', 12), - ('2020-03-01', 14), - ('2020-04-01', 16), - ('2020-05-01', 18), - ('2020-06-01', 20), - ('2020-07-01', 22), - ('2020-08-01', 24), - ('2020-09-01', 26), - ('2020-10-01', 28), - ('2020-11-01', 30), - ('2020-12-01', 32), - ('2021-01-01', 34) - ] + ("2020-01-01", 10), + ("2020-02-01", 12), + ("2020-03-01", 14), + ("2020-04-01", 16), + ("2020-05-01", 18), + ("2020-06-01", 20), + ("2020-07-01", 22), + ("2020-08-01", 24), + ("2020-09-01", 26), + ("2020-10-01", 28), + ("2020-11-01", 30), + ("2020-12-01", 32), + ("2021-01-01", 34), + ] def test_returns_calc(self): - ts = TimeSeries(self.data, frequency='M') - returns = ts.calculate_returns("2021-01-01", compounding=False, interval_type='years', interval_value=1) + ts = TimeSeries(self.data, frequency="M") + returns = ts.calculate_returns("2021-01-01", compounding=False, interval_type="years", interval_value=1) assert returns[1] == 2.4 - returns = ts.calculate_returns("2020-04-01", compounding=False, interval_type='months', interval_value=3) + returns = ts.calculate_returns("2020-04-01", compounding=False, interval_type="months", interval_value=3) assert round(returns[1], 4) == 0.6 - returns = ts.calculate_returns("2020-04-01", compounding=True, interval_type='months', interval_value=3) + returns = ts.calculate_returns("2020-04-01", compounding=True, interval_type="months", interval_value=3) assert round(returns[1], 4) == 5.5536 - returns = ts.calculate_returns("2020-04-01", compounding=False, interval_type='days', interval_value=90) + returns = ts.calculate_returns("2020-04-01", compounding=False, interval_type="days", interval_value=90) assert round(returns[1], 4) == 0.6 - returns = ts.calculate_returns("2020-04-01", compounding=True, interval_type='days', interval_value=90) + returns = ts.calculate_returns("2020-04-01", compounding=True, interval_type="days", interval_value=90) assert round(returns[1], 4) == 5.727 - returns = ts.calculate_returns("2020-04-10", compounding=True, interval_type='days', interval_value=90) + returns = ts.calculate_returns("2020-04-10", compounding=True, interval_type="days", interval_value=90) assert round(returns[1], 4) == 5.727 with pytest.raises(DateNotFoundError): - ts.calculate_returns("2020-04-10", interval_type='days', interval_value=90, as_on_match='exact') + ts.calculate_returns("2020-04-10", interval_type="days", interval_value=90, as_on_match="exact") with pytest.raises(DateNotFoundError): - ts.calculate_returns("2020-04-10", interval_type='days', interval_value=90, prior_match='exact') + ts.calculate_returns("2020-04-10", interval_type="days", interval_value=90, prior_match="exact") def test_date_formats(self): - ts = TimeSeries(self.data, frequency='M') - FincalOptions.date_format = '%d-%m-%Y' + ts = TimeSeries(self.data, frequency="M") + FincalOptions.date_format = "%d-%m-%Y" with pytest.raises(ValueError): - ts.calculate_returns("2020-04-10", compounding=True, interval_type='days', interval_value=90) + ts.calculate_returns("2020-04-10", compounding=True, interval_type="days", interval_value=90) - returns1 = ts.calculate_returns("2020-04-10", interval_type='days', interval_value=90, date_format='%Y-%m-%d') - returns2 = ts.calculate_returns("10-04-2020", interval_type='days', interval_value=90) + returns1 = ts.calculate_returns("2020-04-10", interval_type="days", interval_value=90, date_format="%Y-%m-%d") + returns2 = ts.calculate_returns("10-04-2020", interval_type="days", interval_value=90) assert round(returns1[1], 4) == round(returns2[1], 4) == 5.727 - FincalOptions.date_format = '%m-%d-%Y' + FincalOptions.date_format = "%m-%d-%Y" with pytest.raises(ValueError): - ts.calculate_returns("2020-04-10", compounding=True, interval_type='days', interval_value=90) + ts.calculate_returns("2020-04-10", compounding=True, interval_type="days", interval_value=90) - returns1 = ts.calculate_returns("2020-04-10", interval_type='days', interval_value=90, date_format='%Y-%m-%d') - returns2 = ts.calculate_returns("04-10-2020", interval_type='days', interval_value=90) + returns1 = ts.calculate_returns("2020-04-10", interval_type="days", interval_value=90, date_format="%Y-%m-%d") + returns2 = ts.calculate_returns("04-10-2020", interval_type="days", interval_value=90) assert round(returns1[1], 4) == round(returns2[1], 4) == 5.727 def test_limits(self): - ts = TimeSeries(self.data, frequency='M') + ts = TimeSeries(self.data, frequency="M") + FincalOptions.date_format = "%Y-%m-%d" with pytest.raises(DateNotFoundError): - ts.calculate_returns("2020-04-25", interval_type='days', interval_value=90, closest_max_days=10) + ts.calculate_returns("2020-04-25", interval_type="days", interval_value=90, closest_max_days=10)