@@ -117,13 +117,34 @@ def mean_return_monthly(self) -> float:
117117 def mean_return_annual (self ) -> float :
118118 return Float .annualize_return (self .mean_return_monthly )
119119
120- @property
121- def cagr (self ) -> Union [pd .Series , float ]:
120+ def get_cagr (self , period : Union [str , int , None ] = None ) -> pd .Series :
121+ """
122+ Calculates Compound Annual Growth Rate (CAGR) for a given period:
123+ None: full time
124+ 'YTD': Year To Date compound rate of return (formally not a CAGR)
125+ Integer: several years
126+ """
122127 if hasattr (self , 'inflation' ):
123128 df = pd .concat ([self .returns_ts , self .inflation_ts ], axis = 1 , join = 'inner' , copy = 'false' )
124129 else :
125130 df = self .returns_ts
126- return Frame .get_cagr (df )
131+ dt0 = self .last_date
132+
133+ if not period :
134+ cagr = Frame .get_cagr (df )
135+ elif str (period ).lower () == 'ytd' :
136+ year = dt0 .year
137+ cagr = (df [str (year ):] + 1. ).prod () - 1.
138+ elif isinstance (period , int ):
139+ dt = Date .subtract_years (dt0 , period )
140+ if dt >= self .first_date :
141+ cagr = Frame .get_cagr (df [dt :])
142+ else :
143+ row = {x : None for x in df .columns }
144+ cagr = pd .Series (row )
145+ else :
146+ raise ValueError (f'{ period } is not a valid value for period' )
147+ return cagr
127148
128149 @property
129150 def annual_return_ts (self ) -> pd .DataFrame :
@@ -134,6 +155,8 @@ def dividend_yield(self) -> pd.DataFrame:
134155 """
135156 Calculates dividend yield time series in all base currencies of portfolio assets.
136157 For every currency dividend yield is a weighted sum of the assets dividend yields.
158+ Portfolio asset allocation (weights) is a constant (monthly rebalanced portfolios).
159+ TODO: calculate for not rebalance portfolios (and arbitrary reb period).
137160 """
138161 div_yield_assets = self ._list .dividend_yield
139162 currencies_dict = self ._list .currencies
@@ -144,6 +167,7 @@ def dividend_yield(self) -> pd.DataFrame:
144167 for currency in currencies_list :
145168 assets_with_the_same_currency = [x for x in currencies_dict if currencies_dict [x ] == currency ]
146169 df = div_yield_assets [assets_with_the_same_currency ]
170+ # for monthly rebalanced portfolio
147171 weights = [self .assets_weights [k ] for k in self .assets_weights if k in assets_with_the_same_currency ]
148172 weighted_weights = np .asarray (weights ) / np .asarray (weights ).sum ()
149173 div_yield_series = Frame .get_portfolio_return_ts (weighted_weights , df )
@@ -223,10 +247,10 @@ def describe(self, years: Tuple[int] = (1, 5, 10)) -> pd.DataFrame:
223247 else :
224248 row = {'portfolio' : value }
225249 row .update ({'period' : 'YTD' })
226- row .update ({'rebalancing' : 'Not rebalanced ' })
250+ row .update ({'rebalancing' : '1 year ' })
227251 row .update ({'property' : 'compound return' })
228252 description = description .append (row , ignore_index = True )
229- # CAGR for a list of periods (rebalanced 1 month )
253+ # CAGR for a list of periods (rebalanced 1 year )
230254 for i in years :
231255 dt = Date .subtract_years (dt0 , i )
232256 if dt >= self .first_date :
@@ -258,7 +282,7 @@ def describe(self, years: Tuple[int] = (1, 5, 10)) -> pd.DataFrame:
258282 row .update ({'property' : 'CAGR' })
259283 description = description .append (row , ignore_index = True )
260284 # CAGR rebalanced 1 month
261- value = self .cagr
285+ value = self .get_cagr ()
262286 if hasattr (self , 'inflation' ):
263287 row = value .to_dict ()
264288 full_inflation = value .loc [self .inflation ] # full period inflation is required for following calc
@@ -278,6 +302,15 @@ def describe(self, years: Tuple[int] = (1, 5, 10)) -> pd.DataFrame:
278302 row .update ({'rebalancing' : 'Not rebalanced' })
279303 row .update ({'property' : 'CAGR' })
280304 description = description .append (row , ignore_index = True )
305+ # Dividend Yield
306+ dy = self .dividend_yield
307+ for i , ccy in enumerate (dy ):
308+ value = self .dividend_yield .iloc [- 1 , i ]
309+ row = {'portfolio' : value }
310+ row .update ({'period' : 'LTM' })
311+ row .update ({'rebalancing' : '1 month' })
312+ row .update ({'property' : f'Dividend yield ({ ccy } )' })
313+ description = description .append (row , ignore_index = True )
281314 # risk (rebalanced 1 month)
282315 row = {'portfolio' : self .risk_annual }
283316 row .update ({'period' : f'{ self .period_length } years' })
@@ -319,8 +352,9 @@ def table(self) -> pd.DataFrame:
319352 def get_rolling_cagr (self , years : int = 1 ) -> pd .Series :
320353 """
321354 Rolling portfolio CAGR (annualized rate of return) time series.
322- TODO: check if self.period_length is below 1 year
323355 """
356+ if self .pl .years < 1 :
357+ raise ValueError ('Portfolio history data period length should be at least 12 months.' )
324358 rolling_return = (self .returns_ts + 1. ).rolling (_MONTHS_PER_YEAR * years ).apply (np .prod , raw = True ) ** (1 / years ) - 1.
325359 rolling_return .dropna (inplace = True )
326360 return rolling_return
0 commit comments