From 86f2e5622ae32e40e828800c6f78294fdfa18b51 Mon Sep 17 00:00:00 2001 From: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Date: Fri, 4 Nov 2022 20:10:50 +0100 Subject: [PATCH] Fix Various portfolio/po issues (#3286) * Allow for loading without -f flag * Massive upgrade to params file read/write * Better handling empty dfs and none in prompt toolkit * Fix tr typing and handle none error * Added options * Fixed tests Co-authored-by: James Maslek --- openbb_terminal/custom_prompt_toolkit.py | 20 +- openbb_terminal/miscellaneous/i18n/en.yml | 4 +- .../portfolio_examples/optimization}/dany.ini | 0 .../portfolio_examples/optimization}/dd.ini | 0 .../optimization}/james.ini | 0 .../portfolio_optimization/optimizer_model.py | 5 + .../portfolio_optimization/optimizer_view.py | 3 + .../parameters/params_controller.py | 98 ++----- .../parameters/params_helpers.py | 42 +++ .../parameters/params_statics.py | 219 ++++++++++++++ .../parameters/params_view.py | 277 +++--------------- .../portfolio_optimization/po_controller.py | 67 ++++- .../test_po_controller/test_print_help.txt | 2 +- 13 files changed, 395 insertions(+), 342 deletions(-) rename openbb_terminal/{portfolio/portfolio_optimization/parameters => miscellaneous/portfolio_examples/optimization}/dany.ini (100%) rename openbb_terminal/{portfolio/portfolio_optimization/parameters => miscellaneous/portfolio_examples/optimization}/dd.ini (100%) rename openbb_terminal/{portfolio/portfolio_optimization/parameters => miscellaneous/portfolio_examples/optimization}/james.ini (100%) create mode 100644 openbb_terminal/portfolio/portfolio_optimization/parameters/params_helpers.py create mode 100644 openbb_terminal/portfolio/portfolio_optimization/parameters/params_statics.py diff --git a/openbb_terminal/custom_prompt_toolkit.py b/openbb_terminal/custom_prompt_toolkit.py index 0f543c95691a..2489c4554a1d 100644 --- a/openbb_terminal/custom_prompt_toolkit.py +++ b/openbb_terminal/custom_prompt_toolkit.py @@ -271,7 +271,7 @@ def get_completions( if "-" not in text: completer = self.options.get(first_term) else: - if cmd in self.options: + if cmd in self.options and self.options.get(cmd): completer = self.options.get(cmd).options.get(first_term) # type: ignore else: completer = self.options.get(first_term) @@ -378,16 +378,14 @@ def get_completions( if k not in self.flags_processed } - if ( - cmd - and cmd in self.options.keys() - and [ - text in val - for val in [ - f"{cmd} {opt}" for opt in self.options.get(cmd).options.keys() # type: ignore - ] - ] - ): + command = self.options.get(cmd) + if command: + options = command.options # type: ignore + else: + options = {} + command_options = [f"{cmd} {opt}" for opt in options.keys()] + text_list = [text in val for val in command_options] + if cmd and cmd in self.options.keys() and text_list: completer = WordCompleter( list(self.options.get(cmd).options.keys()), # type: ignore ignore_case=self.ignore_case, diff --git a/openbb_terminal/miscellaneous/i18n/en.yml b/openbb_terminal/miscellaneous/i18n/en.yml index 2e97954dcb89..657bfbc4c7f1 100644 --- a/openbb_terminal/miscellaneous/i18n/en.yml +++ b/openbb_terminal/miscellaneous/i18n/en.yml @@ -491,10 +491,8 @@ en: crypto/disc/games: top blockchain games crypto/disc/dapps: top decentralized apps crypto/disc/dex: top decentralized exchanges - crypto/ov/global: global crypto market info crypto/ov/defi: global DeFi market info crypto/ov/stables: stablecoins - crypto/ov/exchanges: top crypto exchanges crypto/ov/exrates: coin exchange rates crypto/ov/indexes: crypto indexes crypto/ov/derivatives: crypto derivatives @@ -968,7 +966,7 @@ en: portfolio/po/_loaded: Portfolio loaded portfolio/po/_tickers: Tickers portfolio/po/_categories: Categories - portfolio/po/file: select portfolio parameter file + portfolio/po/params/load: select portfolio parameter file portfolio/po/params: specify and show portfolio risk parameters portfolio/po/_parameter: Parameter file portfolio/po/_mean_risk_optimization_: Mean Risk Optimization diff --git a/openbb_terminal/portfolio/portfolio_optimization/parameters/dany.ini b/openbb_terminal/miscellaneous/portfolio_examples/optimization/dany.ini similarity index 100% rename from openbb_terminal/portfolio/portfolio_optimization/parameters/dany.ini rename to openbb_terminal/miscellaneous/portfolio_examples/optimization/dany.ini diff --git a/openbb_terminal/portfolio/portfolio_optimization/parameters/dd.ini b/openbb_terminal/miscellaneous/portfolio_examples/optimization/dd.ini similarity index 100% rename from openbb_terminal/portfolio/portfolio_optimization/parameters/dd.ini rename to openbb_terminal/miscellaneous/portfolio_examples/optimization/dd.ini diff --git a/openbb_terminal/portfolio/portfolio_optimization/parameters/james.ini b/openbb_terminal/miscellaneous/portfolio_examples/optimization/james.ini similarity index 100% rename from openbb_terminal/portfolio/portfolio_optimization/parameters/james.ini rename to openbb_terminal/miscellaneous/portfolio_examples/optimization/james.ini diff --git a/openbb_terminal/portfolio/portfolio_optimization/optimizer_model.py b/openbb_terminal/portfolio/portfolio_optimization/optimizer_model.py index 604f214180e1..890f9f911f9a 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/optimizer_model.py +++ b/openbb_terminal/portfolio/portfolio_optimization/optimizer_model.py @@ -461,6 +461,11 @@ def get_mean_risk_portfolio( threshold=threshold, method=method, ) + if stock_returns.empty: + console.print( + "[red]Not enough data points in range to run calculations.[/red]\n" + ) + return {}, pd.DataFrame() risk_free_rate = risk_free_rate / time_factor[freq.upper()] diff --git a/openbb_terminal/portfolio/portfolio_optimization/optimizer_view.py b/openbb_terminal/portfolio/portfolio_optimization/optimizer_view.py index f2fdd9b27bcf..f6f4d89f87ac 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/optimizer_view.py +++ b/openbb_terminal/portfolio/portfolio_optimization/optimizer_view.py @@ -1186,6 +1186,9 @@ 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") return {} diff --git a/openbb_terminal/portfolio/portfolio_optimization/parameters/params_controller.py b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_controller.py index ecc4471fd58f..f9963a1366ee 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/parameters/params_controller.py +++ b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_controller.py @@ -4,22 +4,18 @@ # pylint: disable=C0302, no-else-return import argparse -import configparser import logging -import os -from pathlib import Path from typing import List, Optional from openbb_terminal.custom_prompt_toolkit import NestedCompleter -from openbb_terminal.core.config.paths import MISCELLANEOUS_DIRECTORY from openbb_terminal import feature_flags as gtff from openbb_terminal.decorators import log_start_end -from openbb_terminal.helper_funcs import log_and_raise from openbb_terminal.menu import session from openbb_terminal.parent_classes import BaseController from openbb_terminal.portfolio.portfolio_optimization.parameters import params_view -from openbb_terminal.portfolio.portfolio_optimization.parameters.params_view import ( +from openbb_terminal.portfolio.portfolio_optimization.parameters import params_helpers +from openbb_terminal.portfolio.portfolio_optimization.parameters.params_statics import ( AVAILABLE_OPTIONS, DEFAULT_PARAMETERS, DEFAULT_BOOL, @@ -30,29 +26,12 @@ logger = logging.getLogger(__name__) -def check_save_file(file: str) -> str: - """Argparse type to check parameter file to be saved""" - if file == "defaults.ini": - log_and_raise( - argparse.ArgumentTypeError( - "Cannot overwrite defaults.ini file, please save with a different name" - ) - ) - else: - if not file.endswith(".ini") and not file.endswith(".xlsx"): - log_and_raise( - argparse.ArgumentTypeError("File to be saved needs to be .ini or .xlsx") - ) - - return file - - class ParametersController(BaseController): """Portfolio Optimization Parameters Controller class""" CHOICES_COMMANDS = [ "set", - "file", + "load", "save", "new", "clear", @@ -80,46 +59,22 @@ class ParametersController(BaseController): current_model = "" current_file = "" - params = configparser.RawConfigParser() def __init__( self, file: str, queue: List[str] = None, - params=None, + params: Optional[dict] = None, current_model=None, ): """Constructor""" super().__init__(queue) + self.params: dict = params if params else {} self.current_file = file self.current_model = current_model self.description: Optional[str] = None - self.DEFAULT_PATH = os.path.abspath( - MISCELLANEOUS_DIRECTORY / "portfolio_examples" / "optimization" - ) - - self.file_types = ["xlsx", "ini"] - self.DATA_FILES = { - filepath.name: filepath - for file_type in self.file_types - for filepath in Path(self.DEFAULT_PATH).rglob(f"*.{file_type}") - if filepath.is_file() - } - - if params: - self.params = params - else: - pass - # TODO: Enable .ini reading - # self.params.read( - # os.path.join( - # self.DEFAULT_PATH, - # self.current_file if self.current_file else "defaults.ini", - # ) - # ) - # self.params.optionxform = str # type: ignore - # self.params = self.params["OPENBB"] + self.DATA_FILES = params_helpers.load_data_files() if session and gtff.USE_PROMPT_TOOLKIT: choices: dict = {c: {} for c in self.controller_choices} @@ -127,9 +82,9 @@ def __init__( choices["set"]["--model"] = {c: None for c in self.models} choices["set"]["-m"] = "--model" choices["arg"] = {c: None for c in AVAILABLE_OPTIONS} - choices["file"] = {c: {} for c in self.DATA_FILES} - choices["file"]["--file"] = {c: {} for c in self.DATA_FILES} - choices["file"]["-f"] = "--file" + choices["load"] = {c: {} for c in self.DATA_FILES} + choices["load"]["--file"] = {c: {} for c in self.DATA_FILES} + choices["load"]["-f"] = "--file" choices["save"]["--file"] = None choices["save"]["-f"] = "--file" choices["arg"] = { @@ -145,7 +100,7 @@ def print_help(self): mt = MenuText("portfolio/po/params/") mt.add_param("_loaded", self.current_file) mt.add_raw("\n") - mt.add_cmd("file") + mt.add_cmd("load") mt.add_cmd("save") mt.add_raw("\n") mt.add_param("_model", self.current_model or "") @@ -190,7 +145,7 @@ def custom_reset(self): return [] @log_start_end(log=logger) - def call_file(self, other_args: List[str]): + def call_load(self, other_args: List[str]): """Process load command""" parser = argparse.ArgumentParser( add_help=False, @@ -208,6 +163,8 @@ def call_file(self, other_args: List[str]): help="Parameter file to be used", ) + if other_args and "-" not in other_args[0][0]: + other_args.insert(0, "-f") ns_parser = self.parse_known_args_and_warn(parser, other_args) if ns_parser: @@ -235,31 +192,17 @@ def call_save(self, other_args: List[str]): "-f", "--file", required=True, - type=check_save_file, + type=params_helpers.check_save_file, dest="file", help="Filename to be saved", ) + if other_args and "-" not in other_args[0][0]: + other_args.insert(0, "-f") ns_parser = self.parse_known_args_and_warn(parser, other_args) if ns_parser: - if ns_parser.file.endswith(".ini"): - # Create file if it does not exist - filepath = os.path.abspath( - os.path.join( - os.path.dirname(__file__), - ns_parser.file, - ) - ) - Path(filepath) - - with open(filepath, "w") as configfile: - self.params.write(configfile) - - self.current_file = ns_parser.file - console.print() - - elif ns_parser.file.endswith(".xlsx"): - console.print("It is not yet possible to save to .xlsx") + self.current_file = str(params_view.save_file(ns_parser.file, self.params)) + console.print() @log_start_end(log=logger) def call_clear(self, other_args: List[str]): @@ -272,11 +215,6 @@ def call_clear(self, other_args: List[str]): ) ns_parser = self.parse_known_args_and_warn(parser, other_args) if ns_parser: - if not self.current_file: - console.print( - "[red]Load portfolio risk parameters first using `file`.\n[/red]" - ) - return self.current_model = "" console.print("") diff --git a/openbb_terminal/portfolio/portfolio_optimization/parameters/params_helpers.py b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_helpers.py new file mode 100644 index 000000000000..e71fd5cd7c3d --- /dev/null +++ b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_helpers.py @@ -0,0 +1,42 @@ +import argparse +from typing import Dict +from pathlib import Path +from openbb_terminal.helper_funcs import log_and_raise +from openbb_terminal.core.config import paths + + +def check_save_file(file: str) -> str: + """Argparse type to check parameter file to be saved""" + if file == "defaults.ini": + log_and_raise( + argparse.ArgumentTypeError( + "Cannot overwrite defaults.ini file, please save with a different name" + ) + ) + else: + if not file.endswith(".ini"): + log_and_raise( + argparse.ArgumentTypeError("File to be saved needs to be .ini") + ) + + return file + + +def load_data_files() -> Dict[str, Path]: + """Loads files from the misc directory and from the user's custom exports + + Returns + ---------- + 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" + data_files = {} + for directory in [default_path, custom_exports]: + for file_type in ["xlsx", "ini"]: + for filepath in Path(directory).rglob(f"*.{file_type}"): + if filepath.is_file(): + data_files[filepath.name] = filepath + + return data_files diff --git a/openbb_terminal/portfolio/portfolio_optimization/parameters/params_statics.py b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_statics.py new file mode 100644 index 000000000000..18a01a877b67 --- /dev/null +++ b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_statics.py @@ -0,0 +1,219 @@ +DEFAULT_RANGE = [value / 1000 for value in range(0, 1001)] +DEFAULT_BOOL = ["True", "False"] + +AVAILABLE_OPTIONS = { + "historic_period": ["d", "w", "mo", "y", "ytd", "max"], + "start_period": ["Any"], + "end_period": ["Any"], + "log_returns": DEFAULT_BOOL, + "return_frequency": ["d", "w", "m"], + "max_nan": DEFAULT_RANGE, + "threshold_value": DEFAULT_RANGE, + "nan_fill_method": [ + "linear", + "time", + "nearest", + "zero", + "slinear", + "quadratic", + "cubic", + ], + "risk_free": DEFAULT_RANGE, + "significance_level": DEFAULT_RANGE, + "risk_measure": [ + "MV", + "MAD", + "MSV", + "FLPM", + "SLPM", + "CVaR", + "EVaR", + "WR", + "ADD", + "UCI", + "CDaR", + "EDaR", + "MDD", + ], + "target_return": DEFAULT_RANGE + [-x for x in DEFAULT_RANGE], + "target_risk": DEFAULT_RANGE + [-x for x in DEFAULT_RANGE], + "expected_return": ["hist", "ewma1", "ewma2"], + "covariance": [ + "hist", + "ewma1", + "ewma2", + "ledoit", + "oas", + "shrunk", + "gl", + "jlogo", + "fixed", + "spectral", + "shrink", + ], + "smoothing_factor_ewma": DEFAULT_RANGE, + "long_allocation": DEFAULT_RANGE, + "short_allocation": DEFAULT_RANGE, + "risk_aversion": [value / 100 for value in range(-500, 501)], + "amount_portfolios": range(1, 10001), + "random_seed": range(1, 10001), + "tangency": DEFAULT_BOOL, + "risk_parity_model": ["A", "B", "C"], + "penal_factor": DEFAULT_RANGE + [-x for x in DEFAULT_RANGE], + "co_dependence": [ + "pearson", + "spearman", + "abs_pearson", + "abs_spearman", + "distance", + "mutual_info", + "tail", + ], + "cvar_simulations": range(1, 10001), + "cvar_significance": DEFAULT_RANGE, + "linkage": [ + "single", + "complete", + "average", + "weighted", + "centroid", + "ward", + "dbht", + ], + "max_clusters": range(1, 101), + "amount_bins": ["KN", "FD", "SC", "HGR", "Integer"], + "alpha_tail": DEFAULT_RANGE, + "leaf_order": DEFAULT_BOOL, + "objective": ["MinRisk", "Utility", "Sharpe", "MaxRet"], +} + +DEFAULT_PARAMETERS = [ + "historic_period", + "start_period", + "end_period", + "log_returns", + "return_frequency", + "max_nan", + "threshold_value", + "nan_fill_method", + "risk_free", + "significance_level", +] +MODEL_PARAMS = { + "maxsharpe": [ + "risk_measure", + "target_return", + "target_risk", + "expected_return", + "covariance", + "smoothing_factor_ewma", + "long_allocation", + "short_allocation", + ], + "minrisk": [ + "risk_measure", + "target_return", + "target_risk", + "expected_return", + "covariance", + "smoothing_factor_ewma", + "long_allocation", + "short_allocation", + ], + "maxutil": [ + "risk_measure", + "target_return", + "target_risk", + "expected_return", + "covariance", + "smoothing_factor_ewma", + "long_allocation", + "short_allocation", + "risk_aversion", + ], + "maxret": [ + "risk_measure", + "target_return", + "target_risk", + "expected_return", + "covariance", + "smoothing_factor_ewma", + "long_allocation", + ], + "maxdiv": ["covariance", "long_allocation"], + "maxdecorr": ["covariance", "long_allocation"], + "ef": [ + "risk_measure", + "long_allocation", + "short_allocation", + "amount_portfolios", + "random_seed", + "tangency", + ], + "equal": ["risk_measure", "long_allocation"], + "mktcap": ["risk_measure", "long_allocation"], + "dividend": ["risk_measure", "long_allocation"], + "riskparity": [ + "risk_measure", + "target_return", + "long_allocation", + "risk_contribution", + ], + "relriskparity": [ + "risk_measure", + "covariance", + "smoothing_factor_ewma", + "long_allocation", + "risk_contribution", + "risk_parity_model", + "penal_factor", + ], + "hrp": [ + "risk_measure", + "covariance", + "smoothing_factor_ewma", + "long_allocation", + "co_dependence", + "cvar_simulations", + "cvar_significance", + "linkage", + "amount_clusters", + "max_clusters", + "amount_bins", + "alpha_tail", + "leaf_order", + "objective", + ], + "herc": [ + "risk_measure", + "covariance", + "smoothing_factor_ewma", + "long_allocation", + "co_dependence", + "cvar_simulations", + "cvar_significance", + "linkage", + "amount_clusters", + "max_clusters", + "amount_bins", + "alpha_tail", + "leaf_order", + "objective", + ], + "nco": [ + "risk_measure", + "covariance", + "smoothing_factor_ewma", + "long_allocation", + "co_dependence", + "cvar_simulations", + "cvar_significance", + "linkage", + "amount_clusters", + "max_clusters", + "amount_bins", + "alpha_tail", + "leaf_order", + "objective", + ], +} diff --git a/openbb_terminal/portfolio/portfolio_optimization/parameters/params_view.py b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_view.py index d3d9a716a2f6..bcaecb34d592 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/parameters/params_view.py +++ b/openbb_terminal/portfolio/portfolio_optimization/parameters/params_view.py @@ -1,271 +1,60 @@ import configparser +from typing import Tuple +from pathlib import Path import pandas as pd from openbb_terminal.helper_funcs import print_rich_table 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.core.config import paths -DEFAULT_RANGE = [value / 1000 for value in range(0, 1001)] -DEFAULT_BOOL = ["True", "False"] -AVAILABLE_OPTIONS = { - "historic_period": ["d", "w", "mo", "y", "ytd", "max"], - "start_period": ["Any"], - "end_period": ["Any"], - "log_returns": DEFAULT_BOOL, - "return_frequency": ["d", "w", "m"], - "max_nan": DEFAULT_RANGE, - "threshold_value": DEFAULT_RANGE, - "nan_fill_method": [ - "linear", - "time", - "nearest", - "zero", - "slinear", - "quadratic", - "cubic", - ], - "risk_free": DEFAULT_RANGE, - "significance_level": DEFAULT_RANGE, - "risk_measure": [ - "MV", - "MAD", - "MSV", - "FLPM", - "SLPM", - "CVaR", - "EVaR", - "WR", - "ADD", - "UCI", - "CDaR", - "EDaR", - "MDD", - ], - "target_return": DEFAULT_RANGE + [-x for x in DEFAULT_RANGE], - "target_risk": DEFAULT_RANGE + [-x for x in DEFAULT_RANGE], - "expected_return": ["hist", "ewma1", "ewma2"], - "covariance": [ - "hist", - "ewma1", - "ewma2", - "ledoit", - "oas", - "shrunk", - "gl", - "jlogo", - "fixed", - "spectral", - "shrink", - ], - "smoothing_factor_ewma": DEFAULT_RANGE, - "long_allocation": DEFAULT_RANGE, - "short_allocation": DEFAULT_RANGE, - "risk_aversion": [value / 100 for value in range(-500, 501)], - "amount_portfolios": range(1, 10001), - "random_seed": range(1, 10001), - "tangency": DEFAULT_BOOL, - "risk_parity_model": ["A", "B", "C"], - "penal_factor": DEFAULT_RANGE + [-x for x in DEFAULT_RANGE], - "co_dependence": [ - "pearson", - "spearman", - "abs_pearson", - "abs_spearman", - "distance", - "mutual_info", - "tail", - ], - "cvar_simulations": range(1, 10001), - "cvar_significance": DEFAULT_RANGE, - "linkage": [ - "single", - "complete", - "average", - "weighted", - "centroid", - "ward", - "dbht", - ], - "max_clusters": range(1, 101), - "amount_bins": ["KN", "FD", "SC", "HGR", "Integer"], - "alpha_tail": DEFAULT_RANGE, - "leaf_order": DEFAULT_BOOL, - "objective": ["MinRisk", "Utility", "Sharpe", "MaxRet"], -} - -DEFAULT_PARAMETERS = [ - "historic_period", - "start_period", - "end_period", - "log_returns", - "return_frequency", - "max_nan", - "threshold_value", - "nan_fill_method", - "risk_free", - "significance_level", -] -MODEL_PARAMS = { - "maxsharpe": [ - "risk_measure", - "target_return", - "target_risk", - "expected_return", - "covariance", - "smoothing_factor_ewma", - "long_allocation", - "short_allocation", - ], - "minrisk": [ - "risk_measure", - "target_return", - "target_risk", - "expected_return", - "covariance", - "smoothing_factor_ewma", - "long_allocation", - "short_allocation", - ], - "maxutil": [ - "risk_measure", - "target_return", - "target_risk", - "expected_return", - "covariance", - "smoothing_factor_ewma", - "long_allocation", - "short_allocation", - "risk_aversion", - ], - "maxret": [ - "risk_measure", - "target_return", - "target_risk", - "expected_return", - "covariance", - "smoothing_factor_ewma", - "long_allocation", - ], - "maxdiv": ["covariance", "long_allocation"], - "maxdecorr": ["covariance", "long_allocation"], - "ef": [ - "risk_measure", - "long_allocation", - "short_allocation", - "amount_portfolios", - "random_seed", - "tangency", - ], - "equal": ["risk_measure", "long_allocation"], - "mktcap": ["risk_measure", "long_allocation"], - "dividend": ["risk_measure", "long_allocation"], - "riskparity": [ - "risk_measure", - "target_return", - "long_allocation", - "risk_contribution", - ], - "relriskparity": [ - "risk_measure", - "covariance", - "smoothing_factor_ewma", - "long_allocation", - "risk_contribution", - "risk_parity_model", - "penal_factor", - ], - "hrp": [ - "risk_measure", - "covariance", - "smoothing_factor_ewma", - "long_allocation", - "co_dependence", - "cvar_simulations", - "cvar_significance", - "linkage", - "amount_clusters", - "max_clusters", - "amount_bins", - "alpha_tail", - "leaf_order", - "objective", - ], - "herc": [ - "risk_measure", - "covariance", - "smoothing_factor_ewma", - "long_allocation", - "co_dependence", - "cvar_simulations", - "cvar_significance", - "linkage", - "amount_clusters", - "max_clusters", - "amount_bins", - "alpha_tail", - "leaf_order", - "objective", - ], - "nco": [ - "risk_measure", - "covariance", - "smoothing_factor_ewma", - "long_allocation", - "co_dependence", - "cvar_simulations", - "cvar_significance", - "linkage", - "amount_clusters", - "max_clusters", - "amount_bins", - "alpha_tail", - "leaf_order", - "objective", - ], -} - - -def load_file(file_location=None): +def load_file(path: str = "") -> Tuple[dict, str]: """ - Loads in the configuration file and return the parameters in a dictionary including the model if available. + Loads in the configuration file and return the parameters in a dictionary including the model + if available. Parameters ---------- - file_location: str + path: str The location of the file to be loaded in either xlsx or ini. Returns ------- - Return the parameters and the model, if available. + Tuple[dict, str] + Return the parameters and the model, if available. """ - if str(file_location).endswith(".ini"): - params = configparser.RawConfigParser() - params.read(file_location) - params.optionxform = str # type: ignore - params = params["OPENBB"] + if str(path).endswith(".ini"): + params_obj = configparser.RawConfigParser() + params_obj.read(path) + params_obj.optionxform = str # type: ignore + params: dict = dict(params_obj["OPENBB"].items()) if "technique" in params: current_model = params["technique"] else: - current_model = None + current_model = "" - elif str(file_location).endswith(".xlsx"): - params, _ = excel_model.load_configuration(file_location) + elif str(path).endswith(".xlsx"): + params, _ = excel_model.load_configuration(path) current_model = params["technique"] else: console.print( "Can not load in the file due to not being an .ini or .xlsx file." ) - return None, None + return {}, "" max_len = max(len(k) for k in params.keys()) help_text = "[info]Parameters:[/info]\n" if current_model: for k, v in params.items(): - all_params = DEFAULT_PARAMETERS + MODEL_PARAMS[current_model] + all_params = ( + params_statics.DEFAULT_PARAMETERS + + params_statics.MODEL_PARAMS[current_model] + ) if k in all_params: help_text += f" [param]{k}{' ' * (max_len - len(k))} :[/param] {v}\n" else: @@ -277,6 +66,26 @@ def load_file(file_location=None): return params, 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" + if not base_path.is_dir(): + base_path.mkdir() + filepath = base_path / path + + config_parser = configparser.RawConfigParser() + config_parser.add_section("OPENBB") + for key, value in params.items(): + config_parser.set("OPENBB", key, value) + + with open(filepath, "w") as configfile: + config_parser.write(configfile) + + return filepath + + def show_arguments(arguments, description=None): """ Show the available arguments and the choices you have for each. If available, also show diff --git a/openbb_terminal/portfolio/portfolio_optimization/po_controller.py b/openbb_terminal/portfolio/portfolio_optimization/po_controller.py index 68939218328f..24a646ce8c2e 100644 --- a/openbb_terminal/portfolio/portfolio_optimization/po_controller.py +++ b/openbb_terminal/portfolio/portfolio_optimization/po_controller.py @@ -383,6 +383,47 @@ def __init__( self.choices["load"] = {c: {} for c in self.DATA_ALLOCATION_FILES} self.choices["load"]["--file"] = {c: {} for c in self.DATA_ALLOCATION_FILES} self.choices["load"]["-f"] = "--file" + self.choices["plot"]["--portfolios"] = None + self.choices["plot"]["-pf"] = "--portfolios" + self.choices["plot"]["--pie"] = None + self.choices["plot"]["-pi"] = "--pie" + self.choices["plot"]["--hist"] = None + self.choices["plot"]["-hi"] = "--hist" + self.choices["plot"]["--drawdown"] = None + self.choices["plot"]["-dd"] = "--drawdown" + self.choices["plot"]["--rc-chart"] = None + self.choices["plot"]["-rc"] = "--rc-chart" + self.choices["plot"]["--heat"] = None + self.choices["plot"]["-he"] = "--heat" + self.choices["plot"]["--risk-measure"] = { + c: {} for c in self.MEAN_RISK_CHOICES + } + self.choices["plot"]["-rm"] = "--risk-measure" + self.choices["plot"]["--method"] = {c: {} for c in self.METHOD_CHOICES} + self.choices["plot"]["-mt"] = "--method" + self.choices["plot"]["--categories"] = None + self.choices["plot"]["-ct"] = "--categories" + self.choices["plot"]["--period"] = {c: {} for c in self.PERIOD_CHOICES} + self.choices["plot"]["-p"] = "--period" + self.choices["plot"]["--start"] = None + self.choices["plot"]["-s"] = "--start" + self.choices["plot"]["--end"] = None + self.choices["plot"]["-e"] = "--end" + self.choices["plot"]["--log-returns"] = None + self.choices["plot"]["-lr"] = "--log-returns" + self.choices["plot"]["--freq"] = {c: {} for c in ["d", "w", "m"]} + self.choices["plot"]["--maxnan"] = None + self.choices["plot"]["-mn"] = "--maxnan" + self.choices["plot"]["--threshold"] = None + self.choices["plot"]["-th"] = "--threshold" + self.choices["plot"]["--risk-free-rate"] = None + self.choices["plot"]["-r"] = "--risk-free-rate" + self.choices["plot"]["--alpha"] = None + self.choices["plot"]["-a"] = "--alpha" + self.choices["plot"]["--value"] = None + self.choices["plot"]["-v"] = "--value" + self.choices["rpf"]["--portfolios"] = None + self.choices["rpf"]["--pf"] = "--portfolios" for fn in models: self.choices[fn]["-p"] = {c: {} for c in self.PERIOD_CHOICES} self.choices[fn]["--period"] = {c: {} for c in self.PERIOD_CHOICES} @@ -469,7 +510,6 @@ 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["plot"] = {c: {} for c in list(self.portfolios.keys())} self.choices = {**self.choices, **self.SUPPORT_CHOICES} self.completer = NestedCompleter.from_nested_dict(self.choices) @@ -1315,6 +1355,7 @@ def call_maxsharpe(self, other_args: List[str]): default=self.params["target_return"] if "target_return" in self.params else -1, + type=int, help="Constraint on minimum level of portfolio's return", ) parser.add_argument( @@ -1412,7 +1453,7 @@ def call_maxsharpe(self, other_args: List[str]): if "historic_period_sa" in vars(ns_parser): table = False - console.print("Optimization can take time. Please be patient...") + console.print("Optimization can take time. Please be patient...\n") weights = optimizer_view.display_max_sharpe( symbols=self.tickers, @@ -1595,7 +1636,7 @@ def call_minrisk(self, other_args: List[str]): if "historic_period_sa" in vars(ns_parser): table = False - console.print("Optimization can take time. Please be patient...") + console.print("Optimization can take time. Please be patient...\n") weights = optimizer_view.display_min_risk( symbols=self.tickers, @@ -1790,7 +1831,7 @@ def call_maxutil(self, other_args: List[str]): if "historic_period_sa" in vars(ns_parser): table = False - console.print("Optimization can take time. Please be patient...") + console.print("Optimization can take time. Please be patient...\n") weights = optimizer_view.display_max_util( symbols=self.tickers, @@ -1977,7 +2018,7 @@ def call_maxret(self, other_args: List[str]): if "historic_period_sa" in vars(ns_parser): table = False - console.print("Optimization can take time. Please be patient...") + console.print("Optimization can take time. Please be patient...\n") weights = optimizer_view.display_max_ret( symbols=self.tickers, @@ -2133,7 +2174,7 @@ def call_maxdiv(self, other_args: List[str]): if "historic_period_sa" in vars(ns_parser): table = False - console.print("Optimization can take time. Please be patient...") + console.print("Optimization can take time. Please be patient...\n") weights = optimizer_view.display_max_div( symbols=self.tickers, @@ -2277,7 +2318,7 @@ def call_maxdecorr(self, other_args: List[str]): if "historic_period_sa" in vars(ns_parser): table = False - console.print("Optimization can take time. Please be patient...") + console.print("Optimization can take time. Please be patient...\n") weights = optimizer_view.display_max_decorr( symbols=self.tickers, @@ -2499,7 +2540,7 @@ def call_blacklitterman(self, other_args: List[str]): if "historic_period_sa" in vars(ns_parser): table = False - console.print("Optimization can take time. Please be patient...") + console.print("Optimization can take time. Please be patient...\n") weights = optimizer_view.display_black_litterman( symbols=self.tickers, @@ -2779,7 +2820,7 @@ def call_riskparity(self, other_args: List[str]): if "historic_period_sa" in vars(ns_parser): table = False - console.print("Optimization can take time. Please be patient...") + console.print("Optimization can take time. Please be patient...\n") weights = optimizer_view.display_risk_parity( symbols=self.tickers, @@ -2937,7 +2978,7 @@ def call_relriskparity(self, other_args: List[str]): if "historic_period_sa" in vars(ns_parser): table = False - console.print("Optimization can take time. Please be patient...") + console.print("Optimization can take time. Please be patient...\n") weights = optimizer_view.display_rel_risk_parity( symbols=self.tickers, @@ -3220,7 +3261,7 @@ def call_hrp(self, other_args: List[str]): if "historic_period_sa" in vars(ns_parser): table = False - console.print("Optimization can take time. Please be patient...") + console.print("Optimization can take time. Please be patient...\n") weights = optimizer_view.display_hrp( symbols=self.tickers, @@ -3521,7 +3562,7 @@ def call_herc(self, other_args: List[str]): if "historic_period_sa" in vars(ns_parser): table = False - console.print("Optimization can take time. Please be patient...") + console.print("Optimization can take time. Please be patient...\n") weights = optimizer_view.display_herc( symbols=self.tickers, @@ -3776,7 +3817,7 @@ def call_nco(self, other_args: List[str]): if "historic_period_sa" in vars(ns_parser): table = False - console.print("Optimization can take time. Please be patient...") + console.print("Optimization can take time. Please be patient...\n") weights = optimizer_view.display_nco( symbols=self.tickers, diff --git a/tests/openbb_terminal/portfolio/portfolio_optimization/txt/test_po_controller/test_print_help.txt b/tests/openbb_terminal/portfolio/portfolio_optimization/txt/test_po_controller/test_print_help.txt index 28489ea68225..6c44e2d568eb 100644 --- a/tests/openbb_terminal/portfolio/portfolio_optimization/txt/test_po_controller/test_print_help.txt +++ b/tests/openbb_terminal/portfolio/portfolio_optimization/txt/test_po_controller/test_print_help.txt @@ -5,7 +5,7 @@ Portfolio loaded: Tickers: Categories: - file select portfolio parameter file + file portfolio/po/file > params specify and show portfolio risk parameters Parameter file: