diff --git a/main.py b/main.py index 6acd748..07ce718 100644 --- a/main.py +++ b/main.py @@ -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() diff --git a/okama/asset_list.py b/okama/asset_list.py index 9accf78..09f7eec 100644 --- a/okama/asset_list.py +++ b/okama/asset_list.py @@ -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. @@ -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. @@ -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. @@ -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. diff --git a/okama/common/make_asset_list.py b/okama/common/make_asset_list.py index 99db569..5c36545 100644 --- a/okama/common/make_asset_list.py +++ b/okama/common/make_asset_list.py @@ -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) diff --git a/okama/frontier/single_period.py b/okama/frontier/single_period.py index 8442e0f..7cf23c2 100644 --- a/okama/frontier/single_period.py +++ b/okama/frontier/single_period.py @@ -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. @@ -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. @@ -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 @@ -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( @@ -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 @@ -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] @@ -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). @@ -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] @@ -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", @@ -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 diff --git a/tests/test_frontier.py b/tests/test_frontier.py index d521a97..09c5bda 100644 --- a/tests/test_frontier.py +++ b/tests/test_frontier.py @@ -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) @@ -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)