Skip to content

Commit

Permalink
feat: add CAGR for y-axis when plotting CML-Line
Browse files Browse the repository at this point in the history
  • Loading branch information
chilango74 committed Oct 29, 2023
1 parent 4fe3dab commit f029839
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 29 deletions.
8 changes: 3 additions & 5 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import matplotlib.pyplot as plt
import okama as ok

al = ok.AssetList(["SP500TR.INDX", "SPY.US", "VOO.US"])

# al.tracking_error(rolling_window=12).plot()
# al.index_beta(rolling_window=12 * 5).plot()
al.index_corr(rolling_window=12 * 5).plot()
indexes = ["RGBITR.INDX", "MCFTR.INDX", "GC.COMM"]
ef = ok.EfficientFrontier(indexes, ccy="RUB", full_frontier=True, inflation=False, n_points=50)
ef.plot_cml(rf_return=0.15, y_axe="cagr")
plt.show()
11 changes: 7 additions & 4 deletions okama/asset_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,8 @@ def tracking_difference(self, rolling_window=None) -> pd.DataFrame:
Return tracking difference for the rate of return of assets.
Tracking difference is calculated by measuring the accumulated difference between the returns of a benchmark
and those of the ETF replicating it (could be mutual funds, or other types of assets).
and those of the ETF replicating it (could be mutual funds, or other types of assets). Tracking difference is
measured in percents.
Benchmark should be in the first position of the symbols list in AssetList parameters.
Expand Down Expand Up @@ -969,7 +970,8 @@ def tracking_difference_annualized(self, rolling_window: Optional[int] = None) -
Calculate annualized tracking difference time series for the rate of return of assets.
Tracking difference is calculated by measuring the accumulated difference between the returns of a benchmark
and ETFs replicating it (could be mutual funds, or other types of assets).
and ETFs replicating it (could be mutual funds, or other types of assets). Tracking difference is
measured in percents.
Benchmark should be in the first position of the symbols list in AssetList parameters.
Expand Down Expand Up @@ -1016,7 +1018,8 @@ def tracking_difference_annual(self) -> pd.DataFrame:
Calculate tracking difference for each calendar year.
Tracking difference is calculated by measuring the accumulated difference between the returns of a benchmark
and ETFs replicating it (could be mutual funds, or other types of assets).
and ETFs replicating it (could be mutual funds, or other types of assets). Tracking difference is
measured in percents.
Benchmark should be in the first position of the symbols list in AssetList parameters.
Expand Down Expand Up @@ -1045,7 +1048,7 @@ def tracking_error(self, rolling_window: Optional[int] = None) -> pd.DataFrame:
Calculate tracking error time series for the rate of return of assets.
Tracking error is defined as the standard deviation of the difference between the returns of the asset
and the returns of the benchmark.
and the returns of the benchmark. Tracking error is measured in percents.
Benchmark should be in the first position of the symbols list in AssetList parameters.
Expand Down
1 change: 1 addition & 0 deletions okama/common/make_asset_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ def plot_assets(
... )
>>> plt.show()
"""
# TODO: rename tickers to labels or annotations
risk_monthly = self.assets_ror.std()
mean_return_monthly = self.assets_ror.mean()
risks = helpers.Float.annualize_risk(risk_monthly, mean_return_monthly)
Expand Down
43 changes: 29 additions & 14 deletions okama/frontier/single_period.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ def gmv_annualized(self) -> Tuple[float, float]:
helpers.Float.annualize_return(self.gmv_monthly[1]),
)

def get_tangency_portfolio(self, cagr: bool = False, rf_return: float = 0) -> dict:
def get_tangency_portfolio(self, rf_return: float = 0, rate_of_return: str = "cagr") -> dict:
"""
Calculate asset weights, risk and return values for tangency portfolio within given bounds.
Expand All @@ -265,8 +265,8 @@ def get_tangency_portfolio(self, cagr: bool = False, rf_return: float = 0) -> di
Parameters
----------
cagr : bool, default False
Use CAGR (Compound annual growth rate) or geometric mean to calculate Sharpe Ratio.
rate_of_return : {cagr, mean_return}, default cagr
Use CAGR (Compound annual growth rate) or arithmetic mean of return to calculate Sharpe Ratio.
rf_return : float, default 0
Risk-free rate of return.
Expand All @@ -285,7 +285,7 @@ def get_tangency_portfolio(self, cagr: bool = False, rf_return: float = 0) -> di
To calculate tangency portfolio parameters for CAGR (geometric mean) set cagr=True:
>>> ef.get_tangency_portfolio(cagr=True, rf_return=0.03)
>>> ef.get_tangency_portfolio(rate_of_return="mean_return", rf_return=0.03)
{'Weights': array([2.95364739e-01, 1.08420217e-17, 7.04635261e-01]), 'Mean_return': 0.10654206521088283, 'Risk': 0.048279725208422115}
"""
assets_ror = self.assets_ror
Expand All @@ -310,7 +310,12 @@ def of_geometric_mean(w):
of_geometric_mean.risk = helpers.Float.annualize_risk(risk_monthly, mean_return_monthly)
return -(of_geometric_mean.rate_of_return - rf_return) / of_geometric_mean.risk

objective_function = of_geometric_mean if cagr else of_arithmetic_mean
if rate_of_return.lower() in {"cagr", "mean_return"}:
rate_of_return = rate_of_return.lower()
else:
raise ValueError("rate_of_return must be 'cagr or 'mean_return'")

objective_function = of_geometric_mean if rate_of_return == "cagr" else of_arithmetic_mean
# construct the constraints
weights_sum_to_1 = {"type": "eq", "fun": lambda weights: np.sum(weights) - 1}
weights = minimize(
Expand Down Expand Up @@ -906,7 +911,7 @@ def plot_transition_map(self, x_axe: str = 'risk', figsize: Optional[tuple] = No
Parameters
----------
bounds: tuple of ((float, float),...)
bounds : tuple of ((float, float),...)
Bounds for the assets weights. Each asset can have weights limitation from 0 to 1.0.
If an asset has limitation for 10 to 20%, bounds are defined as (0.1, 0.2).
bounds = ((0, .5), (0, 1)) shows that in Portfolio with two assets first one has weight limitations
Expand All @@ -916,7 +921,7 @@ def plot_transition_map(self, x_axe: str = 'risk', figsize: Optional[tuple] = No
Show the relation between weights and CAGR (if 'cagr') or between weights and Risk (if 'risk').
CAGR or Risk are displayed on the x-axis.
figsize: (float, float), optional
figsize : (float, float), optional
Figure size: width, height in inches.
If None default matplotlib size is taken: [6.4, 4.8]
Expand Down Expand Up @@ -1034,7 +1039,7 @@ def plot_pair_ef(self, tickers="tickers", figsize: Optional[tuple] = None) -> pl
self.plot_assets(kind="mean", tickers=tickers)
return ax

def plot_cml(self, rf_return: float = 0, figsize: Optional[tuple] = None):
def plot_cml(self, rf_return: float = 0, y_axe: str = "cagr", figsize: Optional[tuple] = None):
"""
Plot Capital Market Line (CML).
Expand All @@ -1045,11 +1050,15 @@ def plot_cml(self, rf_return: float = 0, figsize: Optional[tuple] = None):
Parameters
----------
TODO: add y_axe parameter (arithmetic_mean or cagr)
rf_return : float, default 0
Risk-free rate of return.
figsize: (float, float), optional
y_axe : {'cagr', 'mean_return'}, default 'cagr'
Show the relation between Risk and CAGR (if 'cagr') or
between Risk and Mean rate of return (if 'mean_return').
CAGR or Mean Rate of Return are displayed on the y-axis.
figsize : (float, float), optional
Figure size: width, height in inches.
If None default matplotlib size is taken: [6.4, 4.8]
Expand All @@ -1062,13 +1071,19 @@ def plot_cml(self, rf_return: float = 0, figsize: Optional[tuple] = None):
>>> import matplotlib.pyplot as plt
>>> three_assets = ['MCFTR.INDX', 'RGBITR.INDX', 'GC.COMM']
>>> ef = ok.EfficientFrontier(assets=three_assets, ccy='USD', full_frontier=True)
>>> ef.plot_cml(rf_return=0.05) # Risk-Free return is 5%
>>> ef.plot_cml(rf_return=0.05, y_axe="cagr") # Risk-Free return is 5%
>>> plt.show
"""
if y_axe.lower() not in {"cagr", "mean_return"}:
raise ValueError("rate_of_return must be 'cagr' or 'mean_return'")
y_axe = y_axe.lower()
y_axe_annotation = "CAGR" if y_axe == "cagr" else "Mean return"

ef = self.ef_points
tg = self.get_tangency_portfolio(rf_return)
tg = self.get_tangency_portfolio(rf_return=rf_return, rate_of_return=y_axe)
fig, ax = plt.subplots(figsize=figsize)
ax.plot(ef.Risk, ef["Mean return"], color="black")

ax.plot(ef.Risk, ef[y_axe_annotation], color="black")
ax.scatter(tg["Risk"], tg["Rate_of_return"], linewidth=0, color="green", zorder=10)
ax.annotate(
"MSR",
Expand All @@ -1088,5 +1103,5 @@ def plot_cml(self, rf_return: float = 0, figsize: Optional[tuple] = None):
ax.set_ylim(0, max(returns) * 1.1) # height is 10% more than max return
ax.set_xlim(0, max(risks) * 1.1) # width is 10% more than max risk
# plot the assets
self.plot_assets(kind="mean")
self.plot_assets(kind="mean" if y_axe == "mean_return" else "cagr")
return ax
13 changes: 7 additions & 6 deletions tests/test_frontier.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,19 @@ def test_ef_points(init_efficient_frontier):


test_tangency_data = [
(False, [0.0, 1.0], 0.1603), # cagr = False
(True, [0.0, 1.0], 0.15959), # cagr = True
("mean_return", [0.0, 1.0], 0.1603), # rate_of_return = 'mean_return'
("cagr", [0.0, 1.0], 0.15959), # rate_of_return = 'cagr'
]


@pytest.mark.parametrize(
"cagr, expected_weights, expected_return", test_tangency_data, ids=["MSR Arithmetic mean", "MSR geometric mean"]
"rate_of_return, expected_weights, expected_return", test_tangency_data, ids=["MSR Arithmetic mean", "MSR geometric mean"]
)
@mark.frontier
def test_get_tangency_portfolio(init_efficient_frontier, cagr, expected_weights, expected_return):
def test_get_tangency_portfolio(init_efficient_frontier, rate_of_return, expected_weights, expected_return):
rf_rate = 0.05
dic = init_efficient_frontier.get_tangency_portfolio(cagr=cagr, rf_return=rf_rate)

dic = init_efficient_frontier.get_tangency_portfolio(rate_of_return='cagr', rf_return=rf_rate)
assert_allclose(dic["Weights"], expected_weights, atol=1e-2)
assert dic["Rate_of_return"] == approx(expected_return, rel=1e-2)

Expand Down Expand Up @@ -188,7 +189,7 @@ def test_mdp_points(init_efficient_frontier_three_assets):
@mark.frontier
def test_plot_cml(init_efficient_frontier):
rf_rate = 0.02
axes_data = np.array(init_efficient_frontier.plot_cml(rf_return=rf_rate).lines[1].get_data())
axes_data = np.array(init_efficient_frontier.plot_cml(rf_return=rf_rate, y_axe="mean_return").lines[1].get_data())
expected = np.array([[0, 0.042512], [0.02, 0.159596]])
assert_allclose(axes_data, expected, atol=1e-2)

Expand Down

0 comments on commit f029839

Please sign in to comment.