From 3ffec7b11be7c932f7e470d2722fd646b38a86b8 Mon Sep 17 00:00:00 2001 From: Gourav Kumar Date: Sat, 11 Jun 2022 17:56:42 +0530 Subject: [PATCH] handled issues with frequency validation --- pyfacts/core.py | 15 ++++++++++----- pyfacts/pyfacts.py | 11 +++++++++-- pyfacts/statistics.py | 20 ++++++++++++-------- test_series.py | 4 ++-- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/pyfacts/core.py b/pyfacts/core.py index 37d4631..0d881fe 100644 --- a/pyfacts/core.py +++ b/pyfacts/core.py @@ -180,7 +180,7 @@ class Series(UserList): if len(self) != len(other): raise ValueError("Length of Series must be same for comparison") - elif (self.dtype != float and isinstance(other, Number)) or not isinstance(other, self.dtype): + elif self.dtype != float and isinstance(other, Number): raise Exception(f"Cannot compare type {self.dtype.__name__} to {type(other).__name__}") return other @@ -300,7 +300,9 @@ class Series(UserList): def _validate_frequency( - data: List[Tuple[datetime.datetime, float]], provided_frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None + data: List[Tuple[datetime.datetime, float]], + provided_frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None, + raise_error: bool = True, ): """Checks the data and returns the expected frequency.""" if provided_frequency is not None: @@ -325,7 +327,10 @@ def _validate_frequency( expected_frequency = frequency break else: - raise ValueError("Data does not match any known frequency. Perhaps you have too many missing data points.") + if raise_error: + raise ValueError("Data does not match any known frequency. Perhaps you have too many missing data points.") + else: + expected_frequency = provided_frequency.symbol expected_data_points = expected_data_points[expected_frequency] if provided_frequency is None: @@ -387,7 +392,7 @@ class TimeSeriesCore: ts_data = _preprocess_timeseries(ts_data, date_format=date_format) - validation = _validate_frequency(data=ts_data, provided_frequency=frequency) + validation = _validate_frequency(data=ts_data, provided_frequency=frequency, raise_error=validate_frequency) if frequency is None: frequency = validation["expected_frequency"] @@ -508,7 +513,7 @@ class TimeSeriesCore: """Helper function to retrieve items using a list""" data_to_return = [self._get_item_from_key(key) for key in date_list] - return self.__class__(data_to_return, frequency=self.frequency.symbol) + return self.__class__(data_to_return, frequency=self.frequency.symbol, validate_frequency=False) def _get_item_from_series(self, series: Series): """Helper function to retrieve item using a Series object diff --git a/pyfacts/pyfacts.py b/pyfacts/pyfacts.py index 12a8306..2a8e3af 100644 --- a/pyfacts/pyfacts.py +++ b/pyfacts/pyfacts.py @@ -344,8 +344,8 @@ class TimeSeries(TimeSeriesCore): @date_parser(1, 2) def calculate_rolling_returns( self, - from_date: datetime.date | str, - to_date: datetime.date | str, + from_date: datetime.date | str = None, + to_date: datetime.date | str = None, frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None, as_on_match: str = "closest", prior_match: str = "closest", @@ -429,6 +429,13 @@ class TimeSeries(TimeSeriesCore): frequency = getattr(AllFrequencies, frequency) except AttributeError: raise ValueError(f"Invalid argument for frequency {frequency}") + if from_date is None: + from_date = self.start_date + relativedelta( + days=int(_interval_to_years(return_period_unit, return_period_value) * 365 + 1) + ) + + if to_date is None: + to_date = self.end_date dates = create_date_series(from_date, to_date, frequency.symbol) if frequency == AllFrequencies.D: diff --git a/pyfacts/statistics.py b/pyfacts/statistics.py index 586301b..61c782d 100644 --- a/pyfacts/statistics.py +++ b/pyfacts/statistics.py @@ -2,6 +2,7 @@ from __future__ import annotations import datetime import statistics +from cmath import sqrt from typing import Literal from pyfacts.core import date_parser @@ -472,13 +473,14 @@ def sortino_ratio( closest: Literal["previous", "next"] = "previous", date_format: str = None, ) -> float: - """Calculate the Sharpe ratio of any time series + """Calculate the Sortino ratio of any time series - Sharpe ratio is a measure of returns per unit of risk, - where risk is measured by the standard deviation of the returns. + Sortino ratio is a variation of the Sharpe ratio, + where risk is measured as standard deviation of negative returns only. + Since deviation on the positive side is not undesirable, hence sortino ratio excludes positive deviations. - The formula for Sharpe ratio is: - (average asset return - risk free rate)/volatility of asset returns + The formula for Sortino ratio is: + (average asset return - risk free rate)/volatility of negative asset returns Parameters ---------- @@ -528,7 +530,7 @@ def sortino_ratio( Returns ------- - Value of Sharpe ratio as a float. + Value of Sortino ratio as a float. Raises ------ @@ -559,11 +561,13 @@ def sortino_ratio( "closest": closest, "date_format": date_format, } - average_rr_ts = time_series_data.calculate_rolling_returns(**common_params, annual_compounded_returns=True) + average_rr_ts = time_series_data.calculate_rolling_returns(**common_params, annual_compounded_returns=False) average_rr = statistics.mean(average_rr_ts.values) + annualized_average_rr = (1 + average_rr) ** (365 / interval_days) - 1 - excess_returns = average_rr - risk_free_rate + excess_returns = annualized_average_rr - risk_free_rate sd = statistics.stdev([i for i in average_rr_ts.values if i < 0]) + sd *= sqrt(365 / interval_days) sortino_ratio_value = excess_returns / sd return sortino_ratio_value diff --git a/test_series.py b/test_series.py index 773ce8f..6414346 100644 --- a/test_series.py +++ b/test_series.py @@ -1,6 +1,6 @@ import datetime -from fincal.core import Series +from pyfacts.core import Series s1 = Series([2.5, 6.2, 5.6, 8.4, 7.4, 1.5, 9.6, 5]) @@ -19,7 +19,7 @@ dt_lst = [ datetime.datetime(2020, 6, 19, 0, 0), datetime.datetime(2016, 3, 16, 0, 0), datetime.datetime(2017, 4, 25, 0, 0), - datetime.datetime(2016, 7, 10, 0, 0) + datetime.datetime(2016, 7, 10, 0, 0), ] s2 = Series(dt_lst)