From faca7ab67d1ce5d0ae0e5c862332bcfc37f72ea9 Mon Sep 17 00:00:00 2001 From: montezdesousa <79287829+montezdesousa@users.noreply.github.com> Date: Fri, 2 Dec 2022 23:56:41 +0000 Subject: [PATCH] Portfolio optimization bug fixes (#3675) * remove hcp * add prams dict to statics * little controller bug * fix view bug * avoid crash if one stock * check params on setup * create parameter class * check and convert parameters using class * change params name to avoid confusion * create a parameter statics dict * change some funcs names * remove unused imports * remove default dict * optional type * add multi choices * cast only int and float * fix completer * fix bugs with parameter validation * fix bugs with parameter validation * add excel formatting * sdk needs mapping as well, controller takes care of this in terminal * small formating * add some safe guard try except * controller bugs * oops * change export path of parameters to portfolio folder * add more commands to scripts * catch optimization exception * log errors * black and exceptions * add flag to test * black * flake8 * forgot this * pylint * change defaults * fix ef default * fix bl * sync sdk defaults * sync last defaults * fix plot heat and add more choices to controller autocomplete * patch weights * fix wrong bool parsing Co-authored-by: James Maslek --- .../OpenBB_Parameters_Template_v1.0.0.xlsx | Bin 18022 -> 33733 bytes .../portfolio/test_po_show_plot_rpf.openbb | 1 + .../test_portfolio_boolean_args.openbb | 5 +- .../portfolio/test_portfolio_po.openbb | 22 +- .../optimizer_helper.py | 48 +- .../portfolio_optimization/optimizer_model.py | 721 ++++++++++-------- .../portfolio_optimization/optimizer_view.py | 382 +++++----- .../parameters/Parameter.py | 85 +++ .../parameters/params_controller.py | 17 +- .../parameters/params_helpers.py | 124 ++- .../parameters/params_view.py | 20 +- .../portfolio_optimization/po_controller.py | 44 +- .../portfolio_optimization/po_engine.py | 29 +- .../portfolio_optimization/po_model.py | 159 ++-- .../portfolio_optimization/po_view.py | 37 +- .../portfolio_optimization/statics.py | 334 +++++++- 16 files changed, 1351 insertions(+), 677 deletions(-) create mode 100644 openbb_terminal/portfolio/portfolio_optimization/parameters/Parameter.py diff --git a/openbb_terminal/miscellaneous/portfolio_examples/optimization/OpenBB_Parameters_Template_v1.0.0.xlsx b/openbb_terminal/miscellaneous/portfolio_examples/optimization/OpenBB_Parameters_Template_v1.0.0.xlsx index 3b8a5afb170d679633d3805e9d9c386dbad50e19..56ffb4d5e05832a9a1c53688891a779eefe2a5e5 100644 GIT binary patch literal 33733 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 literal 18022 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 diff --git a/openbb_terminal/miscellaneous/scripts/portfolio/test_po_show_plot_rpf.openbb b/openbb_terminal/miscellaneous/scripts/portfolio/test_po_show_plot_rpf.openbb index 7146022d153..4277466898d 100644 --- a/openbb_terminal/miscellaneous/scripts/portfolio/test_po_show_plot_rpf.openbb +++ b/openbb_terminal/miscellaneous/scripts/portfolio/test_po_show_plot_rpf.openbb @@ -52,3 +52,4 @@ plot -pf name_0 -ct sector -he maxsharpe rpf -pf a,b,c rpf -pf name_0,maxsharpe_1 +exit diff --git a/openbb_terminal/miscellaneous/scripts/portfolio/test_portfolio_boolean_args.openbb b/openbb_terminal/miscellaneous/scripts/portfolio/test_portfolio_boolean_args.openbb index dec916dbccc..97abd141325 100644 --- a/openbb_terminal/miscellaneous/scripts/portfolio/test_portfolio_boolean_args.openbb +++ b/openbb_terminal/miscellaneous/scripts/portfolio/test_portfolio_boolean_args.openbb @@ -2,8 +2,9 @@ portfolio load --file Public_Equity_Orderbook.xlsx po params -file --file OpenBB_Parameters_Template_v1.0.0.xlsx +load OpenBB_Parameters_Template_v1.0.0.xlsx set ef arg tangency False arg log_returns True -exit \ No newline at end of file +clear +exit diff --git a/openbb_terminal/miscellaneous/scripts/portfolio/test_portfolio_po.openbb b/openbb_terminal/miscellaneous/scripts/portfolio/test_portfolio_po.openbb index 979549dab13..57a3e5d812d 100644 --- a/openbb_terminal/miscellaneous/scripts/portfolio/test_portfolio_po.openbb +++ b/openbb_terminal/miscellaneous/scripts/portfolio/test_portfolio_po.openbb @@ -1,13 +1,14 @@ portfolio po load 60_40_Portfolio.xlsx +file --file example.ini maxsharpe -s 2022-10-20 minrisk -r 0.05 -maxutil -a 0.1 +maxutil -a 0.1 -ra 2 -tk 0.4 -tr 8 maxret -e 2022-11-20 maxdiv -mn 0.1 maxdecorr -lr -ef -t +ef -t -se 69 -n 10 equal -th 0.1 riskparity -p 1y relriskparity -de 0.9 @@ -21,4 +22,21 @@ plot -pf MAXSHARPE_0 -dd plot -pf MAXSHARPE_0 -rc plot -pf MAXSHARPE_0 -he rpf NAME_6 +.. +load --file market.csv +po +equal +maxsharpe -s 1995-05-07 +minrisk -r 0.08 +maxutil -a 0.1 -ra 2 -tk 0.4 -tr 8 +maxret -e 2022-11-20 +maxdiv -mn 0.1 +maxdecorr -lr -p max +ef -t -se 420 -n 0 +equal -th 0.1 +riskparity -p 1y +relriskparity -de 0.9 +hrp -cd pearson +herc -at 0.1 +nco -rm EDaR -lr exit diff --git a/openbb_terminal/portfolio/portfolio_optimization/optimizer_helper.py b/openbb_terminal/portfolio/portfolio_optimization/optimizer_helper.py index c97832ce3fb..9284da4bf35 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/optimizer_helper.py +++ b/openbb_terminal/portfolio/portfolio_optimization/optimizer_helper.py @@ -2,9 +2,14 @@ __docformat__ = "numpy" import argparse +from typing import Any import pandas as pd -from openbb_terminal.portfolio.portfolio_optimization import statics +from openbb_terminal.portfolio.portfolio_optimization.statics import ( + RISK_CHOICES, + OPTIMIZATION_PARAMETERS, + TERMINAL_TEMPLATE_MAP, +) from openbb_terminal.rich_config import console # These are all the possible yfinance properties @@ -146,8 +151,45 @@ def validate_risk_measure(risk_measure: str, warning: bool = True) -> str: str Validated risk measure """ - if risk_measure.lower() in statics.RISK_CHOICES: - return statics.RISK_CHOICES[risk_measure.lower()] + if risk_measure.lower() in RISK_CHOICES: + return RISK_CHOICES[risk_measure.lower()] if warning: console.print("[yellow]Risk measure not found. Using 'MV'.[/yellow]") return "MV" + + +def get_kwarg(key: str, kwargs: dict, default: Any = None) -> Any: + """Get a key from kwargs + + If key is in kwargs, returns it. + Otherwise, if default provided, returns it. + Otherwise, if key is in OPTIMIZATION_PARAMETERS, returns it. + + Parameters + ---------- + key : str + The key to be searched + kwargs : dict + The kwargs to be searched + default : Any + The default value to be returned if the key is not found + + Returns + ------- + Any + The value of the key if it exists, else None + """ + + if key in kwargs: + return kwargs[key] + + if default: + return default + + # TODO: Remove this line when mapping between template and terminal is not needed + template_key = TERMINAL_TEMPLATE_MAP.get(key, key) + + PARAMETER = OPTIMIZATION_PARAMETERS.get(template_key) + if PARAMETER is None: + return default + return PARAMETER.default diff --git a/openbb_terminal/portfolio/portfolio_optimization/optimizer_model.py b/openbb_terminal/portfolio/portfolio_optimization/optimizer_model.py index f4a1ec0675d..2a61e251633 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/optimizer_model.py +++ b/openbb_terminal/portfolio/portfolio_optimization/optimizer_model.py @@ -21,7 +21,11 @@ from scipy.interpolate import interp1d from openbb_terminal.decorators import log_start_end from openbb_terminal.portfolio.portfolio_optimization import ( yahoo_finance_model, - optimizer_helper, +) +from openbb_terminal.portfolio.portfolio_optimization.optimizer_helper import ( + get_kwarg, + validate_risk_measure, + valid_property_infos, ) from openbb_terminal.rich_config import console @@ -199,15 +203,15 @@ def get_equal_weights( Dictionary of weights where keys are the tickers, dataframe of stock returns """ - interval = kwargs.get("interval", "3y") - start_date = kwargs.get("start_date", "") - end_date = kwargs.get("end_date", "") - log_returns = kwargs.get("log_returns", False) - freq = kwargs.get("freq", "D") - maxnan = kwargs.get("maxnan", 0.05) - threshold = kwargs.get("threshold", 0.0) - method = kwargs.get("method", "time") - value = kwargs.get("value", 1.0) + interval = get_kwarg("interval", kwargs) + start_date = get_kwarg("start_date", kwargs) + end_date = get_kwarg("end_date", kwargs) + log_returns = get_kwarg("log_returns", kwargs) + freq = get_kwarg("freq", kwargs) + maxnan = get_kwarg("maxnan", kwargs) + threshold = get_kwarg("threshold", kwargs) + method = get_kwarg("method", kwargs) + value = get_kwarg("value", kwargs) stock_prices = yahoo_finance_model.process_stocks( symbols, interval, start_date, end_date @@ -271,17 +275,17 @@ def get_property_weights( Dictionary of portfolio weights or allocations """ - interval = kwargs.get("interval", "3y") - start_date = kwargs.get("start_date", "") - end_date = kwargs.get("end_date", "") - log_returns = kwargs.get("log_returns", False) - freq = kwargs.get("freq", "D") - maxnan = kwargs.get("maxnan", 0.05) - threshold = kwargs.get("threshold", 0.0) - method = kwargs.get("method", "time") - value = kwargs.get("value", 1.0) + interval = get_kwarg("interval", kwargs) + start_date = get_kwarg("start_date", kwargs) + end_date = get_kwarg("end_date", kwargs) + log_returns = get_kwarg("log_returns", kwargs) + freq = get_kwarg("freq", kwargs) + maxnan = get_kwarg("maxnan", kwargs) + threshold = get_kwarg("threshold", kwargs) + method = get_kwarg("method", kwargs) + value = get_kwarg("value", kwargs) - s_property = kwargs.get("s_property", "marketCap") + s_property = get_kwarg("s_property", kwargs, default="marketCap") stock_prices = yahoo_finance_model.process_stocks( symbols, interval, start_date, end_date @@ -427,29 +431,29 @@ def get_mean_risk_portfolio( DataFrame of stock returns. """ - interval = kwargs.get("interval", "3y") - start_date = kwargs.get("start_date", "") - end_date = kwargs.get("end_date", "") - log_returns = kwargs.get("log_returns", False) - freq = kwargs.get("freq", "D") - maxnan = kwargs.get("maxnan", 0.05) - threshold = kwargs.get("threshold", 0.0) - method = kwargs.get("method", "time") - value = kwargs.get("value", 1.0) - value_short = kwargs.get("value_short", 0.0) + interval = get_kwarg("interval", kwargs) + start_date = get_kwarg("start_date", kwargs) + end_date = get_kwarg("end_date", kwargs) + log_returns = get_kwarg("log_returns", kwargs) + freq = get_kwarg("freq", kwargs) + maxnan = get_kwarg("maxnan", kwargs) + threshold = get_kwarg("threshold", kwargs) + method = get_kwarg("method", kwargs) + value = get_kwarg("value", kwargs) + value_short = get_kwarg("value_short", kwargs) - risk_measure = kwargs.get("risk_measure", "MV") - objective = kwargs.get("objective", "Sharpe") - risk_free_rate = kwargs.get("risk_free_rate", 0.0) - risk_aversion = kwargs.get("risk_aversion", 1.0) - alpha = kwargs.get("alpha", 0.05) - target_return = kwargs.get("target_return", -1.0) - target_risk = kwargs.get("target_risk", -1.0) - mean = kwargs.get("mean", "hist") - covariance = kwargs.get("covariance", "hist") - d_ewma = kwargs.get("d_ewma", 0.94) + risk_measure = get_kwarg("risk_measure", kwargs) + objective = get_kwarg("objective", kwargs) + risk_free_rate = get_kwarg("risk_free_rate", kwargs) + risk_aversion = get_kwarg("risk_aversion", kwargs) + alpha = get_kwarg("alpha", kwargs) + target_return = get_kwarg("target_return", kwargs) + target_risk = get_kwarg("target_risk", kwargs) + mean = get_kwarg("mean", kwargs) + covariance = get_kwarg("covariance", kwargs) + d_ewma = get_kwarg("d_ewma", kwargs) - risk_measure = optimizer_helper.validate_risk_measure(risk_measure) + risk_measure = validate_risk_measure(risk_measure) stock_prices = yahoo_finance_model.process_stocks( symbols, interval, start_date, end_date @@ -468,48 +472,64 @@ def get_mean_risk_portfolio( ) return {}, pd.DataFrame() + if stock_returns.shape[1] < 2: + console.print( + f"[red]Given the parameters could only get data for '{stock_returns.columns[0]}'.[/red]\n" + "[red]Optimization needs at least two assets.[/red]\n", + ) + return {}, pd.DataFrame() + + first_day = stock_returns.index[0].strftime("%Y-%m-%d") + console.print( + f"[yellow]First day of data respecting parameters: {first_day}[/yellow]\n" + ) + risk_free_rate = risk_free_rate / time_factor[freq.upper()] - # Building the portfolio object - port = rp.Portfolio(returns=stock_returns, alpha=alpha) + try: + # Building the portfolio object + port = rp.Portfolio(returns=stock_returns, alpha=alpha) - # Estimate input parameters: - port.assets_stats(method_mu=mean, method_cov=covariance, d=d_ewma) + # Estimate input parameters: + port.assets_stats(method_mu=mean, method_cov=covariance, d=d_ewma) - # Budget constraints - port.upperlng = value - if value_short > 0: - port.sht = True - port.uppersht = value_short - port.budget = value - value_short - else: - port.budget = value - - # Estimate optimal portfolio: - model = "Classic" - hist = True - - if target_return > -1: - port.lowerret = float(target_return) / time_factor[freq.upper()] - - if target_risk > -1: - if risk_measure not in ["ADD", "MDD", "CDaR", "EDaR", "UCI"]: - setattr( - port, - upper_risk[risk_measure], - float(target_risk) / time_factor[freq.upper()] ** 0.5, - ) + # Budget constraints + port.upperlng = value + if value_short > 0: + port.sht = True + port.uppersht = value_short + port.budget = value - value_short else: - setattr(port, upper_risk[risk_measure], float(target_risk)) + port.budget = value - weights = port.optimization( - model=model, - rm=risk_measure, - obj=objective, - rf=risk_free_rate, - l=risk_aversion, - hist=hist, - ) + # Estimate optimal portfolio: + model = "Classic" + hist = True + + if target_return > -1: + port.lowerret = float(target_return) / time_factor[freq.upper()] + + if target_risk > -1: + if risk_measure not in ["ADD", "MDD", "CDaR", "EDaR", "UCI"]: + setattr( + port, + upper_risk[risk_measure], + float(target_risk) / time_factor[freq.upper()] ** 0.5, + ) + else: + setattr(port, upper_risk[risk_measure], float(target_risk)) + + weights = port.optimization( + model=model, + rm=risk_measure, + obj=objective, + rf=risk_free_rate, + l=risk_aversion, + hist=hist, + ) + + except Exception as _: + weights = None if weights is not None: weights = weights.round(5) @@ -1032,19 +1052,19 @@ def get_max_diversification_portfolio( DataFrame of stock returns. """ - interval = kwargs.get("interval", "3y") - start_date = kwargs.get("start_date", "") - end_date = kwargs.get("end_date", "") - log_returns = kwargs.get("log_returns", False) - freq = kwargs.get("freq", "D") - maxnan = kwargs.get("maxnan", 0.05) - threshold = kwargs.get("threshold", 0.0) - method = kwargs.get("method", "time") - value = kwargs.get("value", 1.0) - value_short = kwargs.get("value_short", 0.0) + interval = get_kwarg("interval", kwargs) + start_date = get_kwarg("start_date", kwargs) + end_date = get_kwarg("end_date", kwargs) + log_returns = get_kwarg("log_returns", kwargs) + freq = get_kwarg("freq", kwargs) + maxnan = get_kwarg("maxnan", kwargs) + threshold = get_kwarg("threshold", kwargs) + method = get_kwarg("method", kwargs) + value = get_kwarg("value", kwargs) + value_short = get_kwarg("value_short", kwargs) - covariance = kwargs.get("covariance", "hist") - d_ewma = kwargs.get("d_ewma", 0.94) + covariance = get_kwarg("covariance", kwargs) + d_ewma = get_kwarg("d_ewma", kwargs) stock_prices = yahoo_finance_model.process_stocks( symbols, interval, start_date, end_date @@ -1058,24 +1078,29 @@ def get_max_diversification_portfolio( method=method, ) - # Building the portfolio object - port = rp.Portfolio(returns=stock_returns) + try: + # Building the portfolio object + port = rp.Portfolio(returns=stock_returns) - # Estimate input parameters: - port.assets_stats(method_mu="hist", method_cov=covariance, d=d_ewma) - port.mu = stock_returns.std().to_frame().T + # Estimate input parameters: + port.assets_stats(method_mu="hist", method_cov=covariance, d=d_ewma) + port.mu = stock_returns.std().to_frame().T - # Budget constraints - port.upperlng = value - if value_short > 0: - port.sht = True - port.uppersht = value_short - port.budget = value - value_short - else: - port.budget = value + # Budget constraints + port.upperlng = value + if value_short > 0: + port.sht = True + port.uppersht = value_short + port.budget = value - value_short + else: + port.budget = value - # Estimate optimal portfolio: - weights = port.optimization(model="Classic", rm="MV", obj="Sharpe", rf=0, hist=True) + # Estimate optimal portfolio: + weights = port.optimization( + model="Classic", rm="MV", obj="Sharpe", rf=0, hist=True + ) + except Exception as _: + weights = None if weights is not None: weights = weights.round(5) @@ -1151,19 +1176,19 @@ def get_max_decorrelation_portfolio( DataFrame of stock returns. """ - interval = kwargs.get("interval", "3y") - start_date = kwargs.get("start_date", "") - end_date = kwargs.get("end_date", "") - log_returns = kwargs.get("log_returns", False) - freq = kwargs.get("freq", "D") - maxnan = kwargs.get("maxnan", 0.05) - threshold = kwargs.get("threshold", 0.0) - method = kwargs.get("method", "time") - value = kwargs.get("value", 1.0) - value_short = kwargs.get("value_short", 0.0) + interval = get_kwarg("interval", kwargs) + start_date = get_kwarg("start_date", kwargs) + end_date = get_kwarg("end_date", kwargs) + log_returns = get_kwarg("log_returns", kwargs) + freq = get_kwarg("freq", kwargs) + maxnan = get_kwarg("maxnan", kwargs) + threshold = get_kwarg("threshold", kwargs) + method = get_kwarg("method", kwargs) + value = get_kwarg("value", kwargs) + value_short = get_kwarg("value_short", kwargs) - covariance = kwargs.get("covariance", "hist") - d_ewma = kwargs.get("d_ewma", 0.94) + covariance = get_kwarg("covariance", kwargs) + d_ewma = get_kwarg("d_ewma", kwargs) stock_prices = yahoo_finance_model.process_stocks( symbols, interval, start_date, end_date @@ -1177,26 +1202,30 @@ def get_max_decorrelation_portfolio( method=method, ) - # Building the portfolio object - port = rp.Portfolio(returns=stock_returns) + try: - # Estimate input parameters: - port.assets_stats(method_mu="hist", method_cov=covariance, d=d_ewma) - port.cov = rp.cov2corr(port.cov) + # Building the portfolio object + port = rp.Portfolio(returns=stock_returns) - # Budget constraints - port.upperlng = value - if value_short > 0: - port.sht = True - port.uppersht = value_short - port.budget = value - value_short - else: - port.budget = value + # Estimate input parameters: + port.assets_stats(method_mu="hist", method_cov=covariance, d=d_ewma) + port.cov = rp.cov2corr(port.cov) - # Estimate optimal portfolio: - weights = port.optimization( - model="Classic", rm="MV", obj="MinRisk", rf=0, hist=True - ) + # Budget constraints + port.upperlng = value + if value_short > 0: + port.sht = True + port.uppersht = value_short + port.budget = value - value_short + else: + port.budget = value + + # Estimate optimal portfolio: + weights = port.optimization( + model="Classic", rm="MV", obj="MinRisk", rf=0, hist=True + ) + except Exception as _: + weights = None if weights is not None: weights = weights.round(5) @@ -1284,26 +1313,26 @@ def get_black_litterman_portfolio( DataFrame of stock returns. """ - interval = kwargs.get("interval", "3y") - start_date = kwargs.get("start_date", "") - end_date = kwargs.get("end_date", "") - log_returns = kwargs.get("log_returns", False) - freq = kwargs.get("freq", "D") - maxnan = kwargs.get("maxnan", 0.05) - threshold = kwargs.get("threshold", 0.0) - method = kwargs.get("method", "time") - value = kwargs.get("value", 1.0) - value_short = kwargs.get("value_short", 0.0) + interval = get_kwarg("interval", kwargs) + start_date = get_kwarg("start_date", kwargs) + end_date = get_kwarg("end_date", kwargs) + log_returns = get_kwarg("log_returns", kwargs) + freq = get_kwarg("freq", kwargs) + maxnan = get_kwarg("maxnan", kwargs) + threshold = get_kwarg("threshold", kwargs) + method = get_kwarg("method", kwargs) + value = get_kwarg("value", kwargs) + value_short = get_kwarg("value_short", kwargs) - benchmark = kwargs.get("benchmark", None) - p_views = kwargs.get("p_views", None) - q_views = kwargs.get("q_views", None) - objective = kwargs.get("objective", "Sharpe") - risk_free_rate = kwargs.get("risk_free_rate", 0) - risk_aversion = kwargs.get("risk_aversion", 1) - delta = kwargs.get("delta", None) - equilibrium = kwargs.get("equilibrium", True) - optimize = kwargs.get("optimize", True) + benchmark = get_kwarg("benchmark", kwargs) + p_views = get_kwarg("p_views", kwargs) + q_views = get_kwarg("q_views", kwargs) + objective = get_kwarg("objective", kwargs) + risk_free_rate = get_kwarg("risk_free_rate", kwargs) + risk_aversion = get_kwarg("risk_aversion", kwargs) + delta = get_kwarg("delta", kwargs) + equilibrium = get_kwarg("equilibrium", kwargs) + optimize = get_kwarg("optimize", kwargs) stock_prices = yahoo_finance_model.process_stocks( symbols, interval, start_date, end_date @@ -1349,32 +1378,35 @@ def get_black_litterman_portfolio( weights = pd.DataFrame(weights) if optimize: - # Building the portfolio object - port = rp.Portfolio(returns=stock_returns) + try: + # Building the portfolio object + port = rp.Portfolio(returns=stock_returns) - # Estimate input parameters: - port.assets_stats(method_mu="hist", method_cov="hist") - port.mu_bl = pd.DataFrame(mu).T - port.cov_bl = pd.DataFrame(cov) + # Estimate input parameters: + port.assets_stats(method_mu="hist", method_cov="hist") + port.mu_bl = pd.DataFrame(mu).T + port.cov_bl = pd.DataFrame(cov) - # Budget constraints - port.upperlng = value - if value_short > 0: - port.sht = True - port.uppersht = value_short - port.budget = value - value_short - else: - port.budget = value + # Budget constraints + port.upperlng = value + if value_short > 0: + port.sht = True + port.uppersht = value_short + port.budget = value - value_short + else: + port.budget = value - # Estimate optimal portfolio: - weights = port.optimization( - model="BL", - rm="MV", - obj=objective, - rf=risk_free_rate, - l=risk_aversion, - hist=True, - ) + # Estimate optimal portfolio: + weights = port.optimization( + model="BL", + rm="MV", + obj=objective, + rf=risk_free_rate, + l=risk_aversion, + hist=True, + ) + except Exception as _: + weights = None if weights is not None: weights = weights.round(5) @@ -1478,25 +1510,25 @@ def get_ef( frontier, mu, cov, stock_returns, weights, X1, Y1, port """ - interval = kwargs.get("interval", "3y") - start_date = kwargs.get("start_date", "") - end_date = kwargs.get("end_date", "") - log_returns = kwargs.get("log_returns", False) - freq = kwargs.get("freq", "D") - maxnan = kwargs.get("maxnan", 0.05) - threshold = kwargs.get("threshold", 0.05) - method = kwargs.get("method", "time") - value = kwargs.get("value", 1.0) - value_short = kwargs.get("value_short", 0.0) + interval = get_kwarg("interval", kwargs) + start_date = get_kwarg("start_date", kwargs) + end_date = get_kwarg("end_date", kwargs) + log_returns = get_kwarg("log_returns", kwargs) + freq = get_kwarg("freq", kwargs) + maxnan = get_kwarg("maxnan", kwargs) + threshold = get_kwarg("threshold", kwargs) + method = get_kwarg("method", kwargs) + value = get_kwarg("value", kwargs) + value_short = get_kwarg("value_short", kwargs) - risk_measure = kwargs.get("risk_measure", "MV") - risk_free_rate = kwargs.get("risk_free_rate", 0.0) - alpha = kwargs.get("alpha", 0.05) - n_portfolios = kwargs.get("n_portfolios", 100) - seed = kwargs.get("seed", 123) + risk_measure = get_kwarg("risk_measure", kwargs) + risk_free_rate = get_kwarg("risk_free_rate", kwargs) + alpha = get_kwarg("alpha", kwargs) + n_portfolios = get_kwarg("n_portfolios", kwargs) + seed = get_kwarg("seed", kwargs) risk_free_rate = risk_free_rate / time_factor[freq.upper()] - risk_measure = optimizer_helper.validate_risk_measure(risk_measure) + risk_measure = validate_risk_measure(risk_measure) stock_prices = yahoo_finance_model.process_stocks( symbols, interval, start_date, end_date @@ -1510,29 +1542,32 @@ def get_ef( method=method, ) - # Building the portfolio object - port = rp.Portfolio(returns=stock_returns, alpha=alpha) + try: + # Building the portfolio object + port = rp.Portfolio(returns=stock_returns, alpha=alpha) - # Estimate input parameters: - port.assets_stats(method_mu="hist", method_cov="hist") + # Estimate input parameters: + port.assets_stats(method_mu="hist", method_cov="hist") - # Budget constraints - port.upperlng = value - if value_short > 0: - port.sht = True - port.uppersht = value_short - port.budget = value - value_short - else: - port.budget = value + # Budget constraints + port.upperlng = value + if value_short > 0: + port.sht = True + port.uppersht = value_short + port.budget = value - value_short + else: + port.budget = value - # Estimate tangency portfolio: - weights: Optional[pd.DataFrame] = port.optimization( - model="Classic", - rm=risk_choices[risk_measure.lower()], - obj="Sharpe", - rf=risk_free_rate, - hist=True, - ) + # Estimate tangency portfolio: + weights: Optional[pd.DataFrame] = port.optimization( + model="Classic", + rm=risk_choices[risk_measure.lower()], + obj="Sharpe", + rf=risk_free_rate, + hist=True, + ) + except Exception as _: + weights = None points = 20 # Number of points of the frontier frontier = port.efficient_frontier( @@ -1684,24 +1719,24 @@ def get_risk_parity_portfolio( DataFrame of stock returns. """ - interval = kwargs.get("interval", "3y") - start_date = kwargs.get("start_date", "") - end_date = kwargs.get("end_date", "") - log_returns = kwargs.get("log_returns", False) - freq = kwargs.get("freq", "D") - maxnan = kwargs.get("maxnan", 0.05) - threshold = kwargs.get("threshold", 0.05) - method = kwargs.get("method", "time") - value = kwargs.get("value", 1) + interval = get_kwarg("interval", kwargs) + start_date = get_kwarg("start_date", kwargs) + end_date = get_kwarg("end_date", kwargs) + log_returns = get_kwarg("log_returns", kwargs) + freq = get_kwarg("freq", kwargs) + maxnan = get_kwarg("maxnan", kwargs) + threshold = get_kwarg("threshold", kwargs) + method = get_kwarg("method", kwargs) + value = get_kwarg("value", kwargs) - risk_measure = kwargs.get("risk_measure", "MV") - risk_free_rate = kwargs.get("risk_free_rate", 0) - alpha = kwargs.get("alpha", 0.05) - target_return = kwargs.get("target_return", -1.0) - mean = kwargs.get("mean", "hist") - covariance = kwargs.get("covariance", "hist") - d_ewma = kwargs.get("d_ewma", 0.94) - risk_cont = kwargs.get("risk_cont", None) + risk_measure = get_kwarg("risk_measure", kwargs) + risk_free_rate = get_kwarg("risk_free_rate", kwargs) + alpha = get_kwarg("alpha", kwargs) + target_return = get_kwarg("target_return", kwargs) + mean = get_kwarg("mean", kwargs) + covariance = get_kwarg("covariance", kwargs) + d_ewma = get_kwarg("d_ewma", kwargs) + risk_cont = get_kwarg("risk_cont", kwargs) stock_prices = yahoo_finance_model.process_stocks( symbols, interval, start_date, end_date @@ -1716,30 +1751,33 @@ def get_risk_parity_portfolio( ) risk_free_rate = risk_free_rate / time_factor[freq.upper()] - risk_measure = optimizer_helper.validate_risk_measure(risk_measure) + risk_measure = validate_risk_measure(risk_measure) - # Building the portfolio object - port = rp.Portfolio(returns=stock_returns, alpha=alpha) + try: + # Building the portfolio object + port = rp.Portfolio(returns=stock_returns, alpha=alpha) - # Calculating optimal portfolio - port.assets_stats(method_mu=mean, method_cov=covariance, d=d_ewma) + # Calculating optimal portfolio + port.assets_stats(method_mu=mean, method_cov=covariance, d=d_ewma) - # Estimate optimal portfolio: - model = "Classic" - hist = True + # Estimate optimal portfolio: + model = "Classic" + hist = True - if risk_cont is None: - risk_cont_ = None # Risk contribution constraints vector - else: - risk_cont_ = np.array(risk_cont).reshape(1, -1) - risk_cont_ = risk_cont_ / np.sum(risk_cont_) + if risk_cont is None: + risk_cont_ = None # Risk contribution constraints vector + else: + risk_cont_ = np.array(risk_cont).reshape(1, -1) + risk_cont_ = risk_cont_ / np.sum(risk_cont_) - if target_return > -1: - port.lowerret = float(target_return) / time_factor[freq.upper()] + if target_return > -1: + port.lowerret = float(target_return) / time_factor[freq.upper()] - weights = port.rp_optimization( - model=model, rm=risk_measure, rf=risk_free_rate, b=risk_cont_, hist=hist - ) + weights = port.rp_optimization( + model=model, rm=risk_measure, rf=risk_free_rate, b=risk_cont_, hist=hist + ) + except Exception as _: + weights = None if weights is not None: if value > 0.0: @@ -1839,23 +1877,23 @@ def get_rel_risk_parity_portfolio( DataFrame of stock returns. """ - interval = kwargs.get("interval", "3y") - start_date = kwargs.get("start_date", "") - end_date = kwargs.get("end_date", "") - log_returns = kwargs.get("log_returns", False) - freq = kwargs.get("freq", "D") - maxnan = kwargs.get("maxnan", 0.05) - threshold = kwargs.get("threshold", 0.05) - method = kwargs.get("method", "time") - value = kwargs.get("value", 1) + interval = get_kwarg("interval", kwargs) + start_date = get_kwarg("start_date", kwargs) + end_date = get_kwarg("end_date", kwargs) + log_returns = get_kwarg("log_returns", kwargs) + freq = get_kwarg("freq", kwargs) + maxnan = get_kwarg("maxnan", kwargs) + threshold = get_kwarg("threshold", kwargs) + method = get_kwarg("method", kwargs) + value = get_kwarg("value", kwargs) - target_return = kwargs.get("target_return", -1) - mean = kwargs.get("mean", "hist") - covariance = kwargs.get("covariance", "hist") - d_ewma = kwargs.get("d_ewma", 0.94) - risk_cont = kwargs.get("risk_cont", None) - version = kwargs.get("version", "A") - penal_factor = kwargs.get("penal_factor", 1) + target_return = get_kwarg("target_return", kwargs) + mean = get_kwarg("mean", kwargs) + covariance = get_kwarg("covariance", kwargs) + d_ewma = get_kwarg("d_ewma", kwargs) + risk_cont = get_kwarg("risk_cont", kwargs) + version = get_kwarg("version", kwargs) + penal_factor = get_kwarg("penal_factor", kwargs) stock_prices = yahoo_finance_model.process_stocks( symbols, interval, start_date, end_date @@ -1869,28 +1907,31 @@ def get_rel_risk_parity_portfolio( method=method, ) - # Building the portfolio object - port = rp.Portfolio(returns=stock_returns) + try: + # Building the portfolio object + port = rp.Portfolio(returns=stock_returns) - # Calculating optimal portfolio - port.assets_stats(method_mu=mean, method_cov=covariance, d=d_ewma) + # Calculating optimal portfolio + port.assets_stats(method_mu=mean, method_cov=covariance, d=d_ewma) - # Estimate optimal portfolio: - model = "Classic" - hist = True + # Estimate optimal portfolio: + model = "Classic" + hist = True - if risk_cont is None: - risk_cont_ = None # Risk contribution constraints vector - else: - risk_cont_ = np.array(risk_cont).reshape(1, -1) - risk_cont_ = risk_cont_ / np.sum(risk_cont_) + if risk_cont is None: + risk_cont_ = None # Risk contribution constraints vector + else: + risk_cont_ = np.array(risk_cont).reshape(1, -1) + risk_cont_ = risk_cont_ / np.sum(risk_cont_) - if target_return > -1: - port.lowerret = float(target_return) / time_factor[freq.upper()] + if target_return > -1: + port.lowerret = float(target_return) / time_factor[freq.upper()] - weights = port.rrp_optimization( - model=model, version=version, l=penal_factor, b=risk_cont_, hist=hist - ) + weights = port.rrp_optimization( + model=model, version=version, l=penal_factor, b=risk_cont_, hist=hist + ) + except Exception as _: + weights = None if weights is not None: if value > 0.0: @@ -2084,38 +2125,38 @@ def get_hcp_portfolio( DataFrame of stock returns. """ - interval = kwargs.get("interval", "3y") - start_date = kwargs.get("start_date", "") - end_date = kwargs.get("end_date", "") - log_returns = kwargs.get("log_returns", False) - freq = kwargs.get("freq", "D") - maxnan = kwargs.get("maxnan", 0.05) - threshold = kwargs.get("threshold", 0.0) - method = kwargs.get("method", "time") - value = kwargs.get("value", 1.0) + interval = get_kwarg("interval", kwargs) + start_date = get_kwarg("start_date", kwargs) + end_date = get_kwarg("end_date", kwargs) + log_returns = get_kwarg("log_returns", kwargs) + freq = get_kwarg("freq", kwargs) + maxnan = get_kwarg("maxnan", kwargs) + threshold = get_kwarg("threshold", kwargs) + method = get_kwarg("method", kwargs) + value = get_kwarg("value", kwargs) - objective = kwargs.get("objective", "MinRisk") - risk_measure = kwargs.get("risk_measure", "MV") - risk_free_rate = kwargs.get("risk_free_rate", 0.0) - risk_aversion = kwargs.get("risk_aversion", 1.0) - alpha = kwargs.get("alpha", 0.05) - a_sim = kwargs.get("a_sim", 100) - beta = kwargs.get("beta", None) - b_sim = kwargs.get("b_sim", None) - covariance = kwargs.get("covariance", "hist") - d_ewma = kwargs.get("d_ewma", 0.94) + objective = get_kwarg("objective", kwargs) + risk_measure = get_kwarg("risk_measure", kwargs) + risk_free_rate = get_kwarg("risk_free_rate", kwargs) + risk_aversion = get_kwarg("risk_aversion", kwargs) + alpha = get_kwarg("alpha", kwargs) + a_sim = get_kwarg("a_sim", kwargs) + beta = get_kwarg("beta", kwargs) + b_sim = get_kwarg("b_sim", kwargs) + covariance = get_kwarg("covariance", kwargs) + d_ewma = get_kwarg("d_ewma", kwargs) - model = kwargs.get("model", "HRP") + model = get_kwarg("model", kwargs, default="HRP") - codependence = kwargs.get("codependence", "pearson") - linkage = kwargs.get("linkage", "single") - k = kwargs.get("k", None) - max_k = kwargs.get("max_k", 10) - bins_info = kwargs.get("bins_info", "KN") - alpha_tail = kwargs.get("alpha_tail", 0.05) - leaf_order = kwargs.get("leaf_order", True) + codependence = get_kwarg("codependence", kwargs) + linkage = get_kwarg("linkage", kwargs) + k = get_kwarg("k", kwargs) + max_k = get_kwarg("max_k", kwargs) + bins_info = get_kwarg("bins_info", kwargs) + alpha_tail = get_kwarg("alpha_tail", kwargs) + leaf_order = get_kwarg("leaf_order", kwargs) - risk_measure = optimizer_helper.validate_risk_measure(risk_measure) + risk_measure = validate_risk_measure(risk_measure) stock_prices = yahoo_finance_model.process_stocks( symbols, interval, start_date, end_date @@ -2133,31 +2174,35 @@ def get_hcp_portfolio( linkage = linkage.upper() risk_free_rate = risk_free_rate / time_factor[freq.upper()] - # Building the portfolio object - port = rp.HCPortfolio( - returns=stock_returns, - alpha=alpha, - a_sim=a_sim, - beta=beta, - b_sim=b_sim, - ) - weights = port.optimization( - model=model, - codependence=codependence, - covariance=covariance, - obj=objective, - rm=risk_measure, - rf=risk_free_rate, - l=risk_aversion, - linkage=linkage, - k=k, - max_k=max_k, - bins_info=bins_info, - alpha_tail=alpha_tail, - leaf_order=leaf_order, - d=d_ewma, - ) + try: + # Building the portfolio object + port = rp.HCPortfolio( + returns=stock_returns, + alpha=alpha, + a_sim=a_sim, + beta=beta, + b_sim=b_sim, + ) + + weights = port.optimization( + model=model, + codependence=codependence, + covariance=covariance, + obj=objective, + rm=risk_measure, + rf=risk_free_rate, + l=risk_aversion, + linkage=linkage, + k=k, + max_k=max_k, + bins_info=bins_info, + alpha_tail=alpha_tail, + leaf_order=leaf_order, + d=d_ewma, + ) + except Exception as _: + weights = None if weights is not None: if value > 0.0: @@ -2766,12 +2811,12 @@ def black_litterman( Portfolio weights. """ - p_views = kwargs.get("p_views", None) - q_views = kwargs.get("q_views", None) - delta = kwargs.get("delta", None) - risk_free_rate = kwargs.get("risk_free_rate", 0) - equilibrium = kwargs.get("equilibrium", True) - factor = kwargs.get("factor", 252) + p_views = get_kwarg("p_views", kwargs) + q_views = get_kwarg("q_views", kwargs) + delta = get_kwarg("delta", kwargs) + risk_free_rate = get_kwarg("risk_free_rate", kwargs) + equilibrium = get_kwarg("equilibrium", kwargs) + factor = get_kwarg("factor", kwargs, default=252) symbols = stock_returns.columns.tolist() benchmark = pd.Series(benchmark).to_numpy().reshape(-1, 1) @@ -2879,7 +2924,7 @@ def get_properties() -> List[str]: List[str]: List of available properties to use on property optimization. """ - return optimizer_helper.valid_property_infos + return valid_property_infos @log_start_end(log=logger) diff --git a/openbb_terminal/portfolio/portfolio_optimization/optimizer_view.py b/openbb_terminal/portfolio/portfolio_optimization/optimizer_view.py index a6b39724142..20bfc13b624 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/optimizer_view.py +++ b/openbb_terminal/portfolio/portfolio_optimization/optimizer_view.py @@ -225,68 +225,76 @@ def portfolio_performance( Number of CVaRs used to approximate Tail Gini of gains. If None it duplicates a_sim value. The default is None. """ - freq = freq.upper() - weights = pd.Series(weights).to_frame() - returns = data @ weights - mu = returns.mean().item() * time_factor[freq] - sigma = returns.std().item() * time_factor[freq] ** 0.5 - sharpe = (mu - risk_free_rate) / sigma - factor_1 = str(int(time_factor[freq])) + ") " - factor_2 = "√" + factor_1 + try: + freq = freq.upper() + weights = pd.Series(weights).to_frame() + returns = data @ weights + mu = returns.mean().item() * time_factor[freq] + sigma = returns.std().item() * time_factor[freq] ** 0.5 + sharpe = (mu - risk_free_rate) / sigma - print("\nAnnual (by " + factor_1 + f"expected return: {100 * mu:.2f}%") - print("Annual (by " + factor_2 + f"volatility: {100 * sigma:.2f}%") - print(f"Sharpe ratio: {sharpe:.4f}") + factor_1 = str(int(time_factor[freq])) + ") " + factor_2 = "√" + factor_1 - if risk_measure != "MV": - risk = rp.Sharpe_Risk( - weights, - cov=data.cov(), - returns=data, - rm=risk_measure, - rf=risk_free_rate, - alpha=alpha, - a_sim=a_sim, - beta=beta, - b_sim=b_sim, - ) + print("\nAnnual (by " + factor_1 + f"expected return: {100 * mu:.2f}%") + print("Annual (by " + factor_2 + f"volatility: {100 * sigma:.2f}%") + print(f"Sharpe ratio: {sharpe:.4f}") - drawdowns = [ - "MDD", - "ADD", - "DaR", - "CDaR", - "EDaR", - "UCI", - "MDD_Rel", - "ADD_Rel", - "DaR_Rel", - "CDaR_Rel", - "EDaR_Rel", - "UCI_Rel", - ] - - if risk_measure in drawdowns: - sharpe_2 = (mu - risk_free_rate) / risk - print( - risk_names[risk_measure.lower()].capitalize() - + " : " - + f"{100 * risk:.2f}%" + if risk_measure != "MV": + risk = rp.Sharpe_Risk( + weights, + cov=data.cov(), + returns=data, + rm=risk_measure, + rf=risk_free_rate, + alpha=alpha, + a_sim=a_sim, + beta=beta, + b_sim=b_sim, ) - else: - risk = risk * time_factor[freq] ** 0.5 - sharpe_2 = (mu - risk_free_rate) / risk + + drawdowns = [ + "MDD", + "ADD", + "DaR", + "CDaR", + "EDaR", + "UCI", + "MDD_Rel", + "ADD_Rel", + "DaR_Rel", + "CDaR_Rel", + "EDaR_Rel", + "UCI_Rel", + ] + + if risk_measure in drawdowns: + sharpe_2 = (mu - risk_free_rate) / risk + print( + risk_names[risk_measure.lower()].capitalize() + + " : " + + f"{100 * risk:.2f}%" + ) + else: + risk = risk * time_factor[freq] ** 0.5 + sharpe_2 = (mu - risk_free_rate) / risk + print( + "Annual (by " + + factor_2 + + risk_names[risk_measure.lower()] + + " : " + + f"{100 * risk:.2f}%" + ) + print( - "Annual (by " - + factor_2 + "Return / " + risk_names[risk_measure.lower()] - + " : " - + f"{100 * risk:.2f}%" + + f" ratio: {sharpe_2:.4f}" ) - - print( - "Return / " + risk_names[risk_measure.lower()] + f" ratio: {sharpe_2:.4f}" + except Exception as _: + console.print( + "[red]\nFailed to calculate portfolio performance indicators.[/red]" ) @@ -308,7 +316,7 @@ def display_weights(weights: dict, market_neutral: bool = False): if df.empty: return - if math.isclose(df.sum()["value"], 1, rel_tol=0.1): + if math.isclose(df.sum()["value"], 1, rel_tol=0.5): df["value"] = (df["value"] * 100).apply(lambda s: f"{s:.2f}") + " %" df["value"] = ( df["value"] @@ -685,6 +693,10 @@ def display_equal_weight( value=value, ) + if not weights: + console.print("There is no solution with these parameters.") + return {} + if table: console.print(s_title) display_weights(weights) @@ -799,6 +811,10 @@ def display_property_weighting( value=value, ) + if not weights: + console.print("There is no solution with these parameters.") + return {} + if table: console.print(s_title) display_weights(weights) @@ -981,8 +997,8 @@ def display_mean_risk( value_short=value_short, ) - if weights is None: - console.print("\n", "There is no solution with these parameters") + if not weights: + console.print("There is no solution with these parameters.") return {} if table: @@ -1150,11 +1166,8 @@ def display_max_sharpe( value_short=value_short, ) - if stock_returns is None or stock_returns.empty: - return {} - - if weights is None: - console.print("\n", "There is no solution with these parameters") + if not weights: + console.print("There is no solution with these parameters.") return {} if table: @@ -1321,8 +1334,8 @@ def display_min_risk( value_short=value_short, ) - if weights is None: - console.print("\n", "There is no solution with these parameters") + if not weights: + console.print("There is no solution with these parameters.") return {} if table: @@ -1489,8 +1502,8 @@ def display_max_util( value_short=value_short, ) - if weights is None: - console.print("\n", "There is no solution with these parameters") + if not weights: + console.print("There is no solution with these parameters.") return {} if table: @@ -1657,8 +1670,8 @@ def display_max_ret( value_short=value_short, ) - if weights is None: - console.print("\n", "There is no solution with these parameters") + if not weights: + console.print("There is no solution with these parameters.") return {} if table: @@ -1771,8 +1784,9 @@ def display_max_div( value=value, value_short=value_short, ) - if weights is None: - console.print("\n", "There is no solution with this parameters") + + if not weights: + console.print("There is no solution with these parameters.") return {} if table: @@ -1886,8 +1900,8 @@ def display_max_decorr( value_short=value_short, ) - if weights is None: - console.print("\n", "There is no solution with this parameters") + if not weights: + console.print("There is no solution with this parameters") return {} if table: @@ -2026,8 +2040,8 @@ def display_black_litterman( value_short=value_short, ) - if weights is None: - console.print("\n", "There is no solution with this parameters") + if not weights: + console.print("There is no solution with this parameters") return {} if table: @@ -2160,110 +2174,113 @@ def display_ef( seed=seed, ) - risk_free_rate = risk_free_rate / time_factor[freq.upper()] + try: + risk_free_rate = risk_free_rate / time_factor[freq.upper()] - if external_axes is None: - _, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) - else: - ax = external_axes[0] + if external_axes is None: + _, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) + else: + ax = external_axes[0] - ax = rp.plot_frontier( - w_frontier=frontier, - mu=mu, - cov=cov, - returns=stock_returns, - rm=risk_choices[risk_measure.lower()], - rf=risk_free_rate, - alpha=alpha, - cmap="RdYlBu", - w=weights, - label="", - marker="*", - s=16, - c="r", - t_factor=time_factor[freq.upper()], - ax=ax, - ) - - # Add risk free line - if tangency: - ret_sharpe = (mu @ weights).to_numpy().item() * time_factor[freq.upper()] - risk_sharpe = rp.Sharpe_Risk( - weights, + ax = rp.plot_frontier( + w_frontier=frontier, + mu=mu, cov=cov, returns=stock_returns, rm=risk_choices[risk_measure.lower()], rf=risk_free_rate, alpha=alpha, - # a_sim=a_sim, - # beta=beta, - # b_sim=b_sim, + cmap="RdYlBu", + w=weights, + label="", + marker="*", + s=16, + c="r", + t_factor=time_factor[freq.upper()], + ax=ax, ) - if risk_choices[risk_measure.lower()] not in [ - "ADD", - "MDD", - "CDaR", - "EDaR", - "UCI", - ]: - risk_sharpe = risk_sharpe * time_factor[freq.upper()] ** 0.5 - y = ret_sharpe * 1.5 - b = risk_free_rate * time_factor[freq.upper()] - m = (ret_sharpe - b) / risk_sharpe - x2 = (y - b) / m - x = [0, x2] - y = [b, y] - line = Line2D(x, y, label="Capital Allocation Line") - ax.set_xlim(xmin=min(X1) * 0.8) - ax.add_line(line) - - ax.plot(X1, Y1, color="b") - - plot_tickers = True - if plot_tickers: - ticker_plot = pd.DataFrame(columns=["ticker", "var"]) - for ticker in port.cov.columns: - weight_df = pd.DataFrame({"weights": 1}, index=[ticker]) - risk = rp.Sharpe_Risk( - weight_df, - cov=port.cov[ticker][ticker], - returns=stock_returns.loc[:, [ticker]], + # Add risk free line + if tangency: + ret_sharpe = (mu @ weights).to_numpy().item() * time_factor[freq.upper()] + risk_sharpe = rp.Sharpe_Risk( + weights, + cov=cov, + returns=stock_returns, rm=risk_choices[risk_measure.lower()], rf=risk_free_rate, alpha=alpha, + # a_sim=a_sim, + # beta=beta, + # b_sim=b_sim, ) - if risk_choices[risk_measure.lower()] not in [ - "MDD", "ADD", + "MDD", "CDaR", "EDaR", "UCI", ]: - risk = risk * time_factor[freq.upper()] ** 0.5 + risk_sharpe = risk_sharpe * time_factor[freq.upper()] ** 0.5 - ticker_plot = ticker_plot.append( - {"ticker": ticker, "var": risk}, ignore_index=True + y = ret_sharpe * 1.5 + b = risk_free_rate * time_factor[freq.upper()] + m = (ret_sharpe - b) / risk_sharpe + x2 = (y - b) / m + x = [0, x2] + y = [b, y] + line = Line2D(x, y, label="Capital Allocation Line") + ax.set_xlim(xmin=min(X1) * 0.8) + ax.add_line(line) + + ax.plot(X1, Y1, color="b") + + plot_tickers = True + if plot_tickers: + ticker_plot = pd.DataFrame(columns=["ticker", "var"]) + for ticker in port.cov.columns: + weight_df = pd.DataFrame({"weights": 1}, index=[ticker]) + risk = rp.Sharpe_Risk( + weight_df, + cov=port.cov[ticker][ticker], + returns=stock_returns.loc[:, [ticker]], + rm=risk_choices[risk_measure.lower()], + rf=risk_free_rate, + alpha=alpha, + ) + + if risk_choices[risk_measure.lower()] not in [ + "MDD", + "ADD", + "CDaR", + "EDaR", + "UCI", + ]: + risk = risk * time_factor[freq.upper()] ** 0.5 + + ticker_plot = ticker_plot.append( + {"ticker": ticker, "var": risk}, ignore_index=True + ) + ticker_plot = ticker_plot.set_index("ticker") + ticker_plot = ticker_plot.merge( + port.mu.T * time_factor[freq.upper()], right_index=True, left_index=True ) - ticker_plot = ticker_plot.set_index("ticker") - ticker_plot = ticker_plot.merge( - port.mu.T * time_factor[freq.upper()], right_index=True, left_index=True - ) - ticker_plot = ticker_plot.rename(columns={0: "ret"}) - ax.scatter(ticker_plot["var"], ticker_plot["ret"]) - for row in ticker_plot.iterrows(): - ax.annotate(row[0], (row[1]["var"], row[1]["ret"])) - ax.set_title(f"Efficient Frontier simulating {n_portfolios} portfolios") - ax.legend(loc="best", scatterpoints=1) - theme.style_primary_axis(ax) - l, b, w, h = ax.get_position().bounds - ax.set_position([l, b, w * 0.9, h]) - ax1 = ax.get_figure().axes - ll, bb, ww, hh = ax1[-1].get_position().bounds - ax1[-1].set_position([ll * 1.02, bb, ww, hh]) - if external_axes is None: - theme.visualize_output(force_tight_layout=False) + ticker_plot = ticker_plot.rename(columns={0: "ret"}) + ax.scatter(ticker_plot["var"], ticker_plot["ret"]) + for row in ticker_plot.iterrows(): + ax.annotate(row[0], (row[1]["var"], row[1]["ret"])) + ax.set_title(f"Efficient Frontier simulating {n_portfolios} portfolios") + ax.legend(loc="best", scatterpoints=1) + theme.style_primary_axis(ax) + l, b, w, h = ax.get_position().bounds + ax.set_position([l, b, w * 0.9, h]) + ax1 = ax.get_figure().axes + ll, bb, ww, hh = ax1[-1].get_position().bounds + ax1[-1].set_position([ll * 1.02, bb, ww, hh]) + if external_axes is None: + theme.visualize_output(force_tight_layout=False) + except Exception as _: + console.print("[red]Error plotting efficient frontier.[/red]") @log_start_end(log=logger) @@ -2401,8 +2418,8 @@ def display_risk_parity( value=value, ) - if weights is None: - console.print("\n", "There is no solution with this parameters") + if not weights: + console.print("There is no solution with this parameters") return {} if table: @@ -2542,8 +2559,8 @@ def display_rel_risk_parity( value=value, ) - if weights is None: - console.print("\n", "There is no solution with this parameters") + if not weights: + console.print("There is no solution with this parameters") return {} if table: @@ -2807,8 +2824,8 @@ def display_hcp( value=value, ) - if weights is None: - console.print("\n", "There is no solution with this parameters") + if not weights: + console.print("There is no solution with this parameters") return {} if table: @@ -3051,8 +3068,8 @@ def display_hrp( value=value, ) - if weights is None: - console.print("\n", "There is no solution with this parameters") + if not weights: + console.print("There is no solution with this parameters") return {} if table: @@ -3303,8 +3320,8 @@ def display_herc( value=value, ) - if weights is None: - console.print("\n", "There is no solution with this parameters") + if not weights: + console.print("There is no solution with this parameters") return {} if table: @@ -3566,8 +3583,8 @@ def display_nco( value=value, ) - if weights is None: - console.print("\n", "There is no solution with this parameters") + if not weights: + console.print("There is no solution with this parameters") return {} if table: @@ -3703,8 +3720,9 @@ def pie_chart_weights( def additional_plots( weights: Dict, data: pd.DataFrame, - category: Dict = None, - title_opt: str = "", + category_dict: Dict = None, + category: str = "", + portfolio_name: str = "", freq: str = "D", risk_measure: str = "MV", risk_free_rate: float = 0, @@ -3728,8 +3746,12 @@ def additional_plots( Dict of portfolio weights data: pd.DataFrame DataFrame of stock returns - title_opt: str - Title to be used on the pie chart + category_dict: Dict + Dict of categories + category: str + Category to plot + portfolio_name: str + Portfolio name freq: str, optional The frequency used to calculate returns. Default value is 'D'. Possible values are: @@ -3795,12 +3817,15 @@ def additional_plots( external_axes: Optional[List[plt.Axes]] Optional axes to plot data on """ - if category is not None: + + title_opt = category if not portfolio_name else category + " - " + portfolio_name + + if category_dict is not None: weights_df = pd.DataFrame.from_dict( data=weights, orient="index", columns=["value"], dtype=float ) category_df = pd.DataFrame.from_dict( - data=category, orient="index", columns=["category"] + data=category_dict, orient="index", columns=["category"] ) weights_df = weights_df.join(category_df, how="inner") weights_df.sort_index(inplace=True) @@ -3941,7 +3966,10 @@ def additional_plots( if heat: if len(weights) == 1: - console.print(f"Heatmap needs at least two values for '{category}'.") + single_key = list(weights.keys())[0].upper() + console.print( + f"[yellow]Heatmap needs at least two values for '{category}', only found '{single_key}'.[/yellow]" + ) return if external_axes is None: @@ -3971,7 +3999,7 @@ def additional_plots( ax[0].grid(False) ax[0].axis("off") - if category is None: + if category_dict is None: # Vertical dendrogram l, b, w, h = ax[4].get_position().bounds l1 = l * 0.5 diff --git a/openbb_terminal/portfolio/portfolio_optimization/parameters/Parameter.py b/openbb_terminal/portfolio/portfolio_optimization/parameters/Parameter.py new file mode 100644 index 00000000000..fc36685b5a7 --- /dev/null +++ b/openbb_terminal/portfolio/portfolio_optimization/parameters/Parameter.py @@ -0,0 +1,85 @@ +from dataclasses import dataclass +from typing import Any, List, Optional, Type + +# pylint: disable=unidiomatic-typecheck + + +@dataclass(frozen=True) +class Parameter: + """Class for storing parameters. This dataclass is frozen so no attributes can be changed after initialization. + + Parameters + ---------- + name : str + Name of the parameter. + type_ : type + Type of the parameter. + default : Any + Default value of the parameter. + choices : Optional[List[Any]] + List of possible values for the parameter. + """ + + name: str + type_: Type + default: Any + choices: Optional[List[Any]] = None + + def __post_init__(self): + """Post initialization.""" + self.validate() + + def validate(self): + """Validate the value of the parameter. + + Parameters + ---------- + value : Any + Value of the parameter. + + Raises + ------ + TypeError + If the value is not of the correct type. + ValueError + If the value is not in the list of possible values. + """ + if not self.validate_type(self.default): + raise TypeError( + f"Default for '{self.name}' must be of type '{self.type_.__name__}'." + ) + if self.choices is not None: + if not all(self.validate_type(choice) for choice in self.choices): + raise TypeError( + f"Choices must be of the same type as the parameter.\n" + f"'{self.name}': '{self.choices}' -> '{self.type_.__name__}'" + ) + if self.choices is not None and self.default not in self.choices: + raise ValueError( + f"Parameter '{self.name}' must be one of type '{self.choices}'." + ) + + def validate_type(self, value: Any) -> bool: + """Check if the value is of the correct type. + + Parameters + ---------- + value : Any + Value of the parameter. + + Returns + ------- + bool + True if the value is of the correct type, False otherwise. + """ + # Temporary workaround until we drop Python 3.9 for: + # Error: Subscripted generics cannot be used with class and instance checks + # + # isinstance for subscripted generics (e.g. List[str]) is only available + # in Python 3.10, so this method does not check for the type of subscripted generics. + # TODO: Though we should implement it. + # See https://stackoverflow.com/questions/70825351/python-3-typeerror-subscripted-generics-cannot-be-used-with-class-and-instance # noqa: E501 + try: + return isinstance(value, self.type_) + except Exception as _: # noqa: F841 + return True diff --git a/openbb_terminal/portfolio/portfolio_optimization/parameters/params_controller.py b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_controller.py index e9cbd2bb50c..4535277a911 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/parameters/params_controller.py +++ b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_controller.py @@ -9,7 +9,7 @@ from typing import List, Optional from openbb_terminal.custom_prompt_toolkit import NestedCompleter -from openbb_terminal import feature_flags as gtff +from openbb_terminal import feature_flags as obbff from openbb_terminal.decorators import log_start_end from openbb_terminal.menu import session from openbb_terminal.parent_classes import BaseController @@ -76,7 +76,7 @@ class ParametersController(BaseController): self.description: Optional[str] = None self.DATA_FILES = params_helpers.load_data_files() - if session and gtff.USE_PROMPT_TOOLKIT: + if session and obbff.USE_PROMPT_TOOLKIT: choices: dict = {c: {} for c in self.controller_choices} choices["set"] = {c: None for c in self.models} choices["set"]["--model"] = {c: None for c in self.models} @@ -93,8 +93,15 @@ class ParametersController(BaseController): "--show_arguments": {}, "-s": "--show_arguments", } + self.choices = choices self.completer = NestedCompleter.from_nested_dict(choices) + def update_runtime_choices(self): + if session and obbff.USE_PROMPT_TOOLKIT: + self.DATA_FILES = params_helpers.load_data_files() + self.choices["load"]["--file"] = {c: {} for c in self.DATA_FILES} + self.completer = NestedCompleter.from_nested_dict(self.choices) + def print_help(self): """Print help""" mt = MenuText("portfolio/po/params/") @@ -112,8 +119,10 @@ class ParametersController(BaseController): mt.add_raw("\n") mt.add_info("_parameters_") if self.current_model: + max_len = max(len(k) for k in self.params.keys()) for k, v in self.params.items(): + v = params_helpers.booltostr(v) all_params = DEFAULT_PARAMETERS + MODEL_PARAMS[self.current_model] if k in all_params: mt.add_raw( @@ -122,6 +131,7 @@ class ParametersController(BaseController): else: max_len = max(len(k) for k in self.params.keys()) for k, v in self.params.items(): + v = params_helpers.booltostr(v) mt.add_raw( f" [param]{k}{' ' * (max_len - len(k))} :[/param] {v}\n" ) @@ -200,7 +210,8 @@ class ParametersController(BaseController): ns_parser = self.parse_known_args_and_warn(parser, other_args) if ns_parser: self.current_file = str(params_view.save_file(ns_parser.file, self.params)) - console.print() + + self.update_runtime_choices() @log_start_end(log=logger) def call_clear(self, other_args: List[str]): diff --git a/openbb_terminal/portfolio/portfolio_optimization/parameters/params_helpers.py b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_helpers.py index cf1b3dd86e5..4e41ca78ef1 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/parameters/params_helpers.py +++ b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_helpers.py @@ -1,10 +1,14 @@ import argparse -from datetime import datetime -from typing import Dict, List +from datetime import datetime, date +from typing import Any, Dict, List from pathlib import Path from openbb_terminal.helper_funcs import log_and_raise from openbb_terminal.core.config import paths from openbb_terminal.rich_config import console +from openbb_terminal.portfolio.portfolio_optimization.statics import ( + OPTIMIZATION_PARAMETERS, + TERMINAL_TEMPLATE_MAP, +) def check_save_file(file: str) -> str: @@ -33,7 +37,7 @@ def load_data_files() -> Dict[str, Path]: The dictionary of filenames and their paths """ default_path = paths.MISCELLANEOUS_DIRECTORY / "portfolio_examples" / "optimization" - custom_exports = paths.USER_EXPORTS_DIRECTORY / "portfolio" + custom_exports = paths.USER_PORTFOLIO_DATA_DIRECTORY / "optimization" data_files = {} for directory in [default_path, custom_exports]: for file_type in ["xlsx", "ini"]: @@ -44,6 +48,77 @@ def load_data_files() -> Dict[str, Path]: return data_files +def check_convert_parameters(received_parameters: dict) -> dict: + """Check if the argument is in the parameters list. + If it is, try to cast it to the correct type, else use default value. + + Parameters + ---------- + received_parameters: dict + The parameters to be checked + + Returns + ------- + dict + The parameters with the correct types + """ + + converted_parameters = check_convert_dates( + received_parameters, ["start_period", "start_date", "end_period", "end_date"] + ) + + for received_name, received_value in received_parameters.items(): + + # TODO: Remove this line when mapping between template and terminal is not needed + template_name = TERMINAL_TEMPLATE_MAP.get(received_name, received_name) + + if template_name in OPTIMIZATION_PARAMETERS: + PARAMETER = OPTIMIZATION_PARAMETERS[template_name] + if not PARAMETER.validate_type(received_value): + converted_parameters[received_name] = check_convert_parameter( + name=received_name, value=received_value, parameter=PARAMETER + ) + + return converted_parameters + + +def check_convert_parameter(name, value, parameter): + """Converts a parameter to the correct type + + Parameters + ---------- + name: str + The name of the received parameter + value: str + The value of the received parameter + parameter: Parameter + The parameter object + + Returns + ------- + The converted parameter + """ + + try: + # Try to cast the value to the correct type if int or float + if parameter.type_ is int: + new_value = parameter.type_(float(value)) + elif parameter.type_ is float: + new_value = parameter.type_(value) + elif parameter.type_ is bool: + new_value = strtobool(value) + else: + new_value = value + except ValueError: + new_value = parameter.default + console.print( + f"[red]'{name}' format should be '{parameter.type_.__name__}' type[/red]", + f"[red]and could not be converted. Setting default '{new_value}'.\n[/red]", + ) + + return new_value + + def check_convert_dates(params: dict, param_name_list: List[str]) -> dict: """Check if the argument is in the list and convert it to a str object Parameters @@ -62,7 +137,7 @@ def check_convert_dates(params: dict, param_name_list: List[str]) -> dict: for param_name in param_name_list: if param_name in params: param_value = params[param_name] - if isinstance(param_value, datetime): + if isinstance(param_value, date): params[param_name] = param_value.strftime("%Y-%m-%d") elif isinstance(param_value, str): try: @@ -79,3 +154,44 @@ def check_convert_dates(params: dict, param_name_list: List[str]) -> dict: params.pop(param_name) return params + + +def booltostr(value: bool) -> Any: + """Converts a bool to a string or returns the value itself if not bool + + Parameters + ---------- + value: bool + The bool to be converted or the value itself if not bool + + Returns + ------- + Any + The converted value + """ + + if isinstance(value, bool): + return "True" if value else "False" + return value + + +def strtobool(value: str) -> Any: + """Converts a string to a bool or returns the value itself if not string + + Parameters + ---------- + value: str + The string to be converted or the value itself if not string + + Returns + ------- + Any + The converted value + """ + + if isinstance(value, str): + if value.lower() == "true": + return True + elif value.lower() == "false": + return False + return value diff --git a/openbb_terminal/portfolio/portfolio_optimization/parameters/params_view.py b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_view.py index e5a9491e243..ff8cef12228 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/parameters/params_view.py +++ b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_view.py @@ -9,7 +9,8 @@ from openbb_terminal.portfolio.portfolio_optimization import excel_model from openbb_terminal.rich_config import console from openbb_terminal.portfolio.portfolio_optimization.parameters import params_statics from openbb_terminal.portfolio.portfolio_optimization.parameters.params_helpers import ( - check_convert_dates, + booltostr, + check_convert_parameters, ) from openbb_terminal.core.config import paths @@ -47,35 +48,36 @@ def load_file(path: str = "") -> Tuple[dict, str]: console.print("Cannot load in the file due to not being an .ini or .xlsx file.") return {}, "" - params = check_convert_dates( - params=params, param_name_list=["start_period", "end_period"] - ) + converted_parameters = check_convert_parameters(received_parameters=params) - max_len = max(len(k) for k in params.keys()) + max_len = max(len(k) for k in converted_parameters.keys()) help_text = "[info]Parameters:[/info]\n" if current_model: - for k, v in params.items(): + + for k, v in converted_parameters.items(): all_params = ( params_statics.DEFAULT_PARAMETERS + params_statics.MODEL_PARAMS[current_model] ) if k in all_params: + v = booltostr(v) help_text += f" [param]{k}{' ' * (max_len - len(k))} :[/param] {v}\n" else: - for k, v in params.items(): + for k, v in converted_parameters.items(): + v = booltostr(v) help_text += f" [param]{k}{' ' * (max_len - len(k))} :[/param] {v}\n" console.print(help_text) - return params, current_model + return converted_parameters, current_model def save_file(path: str, params: dict) -> Path: if not path.endswith(".ini"): console.print("[red]File to be saved needs to be a .ini file.[/red]\n") # Create file if it does not exist - base_path = paths.USER_EXPORTS_DIRECTORY / "portfolio" + base_path = paths.USER_PORTFOLIO_DATA_DIRECTORY / "optimization" if not base_path.is_dir(): base_path.mkdir() filepath = base_path / path diff --git a/openbb_terminal/portfolio/portfolio_optimization/po_controller.py b/openbb_terminal/portfolio/portfolio_optimization/po_controller.py index d71a68c62f9..f3ab677d8a1 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/po_controller.py +++ b/openbb_terminal/portfolio/portfolio_optimization/po_controller.py @@ -277,9 +277,15 @@ class PortfolioOptimizationController(BaseController): def update_runtime_choices(self): if session and obbff.USE_PROMPT_TOOLKIT: if self.portfolios: - self.choices["show"] = {c: {} for c in list(self.portfolios.keys())} - - self.choices = {**self.choices, **self.SUPPORT_CHOICES} + self.choices["show"]["--portfolios"] = { + c: {} for c in list(self.portfolios.keys()) + } + self.choices["rpf"]["--portfolios"] = { + c: {} for c in list(self.portfolios.keys()) + } + self.choices["plot"]["--portfolios"] = { + c: {} for c in list(self.portfolios.keys()) + } self.completer = NestedCompleter.from_nested_dict(self.choices) def print_help(self): @@ -697,7 +703,6 @@ class PortfolioOptimizationController(BaseController): dest="file", help="Allocation file to be used", choices=self.allocation_file_map.keys(), - metavar="FILE", ) if other_args and "-" not in other_args[0][0]: other_args.insert(0, "--file") @@ -850,7 +855,7 @@ class PortfolioOptimizationController(BaseController): optimizer_view.additional_plots( weights=weights, data=stock_returns[stocks], - title_opt=i, + portfolio_name=i, freq=ns_parser.return_frequency, risk_measure=ns_parser.risk_measure.lower(), risk_free_rate=ns_parser.risk_free, @@ -888,8 +893,9 @@ class PortfolioOptimizationController(BaseController): optimizer_view.additional_plots( weights=weights, data=stock_returns[stocks], - category=filtered_categories, - title_opt=category + " - " + i, + category_dict=filtered_categories, + category=category, + portfolio_name=i, freq=ns_parser.return_frequency, risk_measure=ns_parser.risk_measure.lower(), risk_free_rate=ns_parser.risk_free, @@ -1159,7 +1165,7 @@ class PortfolioOptimizationController(BaseController): default=self.params["target_return"] if "target_return" in self.params else -1, - type=int, + type=float, help="Constraint on minimum level of portfolio's return", ) parser.add_argument( @@ -1167,6 +1173,7 @@ class PortfolioOptimizationController(BaseController): "--target-risk", dest="target_risk", default=self.params["target_risk"] if "target_risk" in self.params else -1, + type=float, help="Constraint on maximum level of portfolio's risk", ) parser.add_argument( @@ -1182,8 +1189,8 @@ class PortfolioOptimizationController(BaseController): parser.add_argument( "-cv", "--covariance", - default=self.params["expected_return"] - if "expected_return" in self.params + default=self.params["covariance"] + if "covariance" in self.params else "hist", dest="covariance", help="""Method used to estimate covariance matrix. Possible values are @@ -1341,6 +1348,7 @@ class PortfolioOptimizationController(BaseController): default=self.params["target_return"] if "target_return" in self.params else -1, + type=float, help="Constraint on minimum level of portfolio's return", ) parser.add_argument( @@ -1348,6 +1356,7 @@ class PortfolioOptimizationController(BaseController): "--target-risk", dest="target_risk", default=self.params["target_risk"] if "target_risk" in self.params else -1, + type=float, help="Constraint on maximum level of portfolio's risk", ) parser.add_argument( @@ -1518,8 +1527,8 @@ class PortfolioOptimizationController(BaseController): "--risk-aversion", type=float, dest="risk_aversion", - default=self.params["long_allocation"] - if "long_allocation" in self.params + default=self.params["risk_aversion"] + if "risk_aversion" in self.params else 1, help="Risk aversion parameter", ) @@ -1530,6 +1539,7 @@ class PortfolioOptimizationController(BaseController): default=self.params["target_return"] if "target_return" in self.params else -1, + type=float, help="Constraint on minimum level of portfolio's return", ) parser.add_argument( @@ -1537,6 +1547,7 @@ class PortfolioOptimizationController(BaseController): "--target-risk", dest="target_risk", default=self.params["target_risk"] if "target_risk" in self.params else -1, + type=float, help="Constraint on maximum level of portfolio's risk", ) parser.add_argument( @@ -1713,6 +1724,7 @@ class PortfolioOptimizationController(BaseController): default=self.params["target_return"] if "target_return" in self.params else -1, + type=float, help="Constraint on minimum level of portfolio's return", ) parser.add_argument( @@ -1720,6 +1732,7 @@ class PortfolioOptimizationController(BaseController): "--target-risk", dest="target_risk", default=self.params["target_risk"] if "target_risk" in self.params else -1, + type=float, help="Constraint on maximum level of portfolio's risk", ) parser.add_argument( @@ -2208,8 +2221,8 @@ class PortfolioOptimizationController(BaseController): "--risk-aversion", type=float, dest="risk_aversion", - default=self.params["long_allocation"] - if "long_allocation" in self.params + default=self.params["risk_aversion"] + if "risk_aversion" in self.params else 1, help="Risk aversion parameter", ) @@ -2218,6 +2231,7 @@ class PortfolioOptimizationController(BaseController): "--delta", default=self.params["delta"] if "delta" in self.params else None, dest="delta", + type=float, help="Risk aversion factor of Black Litterman model", ) parser.add_argument( @@ -2550,6 +2564,7 @@ class PortfolioOptimizationController(BaseController): default=self.params["target_return"] if "target_return" in self.params else -1, + type=float, help="Constraint on minimum level of portfolio's return", ) parser.add_argument( @@ -2707,6 +2722,7 @@ class PortfolioOptimizationController(BaseController): default=self.params["target_return"] if "target_return" in self.params else -1, + type=float, help="Constraint on minimum level of portfolio's return", ) parser.add_argument( diff --git a/openbb_terminal/portfolio/portfolio_optimization/po_engine.py b/openbb_terminal/portfolio/portfolio_optimization/po_engine.py index ca1e1d782bf..4288b1a0a61 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/po_engine.py +++ b/openbb_terminal/portfolio/portfolio_optimization/po_engine.py @@ -194,41 +194,58 @@ class PoEngine: """ self._weights = weights - def get_weights(self) -> Dict[str, float]: + def get_weights(self, warning=True) -> Dict[str, float]: """Get the weights + Parameters + ---------- + warning : bool, optional + Display warning, by default True + Returns ------- Dict[str, float] Weights """ if not self._weights: - console.print("No weights found. Please perform some optimization.") + if warning: + console.print("No weights found. Please perform some optimization.") return {} return self._weights - def get_weights_df(self) -> pd.DataFrame: + def get_weights_df(self, warning=True) -> pd.DataFrame: """Get the weights + Parameters + ---------- + warning : bool, optional + Display warning, by default True + Returns ------- pd.DataFrame Weights """ if not self._weights: - console.print("No weights found. Please perform some optimization.") + if warning: + console.print("No weights found. Please perform some optimization.") return pd.DataFrame() return optimizer_helper.dict_to_df(self._weights) - def set_params(self, params: Dict[str, float]): + def set_params(self, params: Dict[str, float], update=False): """Set the parameters Parameters ---------- params : Dict[str, float] Parameters + update : bool, optional + Update the current model, by default False """ - self._params.update(params) + if update: + self._params.update(params) + else: + self._params = params def get_params(self) -> Dict: """Get the parameters diff --git a/openbb_terminal/portfolio/portfolio_optimization/po_model.py b/openbb_terminal/portfolio/portfolio_optimization/po_model.py index 9b545f1944d..2dab1ec6ac5 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/po_model.py +++ b/openbb_terminal/portfolio/portfolio_optimization/po_model.py @@ -18,11 +18,14 @@ from openbb_terminal.portfolio.portfolio_optimization import ( optimizer_helper, optimizer_model, ) +from openbb_terminal.portfolio.portfolio_optimization.parameters.params_helpers import ( + check_convert_parameters, +) from openbb_terminal.portfolio.portfolio_optimization.statics import ( RISK_NAMES, + TERMINAL_TEMPLATE_MAP, TIME_FACTOR, DRAWDOWNS, - PARAM_TYPES, ) from openbb_terminal.portfolio.portfolio_optimization.po_engine import PoEngine from openbb_terminal.rich_config import console @@ -32,32 +35,6 @@ warnings.filterwarnings("ignore") logger = logging.getLogger(__name__) -@log_start_end(log=logger) -def validate_parameters_type(parameters): - """Validate parameters type - - Parameters - ---------- - parameters : dict - Keyword arguments - """ - for key, value in parameters.items(): - if key in PARAM_TYPES: - expected_type = PARAM_TYPES[key] - if not isinstance(value, expected_type): - if expected_type is str: - parameters.update({key: str(value)}) - elif expected_type is float: - parameters.update({key: float(value)}) - elif expected_type is bool: - parameters.update({key: bool(value)}) - else: - console.print( - f"[info]Parameter {key} should be of type {expected_type.__name__}. Casting failed, reverting to default.[/info]" - ) - parameters.pop(key) - - @log_start_end(log=logger) def generate_portfolio( symbols_categories: Dict[str, Dict[str, str]] = None, @@ -165,7 +142,7 @@ def load_parameters_file( 'maxnan': '0.05', 'threshold': '0.3', 'alpha': '0.05'} - >>> p.set_params({"risk_free_rate": 0.05}) + >>> p.set_params({"risk_free_rate": 0.05}, update=True) >>> p.get_params() {'interval': '3y', 'log_returns': '0', @@ -208,9 +185,15 @@ def validate_inputs( else: console.print("No 'portfolio_engine' provided.") - validate_parameters_type(parameters) + converted_parameters = check_convert_parameters(received_parameters=parameters) - return symbols, portfolio_engine, parameters + terminal_parameters = {} + # TODO: Remove this conversion when mapping between template and terminal is not needed + TEMPLATE_TERMINAL_MAP = {v: k for k, v in TERMINAL_TEMPLATE_MAP.items()} + for key, value in converted_parameters.items(): + terminal_parameters[TEMPLATE_TERMINAL_MAP.get(key, key)] = value + + return symbols, portfolio_engine, terminal_parameters @log_start_end(log=logger) @@ -230,51 +213,61 @@ def get_portfolio_performance(weights: Dict, data: pd.DataFrame, **kwargs) -> Di Portfolio performance """ - freq = kwargs.get("freq", "D") - risk_measure = kwargs.get("risk_measure", "MV") - risk_free_rate = kwargs.get("risk_free_rate", 0.0) - alpha = kwargs.get("alpha", 0.05) - a_sim = kwargs.get("a_sim", 100) - beta = kwargs.get("beta", None) - b_sim = kwargs.get("b_sim", None) + try: + if not weights: + return {} - freq = freq.upper() - weights = pd.Series(weights).to_frame() - returns = data @ weights - mu = returns.mean().item() * TIME_FACTOR[freq] - sigma = returns.std().item() * TIME_FACTOR[freq] ** 0.5 - sharpe = (mu - risk_free_rate) / sigma + freq = optimizer_helper.get_kwarg("freq", kwargs) + risk_measure = optimizer_helper.get_kwarg("risk_measure", kwargs) + risk_free_rate = optimizer_helper.get_kwarg("risk_free_rate", kwargs) + alpha = optimizer_helper.get_kwarg("alpha", kwargs) + a_sim = optimizer_helper.get_kwarg("a_sim", kwargs) + beta = optimizer_helper.get_kwarg("beta", kwargs) + b_sim = optimizer_helper.get_kwarg("b_sim", kwargs) - performance_dict = { - "Return": mu, - "Volatility": sigma, - "Sharpe ratio": sharpe, - } + freq = freq.upper() + weights = pd.Series(weights).to_frame() + returns = data @ weights + mu = returns.mean().item() * TIME_FACTOR[freq] + sigma = returns.std().item() * TIME_FACTOR[freq] ** 0.5 + sharpe = (mu - risk_free_rate) / sigma - risk_measure = optimizer_helper.validate_risk_measure(risk_measure, warning=False) + performance_dict = { + "Return": mu, + "Volatility": sigma, + "Sharpe ratio": sharpe, + } - if risk_measure != "MV": - risk = rp.Sharpe_Risk( - weights, - cov=data.cov(), - returns=data, - rm=risk_measure, - rf=risk_free_rate, - alpha=alpha, - a_sim=a_sim, - beta=beta, - b_sim=b_sim, + risk_measure = optimizer_helper.validate_risk_measure( + risk_measure, warning=False ) - if risk_measure in DRAWDOWNS: - sharpe_2 = (mu - risk_free_rate) / risk - else: - risk = risk * TIME_FACTOR[freq] ** 0.5 - sharpe_2 = (mu - risk_free_rate) / risk + if risk_measure != "MV": + risk = rp.Sharpe_Risk( + weights, + cov=data.cov(), + returns=data, + rm=risk_measure, + rf=risk_free_rate, + alpha=alpha, + a_sim=a_sim, + beta=beta, + b_sim=b_sim, + ) - performance_dict[RISK_NAMES[risk_measure.lower()]] = risk - performance_dict.update({"Sharpe ratio (risk adjusted)": sharpe_2}) + if risk_measure in DRAWDOWNS: + sharpe_2 = (mu - risk_free_rate) / risk + else: + risk = risk * TIME_FACTOR[freq] ** 0.5 + sharpe_2 = (mu - risk_free_rate) / risk + performance_dict[RISK_NAMES[risk_measure.lower()]] = risk + performance_dict.update({"Sharpe ratio (risk adjusted)": sharpe_2}) + except Exception as _: + console.print( + "[red]\nFailed to calculate portfolio performance indicators.[/red]" + ) + performance_dict = {} return performance_dict @@ -421,7 +414,7 @@ def get_maxsharpe( valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -567,7 +560,7 @@ def get_minrisk( valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -713,7 +706,7 @@ def get_maxutil( valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -859,7 +852,7 @@ def get_maxret( valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -982,7 +975,7 @@ def get_maxdiv( valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -1083,7 +1076,7 @@ def get_maxdecorr( valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -1193,7 +1186,7 @@ def get_blacklitterman( valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -1450,7 +1443,7 @@ def get_riskparity( valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -1594,7 +1587,7 @@ def get_relriskparity( valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -1793,7 +1786,7 @@ def get_hrp(portfolio_engine: PoEngine = None, **kwargs) -> Tuple[pd.DataFrame, valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -1992,7 +1985,7 @@ def get_herc(portfolio_engine: PoEngine = None, **kwargs) -> Tuple[pd.DataFrame, valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -2191,7 +2184,7 @@ def get_nco(portfolio_engine: PoEngine = None, **kwargs) -> Tuple[pd.DataFrame, valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -2270,7 +2263,7 @@ def get_equal(portfolio_engine: PoEngine = None, **kwargs) -> Tuple[pd.DataFrame valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -2351,7 +2344,7 @@ def get_mktcap( valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -2432,7 +2425,7 @@ def get_dividend( valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) @@ -2522,7 +2515,7 @@ def get_property( valid_portfolio_engine.set_weights(weights=weights) valid_portfolio_engine.set_returns(returns=returns) - return valid_portfolio_engine.get_weights_df(), performance_dict + return valid_portfolio_engine.get_weights_df(warning=False), performance_dict @log_start_end(log=logger) diff --git a/openbb_terminal/portfolio/portfolio_optimization/po_view.py b/openbb_terminal/portfolio/portfolio_optimization/po_view.py index 8e4d972ceab..b7dc360297e 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/po_view.py +++ b/openbb_terminal/portfolio/portfolio_optimization/po_view.py @@ -28,6 +28,7 @@ from openbb_terminal.portfolio.portfolio_optimization.statics import ( RISK_CHOICES, TIME_FACTOR, ) +from openbb_terminal.portfolio.portfolio_optimization.optimizer_helper import get_kwarg from openbb_terminal.portfolio.portfolio_optimization.po_engine import PoEngine from openbb_terminal.rich_config import console @@ -121,11 +122,11 @@ def display_ef(portfolio_engine: PoEngine = None, **kwargs): >>> openbb.portfolio.po.ef_chart(portfolio_engine=p) """ - n_portfolios = kwargs.get("n_portfolios", 100) - freq = kwargs.get("freq", "D") - risk_measure = kwargs.get("risk_measure", "MV") - risk_free_rate = kwargs.get("risk_free_rate", 0.0) - alpha = kwargs.get("alpha", 0.05) + n_portfolios = get_kwarg("n_portfolios", kwargs) + freq = get_kwarg("freq", kwargs) + risk_measure = get_kwarg("risk_measure", kwargs) + risk_free_rate = get_kwarg("risk_free_rate", kwargs) + alpha = get_kwarg("alpha", kwargs) # Pop chart args tangency = kwargs.pop("tangency", False) @@ -403,7 +404,10 @@ def display_heat(**kwargs): external_axes = kwargs.get("external_axes", None) if len(weights) == 1: - console.print(f"Heatmap needs at least two values for '{category}'.") + single_key = list(weights.keys())[0].upper() + console.print( + f"[yellow]Heatmap needs at least two values for '{category}', only found '{single_key}'.[/yellow]" + ) return if external_axes is None: @@ -482,16 +486,17 @@ def display_rc(**kwargs): weights = kwargs.get("weights", None) data = kwargs.get("data", None) colors = kwargs.get("colors", None) - risk_measure = kwargs.get("risk_measure", "MV") - risk_free_rate = kwargs.get("risk_free_rate", 0.0) title = kwargs.get("title", "") - alpha = kwargs.get("alpha", 0.05) - a_sim = kwargs.get("a_sim", 100.0) - beta = kwargs.get("beta", 0.0) - b_sim = kwargs.get("b_sim", 0.0) - freq = kwargs.get("freq", "D") external_axes = kwargs.get("external_axes", None) + risk_measure = get_kwarg("risk_measure", kwargs) + risk_free_rate = get_kwarg("risk_free_rate", kwargs) + alpha = get_kwarg("alpha", kwargs) + a_sim = get_kwarg("a_sim", kwargs) + beta = get_kwarg("beta", kwargs) + b_sim = get_kwarg("b_sim", kwargs) + freq = get_kwarg("freq", kwargs) + if external_axes is None: _, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) else: @@ -533,9 +538,10 @@ def display_hist(**kwargs): data = kwargs.get("data", None) colors = kwargs.get("colors", None) title = kwargs.get("title", "") - alpha = kwargs.get("alpha", 0.05) external_axes = kwargs.get("external_axes", None) + alpha = kwargs.get("alpha", 0.05) + if external_axes is None: _, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) else: @@ -571,9 +577,10 @@ def display_dd(**kwargs): data = kwargs.get("data", None) colors = kwargs.get("colors", None) title = kwargs.get("title", "") - alpha = kwargs.get("alpha", 0.05) external_axes = kwargs.get("external_axes", None) + alpha = get_kwarg("alpha", kwargs) + if external_axes is None: _, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) else: diff --git a/openbb_terminal/portfolio/portfolio_optimization/statics.py b/openbb_terminal/portfolio/portfolio_optimization/statics.py index 7684ec60ffa..3da9963b6f5 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/statics.py +++ b/openbb_terminal/portfolio/portfolio_optimization/statics.py @@ -1,3 +1,10 @@ +from typing import List, Optional +from openbb_terminal.helper_funcs import get_rf +from openbb_terminal.portfolio.portfolio_optimization.parameters.Parameter import ( + Parameter, +) + + PERIOD_CHOICES = [ "1d", "5d", @@ -6,6 +13,7 @@ PERIOD_CHOICES = [ "6mo", "1y", "2y", + "3y", "5y", "10y", "ytd", @@ -234,25 +242,309 @@ DRAWDOWNS = [ "UCI_Rel", ] -PARAM_TYPES = { - "interval": str, - "start_date": str, - "end_date": str, - "log_returns": bool, - "freq": str, - "maxnan": float, - "threshold": float, - "method": str, - "risk_measure": str, - "objective": str, - "risk_free_rate": float, - "risk_aversion": float, - "alpha": float, - "target_return": float, - "target_risk": float, - "mean": str, - "covariance": str, - "d_ewma": float, - "value": float, - "value_short": float, +NAN_FILL_METHOD_CHOICES = [ + "linear", + "time", + "nearest", + "zero", + "slinear", + "quadratic", + "cubic", + "barycentric", +] + +TECHNIQUE_CHOICES = [ + "maxsharpe", + "minrisk", + "maxutil", + "maxret", + "maxdiv", + "maxdecorr", + "ef", + "equal", + "mktcap", + "dividend", + "blacklitterman", + "riskparity", + "relriskparity", + "hrp", + "herc", + "nco", +] + + +# Parameter template files +OPTIMIZATION_PARAMETERS = { + "historic_period": Parameter( + name="historic_period", + type_=str, + default="3y", + choices=PERIOD_CHOICES, + ), + "start_period": Parameter( + name="start_period", + type_=str, + default="", + ), + "end_period": Parameter( + name="end_period", + type_=str, + default="", + ), + "log_returns": Parameter( + name="log_returns", + type_=bool, + default=False, + ), + "return_frequency": Parameter( + name="return_frequency", + type_=str, + default="d", + choices=FREQ_CHOICES, + ), + "max_nan": Parameter( + name="max_nan", + type_=float, + default=0.05, + ), + "threshold_value": Parameter( + name="threshold_value", + type_=float, + default=0.3, + ), + "nan_fill_method": Parameter( + name="nan_fill_method", + type_=str, + default="time", + choices=NAN_FILL_METHOD_CHOICES, + ), + "risk_free": Parameter( + name="risk_free", + type_=float, + default=get_rf(), + ), + "significance_level": Parameter( + name="significance_level", + type_=float, + default=0.05, + ), + "technique": Parameter( + name="technique", + type_=str, + default="maxsharpe", + choices=TECHNIQUE_CHOICES, + ), + "risk_measure": Parameter( + name="risk_measure", + type_=str, + default="MV", + choices=MEAN_RISK_CHOICES + RISK_PARITY_CHOICES + HCP_CHOICES, + ), + "target_return": Parameter( + name="target_return", + type_=float, + default=-1.0, + ), + "target_risk": Parameter( + name="target_risk", + type_=float, + default=-1.0, + ), + "expected_return": Parameter( + name="expected_return", + type_=str, + default="hist", + choices=MEAN_CHOICES, + ), + "covariance": Parameter( + name="covariance", + type_=str, + default="hist", + choices=COVARIANCE_CHOICES, + ), + "smoothing_factor_ewma": Parameter( + name="smoothing_factor_ewma", + type_=float, + default=0.94, + ), + "long_allocation": Parameter( + name="long_allocation", + type_=float, + default=1.0, + ), + "short_allocation": Parameter( + name="short_allocation", + type_=float, + default=0.0, + ), + "risk_aversion": Parameter( + name="risk_aversion", + type_=float, + default=1.0, + ), + "amount_portfolios": Parameter( + name="amount_portfolios", + type_=int, + default=100, + ), + "random_seed": Parameter( + name="random_seed", + type_=int, + default=123, + ), + "tangency": Parameter( + name="tangency", + type_=bool, + default=False, + ), + "risk_contribution": Parameter( + name="risk_contribution", + type_=Optional[List[str]], + default=None, + ), + "risk_parity_model": Parameter( + name="risk_parity_model", + type_=str, + default="A", + choices=REL_RISK_PARITY_CHOICES, + ), + "penal_factor": Parameter( + name="penal_factor", + type_=float, + default=1.0, + ), + "p_views": Parameter( + name="p_views", + type_=Optional[List[List[float]]], + default=None, + ), + "q_views": Parameter( + name="q_views", + type_=Optional[List[List[float]]], + default=None, + ), + "delta": Parameter( + name="delta", + type_=Optional[float], + default=None, + ), + "equilibrium": Parameter( + name="equilibrium", + type_=bool, + default=True, + ), + "optimize": Parameter( + name="optimize", + type_=bool, + default=True, + ), + "co_dependence": Parameter( + name="co_dependence", + type_=str, + default="pearson", + choices=CODEPENDENCE_CHOICES, + ), + "cvar_simulations_losses": Parameter( + name="cvar_simulations_losses", + type_=int, + default=100, + ), + "cvar_simulations_gains": Parameter( + name="cvar_simulations_gains", + type_=Optional[int], + default=None, + ), + "cvar_significance": Parameter( + name="cvar_significance", + type_=Optional[float], + default=None, + ), + "linkage": Parameter( + name="linkage", + type_=str, + default="single", + choices=LINKAGE_CHOICES, + ), + "amount_clusters": Parameter( + name="amount_clusters", + type_=Optional[int], + default=None, + ), + "max_clusters": Parameter( + name="max_clusters", + type_=int, + default=10, + ), + "amount_bins": Parameter( + name="amount_bins", + type_=str, + default="KN", + choices=BINS_CHOICES, + ), + "alpha_tail": Parameter( + name="alpha_tail", + type_=float, + default=0.05, + ), + "leaf_order": Parameter( + name="leaf_order", + type_=bool, + default=True, + ), + "objective": Parameter( + name="objective", + type_=str, + default="MinRisk", + choices=OBJECTIVE_CHOICES + NCO_OBJECTIVE_CHOICES, + ), +} + +# Model functions +# TODO: The dictionary below should not be needed, we can input defaults in the Parameter directly +# But since template variable names are different from functions argument variable names +# we need to map them before removing this dictionary. When we standardize the names, we can remove this. + +TERMINAL_TEMPLATE_MAP = { + "a_sim": "cvar_simulations_losses", + "alpha": "significance_level", + "alpha_tail": "alpha_tail", + "n_portfolios": "amount_portfolios", + "b_sim": "cvar_simulations_gains", + "beta": "cvar_significance", + "bins_info": "amount_bins", + "codependence": "co_dependence", + "covariance": "covariance", + "d_ewma": "smoothing_factor_ewma", + "delta": "delta", + "end_date": "end_period", + "equilibrium": "equilibrium", + "freq": "return_frequency", + "interval": "historic_period", + "k": "amount_clusters", + "leaf_order": "leaf_order", + "linkage": "linkage", + "log_returns": "log_returns", + "max_k": "max_clusters", + "maxnan": "max_nan", + "mean": "expected_return", + "method": "nan_fill_method", + "objective": "objective", + "optimize": "optimize", + "p_views": "p_views", + "penal_factor": "penal_factor", + "q_views": "q_views", + "seed": "random_seed", + "risk_aversion": "risk_aversion", + "risk_cont": "risk_contribution", + "risk_free_rate": "risk_free", + "risk_measure": "risk_measure", + "version": "risk_parity_model", + "start_date": "start_period", + "tangency": "tangency", + "target_return": "target_return", + "target_risk": "target_risk", + "technique": "technique", + "threshold": "threshold_value", + "value": "long_allocation", + "value_short": "short_allocation", }