improved documentation and usage of date_parser decorator

This commit is contained in:
Gourav Kumar 2022-03-05 23:23:31 +05:30
parent 32e4f25f59
commit 17b3e348a2
4 changed files with 68 additions and 50 deletions

View File

@ -18,6 +18,25 @@ class Frequency:
def date_parser(*pos): 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 parse_dates(func):
def wrapper_func(*args, **kwargs): def wrapper_func(*args, **kwargs):
date_format = kwargs.get("date_format", None) date_format = kwargs.get("date_format", None)

View File

@ -6,14 +6,10 @@ from typing import Iterable, List, Literal, Mapping, Union
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from .core import AllFrequencies, TimeSeriesCore, date_parser from .core import AllFrequencies, TimeSeriesCore, date_parser
from .utils import ( from .utils import _find_closest_date, _interval_to_years, _preprocess_match_options
_find_closest_date,
_interval_to_years,
_parse_date,
_preprocess_match_options,
)
@date_parser(0, 1)
def create_date_series( def create_date_series(
start_date: Union[str, datetime.datetime], start_date: Union[str, datetime.datetime],
end_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: 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}") raise ValueError(f"eomonth cannot be set to True if frequency is higher than {AllFrequencies.M.name}")
start_date = _parse_date(start_date) # start_date = _parse_date(start_date)
end_date = _parse_date(end_date) # end_date = _parse_date(end_date)
datediff = (end_date - start_date).days / frequency.days + 1 datediff = (end_date - start_date).days / frequency.days + 1
dates = [] dates = []
@ -261,7 +257,6 @@ class TimeSeries(TimeSeriesCore):
(datetime.datetime(2020, 1, 1, 0, 0), .0567) (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) as_on_delta, prior_delta = _preprocess_match_options(as_on_match, prior_match, closest)
prev_date = as_on - relativedelta(**{interval_type: interval_value}) prev_date = as_on - relativedelta(**{interval_type: interval_value})
@ -359,9 +354,6 @@ class TimeSeries(TimeSeriesCore):
TimeSeries.calculate_returns TimeSeries.calculate_returns
""" """
# from_date = _parse_date(from_date, date_format)
# to_date = _parse_date(to_date, date_format)
if frequency is None: if frequency is None:
frequency = self.frequency frequency = self.frequency
else: else:

View File

@ -85,20 +85,26 @@ def _preprocess_match_options(as_on_match: str, prior_match: str, closest: str)
return as_on_delta, prior_delta 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""" """Helper function to find data for the closest available date"""
if delta.days < 0 and date < min(data): if delta.days < 0 and date < min(data):
raise DateOutOfRangeError(date, 'min') raise DateOutOfRangeError(date, "min")
if delta.days > 0 and date > max(data): if delta.days > 0 and date > max(data):
raise DateOutOfRangeError(date, 'max') raise DateOutOfRangeError(date, "max")
row = data.get(date, None) row = data.get(date, None)
if row is not None: if row is not None:
return date, row return date, row
if delta and limit_days != 0: 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": if if_not_found == "fail":
raise DateNotFoundError("Data not found for date", date) raise DateNotFoundError("Data not found for date", date)

View File

@ -182,59 +182,60 @@ class TestFincalBasic:
class TestReturns: class TestReturns:
data = [ data = [
('2020-01-01', 10), ("2020-01-01", 10),
('2020-02-01', 12), ("2020-02-01", 12),
('2020-03-01', 14), ("2020-03-01", 14),
('2020-04-01', 16), ("2020-04-01", 16),
('2020-05-01', 18), ("2020-05-01", 18),
('2020-06-01', 20), ("2020-06-01", 20),
('2020-07-01', 22), ("2020-07-01", 22),
('2020-08-01', 24), ("2020-08-01", 24),
('2020-09-01', 26), ("2020-09-01", 26),
('2020-10-01', 28), ("2020-10-01", 28),
('2020-11-01', 30), ("2020-11-01", 30),
('2020-12-01', 32), ("2020-12-01", 32),
('2021-01-01', 34) ("2021-01-01", 34),
] ]
def test_returns_calc(self): def test_returns_calc(self):
ts = TimeSeries(self.data, frequency='M') ts = TimeSeries(self.data, frequency="M")
returns = ts.calculate_returns("2021-01-01", compounding=False, interval_type='years', interval_value=1) returns = ts.calculate_returns("2021-01-01", compounding=False, interval_type="years", interval_value=1)
assert returns[1] == 2.4 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 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 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 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 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 assert round(returns[1], 4) == 5.727
with pytest.raises(DateNotFoundError): 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): 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): def test_date_formats(self):
ts = TimeSeries(self.data, frequency='M') ts = TimeSeries(self.data, frequency="M")
FincalOptions.date_format = '%d-%m-%Y' FincalOptions.date_format = "%d-%m-%Y"
with pytest.raises(ValueError): 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') 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) returns2 = ts.calculate_returns("10-04-2020", interval_type="days", interval_value=90)
assert round(returns1[1], 4) == round(returns2[1], 4) == 5.727 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): 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') 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) returns2 = ts.calculate_returns("04-10-2020", interval_type="days", interval_value=90)
assert round(returns1[1], 4) == round(returns2[1], 4) == 5.727 assert round(returns1[1], 4) == round(returns2[1], 4) == 5.727
def test_limits(self): 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): 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)