From 5512a647ad24fabe3c9d1c00efdc4d59a12cec83 Mon Sep 17 00:00:00 2001 From: Gourav Kumar Date: Thu, 12 May 2022 10:40:47 +0530 Subject: [PATCH] made eomonth parsing more intelligent Corrected tests and code to account for the same --- fincal/fincal.py | 9 +++++++-- fincal/utils.py | 13 +++++++++++++ tests/conftest.py | 2 +- tests/test_fincal.py | 46 +++++++++++++++++++++++++++++++++++++------- 4 files changed, 60 insertions(+), 10 deletions(-) diff --git a/fincal/fincal.py b/fincal/fincal.py index 0d81c05..b531315 100644 --- a/fincal/fincal.py +++ b/fincal/fincal.py @@ -14,6 +14,7 @@ from .utils import ( FincalOptions, _find_closest_date, _interval_to_years, + _is_eomonth, _preprocess_match_options, ) @@ -146,7 +147,7 @@ class TimeSeries(TimeSeriesCore): return res_string.format(self.start_date, self.end_date, total_dates) def ffill( - self, inplace: bool = False, limit: int = 1000, skip_weekends: bool = False, eomonth: bool = False + self, inplace: bool = False, limit: int = 1000, skip_weekends: bool = False, eomonth: bool = None ) -> TimeSeries | None: """Forward fill missing dates in the time series @@ -165,6 +166,8 @@ class TimeSeries(TimeSeriesCore): ------- Returns a TimeSeries object if inplace is False, otherwise None """ + if eomonth is None: + eomonth = _is_eomonth(self.dates) dates_to_fill = create_date_series( self.start_date, self.end_date, self.frequency.symbol, eomonth, skip_weekends=skip_weekends @@ -190,7 +193,7 @@ class TimeSeries(TimeSeriesCore): return self.__class__(new_ts, frequency=self.frequency.symbol) def bfill( - self, inplace: bool = False, limit: int = 1000, skip_weekends: bool = False, eomonth: bool = False + self, inplace: bool = False, limit: int = 1000, skip_weekends: bool = False, eomonth: bool = None ) -> TimeSeries | None: """Backward fill missing dates in the time series @@ -209,6 +212,8 @@ class TimeSeries(TimeSeriesCore): ------- Returns a TimeSeries object if inplace is False, otherwise None """ + if eomonth is None: + eomonth = _is_eomonth(self.dates) dates_to_fill = create_date_series( self.start_date, self.end_date, self.frequency.symbol, eomonth, skip_weekends=skip_weekends diff --git a/fincal/utils.py b/fincal/utils.py index 8c1fcbf..4391325 100644 --- a/fincal/utils.py +++ b/fincal/utils.py @@ -2,6 +2,8 @@ import datetime from dataclasses import dataclass from typing import List, Literal, Mapping, Sequence, Tuple +from dateutil.relativedelta import relativedelta + from .exceptions import DateNotFoundError, DateOutOfRangeError @@ -174,3 +176,14 @@ def _interval_to_years(interval_type: Literal["years", "months", "day"], interva year_conversion_factor: dict = {"years": 1, "months": 12, "days": 365} years: float = interval_value / year_conversion_factor[interval_type] return years + + +def _is_eomonth(dates: Sequence[datetime.datetime], threshold: float = 0.7): + """Checks if a series is should be treated as end of month date series or not. + + If eomonth dates exceed threshold percentage, it will be treated as eomonth series. + This can be used for any frequency, but will work only for monthly and lower frequencies. + """ + eomonth_dates = [date.month != (date + relativedelta(days=1)).month for date in dates] + eomonth_proportion = sum(eomonth_dates) / len(dates) + return eomonth_proportion > threshold diff --git a/tests/conftest.py b/tests/conftest.py index 8ad3323..d94c290 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,6 +56,7 @@ def create_prices(s0: float, mu: float, sigma: float, num_prices: int) -> list: def sample_data_generator( frequency: fc.Frequency, + start_date: datetime.date = datetime.date(2017, 1, 1), num: int = 1000, skip_weekends: bool = False, mu: float = 0.1, @@ -87,7 +88,6 @@ def sample_data_generator( Returns a TimeSeries object """ - start_date = datetime.datetime(2017, 1, 1) timedelta_dict = { frequency.freq_type: int( frequency.value * num * (7 / 5 if frequency == fc.AllFrequencies.D and skip_weekends else 1) diff --git a/tests/test_fincal.py b/tests/test_fincal.py index 8e471da..6f2d554 100644 --- a/tests/test_fincal.py +++ b/tests/test_fincal.py @@ -198,6 +198,23 @@ class TestTimeSeriesBasics: assert "2017-08-31" in bf assert bf["2017-08-31"][1] == bf["2017-09-30"][1] + def test_fill_quarterly(self, create_test_data): + ts_data = create_test_data(frequency=AllFrequencies.Q, num=10, eomonth=True) + ts_data.pop(2) + ts_data.pop(6) + ts = TimeSeries(ts_data, frequency="Q") + assert len(ts) == 8 + + ff = ts.ffill() + assert len(ff) == 10 + assert "2017-07-31" in ff + assert ff["2017-07-31"][1] == ff["2017-04-30"][1] + + bf = ts.bfill() + assert len(bf) == 10 + assert "2018-10-31" in bf + assert bf["2018-10-31"][1] == bf["2019-01-31"][1] + class TestReturns: def test_returns_calc(self, create_test_data): @@ -268,13 +285,13 @@ class TestReturns: ts.calculate_returns("2020-11-25", return_period_unit="days", return_period_value=90, closest_max_days=10) def test_rolling_returns(self): - # Yet to be written + # To-do return True class TestExpand: def test_weekly_to_daily(self, create_test_data): - ts_data = create_test_data(AllFrequencies.W, 10) + ts_data = create_test_data(AllFrequencies.W, num=10) ts = TimeSeries(ts_data, "W") expanded_ts = ts.expand("D", "ffill") assert len(expanded_ts) == 64 @@ -282,7 +299,7 @@ class TestExpand: assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1] def test_weekly_to_daily_no_weekends(self, create_test_data): - ts_data = create_test_data(AllFrequencies.W, 10) + ts_data = create_test_data(AllFrequencies.W, num=10) ts = TimeSeries(ts_data, "W") expanded_ts = ts.expand("D", "ffill", skip_weekends=True) assert len(expanded_ts) == 46 @@ -290,7 +307,7 @@ class TestExpand: assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1] def test_monthly_to_daily(self, create_test_data): - ts_data = create_test_data(AllFrequencies.M, 6) + ts_data = create_test_data(AllFrequencies.M, num=6) ts = TimeSeries(ts_data, "M") expanded_ts = ts.expand("D", "ffill") assert len(expanded_ts) == 152 @@ -298,7 +315,7 @@ class TestExpand: assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1] def test_monthly_to_daily_no_weekends(self, create_test_data): - ts_data = create_test_data(AllFrequencies.M, 6) + ts_data = create_test_data(AllFrequencies.M, num=6) ts = TimeSeries(ts_data, "M") expanded_ts = ts.expand("D", "ffill", skip_weekends=True) assert len(expanded_ts) == 109 @@ -306,7 +323,7 @@ class TestExpand: assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1] def test_monthly_to_weekly(self, create_test_data): - ts_data = create_test_data(AllFrequencies.M, 6) + ts_data = create_test_data(AllFrequencies.M, num=6) ts = TimeSeries(ts_data, "M") expanded_ts = ts.expand("W", "ffill") assert len(expanded_ts) == 22 @@ -314,7 +331,7 @@ class TestExpand: assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1] def test_yearly_to_monthly(self, create_test_data): - ts_data = create_test_data(AllFrequencies.Y, 5) + ts_data = create_test_data(AllFrequencies.Y, num=5) ts = TimeSeries(ts_data, "Y") expanded_ts = ts.expand("M", "ffill") assert len(expanded_ts) == 49 @@ -322,6 +339,21 @@ class TestExpand: assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1] +class TestShrink: + # To-do + pass + + +class TestMeanReturns: + # To-do + pass + + +class TestReadCsv: + # To-do + pass + + class TestReturnsAgain: data = [ ("2020-01-01", 10),