Compare commits

...

5 Commits

6 changed files with 81 additions and 39 deletions

View File

@ -101,7 +101,7 @@ class _IndexSlicer:
def __init__(self, parent_obj: object): def __init__(self, parent_obj: object):
self.parent = parent_obj self.parent = parent_obj
def __getitem__(self, n): def __getitem__(self, n) -> Mapping:
if isinstance(n, int): if isinstance(n, int):
keys: list = [self.parent.dates[n]] keys: list = [self.parent.dates[n]]
else: else:
@ -378,7 +378,7 @@ class TimeSeriesCore:
validate_frequency: boolean, default True validate_frequency: boolean, default True
Whether the provided frequency should be validated against the data. Whether the provided frequency should be validated against the data.
When set to True, if the expected number of data points are not withint the expected limits, When set to True, if the expected number of data points are not within the expected limits,
it will raise an Exception and object creation will fail. it will raise an Exception and object creation will fail.
Validation is performed only if data contains at least 12 data points, as a fewer number of Validation is performed only if data contains at least 12 data points, as a fewer number of
data points are not sufficient to determine the frequency correctly. data points are not sufficient to determine the frequency correctly.
@ -401,7 +401,7 @@ class TimeSeriesCore:
if validate_frequency and len(ts_data) >= 12: if validate_frequency and len(ts_data) >= 12:
if validation["frequency_match"] is not None and not validation["frequency_match"]: if validation["frequency_match"] is not None and not validation["frequency_match"]:
raise ValueError( raise ValueError(
f"Data appears to be of frquency {validation['expected_frequency']!r}, " f"Data appears to be of frequency {validation['expected_frequency']!r}, "
f"but {frequency!r} was provided. Pass the correct frequency." f"but {frequency!r} was provided. Pass the correct frequency."
"\nPass validate_frequency=False to disable this validation." "\nPass validate_frequency=False to disable this validation."
) )
@ -685,7 +685,7 @@ class TimeSeriesCore:
return key in self.data return key in self.data
def _arithmatic_validator(self, other): def _arithmatic_validator(self, other):
"""Validates input data before performing math operatios""" """Validates input data before performing math operations"""
if not isinstance(other, (Number, Series, TimeSeriesCore)): if not isinstance(other, (Number, Series, TimeSeriesCore)):
raise TypeError( raise TypeError(

View File

@ -262,7 +262,7 @@ class TimeSeries(TimeSeriesCore):
return_period_unit: Literal["years", "months", "days"] = "years", return_period_unit: Literal["years", "months", "days"] = "years",
return_period_value: int = 1, return_period_value: int = 1,
date_format: str = None, date_format: str = None,
) -> float: ) -> Tuple[datetime.datetime, float]:
"""Method to calculate returns for a certain time-period as on a particular date """Method to calculate returns for a certain time-period as on a particular date
Parameters Parameters
@ -295,7 +295,7 @@ class TimeSeries(TimeSeriesCore):
* fail: Raise a ValueError * fail: Raise a ValueError
* nan: Return nan as the value * nan: Return nan as the value
compounding : bool, optional annual_compounded_returns : bool, optional
Whether the return should be compounded annually. Whether the return should be compounded annually.
return_period_unit : 'years', 'months', 'days' return_period_unit : 'years', 'months', 'days'
@ -321,14 +321,14 @@ class TimeSeries(TimeSeriesCore):
Example Example
-------- --------
>>> calculate_returns(datetime.date(2020, 1, 1), years=1) >>> ts.calculate_returns(datetime.date(2020, 1, 1), years=1)
(datetime.datetime(2020, 1, 1, 0, 0), .0567) (datetime.datetime(2020, 1, 1, 0, 0), .0567)
""" """
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(**{return_period_unit: return_period_value})
current = _find_closest_date(self.data, as_on, closest_max_days, as_on_delta, if_not_found) current = _find_closest_date(self.data, as_on, closest_max_days, as_on_delta, if_not_found)
prev_date = as_on - relativedelta(**{return_period_unit: return_period_value})
if current[1] != str("nan"): if current[1] != str("nan"):
previous = _find_closest_date(self.data, prev_date, closest_max_days, prior_delta, if_not_found) previous = _find_closest_date(self.data, prev_date, closest_max_days, prior_delta, if_not_found)
@ -368,16 +368,16 @@ class TimeSeries(TimeSeriesCore):
End date for the returns calculation. End date for the returns calculation.
frequency : str, optional frequency : str, optional
Frequency at which the returns should be calcualated. Frequency at which the returns should be calculated.
Valid values are {D, W, M, Q, H, Y} Valid values are {D, W, M, Q, H, Y}
as_on_match : str, optional as_on_match : str, optional
The match mode to be used for the as on date. The match mode to be used for the as on date.
If not specified, the value for the closes parameter will be used. If not specified, the value for the closest parameter will be used.
prior_match : str, optional prior_match : str, optional
The match mode to be used for the prior date, i.e., the date against which the return will be calculated. The match mode to be used for the prior date, i.e., the date against which the return will be calculated.
If not specified, the value for the closes parameter will be used. If not specified, the value for the closest parameter will be used.
closest : previous | next | exact closest : previous | next | exact
The default match mode for dates. The default match mode for dates.
@ -395,7 +395,7 @@ class TimeSeries(TimeSeriesCore):
For instance, if the input date is before the starting of the first date of the time series, For instance, if the input date is before the starting of the first date of the time series,
but match mode is set to previous. A DateOutOfRangeError will be raised in such cases. but match mode is set to previous. A DateOutOfRangeError will be raised in such cases.
compounding : bool, optional annual_compounded_returns : bool, optional
Should the returns be compounded annually. Should the returns be compounded annually.
return_period_unit : years | month | days return_period_unit : years | month | days
@ -410,7 +410,7 @@ class TimeSeries(TimeSeriesCore):
Returns Returns
------- -------
Returs the rolling returns as a TimeSeries object. Returns the rolling returns as a TimeSeries object.
Raises Raises
------ ------
@ -431,7 +431,7 @@ class TimeSeries(TimeSeriesCore):
raise ValueError(f"Invalid argument for frequency {frequency}") raise ValueError(f"Invalid argument for frequency {frequency}")
if from_date is None: if from_date is None:
from_date = self.start_date + relativedelta( from_date = self.start_date + relativedelta(
days=int(_interval_to_years(return_period_unit, return_period_value) * 365 + 1) days=math.ceil(_interval_to_years(return_period_unit, return_period_value) * 365)
) )
if to_date is None: if to_date is None:
@ -476,7 +476,7 @@ class TimeSeries(TimeSeriesCore):
) -> float: ) -> float:
"""Calculates the volatility of the time series.add() """Calculates the volatility of the time series.add()
The volatility is calculated as the standard deviaion of periodic returns. The volatility is calculated as the standard deviation of periodic returns.
The periodicity of returns is based on the periodicity of underlying data. The periodicity of returns is based on the periodicity of underlying data.
Parameters: Parameters:
@ -761,10 +761,10 @@ class TimeSeries(TimeSeriesCore):
Parameters: Parameters:
----------- -----------
other: TimeSeries other: TimeSeries
Another object of TimeSeries class whose dates need to be syncronized Another object of TimeSeries class whose dates need to be synchronized
fill_method: ffill | bfill, default ffill fill_method: ffill | bfill, default ffill
Method to use to fill missing values in time series when syncronizing Method to use to fill missing values in time series when synchronizing
Returns: Returns:
-------- --------
@ -903,7 +903,7 @@ def read_csv(
header = data[read_start_row] header = data[read_start_row]
print(header) print(header)
# fmt: off # fmt: off
# Black and pylance disagree on the foratting of the following line, hence formatting is disabled # Black and pylance disagree on the formatting of the following line, hence formatting is disabled
data = data[(read_start_row + 1):read_end_row] data = data[(read_start_row + 1):read_end_row]
# fmt: on # fmt: on

View File

@ -8,7 +8,9 @@ from typing import Literal
from pyfacts.core import date_parser from pyfacts.core import date_parser
from .pyfacts import TimeSeries from .pyfacts import TimeSeries
from .utils import _interval_to_years, covariance from .utils import _interval_to_years, _preprocess_from_to_date, covariance
# from dateutil.relativedelta import relativedelta
@date_parser(3, 4) @date_parser(3, 4)
@ -540,10 +542,21 @@ def sortino_ratio(
interval_days = math.ceil(_interval_to_years(return_period_unit, return_period_value) * 365) interval_days = math.ceil(_interval_to_years(return_period_unit, return_period_value) * 365)
if from_date is None: # if from_date is None:
from_date = time_series_data.start_date + datetime.timedelta(days=interval_days) # from_date = time_series_data.start_date + relativedelta(**{return_period_unit: return_period_value})
if to_date is None: # if to_date is None:
to_date = time_series_data.end_date # to_date = time_series_data.end_date
from_date, to_date = _preprocess_from_to_date(
from_date,
to_date,
time_series_data,
False,
return_period_unit,
return_period_value,
as_on_match,
prior_match,
closest,
)
if risk_free_data is None and risk_free_rate is None: if risk_free_data is None and risk_free_rate is None:
raise ValueError("At least one of risk_free_data or risk_free rate is required") raise ValueError("At least one of risk_free_data or risk_free rate is required")
@ -566,7 +579,8 @@ def sortino_ratio(
annualized_average_rr = (1 + average_rr) ** (365 / interval_days) - 1 annualized_average_rr = (1 + average_rr) ** (365 / interval_days) - 1
excess_returns = annualized_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]) my_list = [i for i in average_rr_ts.values if i < 0]
sd = statistics.stdev(my_list) # [i for i in average_rr_ts.values if i < 0])
sd *= math.sqrt(365 / interval_days) sd *= math.sqrt(365 / interval_days)
sortino_ratio_value = excess_returns / sd sortino_ratio_value = excess_returns / sd

View File

@ -144,13 +144,43 @@ 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 _preprocess_from_to_date(
from_date: datetime.date | str,
to_date: datetime.date | str,
time_series: Mapping = None,
align_dates: bool = True,
return_period_unit: Literal["years", "months", "days"] = None,
return_period_value: int = None,
as_on_match: str = "closest",
prior_match: str = "closest",
closest: Literal["previous", "next", "exact"] = "previous",
) -> tuple:
as_on_match, prior_match = _preprocess_match_options(as_on_match, prior_match, closest)
if (from_date is None or to_date is None) and time_series is None:
raise ValueError("Provide either to_date and from_date or time_series data")
if time_series is not None and (return_period_unit is None or return_period_value is None):
raise ValueError("Provide return period for calculation of from_date")
if from_date is None:
expected_start_date = time_series.start_date + relativedelta(**{return_period_unit: return_period_value})
from_date = _find_closest_date(time_series.data, expected_start_date, 999, as_on_match, "fail")[0]
if to_date is None:
to_date = time_series.end_date
return from_date, to_date
def _find_closest_date( def _find_closest_date(
data: Mapping[datetime.datetime, float], data: Mapping[datetime.datetime, float],
date: datetime.datetime, date: datetime.datetime,
limit_days: int, limit_days: int,
delta: datetime.timedelta, delta: datetime.timedelta,
if_not_found: Literal["fail", "nan"], if_not_found: Literal["fail", "nan"],
): ) -> Tuple[datetime.datetime, float]:
"""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):

View File

@ -3,8 +3,8 @@ import datetime
import pytest import pytest
from pyfacts import ( from pyfacts import (
AllFrequencies, AllFrequencies,
PyfactsOptions,
Frequency, Frequency,
PyfactsOptions,
TimeSeries, TimeSeries,
create_date_series, create_date_series,
) )
@ -248,7 +248,7 @@ class TestReturns:
with pytest.raises(DateNotFoundError): with pytest.raises(DateNotFoundError):
ts.calculate_returns("2020-04-04", return_period_unit="days", return_period_value=90, as_on_match="exact") ts.calculate_returns("2020-04-04", return_period_unit="days", return_period_value=90, as_on_match="exact")
with pytest.raises(DateNotFoundError): with pytest.raises(DateNotFoundError):
ts.calculate_returns("2020-04-04", return_period_unit="months", return_period_value=3, prior_match="exact") ts.calculate_returns("2020-04-08", return_period_unit="months", return_period_value=1, prior_match="exact")
def test_date_formats(self, create_test_data): def test_date_formats(self, create_test_data):
ts_data = create_test_data(AllFrequencies.D, skip_weekends=True) ts_data = create_test_data(AllFrequencies.D, skip_weekends=True)

View File

@ -106,18 +106,16 @@ class TestSortino:
) )
assert round(sortino_ratio, 4) == 1.2564 assert round(sortino_ratio, 4) == 1.2564
# def test_sharpe_weekly_freq(self, create_test_data): def test_sortino_weekly_freq(self, create_test_data):
# data = create_test_data(num=261, frequency=pft.AllFrequencies.W, mu=0.6, sigma=0.7) data = create_test_data(num=500, frequency=pft.AllFrequencies.W, mu=0.12, sigma=0.06)
# ts = pft.TimeSeries(data, "W") ts = pft.TimeSeries(data, "W")
# sharpe_ratio = pft.sharpe_ratio( sortino = pft.sortino_ratio(
# ts, ts,
# risk_free_rate=0.052, risk_free_rate=0.06,
# from_date="2017-01-08", return_period_unit="years",
# to_date="2021-12-31", return_period_value=1,
# return_period_unit="days", )
# return_period_value=7, assert round(sortino, 4) == -5.5233
# )
# assert round(sharpe_ratio, 4) == 0.4533
# sharpe_ratio = pft.sharpe_ratio( # sharpe_ratio = pft.sharpe_ratio(
# ts, # ts,