@@ -117,6 +117,7 @@ def __init__(
117117 self .verbose = verbose
118118 self .full_frontier = full_frontier
119119 self ._ef_points = pd .DataFrame (dtype = float )
120+ self ._mdp_points = pd .DataFrame (dtype = float )
120121
121122 def __repr__ (self ):
122123 dic = {
@@ -169,6 +170,7 @@ def bounds(self) -> Tuple[Tuple[float, ...], ...]:
169170 def bounds (self , bounds ):
170171
171172 self ._ef_points = pd .DataFrame (dtype = float )
173+ self ._mdp_points = pd .DataFrame (dtype = float )
172174
173175 if bounds :
174176 if len (bounds ) != len (self .symbols ):
@@ -209,6 +211,7 @@ def n_points(self, n_points: int):
209211
210212 def _clear_cache (self ):
211213 self ._ef_points = pd .DataFrame (dtype = float ) # renew EF points DataFrame
214+ self ._mdp_points = pd .DataFrame (dtype = float ) # renew MDP points DataFrame
212215
213216 @property
214217 def rebalancing_strategy (self ) -> Rebalance :
@@ -284,7 +287,108 @@ def verbose(self, verbose: bool):
284287 self ._clear_cache ()
285288 self ._verbose = verbose
286289
287- # TODO: add get_most_diversified_portfolio (as in EfficientFronier)
290+ def get_most_diversified_portfolio (
291+ self ,
292+ target_return : Optional [float ] = None ,
293+ ) -> Dict [str , float ]:
294+ """
295+ Calculate assets weights, annualized values for risk and return, Diversification ratio
296+ for the most diversified portfolio given the target CAGR within given bounds.
297+
298+ The most diversified portfolio has the largest Diversification Ratio.
299+
300+ The Diversification Ratio is the ratio of the weighted average of assets risks divided by the portfolio risk.
301+ In this case risk is the annualized standard deviation for the rate of return.
302+
303+ Returns
304+ -------
305+ dict
306+ Weights of assets and annualized values for risk, CAGR and Diversification ratio of the most diversified portfolio.
307+
308+ Parameters
309+ ----------
310+ target_return : float, optional
311+ Target Compound Annual Growth Rate (CAGR) for the portfolio. The optimization process looks for a portfolio
312+ with the target_return and largest Diversification ratio. If not specified global most diversified portfolio
313+ is obtained.
314+
315+ Examples
316+ --------
317+ >>> ls4 = ['SPY.US', 'AGG.US', 'VNQ.US', 'GLD.US']
318+ >>> x = ok.EfficientFrontierReb(assets=ls4, ccy='USD', last_date='2021-12')
319+ >>> x.get_most_diversified_portfolio() # get a global most diversified portfolio
320+ {'SPY.US': 0.19612726258395477,
321+ 'AGG.US': 0.649730553241489,
322+ 'VNQ.US': 0.020096313783052246,
323+ 'GLD.US': 0.13404587039150392,
324+ 'CAGR': 0.062355715886719176,
325+ 'Risk': 0.05510135025563423,
326+ 'Diversification ratio': 1.5665720501693001}
327+
328+ It is possible to get the most diversified portfolio for a given target CAGR.
329+
330+ >>> x.get_most_diversified_portfolio(target_return=0.10)
331+ {'SPY.US': 0.3389762570274293,
332+ 'AGG.US': 0.12915657041748244,
333+ 'VNQ.US': 0.15083042115027034,
334+ 'GLD.US': 0.3810367514048179,
335+ 'CAGR': 0.09370688842211439,
336+ 'Risk': 0.11725067815643951,
337+ 'Diversification ratio': 1.4419864802150442}
338+ """
339+ ror = self .assets_ror
340+ n = self .assets_ror .shape [1 ]
341+ init_guess = np .repeat (1 / n , n )
342+
343+ args = dict (
344+ period = self .rebalancing_strategy .period ,
345+ )
346+
347+ def objective_function (w ):
348+ # Diversification Ratio
349+ assets_risk = ror .std ()
350+ assets_mean_return = self .assets_ror .mean ()
351+ assets_annualized_risk = helpers .Float .annualize_risk (assets_risk , assets_mean_return )
352+ weights = np .asarray (w )
353+ assets_sigma_weighted_sum = weights .T @ assets_annualized_risk
354+
355+ portfolio_ror = Rebalance (** args ).return_ror_ts_ef (w , ror )
356+ portfolio_mean_return_monthly = portfolio_ror .mean ()
357+ portfolio_risk_monthly = portfolio_ror .std ()
358+
359+ objective_function .annual_risk = helpers .Float .annualize_risk (
360+ portfolio_risk_monthly , portfolio_mean_return_monthly
361+ )
362+ return - assets_sigma_weighted_sum / objective_function .annual_risk
363+
364+ # construct the constraints
365+ weights_sum_to_1 = {"type" : "eq" , "fun" : lambda weights : np .sum (weights ) - 1 }
366+ cagr_is_target = {
367+ "type" : "eq" ,
368+ "fun" : lambda weights : target_return - self ._get_cagr (weights ),
369+ }
370+ constraints = (weights_sum_to_1 ,) if target_return is None else (weights_sum_to_1 , cagr_is_target )
371+
372+ # set optimizer
373+ weights = minimize (
374+ objective_function ,
375+ init_guess ,
376+ method = "SLSQP" ,
377+ options = {"disp" : False },
378+ constraints = constraints ,
379+ bounds = self .bounds ,
380+ )
381+ if weights .success :
382+ # CAGR calculation
383+ cagr = self ._get_cagr (weights .x )
384+ asset_labels = self .symbols if self .ticker_names else list (self .names .values ())
385+ point = {x : y for x , y in zip (asset_labels , weights .x )}
386+ point ["CAGR" ] = cagr
387+ point ["Risk" ] = objective_function .annual_risk
388+ point ["Diversification ratio" ] = - weights .fun
389+ return point
390+ else :
391+ raise RecursionError ("No solutions where found" )
288392
289393 @property
290394 def gmv_monthly_weights (self ) -> np .ndarray :
@@ -952,6 +1056,67 @@ def compute_right_part_of_ef(i, target_cagr):
9521056 logger .info (f"Total time taken is { (main_end_time - main_start_time ) / 60 :.2f} min." )
9531057 self ._ef_points = df
9541058
1059+ @property
1060+ def mdp_points (self ) -> pd .DataFrame :
1061+ """
1062+ Generate Most diversified portfolios frontier for rebalanced portfolios.
1063+
1064+ Each point on the Most diversified portfolios frontier is a rebalanced portfolio with optimized
1065+ Diversification ratio for a given CAGR.
1066+
1067+ The points are obtained through the constrained optimization process (optimization with bounds).
1068+ Bounds are defined with 'bounds' property.
1069+
1070+ Returns
1071+ -------
1072+ DataFrame
1073+ Table of weights and risk/return values for the Most Diversified Portfolios Frontier.
1074+ The columns:
1075+
1076+ - assets weights
1077+ - CAGR (geometric mean)
1078+ - Risk (standard deviation)
1079+ - Diversification ratio
1080+
1081+ All the values are annualized.
1082+
1083+ Examples
1084+ --------
1085+ >>> ls4 = ['SP500TR.INDX', 'MCFTR.INDX', 'RGBITR.INDX', 'GC.COMM']
1086+ >>> y = ok.EfficientFrontierReb(assets=ls4, ccy='RUB', last_date='2021-12', n_points=20)
1087+ >>> y.mdp_points # print mdp weights, risk, CAGR and Diversification ratio
1088+ Risk CAGR Diversification ratio ... MCFTR.INDX RGBITR.INDX SP500TR.INDX
1089+ 0 0.066040 0.092220 1.234567 ... 2.081668e-16 1.000000e+00 0.000000e+00
1090+ 1 0.064299 0.093451 1.245678 ... 0.000000e+00 9.844942e-01 5.828671e-16
1091+ ...
1092+
1093+ To plot the Most diversification portfolios line use the DataFrame with the points data.
1094+ Additionally 'Plot.plot_assets()' can be used to show the assets in the chart.
1095+
1096+ >>> import matplotlib.pyplot as plt
1097+ >>> fig = plt.figure()
1098+ >>> # Plot the assets points
1099+ >>> y.plot_assets(kind='cagr') # kind should be set to "cagr" as we take "CAGR" column from the mdp_points.
1100+ >>> ax = plt.gca()
1101+ >>> # Plot the Most diversified portfolios line
1102+ >>> df = y.mdp_points
1103+ >>> ax.plot(df['Risk'], df['CAGR']) # we chose to plot CAGR which is geometric mean of return series
1104+ >>> # Set the axis labels and the title
1105+ >>> ax.set_title('Most diversified portfolios line')
1106+ >>> ax.set_xlabel('Risk (Standard Deviation)')
1107+ >>> ax.set_ylabel('Return (CAGR)')
1108+ >>> plt.show()
1109+ """
1110+ if self ._mdp_points .empty :
1111+ target_cagrs = self ._target_cagr_range_left
1112+ df = pd .DataFrame (dtype = "float" )
1113+ for x in target_cagrs :
1114+ row = self .get_most_diversified_portfolio (target_return = x )
1115+ df = pd .concat ([df , pd .DataFrame (row , index = [0 ])], ignore_index = True )
1116+ df = helpers .Frame .change_columns_order (df , ["Risk" , "CAGR" ])
1117+ self ._mdp_points = df
1118+ return self ._mdp_points
1119+
9551120 def get_monte_carlo (self , n : int = 100 ) -> pd .DataFrame :
9561121 """
9571122 Generate N random rebalanced portfolios with Monte Carlo simulation.
0 commit comments