@@ -390,6 +390,95 @@ def objective_function(w):
390390 else :
391391 raise RecursionError ("No solutions where found" )
392392
393+ def get_tangency_portfolio (self , rf_return : float = 0 , rate_of_return : str = "cagr" ) -> dict :
394+ """
395+ Calculate asset weights, risk and return values for tangency portfolio within given bounds.
396+
397+ Tangency portfolio or Maximum Sharpe Ratio (MSR) is the point on the Efficient Frontier where
398+ Sharpe Ratio reaches its maximum.
399+
400+ The Sharpe ratio is the average annual return in excess of the risk-free rate
401+ per unit of risk (annualized standard deviation).
402+
403+ Bounds are defined with 'bounds' property.
404+
405+ Parameters
406+ ----------
407+ rate_of_return : {cagr, mean_return}, default cagr
408+ Use CAGR (Compound annual growth rate) or arithmetic mean of return to calculate Sharpe Ratio.
409+
410+ rf_return : float, default 0
411+ Risk-free rate of return.
412+
413+ Returns
414+ -------
415+ dict
416+ Weights of assets, risk and return of the tangency portfolio.
417+
418+ Examples
419+ --------
420+ >>> three_assets = ['MCFTR.INDX', 'RGBITR.INDX', 'GC.COMM']
421+ >>> ef = ok.EfficientFrontierReb(assets=three_assets, ccy='USD', last_date='2022-06')
422+ >>> ef.get_tangency_portfolio(rf_return=0.03) # risk free rate of return is 3%
423+ {'Weights': array([0.30672901, 0. , 0.69327099]), 'Rate_of_return': 0.12265215404959617, 'Risk': 0.1882249366394522}
424+
425+ To calculate tangency portfolio parameters for arithmetic mean set rate_of_return='mean_return':
426+
427+ >>> ef.get_tangency_portfolio(rate_of_return="mean_return", rf_return=0.03)
428+ {'Weights': array([2.95364739e-01, 1.08420217e-17, 7.04635261e-01]), 'Rate_of_return': 0.10654206521088283, 'Risk': 0.048279725208422115}
429+ """
430+ ror = self .assets_ror
431+ n = self .assets_ror .shape [1 ]
432+ init_guess = np .repeat (1 / n , n )
433+
434+ args = dict (
435+ period = self .rebalancing_strategy .period ,
436+ )
437+
438+ def of_arithmetic_mean (w ):
439+ # Sharpe ratio with arithmetic mean
440+ portfolio_ror = Rebalance (** args ).return_ror_ts_ef (w , ror )
441+ mean_return_monthly = portfolio_ror .mean ()
442+ risk_monthly = portfolio_ror .std ()
443+ of_arithmetic_mean .rate_of_return = helpers .Float .annualize_return (mean_return_monthly )
444+ of_arithmetic_mean .risk = helpers .Float .annualize_risk (risk_monthly , mean_return_monthly )
445+ return - (of_arithmetic_mean .rate_of_return - rf_return ) / of_arithmetic_mean .risk
446+
447+ def of_geometric_mean (w ):
448+ # Sharpe ratio with CAGR
449+ portfolio_ror = Rebalance (** args ).return_ror_ts_ef (w , ror )
450+ mean_return_monthly = portfolio_ror .mean ()
451+ of_geometric_mean .rate_of_return = helpers .Frame .get_cagr (portfolio_ror )
452+ # Risk
453+ risk_monthly = portfolio_ror .std ()
454+ of_geometric_mean .risk = helpers .Float .annualize_risk (risk_monthly , mean_return_monthly )
455+ return - (of_geometric_mean .rate_of_return - rf_return ) / of_geometric_mean .risk
456+
457+ if rate_of_return .lower () in {"cagr" , "mean_return" }:
458+ rate_of_return = rate_of_return .lower ()
459+ else :
460+ raise ValueError ("rate_of_return must be 'cagr' or 'mean_return'" )
461+
462+ objective_function = of_geometric_mean if rate_of_return == "cagr" else of_arithmetic_mean
463+ # construct the constraints
464+ weights_sum_to_1 = {"type" : "eq" , "fun" : lambda weights : np .sum (weights ) - 1 }
465+ weights = minimize (
466+ objective_function ,
467+ init_guess ,
468+ method = "SLSQP" ,
469+ options = {"disp" : False },
470+ constraints = (weights_sum_to_1 ,),
471+ bounds = self .bounds ,
472+ )
473+ if weights .success :
474+ return {
475+ "Weights" : weights .x ,
476+ "Rate_of_return" : objective_function .rate_of_return ,
477+ "Risk" : objective_function .risk ,
478+ }
479+ else :
480+ raise RecursionError ("No solutions where found" )
481+
393482 @property
394483 def gmv_monthly_weights (self ) -> np .ndarray :
395484 """
@@ -1268,3 +1357,60 @@ def plot_pair_ef(self, tickers="tickers", figsize: Optional[tuple] = None) -> pl
12681357 ax .plot (ef ["Risk" ], ef ["CAGR" ])
12691358 self .plot_assets (kind = "cagr" , tickers = tickers )
12701359 return ax
1360+
1361+ def plot_cml (self , rf_return : float = 0 , figsize : Optional [tuple ] = None ):
1362+ """
1363+ Plot Capital Market Line (CML).
1364+
1365+ The Capital Market Line (CML) is the tangent line drawn from the point of the risk-free asset (volatility is
1366+ zero) to the point of tangency portfolio or Maximum Sharpe Ratio (MSR) point.
1367+
1368+ The slope of the CML is the Sharpe ratio of the tangency portfolio.
1369+
1370+ Parameters
1371+ ----------
1372+ rf_return : float, default 0
1373+ Risk-free rate of return.
1374+
1375+ figsize : (float, float), optional
1376+ Figure size: width, height in inches.
1377+ If None default matplotlib size is taken: [6.4, 4.8]
1378+
1379+ Returns
1380+ -------
1381+ Axes : 'matplotlib.axes._subplots.AxesSubplot'
1382+
1383+ Examples
1384+ --------
1385+ >>> import matplotlib.pyplot as plt
1386+ >>> three_assets = ['MCFTR.INDX', 'RGBITR.INDX', 'GC.COMM']
1387+ >>> ef = ok.EfficientFrontierReb(assets=three_assets, ccy='USD', full_frontier=True)
1388+ >>> ef.plot_cml(rf_return=0.05) # Risk-Free return is 5%
1389+ >>> plt.show
1390+ """
1391+ ef = self .ef_points
1392+ tg = self .get_tangency_portfolio (rf_return = rf_return , rate_of_return = "cagr" )
1393+ fig , ax = plt .subplots (figsize = figsize )
1394+
1395+ ax .plot (ef .Risk , ef ["CAGR" ], color = "black" )
1396+ ax .scatter (tg ["Risk" ], tg ["Rate_of_return" ], linewidth = 0 , color = "green" , zorder = 10 )
1397+ ax .annotate (
1398+ "MSR" ,
1399+ (tg ["Risk" ], tg ["Rate_of_return" ]),
1400+ textcoords = "offset points" , # how to position the text
1401+ xytext = (- 10 , 10 ), # distance from text to points (x,y)
1402+ ha = "center" , # horizontal alignment can be left, right or center
1403+ )
1404+ # plot the line
1405+ x , y = [0 , tg ["Risk" ]], [rf_return , tg ["Rate_of_return" ]]
1406+ ax .plot (x , y , linewidth = 1 )
1407+ # set the axis size
1408+ risk_monthly = self .assets_ror .std ()
1409+ mean_return_monthly = self .assets_ror .mean ()
1410+ risks = helpers .Float .annualize_risk (risk_monthly , mean_return_monthly )
1411+ max_return = self .global_max_return_portfolio ["CAGR" ]
1412+ ax .set_ylim (0 , max_return * 1.1 ) # height is 10% more than max portfolio CAGR
1413+ ax .set_xlim (0 , max (risks ) * 1.1 ) # width is 10% more than max risk
1414+ # plot the assets
1415+ self .plot_assets (kind = "cagr" )
1416+ return ax
0 commit comments