Skip to content

Commit fee3ef2

Browse files
committed
feat: Portfolio.recovery_period must return a time series of recovery periods
1 parent e9dce7e commit fee3ef2

File tree

6 files changed

+62
-158
lines changed

6 files changed

+62
-158
lines changed

examples/01 howto.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@
615615
}
616616
],
617617
"source": [
618-
"x.assets_dividend_yield"
618+
"x.dividend_yield"
619619
]
620620
},
621621
{
@@ -642,7 +642,7 @@
642642
}
643643
],
644644
"source": [
645-
"x.assets_dividend_yield.plot();"
645+
"x.dividend_yield.plot();"
646646
]
647647
},
648648
{

examples/03 investment portfolios.ipynb

Lines changed: 24 additions & 123 deletions
Large diffs are not rendered by default.

main.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22

33
import okama as ok
44
# Portfolio WithDrawls
5-
weights = [0.32, 0.31, 0.18, .19]
6-
portf = ok.Portfolio(['RGBITR.INDX', 'RUCBTRNS.INDX', 'MCFTR.INDX', 'GC.COMM'],
7-
ccy="RUB",
8-
weights=weights,
9-
inflation=False,
10-
symbol="retirement_portf.PF",
11-
rebalancing_period='year',
12-
cashflow=-150_000,
13-
initial_amount=39_000_000,
14-
discount_rate=0.01
15-
)
5+
# weights = [0.32, 0.31, 0.18, .19]
6+
# portf = ok.Portfolio(['RGBITR.INDX', 'RUCBTRNS.INDX', 'MCFTR.INDX', 'GC.COMM'],
7+
# ccy="RUB",
8+
# weights=weights,
9+
# inflation=False,
10+
# symbol="retirement_portf.PF",
11+
# rebalancing_period='year',
12+
# cashflow=-150_000,
13+
# initial_amount=39_000_000,
14+
# discount_rate=0.01
15+
# )
1616

1717
# print(portf.discount_rate)
1818
# print(portf)
@@ -24,14 +24,14 @@
2424
#
2525
# portf.plot_forecast_monte_carlo(distr="norm", years=30, backtest=True, n=100)
2626

27-
s_periods = portf.monte_carlo_survival_period(distr="lognorm", years=25, n=10)
28-
print(f"медиана {s_periods.quantile(50 / 100)}")
27+
# s_periods = portf.monte_carlo_survival_period(distr="lognorm", years=25, n=10)
28+
# print(f"медиана {s_periods.quantile(50 / 100)}")
2929
# print(f"первый порцентиль {s_periods.quantile(1 / 100)}")
3030
# print(f"99й порцентиль {s_periods.quantile(99 / 100)}")
3131
# print(f"минимум {s_periods.min()}")
3232
# print(f"среднее {s_periods.mean()}")
3333

34-
plt.show()
34+
# plt.show()
3535

3636
# Rolling / Expanding Risk
3737
# al = ok.AssetList(['DJI.INDX',
@@ -46,3 +46,9 @@
4646
# pf = ok.Portfolio(['SPY.US',
4747
# 'BND.US'
4848
# ])
49+
rf3 = ok.Portfolio(
50+
["BND.US", "VTI.US", "VXUS.US"],
51+
weights=[0.40, 0.40, 0.20],
52+
rebalancing_period="year",
53+
)
54+
print(rf3.recovery_period)

okama/common/make_asset_list.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ def _get_single_asset_dividends(self, tick: str, remove_forecast: bool = True) -
267267
if asset.currency != self.currency:
268268
s = self._adjust_price_to_currency_monthly(s, asset.currency)
269269
if remove_forecast:
270-
s = s[: pd.Period.now("M")] # Period.now() must be without arguments to be compatible with pandas 2.0
270+
s = s[: pd.Period.now(freq="M")] # Period.now() must be without arguments to be compatible with pandas 2.0
271271
# Create time series with zeros to pad the empty spaces in dividends time series
272272
index = pd.date_range(start=self.first_date, end=self.last_date, freq="MS") # 'MS' to include the last period
273273
period = index.to_period("M")

okama/portfolio.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,47 +1301,44 @@ def drawdowns(self) -> pd.Series:
13011301
return helpers.Frame.get_drawdowns(self.ror)
13021302

13031303
@property
1304-
def recovery_period(self) -> int:
1304+
def recovery_period(self) -> pd.Series:
13051305
"""
1306-
Calculate the longest recovery period for the portfolio assets value.
1306+
Get recovery period time series for the portfolio value.
13071307
13081308
The recovery period (drawdown duration) is the number of months to reach the value of the last maximum.
13091309
13101310
Returns
13111311
-------
1312-
Integer
1313-
Max recovery period for the protfolio assets value in months.
1312+
pd.Series
1313+
Recovery period time series for the portfolio value
13141314
13151315
Notes
13161316
-----
1317-
If the last maximum value is not recovered NaN is returned.
13181317
The largest recovery period does not necessary correspond to the max drawdown.
13191318
13201319
Examples
13211320
--------
13221321
>>> pf = ok.Portfolio(['SPY.US', 'AGG.US'], weights=[0.5, 0.5])
1323-
>>> pf.recovery_period
1324-
35
1322+
>>> pf.recovery_period.nlargest()
1323+
date
1324+
2010-10 35
1325+
2004-10 7
1326+
2012-01 7
1327+
2019-03 6
1328+
2018-07 5
1329+
Freq: M, Name: portfolio_5724.PF, dtype: int32
13251330
13261331
See Also
13271332
--------
13281333
drawdowns : Calculate drawdowns time series.
13291334
"""
1330-
if hasattr(self, "inflation"):
1331-
w_index = self.wealth_index.drop(columns=[self.inflation])
1332-
else:
1333-
w_index = self.wealth_index
1334-
if isinstance(w_index, pd.DataFrame):
1335-
# time series should be a Series to use groupby
1336-
w_index = w_index.squeeze()
1335+
w_index = self.wealth_index_with_assets[self.symbol]
13371336
cummax = w_index.cummax()
13381337
s = cummax.pct_change()[1:]
13391338
s1 = s.where(s == 0).notnull().astype(int)
13401339
s1_1 = s.where(s == 0).isnull().astype(int).cumsum()
13411340
s2 = s1.groupby(s1_1).cumsum()
1342-
# Max recovery period date should not be in the border (means it's not recovered)
1343-
max_period = s2.max() if s2.idxmax().to_timestamp() != self.last_date else np.NAN
1344-
return max_period
1341+
return s2[s2.shift(-1) < s2]
13451342

13461343
def describe(self, years: Tuple[int] = (1, 5, 10)) -> pd.DataFrame:
13471344
"""

tests/test_portfolio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def test_drawdowns(portfolio_not_rebalanced):
176176

177177

178178
def test_recovery_period(portfolio_not_rebalanced):
179-
assert portfolio_not_rebalanced.recovery_period == 6
179+
assert portfolio_not_rebalanced.recovery_period.max() == 6
180180

181181

182182
def test_get_cagr(portfolio_rebalanced_month, portfolio_no_inflation):

0 commit comments

Comments
 (0)