Skip to content

Commit 8f8ee17

Browse files
committed
feat: add get_tangency_portfolio and plot_cml to EfficientFrontierReb
1 parent 20bed2c commit 8f8ee17

File tree

1 file changed

+146
-0
lines changed

1 file changed

+146
-0
lines changed

okama/frontier/multi_period.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)