import datetime import pytest from fincal import ( AllFrequencies, FincalOptions, Frequency, TimeSeries, create_date_series, ) from fincal.exceptions import DateNotFoundError class TestDateSeries: def test_daily(self): start_date = datetime.datetime(2020, 1, 1) end_date = datetime.datetime(2020, 12, 31) d = create_date_series(start_date, end_date, frequency="D") assert len(d) == 366 start_date = datetime.datetime(2017, 1, 1) end_date = datetime.datetime(2017, 12, 31) d = create_date_series(start_date, end_date, frequency="D") assert len(d) == 365 with pytest.raises(ValueError): create_date_series(start_date, end_date, frequency="D", eomonth=True) def test_monthly(self): start_date = datetime.datetime(2020, 1, 1) end_date = datetime.datetime(2020, 12, 31) d = create_date_series(start_date, end_date, frequency="M") assert len(d) == 12 d = create_date_series(start_date, end_date, frequency="M", eomonth=True) assert datetime.datetime(2020, 2, 29) in d start_date = datetime.datetime(2020, 1, 31) d = create_date_series(start_date, end_date, frequency="M") assert datetime.datetime(2020, 2, 29) in d assert datetime.datetime(2020, 8, 31) in d assert datetime.datetime(2020, 10, 30) not in d start_date = datetime.datetime(2020, 2, 29) d = create_date_series(start_date, end_date, frequency="M") assert len(d) == 11 assert datetime.datetime(2020, 2, 29) in d assert datetime.datetime(2020, 8, 31) not in d assert datetime.datetime(2020, 10, 29) in d def test_quarterly(self): start_date = datetime.datetime(2018, 1, 1) end_date = datetime.datetime(2020, 12, 31) d = create_date_series(start_date, end_date, frequency="Q") assert len(d) == 12 d = create_date_series(start_date, end_date, frequency="Q", eomonth=True) assert datetime.datetime(2020, 4, 30) in d start_date = datetime.datetime(2020, 1, 31) d = create_date_series(start_date, end_date, frequency="Q") assert len(d) == 4 assert datetime.datetime(2020, 2, 29) not in d assert max(d) == datetime.datetime(2020, 10, 31) start_date = datetime.datetime(2020, 2, 29) d = create_date_series(start_date, end_date, frequency="Q") assert datetime.datetime(2020, 2, 29) in d assert datetime.datetime(2020, 8, 31) not in d assert datetime.datetime(2020, 11, 29) in d d = create_date_series(start_date, end_date, frequency="Q", eomonth=True) assert datetime.datetime(2020, 11, 30) in d class TestTimeSeriesCreation: def test_creation_with_list_of_tuples(self, create_test_data): ts_data = create_test_data(frequency=AllFrequencies.D, num=50) ts = TimeSeries(ts_data, frequency="D") assert len(ts) == 50 assert isinstance(ts.frequency, Frequency) assert ts.frequency.days == 1 def test_creation_with_string_dates(self, create_test_data): ts_data = create_test_data(frequency=AllFrequencies.D, num=50) ts_data1 = [(dt.strftime("%Y-%m-%d"), val) for dt, val in ts_data] ts = TimeSeries(ts_data1, frequency="D") datetime.datetime(2017, 1, 1) in ts ts_data1 = [(dt.strftime("%d-%m-%Y"), val) for dt, val in ts_data] ts = TimeSeries(ts_data1, frequency="D", date_format="%d-%m-%Y") datetime.datetime(2017, 1, 1) in ts ts_data1 = [(dt.strftime("%m-%d-%Y"), val) for dt, val in ts_data] ts = TimeSeries(ts_data1, frequency="D", date_format="%m-%d-%Y") datetime.datetime(2017, 1, 1) in ts ts_data1 = [(dt.strftime("%m-%d-%Y %H:%M"), val) for dt, val in ts_data] ts = TimeSeries(ts_data1, frequency="D", date_format="%m-%d-%Y %H:%M") datetime.datetime(2017, 1, 1, 0, 0) in ts def test_creation_with_list_of_dicts(self, create_test_data): ts_data = create_test_data(frequency=AllFrequencies.D, num=50) ts_data1 = [{"date": dt.strftime("%Y-%m-%d"), "value": val} for dt, val in ts_data] ts = TimeSeries(ts_data1, frequency="D") datetime.datetime(2017, 1, 1) in ts def test_creation_with_list_of_lists(self, create_test_data): ts_data = create_test_data(frequency=AllFrequencies.D, num=50) ts_data1 = [[dt.strftime("%Y-%m-%d"), val] for dt, val in ts_data] ts = TimeSeries(ts_data1, frequency="D") datetime.datetime(2017, 1, 1) in ts def test_creation_with_dict(self, create_test_data): ts_data = create_test_data(frequency=AllFrequencies.D, num=50) ts_data1 = [{dt.strftime("%Y-%m-%d"): val} for dt, val in ts_data] ts = TimeSeries(ts_data1, frequency="D") datetime.datetime(2017, 1, 1) in ts class TestTimeSeriesBasics: def test_fill(self, create_test_data): FincalOptions.get_closest = "exact" ts_data = create_test_data(frequency=AllFrequencies.D, num=50, skip_weekends=True) ts = TimeSeries(ts_data, frequency="D") ffill_data = ts.ffill() assert len(ffill_data) == 68 ffill_data = ts.ffill(inplace=True) assert ffill_data is None assert len(ts) == 68 ts_data = create_test_data(frequency=AllFrequencies.D, num=50, skip_weekends=True) ts = TimeSeries(ts_data, frequency="D") bfill_data = ts.bfill() assert len(bfill_data) == 68 bfill_data = ts.bfill(inplace=True) assert bfill_data is None assert len(ts) == 68 data = [("2021-01-01", 220), ("2021-01-02", 230), ("2021-01-04", 240)] ts = TimeSeries(data, frequency="D") ff = ts.ffill() assert ff["2021-01-03"][1] == 230 bf = ts.bfill() assert bf["2021-01-03"][1] == 240 def test_fill_weekly(self, create_test_data): ts_data = create_test_data(frequency=AllFrequencies.W, num=10) ts_data.pop(2) ts_data.pop(6) ts = TimeSeries(ts_data, frequency="W") assert len(ts) == 8 ff = ts.ffill() assert len(ff) == 10 assert "2017-01-15" in ff assert ff["2017-01-15"][1] == ff["2017-01-08"][1] bf = ts.bfill() assert len(ff) == 10 assert "2017-01-15" in bf assert bf["2017-01-15"][1] == bf["2017-01-22"][1] def test_fill_monthly(self, create_test_data): ts_data = create_test_data(frequency=AllFrequencies.M, num=10) ts_data.pop(2) ts_data.pop(6) ts = TimeSeries(ts_data, frequency="M") assert len(ts) == 8 ff = ts.ffill() assert len(ff) == 10 assert "2017-03-01" in ff assert ff["2017-03-01"][1] == ff["2017-02-01"][1] bf = ts.bfill() assert len(bf) == 10 assert "2017-08-01" in bf assert bf["2017-08-01"][1] == bf["2017-09-01"][1] def test_fill_eomonthly(self, create_test_data): ts_data = create_test_data(frequency=AllFrequencies.M, num=10, eomonth=True) ts_data.pop(2) ts_data.pop(6) ts = TimeSeries(ts_data, frequency="M") assert len(ts) == 8 ff = ts.ffill() assert len(ff) == 10 assert "2017-03-31" in ff assert ff["2017-03-31"][1] == ff["2017-02-28"][1] bf = ts.bfill() assert len(bf) == 10 assert "2017-08-31" in bf assert bf["2017-08-31"][1] == bf["2017-09-30"][1] class TestReturns: def test_returns_calc(self, create_test_data): ts_data = create_test_data(AllFrequencies.D, skip_weekends=True) ts = TimeSeries(ts_data, "D") returns = ts.calculate_returns( "2020-01-01", annual_compounded_returns=False, return_period_unit="years", return_period_value=1 ) assert round(returns[1], 6) == 0.112913 returns = ts.calculate_returns( "2020-04-01", annual_compounded_returns=False, return_period_unit="months", return_period_value=3 ) assert round(returns[1], 6) == 0.015908 returns = ts.calculate_returns( "2020-04-01", annual_compounded_returns=True, return_period_unit="months", return_period_value=3 ) assert round(returns[1], 6) == 0.065167 returns = ts.calculate_returns( "2020-04-01", annual_compounded_returns=False, return_period_unit="days", return_period_value=90 ) assert round(returns[1], 6) == 0.017673 returns = ts.calculate_returns( "2020-04-01", annual_compounded_returns=True, return_period_unit="days", return_period_value=90 ) assert round(returns[1], 6) == 0.073632 with pytest.raises(DateNotFoundError): ts.calculate_returns("2020-04-04", return_period_unit="days", return_period_value=90, as_on_match="exact") with pytest.raises(DateNotFoundError): ts.calculate_returns("2020-04-04", return_period_unit="months", return_period_value=3, prior_match="exact") def test_date_formats(self, create_test_data): ts_data = create_test_data(AllFrequencies.D, skip_weekends=True) ts = TimeSeries(ts_data, "D") FincalOptions.date_format = "%d-%m-%Y" with pytest.raises(ValueError): ts.calculate_returns( "2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90 ) returns1 = ts.calculate_returns( "2020-04-01", return_period_unit="days", return_period_value=90, date_format="%Y-%m-%d" ) returns2 = ts.calculate_returns("01-04-2020", return_period_unit="days", return_period_value=90) assert round(returns1[1], 6) == round(returns2[1], 6) == 0.073632 FincalOptions.date_format = "%m-%d-%Y" with pytest.raises(ValueError): ts.calculate_returns( "2020-04-01", annual_compounded_returns=True, return_period_unit="days", return_period_value=90 ) returns1 = ts.calculate_returns( "2020-04-01", return_period_unit="days", return_period_value=90, date_format="%Y-%m-%d" ) returns2 = ts.calculate_returns("04-01-2020", return_period_unit="days", return_period_value=90) assert round(returns1[1], 6) == round(returns2[1], 6) == 0.073632 def test_limits(self, create_test_data): FincalOptions.date_format = "%Y-%m-%d" ts_data = create_test_data(AllFrequencies.D) ts = TimeSeries(ts_data, "D") with pytest.raises(DateNotFoundError): 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 return True class TestExpand: def test_weekly_to_daily(self, create_test_data): ts_data = create_test_data(AllFrequencies.W, 10) ts = TimeSeries(ts_data, "W") expanded_ts = ts.expand("D", "ffill") assert len(expanded_ts) == 64 assert expanded_ts.frequency.name == "daily" 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 = TimeSeries(ts_data, "W") expanded_ts = ts.expand("D", "ffill", skip_weekends=True) assert len(expanded_ts) == 46 assert expanded_ts.frequency.name == "daily" 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 = TimeSeries(ts_data, "M") expanded_ts = ts.expand("D", "ffill") assert len(expanded_ts) == 152 assert expanded_ts.frequency.name == "daily" 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 = TimeSeries(ts_data, "M") expanded_ts = ts.expand("D", "ffill", skip_weekends=True) assert len(expanded_ts) == 109 assert expanded_ts.frequency.name == "daily" 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 = TimeSeries(ts_data, "M") expanded_ts = ts.expand("W", "ffill") assert len(expanded_ts) == 22 assert expanded_ts.frequency.name == "weekly" 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 = TimeSeries(ts_data, "Y") expanded_ts = ts.expand("M", "ffill") assert len(expanded_ts) == 49 assert expanded_ts.frequency.name == "monthly" assert expanded_ts.iloc[0][1] == expanded_ts.iloc[1][1] class TestReturnsAgain: 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), ] def test_returns_calc(self): ts = TimeSeries(self.data, frequency="M") returns = ts.calculate_returns( "2021-01-01", annual_compounded_returns=False, return_period_unit="years", return_period_value=1 ) assert returns[1] == 2.4 returns = ts.calculate_returns( "2020-04-01", annual_compounded_returns=False, return_period_unit="months", return_period_value=3 ) assert round(returns[1], 4) == 0.6 returns = ts.calculate_returns( "2020-04-01", annual_compounded_returns=True, return_period_unit="months", return_period_value=3 ) assert round(returns[1], 4) == 5.5536 returns = ts.calculate_returns( "2020-04-01", annual_compounded_returns=False, return_period_unit="days", return_period_value=90 ) assert round(returns[1], 4) == 0.6 returns = ts.calculate_returns( "2020-04-01", annual_compounded_returns=True, return_period_unit="days", return_period_value=90 ) assert round(returns[1], 4) == 5.727 returns = ts.calculate_returns( "2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90 ) assert round(returns[1], 4) == 5.727 with pytest.raises(DateNotFoundError): ts.calculate_returns("2020-04-10", return_period_unit="days", return_period_value=90, as_on_match="exact") with pytest.raises(DateNotFoundError): ts.calculate_returns("2020-04-10", return_period_unit="days", return_period_value=90, prior_match="exact") def test_date_formats(self): ts = TimeSeries(self.data, frequency="M") FincalOptions.date_format = "%d-%m-%Y" with pytest.raises(ValueError): ts.calculate_returns( "2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90 ) returns1 = ts.calculate_returns( "2020-04-10", return_period_unit="days", return_period_value=90, date_format="%Y-%m-%d" ) returns2 = ts.calculate_returns("10-04-2020", return_period_unit="days", return_period_value=90) assert round(returns1[1], 4) == round(returns2[1], 4) == 5.727 FincalOptions.date_format = "%m-%d-%Y" with pytest.raises(ValueError): ts.calculate_returns( "2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90 ) returns1 = ts.calculate_returns( "2020-04-10", return_period_unit="days", return_period_value=90, date_format="%Y-%m-%d" ) returns2 = ts.calculate_returns("04-10-2020", return_period_unit="days", return_period_value=90) assert round(returns1[1], 4) == round(returns2[1], 4) == 5.727 def test_limits(self): ts = TimeSeries(self.data, frequency="M") FincalOptions.date_format = "%Y-%m-%d" with pytest.raises(DateNotFoundError): ts.calculate_returns("2020-04-25", return_period_unit="days", return_period_value=90, closest_max_days=10) class TestVolatility: def test_daily_ts(self, create_test_data): ts_data = create_test_data(AllFrequencies.D) ts = TimeSeries(ts_data, "D") assert len(ts) == 1000 sd = ts.volatility(annualize_volatility=False) assert round(sd, 6) == 0.002622 sd = ts.volatility() assert round(sd, 6) == 0.050098 sd = ts.volatility(annual_compounded_returns=True) assert round(sd, 4) == 37.9329 sd = ts.volatility(return_period_unit="months", annual_compounded_returns=True) assert round(sd, 4) == 0.6778 sd = ts.volatility(return_period_unit="years") assert round(sd, 6) == 0.023164 sd = ts.volatility(from_date="2017-10-01", to_date="2019-08-31", annualize_volatility=True) assert round(sd, 6) == 0.050559 sd = ts.volatility(from_date="2017-02-01", frequency="M", return_period_unit="months") assert round(sd, 6) == 0.050884 sd = ts.volatility( frequency="M", return_period_unit="months", return_period_value=3, annualize_volatility=False, ) assert round(sd, 6) == 0.020547 class TestDrawdown: def test_daily_ts(self, create_test_data): ts_data = create_test_data(AllFrequencies.D, skip_weekends=True) ts = TimeSeries(ts_data, "D") mdd = ts.max_drawdown() assert isinstance(mdd, dict) assert len(mdd) == 3 assert all(i in mdd for i in ["start_date", "end_date", "drawdown"]) expeced_response = { "start_date": datetime.datetime(2017, 6, 6, 0, 0), "end_date": datetime.datetime(2017, 7, 31, 0, 0), "drawdown": -0.028293686030751997, } assert mdd == expeced_response def test_weekly_ts(self, create_test_data): ts_data = create_test_data(AllFrequencies.W, mu=1, sigma=0.5) ts = TimeSeries(ts_data, "W") mdd = ts.max_drawdown() assert isinstance(mdd, dict) assert len(mdd) == 3 assert all(i in mdd for i in ["start_date", "end_date", "drawdown"]) expeced_response = { "start_date": datetime.datetime(2019, 2, 17, 0, 0), "end_date": datetime.datetime(2019, 11, 17, 0, 0), "drawdown": -0.2584760499552089, } assert mdd == expeced_response class TestSync: def test_weekly_to_daily(self, create_test_data): daily_data = create_test_data(AllFrequencies.D, num=15) weekly_data = create_test_data(AllFrequencies.W, num=3) daily_ts = TimeSeries(daily_data, frequency="D") weekly_ts = TimeSeries(weekly_data, frequency="W") synced_weekly_ts = daily_ts.sync(weekly_ts) assert len(daily_ts) == len(synced_weekly_ts) assert synced_weekly_ts.frequency == AllFrequencies.D assert "2017-01-02" in synced_weekly_ts assert synced_weekly_ts["2017-01-02"][1] == synced_weekly_ts["2017-01-01"][1]