Compare commits
5 Commits
cae704b658
...
c4e1d8b586
Author | SHA1 | Date | |
---|---|---|---|
c4e1d8b586 | |||
db8377f0ef | |||
583ca98e51 | |||
b1305ca89d | |||
ef68ae0293 |
@ -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(
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user