Merge branch 'master' of http://192.168.0.114:3000/buddy/fincal
This commit is contained in:
commit
b34c14d778
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import datetime
|
import datetime
|
||||||
import math
|
import math
|
||||||
import statistics
|
import statistics
|
||||||
from typing import Iterable, List, Literal, Mapping, Union
|
from typing import Iterable, List, Literal, Mapping, TypedDict, Union
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
@ -16,6 +16,12 @@ from .utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MaxDrawdown(TypedDict):
|
||||||
|
start_date: datetime.datetime
|
||||||
|
end_date: datetime.datetime
|
||||||
|
drawdown: float
|
||||||
|
|
||||||
|
|
||||||
@date_parser(0, 1)
|
@date_parser(0, 1)
|
||||||
def create_date_series(
|
def create_date_series(
|
||||||
start_date: Union[str, datetime.datetime],
|
start_date: Union[str, datetime.datetime],
|
||||||
@ -115,11 +121,11 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
|
|
||||||
super().__init__(data, frequency, date_format)
|
super().__init__(data, frequency, date_format)
|
||||||
|
|
||||||
def info(self):
|
def info(self) -> str:
|
||||||
"""Summary info about the TimeSeries object"""
|
"""Summary info about the TimeSeries object"""
|
||||||
|
|
||||||
total_dates = len(self.data.keys())
|
total_dates: int = len(self.data.keys())
|
||||||
res_string = "First date: {}\nLast date: {}\nNumber of rows: {}"
|
res_string: str = "First date: {}\nLast date: {}\nNumber of rows: {}"
|
||||||
return res_string.format(self.start_date, self.end_date, total_dates)
|
return res_string.format(self.start_date, self.end_date, total_dates)
|
||||||
|
|
||||||
def ffill(self, inplace: bool = False, limit: int = None) -> Union[TimeSeries, None]:
|
def ffill(self, inplace: bool = False, limit: int = None) -> Union[TimeSeries, None]:
|
||||||
@ -138,7 +144,7 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
Returns a TimeSeries object if inplace is False, otherwise None
|
Returns a TimeSeries object if inplace is False, otherwise None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
eomonth = True if self.frequency.days >= AllFrequencies.M.days else False
|
eomonth: bool = True if self.frequency.days >= AllFrequencies.M.days else False
|
||||||
dates_to_fill = create_date_series(self.start_date, self.end_date, self.frequency.symbol, eomonth)
|
dates_to_fill = create_date_series(self.start_date, self.end_date, self.frequency.symbol, eomonth)
|
||||||
|
|
||||||
new_ts = dict()
|
new_ts = dict()
|
||||||
@ -171,7 +177,7 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
Returns a TimeSeries object if inplace is False, otherwise None
|
Returns a TimeSeries object if inplace is False, otherwise None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
eomonth = True if self.frequency.days >= AllFrequencies.M.days else False
|
eomonth: bool = True if self.frequency.days >= AllFrequencies.M.days else False
|
||||||
dates_to_fill = create_date_series(self.start_date, self.end_date, self.frequency.symbol, eomonth)
|
dates_to_fill = create_date_series(self.start_date, self.end_date, self.frequency.symbol, eomonth)
|
||||||
dates_to_fill.append(self.end_date)
|
dates_to_fill.append(self.end_date)
|
||||||
|
|
||||||
@ -517,21 +523,31 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
rr = self.calculate_rolling_returns(**kwargs)
|
rr = self.calculate_rolling_returns(**kwargs)
|
||||||
return statistics.mean(rr.values)
|
return statistics.mean(rr.values)
|
||||||
|
|
||||||
def max_drawdown(self):
|
def max_drawdown(self) -> MaxDrawdown:
|
||||||
max_val_dict = {}
|
"""Calculates the maximum fall the stock has taken between any two points.
|
||||||
|
|
||||||
prev_val = 0
|
Returns
|
||||||
prev_date = list(self.data)[0]
|
-------
|
||||||
|
MaxDrawdown
|
||||||
|
Returns the start_date, end_date, and the drawdown value in decimal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
drawdowns: dict = dict()
|
||||||
|
|
||||||
|
prev_val: float = 0
|
||||||
|
prev_date: datetime.datetime = list(self.data)[0]
|
||||||
|
|
||||||
for dt, val in self.data.items():
|
for dt, val in self.data.items():
|
||||||
if val > prev_val:
|
if val > prev_val:
|
||||||
max_val_dict[dt] = (dt, val, 0)
|
drawdowns[dt] = (dt, val, 0)
|
||||||
prev_date, prev_val = dt, val
|
prev_date, prev_val = dt, val
|
||||||
else:
|
else:
|
||||||
max_val_dict[dt] = (prev_date, prev_val, val / prev_val - 1)
|
drawdowns[dt] = (prev_date, prev_val, val / prev_val - 1)
|
||||||
|
|
||||||
max_drawdown = min(max_val_dict.items(), key=lambda x: x[1][2])
|
max_drawdown = min(drawdowns.items(), key=lambda x: x[1][2])
|
||||||
max_drawdown = dict(start_date=max_drawdown[1][0], end_date=max_drawdown[0], drawdown=max_drawdown[1][2])
|
max_drawdown: MaxDrawdown = dict(
|
||||||
|
start_date=max_drawdown[1][0], end_date=max_drawdown[0], drawdown=max_drawdown[1][2]
|
||||||
|
)
|
||||||
|
|
||||||
return max_drawdown
|
return max_drawdown
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
|
from unittest import skip
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from fincal.core import AllFrequencies, Frequency
|
from fincal.core import AllFrequencies, Frequency
|
||||||
from fincal.exceptions import DateNotFoundError
|
from fincal.exceptions import DateNotFoundError
|
||||||
from fincal.fincal import TimeSeries, create_date_series
|
from fincal.fincal import MaxDrawdown, TimeSeries, create_date_series
|
||||||
from fincal.utils import FincalOptions
|
from fincal.utils import FincalOptions
|
||||||
|
|
||||||
|
|
||||||
@ -77,7 +78,9 @@ def create_test_timeseries(
|
|||||||
|
|
||||||
start_date = datetime.datetime(2017, 1, 1)
|
start_date = datetime.datetime(2017, 1, 1)
|
||||||
timedelta_dict = {
|
timedelta_dict = {
|
||||||
frequency.freq_type: int(frequency.value * num * (7 / 5 if frequency == "D" and skip_weekends else 1))
|
frequency.freq_type: int(
|
||||||
|
frequency.value * num * (7 / 5 if frequency == AllFrequencies.D and skip_weekends else 1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
end_date = start_date + relativedelta(**timedelta_dict)
|
end_date = start_date + relativedelta(**timedelta_dict)
|
||||||
dates = create_date_series(start_date, end_date, frequency.symbol, skip_weekends=skip_weekends)
|
dates = create_date_series(start_date, end_date, frequency.symbol, skip_weekends=skip_weekends)
|
||||||
@ -88,7 +91,7 @@ def create_test_timeseries(
|
|||||||
|
|
||||||
class TestReturns:
|
class TestReturns:
|
||||||
def test_returns_calc(self):
|
def test_returns_calc(self):
|
||||||
ts = create_test_timeseries()
|
ts = create_test_timeseries(AllFrequencies.D, skip_weekends=True)
|
||||||
returns = ts.calculate_returns(
|
returns = ts.calculate_returns(
|
||||||
"2020-01-01", annual_compounded_returns=False, return_period_unit="years", return_period_value=1
|
"2020-01-01", annual_compounded_returns=False, return_period_unit="years", return_period_value=1
|
||||||
)
|
)
|
||||||
@ -120,7 +123,7 @@ class TestReturns:
|
|||||||
ts.calculate_returns("2020-04-04", return_period_unit="months", return_period_value=3, prior_match="exact")
|
ts.calculate_returns("2020-04-04", return_period_unit="months", return_period_value=3, prior_match="exact")
|
||||||
|
|
||||||
def test_date_formats(self):
|
def test_date_formats(self):
|
||||||
ts = create_test_timeseries()
|
ts = create_test_timeseries(AllFrequencies.D, skip_weekends=True)
|
||||||
FincalOptions.date_format = "%d-%m-%Y"
|
FincalOptions.date_format = "%d-%m-%Y"
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ts.calculate_returns(
|
ts.calculate_returns(
|
||||||
@ -147,7 +150,7 @@ class TestReturns:
|
|||||||
|
|
||||||
def test_limits(self):
|
def test_limits(self):
|
||||||
FincalOptions.date_format = "%Y-%m-%d"
|
FincalOptions.date_format = "%Y-%m-%d"
|
||||||
ts = create_test_timeseries()
|
ts = create_test_timeseries(AllFrequencies.D)
|
||||||
with pytest.raises(DateNotFoundError):
|
with pytest.raises(DateNotFoundError):
|
||||||
ts.calculate_returns("2020-11-25", return_period_unit="days", return_period_value=90, closest_max_days=10)
|
ts.calculate_returns("2020-11-25", return_period_unit="days", return_period_value=90, closest_max_days=10)
|
||||||
|
|
||||||
@ -177,3 +180,31 @@ class TestVolatility:
|
|||||||
annualize_volatility=False,
|
annualize_volatility=False,
|
||||||
)
|
)
|
||||||
assert round(sd, 6) == 0.020547
|
assert round(sd, 6) == 0.020547
|
||||||
|
|
||||||
|
|
||||||
|
class TestDrawdown:
|
||||||
|
def test_daily_ts(self):
|
||||||
|
ts = create_test_timeseries(AllFrequencies.D, skip_weekends=True)
|
||||||
|
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):
|
||||||
|
ts = create_test_timeseries(AllFrequencies.W, mu=1, sigma=0.5)
|
||||||
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user