Skip to content

Commit

Permalink
feat: recovery_period method in Portfolio
Browse files Browse the repository at this point in the history
  • Loading branch information
chilango74 committed Jun 10, 2021
1 parent a8315ab commit 2dc4ebe
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 16 deletions.
4 changes: 2 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import okama as ok
pf = ok.Portfolio(['T.US', 'XOM.US'], weights=[0.8, 0.2], first_date='2010-01', last_date='2021-01', ccy='USD')
print(pf.dividend_yield)
x = ok.Portfolio(['SPY.US', 'SBERP.MOEX'], weights=[0.2, 0.8])
print(x.recovery_period)
6 changes: 3 additions & 3 deletions okama/asset_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,9 @@ def drawdowns(self) -> pd.DataFrame:
@property
def recovery_periods(self) -> pd.Series:
"""
Calculate longest recovery periods for the assets.
Calculate the longest recovery periods for the assets.
The recovery period is the number of months to reach the value of the last maximum.
The recovery period (drawdown duration) is the number of months to reach the value of the last maximum.
Returns
-------
Expand All @@ -216,7 +216,7 @@ def recovery_periods(self) -> pd.Series:
Notes
-----
If the maximum value is not recovered NaN is returned.
If the last asset maximum value is not recovered NaN is returned.
The largest recovery period does not necessary correspond to the max drawdown.
Examples
Expand Down
10 changes: 5 additions & 5 deletions okama/common/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,15 +247,15 @@ def get_okid_index(rates: pd.Series, symbol: str) -> pd.Series:
# Risk metrics

@classmethod
def get_portfolio_risk(cls, weights: list, ror: pd.DataFrame) -> float:
def get_portfolio_risk(cls, weights: list, assets_ror: pd.DataFrame) -> float:
"""
Computes the std of portfolio returns.
Compute the standard deviation of return for monthly rebalanced portfolio.
"""
# cls.weights_sum_is_one(weights)
if isinstance(ror, pd.Series): # required for a single asset portfolio
return ror.std()
if isinstance(assets_ror, pd.Series): # required for a single asset portfolio
return assets_ror.std()
weights = np.array(weights)
covmat = ror.cov()
covmat = assets_ror.cov()
return math.sqrt(weights.T @ covmat @ weights)

@staticmethod
Expand Down
5 changes: 2 additions & 3 deletions okama/common/make_asset_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,8 @@ def _make_list(self, ls: list) -> dict:
first_dates_sorted: list = sorted(first_dates.items(), key=lambda y: y[1])
last_dates_sorted: list = sorted(last_dates.items(), key=lambda y: y[1])
if isinstance(df, pd.Series):
df = (
df.to_frame()
) # required to convert Series to DataFrame for single asset list
# required to convert Series to DataFrame for single asset list
df = df.to_frame()
return dict(
asset_obj_list=asset_obj_dict,
first_date=first_dates_sorted[-1][1],
Expand Down
49 changes: 46 additions & 3 deletions okama/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ class Portfolio(ListMaker):
# TODO: Finish description.
"""
# TODO: add withdrawals as an attribute:
# frequency (monthly/annual), amount (nominal, percentage), inflation_adjusted

def __init__(
self,
Expand Down Expand Up @@ -616,7 +614,7 @@ def risk_monthly(self) -> float:
>>> pf.risk_monthly
0.09415483565833212
"""
return Frame.get_portfolio_risk(self.weights, self.assets_ror)
return self.ror.std()

@property
def risk_annual(self) -> float:
Expand Down Expand Up @@ -746,6 +744,50 @@ def drawdowns(self) -> pd.Series:
"""
return Frame.get_drawdowns(self.ror)

@property
def recovery_period(self) -> int:
"""
Calculate the longest recovery period for the portfolio assets value.
The recovery period (drawdown duration) is the number of months to reach the value of the last maximum.
Returns
-------
Integer
Max recovery period for the protfolio assets value in months.
Notes
-----
If the last maximum value is not recovered NaN is returned.
The largest recovery period does not necessary correspond to the max drawdown.
Examples
--------
>>> pf = ok.Portfolio(['SPY.US', 'AGG.US'], weights=[0.5, 0.5])
>>> pf.recovery_period
35
See Also
--------
drawdowns : Calculate drawdowns time series.
"""
if hasattr(self, "inflation"):
w_index = self.wealth_index.drop(columns=[self.inflation])
else:
w_index = self.wealth_index
if isinstance(w_index, pd.DataFrame):
# time series should be a Series to use groupby
w_index = w_index.squeeze()
cummax = w_index.cummax()
s = cummax.pct_change()[1:]
s1 = s.where(s == 0).notnull().astype(int)
s1_1 = s.where(s == 0).isnull().astype(int).cumsum()
s2 = s1.groupby(s1_1).cumsum()
# Max recovery period date should not be in the border (means it's not recovered)
max_period = s2.max() if s2.idxmax().to_timestamp() != self.last_date else np.NAN
return max_period


def describe(self, years: Tuple[int] = (1, 5, 10)) -> pd.DataFrame:
"""
Generate descriptive statistics for the portfolio.
Expand Down Expand Up @@ -779,6 +821,7 @@ def describe(self, years: Tuple[int] = (1, 5, 10)) -> pd.DataFrame:
get_cvar : Calculate historic Conditional Value at Risk (CVAR, expected shortfall).
drawdowns : Calculate drawdowns.
"""
# TODO: Remove rebalancing. All metrics should work with self.ror
description = pd.DataFrame()
dt0 = self.last_date
df = self._add_inflation()
Expand Down
9 changes: 9 additions & 0 deletions tests/test_portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ def test_get_cvar_historic(portfolio_rebalanced_month):
) == approx(0.10762, rel=1e-2)


def test_drawdowns(portfolio_not_rebalanced):
assert portfolio_not_rebalanced.drawdowns.min() == approx(-0.1265, rel=1e-2)


def test_recovery_period(portfolio_not_rebalanced):
assert portfolio_not_rebalanced.recovery_period == 12



def test_get_cagr(portfolio_rebalanced_month):
values = pd.Series({"portfolio": 0.1303543, "RUB.INFL": 0.05548082428015655})
actual = portfolio_rebalanced_month.get_cagr()
Expand Down

0 comments on commit 2dc4ebe

Please sign in to comment.