""" Portfolio Optimization Controller Module """
__docformat__ = "numpy"

# pylint: disable=too-many-lines,too-many-instance-attributes

import argparse
import logging
from typing import List, Dict, Tuple

from openbb_terminal.custom_prompt_toolkit import NestedCompleter

from openbb_terminal import feature_flags as obbff
from openbb_terminal import parent_classes
from openbb_terminal.core.config.paths import (
    MISCELLANEOUS_DIRECTORY,
    USER_EXPORTS_DIRECTORY,
    USER_PORTFOLIO_DATA_DIRECTORY,
)
from openbb_terminal.decorators import log_start_end
from openbb_terminal.helper_funcs import (
    check_non_negative,
    get_rf,
)
from openbb_terminal.menu import session
from openbb_terminal.parent_classes import BaseController
from openbb_terminal.portfolio.portfolio_optimization import excel_model
from openbb_terminal.portfolio.portfolio_optimization import (
    optimizer_helper,
    optimizer_model,
    optimizer_view,
    statics,
)
from openbb_terminal.portfolio.portfolio_optimization import yahoo_finance_model
from openbb_terminal.portfolio.portfolio_optimization.parameters import (
    params_controller,
    params_view,
)
from openbb_terminal.rich_config import console, MenuText

logger = logging.getLogger(__name__)


def add_arguments(parser_update, parser, not_in_list):
    parser_dict = vars(parser)
    for i in parser_dict["_actions"]:
        data_dict = vars(i)
        variables = list(data_dict.keys())
        if variables[0] == "option_strings" and data_dict["dest"] not in not_in_list:
            args = [data_dict["option_strings"][0] + "-sa"]
            if len(data_dict["option_strings"]) == 2:
                args.append(data_dict["option_strings"][1] + "-sa")
            if len(data_dict["option_strings"]) in [1, 2]:
                parser_update.add_argument(
                    *args,
                    type=data_dict["type"],
                    default=data_dict["default"],
                    dest=data_dict["dest"] + "_sa",
                    choices=data_dict["choices"],
                    help=data_dict["help"],
                )


def check_input(
    input_type: str, input_list: List[str], available_list: List[str]
) -> List[str]:
    """Check if input is valid

    Parameters
    ----------
    input_type : str
        Type of input
    input_list : List[str]
        List of input
    available_list : List[str]
        List of available input

    Returns
    -------
    List[str]
        Valid categories
    """

    valid: List[str] = []

    for i in input_list:
        if i in available_list:
            valid.append(i)
        else:
            console.print(f"[red]{input_type} '{i}' not available.[/red]\n")
    return valid


def get_valid_portfolio_categories(
    input_portfolios: List[str],
    available_portfolios: Dict,
    input_categories: List[str],
    available_categories: Dict,
) -> Tuple[List[str], List[str]]:
    """Get valid portfolios and categories

    Parameters
    ----------
    input_portfolios : List[str]
        List of input portfolios
    available_portfolios : Dict
        Dict of available portfolios
    input_categories : List[str]
        List of input categories
    available_categories : Dict
        Dict of available categories

    Returns
    -------
    Tuple[List[str], List[str]]
        Valid portfolios and categories
    """
    portfolios_list = list(set(available_portfolios.keys()))
    categories_list = list(set(available_categories.keys()))

    if not portfolios_list:
        portfolio_msg = "None. Perform some optimization to build a portfolio."
    else:
        if not input_portfolios:
            console.print("[yellow]Please select at least one portfolio.[/yellow]\n")
        portfolio_msg = ", ".join(portfolios_list)

    if not categories_list:
        categories_msg = "None. Attribute some categories in the loaded file."
    else:
        categories_msg = ", ".join(categories_list)

    console.print(
        f"[yellow]Current Portfolios: [/yellow]{portfolio_msg}\n",
    )

    console.print(
        f"[yellow]Current Categories: [/yellow]{categories_msg}\n",
    )

    valid_portfolios = check_input(
        input_type="Portfolio",
        input_list=input_portfolios,
        available_list=portfolios_list,
    )

    valid_categories = check_input(
        input_type="Category",
        input_list=input_categories,
        available_list=categories_list,
    )

    return valid_portfolios, valid_categories


class PortfolioOptimizationController(BaseController):
    """Portfolio Optimization Controller class"""

    DEFAULT_PORTFOLIO_DIRECTORY = MISCELLANEOUS_DIRECTORY / "portfolio_examples"
    DEFAULT_ALLOCATION_DIRECTORY = DEFAULT_PORTFOLIO_DIRECTORY / "allocation"
    DEFAULT_OPTIMIZATION_DIRECTORY = DEFAULT_PORTFOLIO_DIRECTORY / "optimization"
    FILE_TYPE_LIST = ["xlsx", "ini"]

    CHOICES_COMMANDS = [
        "show",
        "rpf",
        "load",
        "plot",
        "equal",
        "mktcap",
        "dividend",
        "property",
        "maxsharpe",
        "minrisk",
        "maxutil",
        "maxret",
        "maxdiv",
        "maxdecorr",
        "blacklitterman",
        "riskparity",
        "relriskparity",
        "hrp",
        "herc",
        "nco",
        "ef",
        "file",
    ]
    CHOICES_MENUS = ["params"]

    PATH = "/portfolio/po/"
    CHOICES_GENERATION = True

    files_available: List = list()

    @classmethod
    def build_allocation_file_map(cls) -> dict:
        allocation_file_map = {
            filepath.name: filepath
            for file_type in cls.FILE_TYPE_LIST
            for filepath in cls.DEFAULT_ALLOCATION_DIRECTORY.rglob(f"*.{file_type}")
        }
        allocation_file_map.update(
            {
                filepath.name: filepath
                for file_type in cls.FILE_TYPE_LIST
                for filepath in (USER_PORTFOLIO_DATA_DIRECTORY / "allocation").rglob(
                    f"*.{file_type}"
                )
            }
        )

        return allocation_file_map

    @classmethod
    def build_optimization_file_map(cls) -> dict:
        optimization_file_map = {
            filepath.name: filepath
            for file_type in cls.FILE_TYPE_LIST
            for filepath in cls.DEFAULT_OPTIMIZATION_DIRECTORY.rglob(f"*.{file_type}")
        }

        optimization_file_map.update(
            {
                filepath.name: filepath
                for file_type in cls.FILE_TYPE_LIST
                for filepath in (USER_PORTFOLIO_DATA_DIRECTORY / "optimization").rglob(
                    f"*.{file_type}"
                )
            }
        )

        return optimization_file_map

    def __init__(
        self,
        tickers: List[str] = None,
        portfolios: Dict = None,
        categories: Dict = None,
        queue: List[str] = None,
    ):
        """Constructor"""
        super().__init__(queue)
        self.current_model = None

        if tickers:
            self.tickers = list(set(tickers))
            self.tickers.sort()
        else:
            self.tickers = list()

        if portfolios:
            self.portfolios = dict(portfolios)
        else:
            self.portfolios = dict()

        if categories:
            self.categories = dict(categories)
            self.available_categories = list(self.categories.keys())
        else:
            self.categories = dict()
            self.available_categories = list()

        self.count = 0
        self.current_portfolio = ""

        self.allocation_file_map = self.build_allocation_file_map()
        self.optimization_file_map = self.build_optimization_file_map()

        self.current_file = ""

        self.params: Dict = {}

        if session and obbff.USE_PROMPT_TOOLKIT:
            choices: dict = self.choices_default
            self.choices = choices
            self.completer = NestedCompleter.from_nested_dict(choices)

    def update_runtime_choices(self):
        if session and obbff.USE_PROMPT_TOOLKIT:
            if self.portfolios:
                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):
        """Print help"""
        mt = MenuText("portfolio/po/")
        mt.add_cmd("load")
        mt.add_raw("\n")
        mt.add_param("_loaded", self.current_portfolio or "")
        mt.add_raw("\n")
        mt.add_param("_tickers", ", ".join(self.tickers))
        mt.add_param("_categories", ", ".join(self.available_categories))
        mt.add_raw("\n")
        mt.add_cmd("file")
        mt.add_menu("params")
        mt.add_raw("\n")
        mt.add_param("_parameter", self.current_file)
        mt.add_raw("\n")
        mt.add_info("_mean_risk_optimization_")
        mt.add_cmd("maxsharpe", self.tickers)
        mt.add_cmd("minrisk", self.tickers)
        mt.add_cmd("maxutil", self.tickers)
        mt.add_cmd("maxret", self.tickers)
        mt.add_cmd("maxdiv", self.tickers)
        mt.add_cmd("maxdecorr", self.tickers)
        mt.add_cmd("blacklitterman", self.tickers)
        mt.add_cmd("ef", self.tickers)
        mt.add_raw("\n")

        mt.add_info("_risk_parity_optimization_")
        mt.add_cmd("riskparity", self.tickers)
        mt.add_cmd("relriskparity", self.tickers)
        mt.add_raw("\n")

        mt.add_info("_hierarchical_clustering_models_")
        mt.add_cmd("hrp", self.tickers)
        mt.add_cmd("herc", self.tickers)
        mt.add_cmd("nco", self.tickers)
        mt.add_raw("\n")

        mt.add_info("_other_optimization_techniques_")
        mt.add_cmd("equal", self.tickers)
        mt.add_cmd("mktcap", self.tickers)
        mt.add_cmd("dividend", self.tickers)
        mt.add_cmd("property", self.tickers)

        mt.add_raw("\n")
        mt.add_param("_optimized_portfolio", ", ".join(self.portfolios.keys()))
        mt.add_raw("\n")

        mt.add_cmd("rpf", bool(self.portfolios.keys()))
        mt.add_cmd("show", bool(self.portfolios.keys()))
        mt.add_cmd("plot", bool(self.portfolios.keys()))

        console.print(text=mt.menu_text, menu="Portfolio - Portfolio Optimization")

    # pylint: disable=too-many-arguments
    def po_parser(
        self,
        parser,
        rm: bool = False,
        mt: bool = False,
        ct: bool = False,
        p: bool = False,
        s: bool = False,
        e: bool = False,
        lr: bool = False,
        freq: bool = False,
        mn: bool = False,
        th: bool = False,
        r: bool = False,
        a: bool = False,
        v: bool = True,
        name: str = "",
    ):
        """Holds common parser arguments to eliminate repetition"""
        if rm:
            parser.add_argument(
                "-rm",
                "--risk-measure",
                default=self.params["risk_measure"]
                if "risk_measure" in self.params
                else "MV",
                dest="risk_measure",
                help="""Risk measure used to optimize the portfolio. Possible values are:
                        'MV' : Variance
                        'MAD' : Mean Absolute Deviation
                        'MSV' : Semi Variance (Variance of negative returns)
                        'FLPM' : First Lower Partial Moment
                        'SLPM' : Second Lower Partial Moment
                        'CVaR' : Conditional Value at Risk
                        'EVaR' : Entropic Value at Risk
                        'WR' : Worst Realization
                        'ADD' : Average Drawdown of uncompounded returns
                        'UCI' : Ulcer Index of uncompounded returns
                        'CDaR' : Conditional Drawdown at Risk of uncompounded returns
                        'EDaR' : Entropic Drawdown at Risk of uncompounded returns
                        'MDD' : Maximum Drawdown of uncompounded returns
                        """,
                choices=statics.MEAN_RISK_CHOICES,
            )
        if mt:
            parser.add_argument(
                "-mt",
                "--method",
                default=self.params["nan_fill_method"]
                if "nan_fill_method" in self.params
                else "time",
                dest="nan_fill_method",
                help="""Method used to fill nan values in time series, by default time.
                        Possible values are:
                        'linear': linear interpolation
                        'time': linear interpolation based on time index
                        'nearest': use nearest value to replace nan values
                        'zero': spline of zeroth order
                        'slinear': spline of first order
                        'quadratic': spline of second order
                        'cubic': spline of third order
                        'barycentric': builds a polynomial that pass for all points""",
                choices=statics.METHOD_CHOICES,
                metavar="METHOD",
            )
        if ct:
            parser.add_argument(
                "-ct",
                "--categories",
                dest="categories",
                type=lambda s: [str(item).upper() for item in s.split(",")],
                default=self.available_categories,
                help="Show selected categories",
            )
        if p:
            parser.add_argument(
                "-p",
                "--period",
                default=self.params["historic_period"]
                if "historic_period" in self.params
                else "3y",
                dest="historic_period",
                help="""Period to get yfinance data from.
                        Possible frequency strings are:
                        'd': means days, for example '252d' means 252 days
                        'w': means weeks, for example '52w' means 52 weeks
                        'mo': means months, for example '12mo' means 12 months
                        'y': means years, for example '1y' means 1 year
                        'ytd': downloads data from beginning of year to today
                        'max': downloads all data available for each asset""",
                choices=statics.PERIOD_CHOICES,
                metavar="PERIOD",
            )
        if s:
            parser.add_argument(
                "-s",
                "--start",
                default=self.params["start_period"]
                if "start_period" in self.params
                else "",
                dest="start_period",
                help="""Start date to get yfinance data from. Must be in
                        'YYYY-MM-DD' format""",
            )
        if e:
            parser.add_argument(
                "-e",
                "--end",
                default=self.params["end_period"]
                if "end_period" in self.params
                else "",
                dest="end_period",
                help="""End date to get yfinance data from. Must be in
                        'YYYY-MM-DD' format""",
            )
        if lr:
            parser.add_argument(
                "-lr",
                "--log-returns",
                action="store_true",
                default=self.params["log_returns"]
                if "log_returns" in self.params
                else False,
                dest="log_returns",
                help="If use logarithmic or arithmetic returns to calculate returns",
            )
        if freq:
            parser.add_argument(
                "--freq",
                default=self.params["return_frequency"]
                if "return_frequency" in self.params
                else "d",
                dest="return_frequency",
                help="""Frequency used to calculate returns. Possible values are:
                        'd': for daily returns
                        'w': for weekly returns
                        'm': for monthly returns
                        """,
                choices=statics.FREQ_CHOICES,
            )
        if mn:
            parser.add_argument(
                "-mn",
                "--maxnan",
                type=float,
                default=self.params["max_nan"] if "max_nan" in self.params else 0.05,
                dest="max_nan",
                help="""Max percentage of nan values accepted per asset to be
                    considered in the optimization process""",
            )
        if th:
            parser.add_argument(
                "-th",
                "--threshold",
                type=float,
                default=self.params["threshold_value"]
                if "threshold_value" in self.params
                else 0.30,
                dest="threshold_value",
                help="""Value used to replace outliers that are higher to threshold
                        in absolute value""",
            )
        if r:
            parser.add_argument(
                "-r",
                "--risk-free-rate",
                type=float,
                dest="risk_free",
                default=self.params["risk_free"]
                if "risk_free" in self.params
                else get_rf(),
                help="""Risk-free rate of borrowing/lending. The period of the
                    risk-free rate must be annual""",
            )
        if a:
            parser.add_argument(
                "-a",
                "--alpha",
                type=float,
                default=self.params["significance_level"]
                if "significance_level" in self.params
                else 0.05,
                dest="significance_level",
                help="Significance level of CVaR, EVaR, CDaR and EDaR",
            )
        if v:
            parser.add_argument(
                "-v",
                "--value",
                default=self.params["long_allocation"]
                if "long_allocation" in self.params
                else 1,
                type=float,
                dest="long_allocation",
                help="Amount to allocate to portfolio",
            )
        if name:
            parser.add_argument(
                "--name",
                type=str,
                dest="name",
                default=name + str(self.count),
                help="Save portfolio with personalized or default name",
            )
        return parser

    def custom_reset(self):
        """Class specific component of reset command"""
        objects_to_reload = ["portfolio", "po"]
        if self.current_portfolio:
            objects_to_reload.append(f"load {self.current_portfolio}")
        if self.current_file:
            objects_to_reload.append(f"file {self.current_file}")
        return objects_to_reload

    @log_start_end(log=logger)
    def call_file(self, other_args: List[str]):
        """Process file command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="file",
            description="Select parameter file to use",
        )

        parser.add_argument(
            "-f",
            "--file",
            required="-h" not in other_args,
            nargs="+",
            dest="file",
            help="Parameter file to be used",
            choices=self.optimization_file_map.keys(),
            metavar="FILE",
        )
        if other_args and "-" not in other_args[0][0]:
            other_args.insert(0, "--file")

        ns_parser = self.parse_known_args_and_warn(parser, other_args)

        if ns_parser:
            self.current_file = " ".join(ns_parser.file)

            if self.current_file in self.optimization_file_map:
                file_location = self.optimization_file_map[self.current_file]
            else:
                file_location = self.current_file  # type: ignore

            self.params, self.current_model = params_view.load_file(file_location)

    @log_start_end(log=logger)
    def call_params(self, _):
        """Process params command"""
        self.queue = self.load_class(
            params_controller.ParametersController,
            self.current_file,
            self.queue,
            self.params,
            self.current_model,
        )
        self.current_file = parent_classes.controllers[
            "/portfolio/po/params/"
        ].current_file
        self.current_model = parent_classes.controllers[
            "/portfolio/po/params/"
        ].current_model
        self.params = parent_classes.controllers["/portfolio/po/params/"].params

    @log_start_end(log=logger)
    def call_show(self, other_args: List[str]):
        """Show saved portfolios"""
        parser = argparse.ArgumentParser(
            add_help=False,
            prog="show",
            description="""Show selected saved portfolios""",
        )
        parser.add_argument(
            "-pf",
            "--portfolios",
            dest="portfolios",
            type=lambda s: [str(item).upper() for item in s.split(",")],
            default=[],
            help="Show selected saved portfolios",
        )
        if other_args:
            if "-" not in other_args[0]:
                other_args.insert(0, "-pf")

        parser = self.po_parser(parser, ct=True)
        ns_parser = self.parse_known_args_and_warn(parser, other_args)

        if ns_parser:
            portfolios, categories = get_valid_portfolio_categories(
                input_portfolios=ns_parser.portfolios,
                available_portfolios=self.portfolios,
                input_categories=ns_parser.categories,
                available_categories=self.categories,
            )

            for p in portfolios:
                console.print("[yellow]Portfolio[/yellow]: " + p + "\n")
                optimizer_view.display_show(
                    weights=self.portfolios[p],
                    tables=categories,
                    categories_dict=self.categories,
                )

    @log_start_end(log=logger)
    def call_rpf(self, other_args: List[str]):
        """Remove one portfolio"""
        parser = argparse.ArgumentParser(
            add_help=False,
            prog="rpf",
            description="""Remove one of the portfolios""",
        )
        parser.add_argument(
            "-pf",
            "--portfolios",
            dest="portfolios",
            type=lambda s: [str(item).upper() for item in s.split(",")],
            default=[],
            help="portfolios to be removed from the saved portfolios",
        )
        if other_args:
            if "-" not in other_args[0]:
                other_args.insert(0, "-pf")

        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            portfolios = set(self.portfolios.keys())
            for portfolio in ns_parser.portfolios:
                if portfolio in portfolios:
                    self.portfolios.pop(portfolio)
                    portfolios.remove(portfolio)
                    console.print(f"[yellow]Removed '{portfolio}'.[/yellow]")
                else:
                    console.print(f"[red]Portfolio '{portfolio}' does not exist.[/red]")

            if self.portfolios:
                console.print(
                    f"\n[yellow]Current Portfolios: [/yellow]{('None', ', '.join(portfolios))[bool(portfolios)]}"
                )

            self.update_runtime_choices()

    @log_start_end(log=logger)
    def call_load(self, other_args: List[str]):
        """Load file with stocks tickers and categories"""
        parser = argparse.ArgumentParser(
            add_help=False,
            prog="load",
            description="""Load file of stocks tickers with optional categories""",
        )
        parser.add_argument(
            "-f",
            "--file",
            required="-h" not in other_args,
            nargs="+",
            dest="file",
            help="Allocation file to be used",
            choices=self.allocation_file_map.keys(),
        )
        if other_args and "-" not in other_args[0][0]:
            other_args.insert(0, "--file")
        ns_parser = self.parse_known_args_and_warn(parser, other_args)

        if ns_parser:
            filename = " ".join(ns_parser.file)

            if filename in self.allocation_file_map:
                file_location = self.allocation_file_map[filename]
            else:
                file_location = filename  # type: ignore

            self.tickers, self.categories = excel_model.load_allocation(file_location)
            self.available_categories = list(self.categories.keys())
            if "CURRENT_INVESTED_AMOUNT" in self.available_categories:
                self.available_categories.remove("CURRENT_INVESTED_AMOUNT")
            self.portfolios = dict()
            self.update_runtime_choices()
            self.current_portfolio = filename

    @log_start_end(log=logger)
    def call_plot(self, other_args: List[str]):
        """Process plot command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="plot",
            description="Plot selected charts for portfolios",
        )
        parser.add_argument(
            "-pf",
            "--portfolios",
            type=lambda s: [str(item).upper() for item in s.split(",")],
            default=[],
            dest="portfolios",
            help="Selected portfolios that will be plotted",
        )
        parser.add_argument(
            "-pi",
            "--pie",
            action="store_true",
            dest="pie",
            default=False,
            help="Display a pie chart for weights",
        )
        parser.add_argument(
            "-hi",
            "--hist",
            action="store_true",
            dest="hist",
            default=False,
            help="Display a histogram with risk measures",
        )
        parser.add_argument(
            "-dd",
            "--drawdown",
            action="store_true",
            dest="dd",
            default=False,
            help="Display a drawdown chart with risk measures",
        )
        parser.add_argument(
            "-rc",
            "--rc-chart",
            action="store_true",
            dest="rc_chart",
            default=False,
            help="Display a risk contribution chart for assets",
        )
        parser.add_argument(
            "-he",
            "--heat",
            action="store_true",
            dest="heat",
            default=False,
            help="Display a heatmap of correlation matrix with dendrogram",
        )
        parser = self.po_parser(
            parser,
            rm=True,
            mt=True,
            ct=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
        )
        if other_args:
            if "-" not in other_args[0]:
                other_args.insert(0, "-pf")

        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            portfolios, categories = get_valid_portfolio_categories(
                input_portfolios=ns_parser.portfolios,
                available_portfolios=self.portfolios,
                input_categories=ns_parser.categories,
                available_categories=self.categories,
            )
            if not portfolios:
                return

            if not (
                ns_parser.pie
                or ns_parser.hist
                or ns_parser.dd
                or ns_parser.rc_chart
                or ns_parser.heat
            ):
                console.print(
                    "[yellow]Please select at least one chart to plot[/yellow]",
                    "[yellow]from the following: -pi, -hi, -dd, -rc, -he.[/yellow]",
                )
                return

            stocks = []
            for i in portfolios:
                stocks += list(self.portfolios[i].keys())
            stocks = list(set(stocks))
            stocks.sort()

            _, stock_returns = optimizer_model.get_equal_weights(
                symbols=stocks,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                value=1,
            )

            if ns_parser.hist or ns_parser.dd:
                for i in portfolios:
                    weights = self.portfolios[i]
                    weights = dict(
                        sorted(weights.items(), key=lambda x: x[1], reverse=True)
                    )
                    stocks = list(weights.keys())

                    # hist and dd are transversal to all categories
                    optimizer_view.additional_plots(
                        weights=weights,
                        data=stock_returns[stocks],
                        portfolio_name=i,
                        freq=ns_parser.return_frequency,
                        risk_measure=ns_parser.risk_measure.lower(),
                        risk_free_rate=ns_parser.risk_free,
                        alpha=ns_parser.significance_level,
                        a_sim=100,
                        beta=ns_parser.significance_level,
                        b_sim=100,
                        hist=ns_parser.hist,
                        dd=ns_parser.dd,
                    )

            if ns_parser.pie or ns_parser.rc_chart or ns_parser.heat:

                if not categories:
                    console.print(
                        "[yellow]Categories must be provided to use -pi, -rc or -he.[/yellow]"
                    )
                    return

                for i in portfolios:
                    weights = self.portfolios[i]
                    weights = dict(
                        sorted(weights.items(), key=lambda x: x[1], reverse=True)
                    )
                    stocks = list(weights.keys())

                    # pie, rc_chart and heat apply to each category
                    for category in categories:
                        filtered_categories = dict(
                            filter(
                                lambda elem: elem[0] in stocks,
                                self.categories[category].items(),
                            )
                        )
                        optimizer_view.additional_plots(
                            weights=weights,
                            data=stock_returns[stocks],
                            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,
                            alpha=ns_parser.significance_level,
                            a_sim=100,
                            beta=ns_parser.significance_level,
                            b_sim=100,
                            pie=ns_parser.pie,
                            rc_chart=ns_parser.rc_chart,
                            heat=ns_parser.heat,
                        )

    @log_start_end(log=logger)
    def call_equal(self, other_args: List[str]):
        """Process equal command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="equal",
            description="Returns an equally weighted portfolio",
        )
        parser = self.po_parser(
            parser,
            rm=True,
            mt=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
            v=True,
            name="NAME_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            weights = optimizer_view.display_equal_weight(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                risk_measure=ns_parser.risk_measure.lower(),
                risk_free_rate=ns_parser.risk_free,
                alpha=ns_parser.significance_level,
                value=ns_parser.long_allocation,
                table=True,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

    @log_start_end(log=logger)
    def call_mktcap(self, other_args: List[str]):
        """Process mktcap command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="mktcap",
            description="Returns a portfolio that is weighted based on Market Cap.",
        )
        parser = self.po_parser(
            parser,
            rm=True,
            mt=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
            v=True,
            name="MKTCAP_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 stocks selected to perform calculations."
                )
                return

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_property_weighting(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                s_property="marketCap",
                risk_measure=ns_parser.risk_measure.lower(),
                risk_free_rate=ns_parser.risk_free,
                alpha=ns_parser.significance_level,
                value=ns_parser.long_allocation,
                table=True,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

    @log_start_end(log=logger)
    def call_dividend(self, other_args: List[str]):
        """Process dividend command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="dividend",
            description="Returns a portfolio that is weighted based dividend yield.",
        )
        parser = self.po_parser(
            parser,
            rm=True,
            mt=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
            v=True,
            name="DIVIDEND_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 stocks selected to perform calculations."
                )
                return

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_property_weighting(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                s_property="dividendYield",
                risk_measure=ns_parser.risk_measure.lower(),
                risk_free_rate=ns_parser.risk_free,
                alpha=ns_parser.significance_level,
                value=ns_parser.long_allocation,
                table=True,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

    @log_start_end(log=logger)
    def call_property(self, other_args: List[str]):
        """Process property command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="property",
            description="Returns a portfolio that is weighted based on selected property.",
        )
        parser.add_argument(
            "-pr",
            "--property",
            required=bool("-h" not in other_args),
            type=optimizer_helper.check_valid_property_type,
            dest="s_property",
            choices=yahoo_finance_model.yf_info_choices,
            help="""Property info to weight. Use one of yfinance info options.""",
            metavar="PROPERTY",
        )
        parser = self.po_parser(
            parser,
            rm=True,
            mt=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
            v=True,
            name="PROPERTY_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 stocks selected to perform calculations."
                )
                return

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_property_weighting(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                s_property=ns_parser.s_property,
                risk_measure=ns_parser.risk_measure.lower(),
                risk_free_rate=ns_parser.risk_free,
                alpha=ns_parser.significance_level,
                value=ns_parser.long_allocation,
                table=True,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

    @log_start_end(log=logger)
    def call_maxsharpe(self, other_args: List[str]):
        """Process maxsharpe command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="maxsharpe",
            description="Maximizes the portfolio's return/risk ratio",
        )
        parser.add_argument(
            "-tr",
            "--target-return",
            dest="target_return",
            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(
            "-tk",
            "--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(
            "-m",
            "--mean",
            default=self.params["expected_return"]
            if "expected_return" in self.params
            else "hist",
            dest="expected_return",
            help="Method used to estimate the expected return vector",
            choices=statics.MEAN_CHOICES,
        )
        parser.add_argument(
            "-cv",
            "--covariance",
            default=self.params["covariance"]
            if "covariance" in self.params
            else "hist",
            dest="covariance",
            help="""Method used to estimate covariance matrix. Possible values are
                    'hist': historical method
                    'ewma1': exponential weighted moving average with adjust=True
                    'ewma2': exponential weighted moving average with adjust=False
                    'ledoit': Ledoit and Wolf shrinkage method
                    'oas': oracle shrinkage method
                    'shrunk': scikit-learn shrunk method
                    'gl': graphical lasso method
                    'jlogo': j-logo covariance
                    'fixed': takes average of eigenvalues above max Marchenko Pastour limit
                    'spectral':  makes zero eigenvalues above max Marchenko Pastour limit
                    'shrink': Lopez de Prado's book shrinkage method
                    """,
            choices=statics.COVARIANCE_CHOICES,
        )
        parser.add_argument(
            "-de",
            "--d-ewma",
            type=float,
            default=self.params["smoothing_factor_ewma"]
            if "smoothing_factor_ewma" in self.params
            else 0.94,
            dest="smoothing_factor_ewma",
            help="Smoothing factor for ewma estimators",
        )
        parser.add_argument(
            "-vs",
            "--value-short",
            dest="short_allocation",
            help="Amount to allocate to portfolio in short positions",
            type=float,
            default=self.params["short_allocation"]
            if "short_allocation" in self.params
            else 0.0,
        )

        parser = self.po_parser(
            parser,
            rm=True,
            mt=True,
            ct=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
            v=True,
            name="MAXSHARPE_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            table = True
            if "historic_period_sa" in vars(ns_parser):
                table = False

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_max_sharpe(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                risk_measure=ns_parser.risk_measure.lower(),
                risk_free_rate=ns_parser.risk_free,
                alpha=ns_parser.significance_level,
                target_return=ns_parser.target_return,
                target_risk=ns_parser.target_risk,
                mean=ns_parser.expected_return.lower(),
                covariance=ns_parser.covariance.lower(),
                d_ewma=ns_parser.smoothing_factor_ewma,
                value=ns_parser.long_allocation,
                value_short=ns_parser.short_allocation,
                table=table,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

            if table is False:
                weights_sa = optimizer_view.display_max_sharpe(
                    symbols=self.tickers,
                    interval=ns_parser.historic_period_sa,
                    start_date=ns_parser.start_period_sa,
                    end_date=ns_parser.end_period_sa,
                    log_returns=ns_parser.log_returns_sa,
                    freq=ns_parser.return_frequency_sa,
                    maxnan=ns_parser.max_nan_sa,
                    threshold=ns_parser.threshold_value_sa,
                    method=ns_parser.nan_fill_method_sa,
                    risk_measure=ns_parser.risk_measure_sa.lower(),
                    risk_free_rate=ns_parser.risk_free_sa,
                    alpha=ns_parser.significance_level_sa,
                    target_return=ns_parser.target_return_sa,
                    target_risk=ns_parser.target_risk_sa,
                    mean=ns_parser.expected_return_sa.lower(),
                    covariance=ns_parser.covariance_sa.lower(),
                    d_ewma=ns_parser.smoothing_factor_ewma_sa,
                    value=ns_parser.long_allocation_sa,
                    value_short=ns_parser.short_allocation_sa,
                    table=table,
                )

                console.print("")
                optimizer_view.display_weights_sa(
                    weights=weights, weights_sa=weights_sa
                )

                if not ns_parser.categories:
                    categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
                else:
                    categories = ns_parser.categories

                for category in categories:
                    optimizer_view.display_categories_sa(
                        weights=weights,
                        weights_sa=weights_sa,
                        categories=self.categories,
                        column=category,
                        title="Category - " + category.title(),
                    )

    @log_start_end(log=logger)
    def call_minrisk(self, other_args: List[str]):
        """Process minrisk command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="minrisk",
            description="Minimizes portfolio's risk",
        )
        parser.add_argument(
            "-tr",
            "--target-return",
            dest="target_return",
            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(
            "-tk",
            "--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(
            "-m",
            "--mean",
            default=self.params["expected_return"]
            if "expected_return" in self.params
            else "hist",
            dest="expected_return",
            help="Method used to estimate expected returns vector",
            choices=statics.MEAN_CHOICES,
        )
        parser.add_argument(
            "-cv",
            "--covariance",
            default=self.params["covariance"] if "mean_ewma" in self.params else "hist",
            dest="covariance",
            help="""Method used to estimate covariance matrix. Possible values are
                    'hist': historical method
                    'ewma1': exponential weighted moving average with adjust=True
                    'ewma2': exponential weighted moving average with adjust=False
                    'ledoit': Ledoit and Wolf shrinkage method
                    'oas': oracle shrinkage method
                    'shrunk': scikit-learn shrunk method
                    'gl': graphical lasso method
                    'jlogo': j-logo covariance
                    'fixed': takes average of eigenvalues above max Marchenko Pastour limit
                    'spectral':  makes zero eigenvalues above max Marchenko Pastour limit
                    'shrink': Lopez de Prado's book shrinkage method
                    """,
            choices=statics.COVARIANCE_CHOICES,
        )
        parser.add_argument(
            "-de",
            "--d-ewma",
            type=float,
            default=self.params["smoothing_factor_ewma"]
            if "smoothing_factor_ewma" in self.params
            else 0.94,
            dest="smoothing_factor_ewma",
            help="Smoothing factor for ewma estimators",
        )
        parser.add_argument(
            "-vs",
            "--value-short",
            type=float,
            default=self.params["short_allocation"]
            if "short_allocation" in self.params
            else 0.0,
            dest="short_allocation",
            help="Amount to allocate to portfolio in short positions",
        )

        parser = self.po_parser(
            parser,
            rm=True,
            mt=True,
            ct=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
            v=True,
            name="MINRISK_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            table = True
            if "historic_period_sa" in vars(ns_parser):
                table = False

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_min_risk(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                risk_measure=ns_parser.risk_measure.lower(),
                risk_free_rate=ns_parser.risk_free,
                alpha=ns_parser.significance_level,
                target_return=ns_parser.target_return,
                target_risk=ns_parser.target_risk,
                mean=ns_parser.expected_return.lower(),
                covariance=ns_parser.covariance.lower(),
                d_ewma=ns_parser.smoothing_factor_ewma,
                value=ns_parser.long_allocation,
                value_short=ns_parser.short_allocation,
                table=table,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

            if table is False:
                weights_sa = optimizer_view.display_min_risk(
                    symbols=self.tickers,
                    interval=ns_parser.historic_period_sa,
                    start_date=ns_parser.start_period_sa,
                    end_date=ns_parser.end_period_sa,
                    log_returns=ns_parser.log_returns_sa,
                    freq=ns_parser.return_frequency_sa,
                    maxnan=ns_parser.max_nan_sa,
                    threshold=ns_parser.threshold_value_sa,
                    method=ns_parser.nan_fill_method_sa,
                    risk_measure=ns_parser.risk_measure_sa.lower(),
                    risk_free_rate=ns_parser.risk_free_sa,
                    alpha=ns_parser.significance_level_sa,
                    target_return=ns_parser.target_return_sa,
                    target_risk=ns_parser.target_risk_sa,
                    mean=ns_parser.expected_return_sa.lower(),
                    covariance=ns_parser.covariance_sa.lower(),
                    d_ewma=ns_parser.smoothing_factor_ewma_sa,
                    value=ns_parser.long_allocation_sa,
                    value_short=ns_parser.short_allocation_sa,
                    table=table,
                )

                console.print("")
                optimizer_view.display_weights_sa(
                    weights=weights, weights_sa=weights_sa
                )

                if not ns_parser.categories:
                    categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
                else:
                    categories = ns_parser.categories

                for category in categories:
                    optimizer_view.display_categories_sa(
                        weights=weights,
                        weights_sa=weights_sa,
                        categories=self.categories,
                        column=category,
                        title="Category - " + category.title(),
                    )

    @log_start_end(log=logger)
    def call_maxutil(self, other_args: List[str]):
        """Process maxutil command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="maxutil",
            description="Maximizes a risk averse utility function",
        )
        parser.add_argument(
            "-ra",
            "--risk-aversion",
            type=float,
            dest="risk_aversion",
            default=self.params["risk_aversion"]
            if "risk_aversion" in self.params
            else 1,
            help="Risk aversion parameter",
        )
        parser.add_argument(
            "-tr",
            "--target-return",
            dest="target_return",
            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(
            "-tk",
            "--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(
            "-m",
            "--mean",
            default=self.params["expected_return"]
            if "expected_return" in self.params
            else "hist",
            dest="expected_return",
            help="Method used to estimate the expected return vector",
            choices=statics.MEAN_CHOICES,
        )
        parser.add_argument(
            "-cv",
            "--covariance",
            default=self.params["covariance"]
            if "covariance" in self.params
            else "hist",
            dest="covariance",
            help="""Method used to estimate covariance matrix. Possible values are
                    'hist': historical method
                    'ewma1': exponential weighted moving average with adjust=True
                    'ewma2': exponential weighted moving average with adjust=False
                    'ledoit': Ledoit and Wolf shrinkage method
                    'oas': oracle shrinkage method
                    'shrunk': scikit-learn shrunk method
                    'gl': graphical lasso method
                    'jlogo': j-logo covariance
                    'fixed': takes average of eigenvalues above max Marchenko Pastour limit
                    'spectral':  makes zero eigenvalues above max Marchenko Pastour limit
                    'shrink': Lopez de Prado's book shrinkage method
                    """,
            choices=statics.COVARIANCE_CHOICES,
        )
        parser.add_argument(
            "-de",
            "--d-ewma",
            type=float,
            default=self.params["smoothing_factor_ewma"]
            if "smoothing_factor_ewma" in self.params
            else 0.94,
            dest="smoothing_factor_ewma",
            help="Smoothing factor for ewma estimators",
        )
        parser.add_argument(
            "-vs",
            "--value-short",
            dest="short_allocation",
            help="Amount to allocate to portfolio in short positions",
            type=float,
            default=self.params["short_allocation"]
            if "short_allocation" in self.params
            else 0.0,
        )

        parser = self.po_parser(
            parser,
            rm=True,
            mt=True,
            ct=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
            v=True,
            name="MAXUTIL_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            table = True
            if "historic_period_sa" in vars(ns_parser):
                table = False

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_max_util(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                risk_measure=ns_parser.risk_measure.lower(),
                risk_free_rate=ns_parser.risk_free,
                risk_aversion=ns_parser.risk_aversion,
                alpha=ns_parser.significance_level,
                target_return=ns_parser.target_return,
                target_risk=ns_parser.target_risk,
                mean=ns_parser.expected_return.lower(),
                covariance=ns_parser.covariance.lower(),
                d_ewma=ns_parser.smoothing_factor_ewma,
                value=ns_parser.long_allocation,
                value_short=ns_parser.short_allocation,
                table=table,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

            if table is False:
                weights_sa = optimizer_view.display_max_util(
                    symbols=self.tickers,
                    interval=ns_parser.historic_period_sa,
                    start_date=ns_parser.start_period_sa,
                    end_date=ns_parser.end_period_sa,
                    log_returns=ns_parser.log_returns_sa,
                    freq=ns_parser.return_frequency_sa,
                    maxnan=ns_parser.max_nan_sa,
                    threshold=ns_parser.threshold_value_sa,
                    method=ns_parser.nan_fill_method_sa,
                    risk_measure=ns_parser.risk_measure_sa.lower(),
                    risk_free_rate=ns_parser.risk_free_sa,
                    risk_aversion=ns_parser.risk_aversion_sa,
                    alpha=ns_parser.significance_level_sa,
                    target_return=ns_parser.target_return_sa,
                    target_risk=ns_parser.target_risk_sa,
                    mean=ns_parser.expected_return_sa.lower(),
                    covariance=ns_parser.covariance_sa.lower(),
                    d_ewma=ns_parser.smoothing_factor_ewma_sa,
                    value=ns_parser.long_allocation_sa,
                    value_short=ns_parser.short_allocation_sa,
                    table=table,
                )

                console.print("")
                optimizer_view.display_weights_sa(
                    weights=weights, weights_sa=weights_sa
                )

                if not ns_parser.categories:
                    categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
                else:
                    categories = ns_parser.categories

                for category in categories:
                    optimizer_view.display_categories_sa(
                        weights=weights,
                        weights_sa=weights_sa,
                        categories=self.categories,
                        column=category,
                        title="Category - " + category.title(),
                    )

    @log_start_end(log=logger)
    def call_maxret(self, other_args: List[str]):
        """Process maxret command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="maxret",
            description="Maximizes the portfolio's return ",
        )
        parser.add_argument(
            "-tr",
            "--target-return",
            dest="target_return",
            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(
            "-tk",
            "--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(
            "-m",
            "--mean",
            default=self.params["expected_return"]
            if "expected_return" in self.params
            else "hist",
            dest="expected_return",
            help="Method used to estimate the expected return vector",
            choices=statics.MEAN_CHOICES,
        )
        parser.add_argument(
            "-cv",
            "--covariance",
            default=self.params["covariance"]
            if "covariance" in self.params
            else "hist",
            dest="covariance",
            help="""Method used to estimate covariance matrix. Possible values are
                    'hist': historical method
                    'ewma1': exponential weighted moving average with adjust=True
                    'ewma2': exponential weighted moving average with adjust=False
                    'ledoit': Ledoit and Wolf shrinkage method
                    'oas': oracle shrinkage method
                    'shrunk': scikit-learn shrunk method
                    'gl': graphical lasso method
                    'jlogo': j-logo covariance
                    'fixed': takes average of eigenvalues above max Marchenko Pastour limit
                    'spectral':  makes zero eigenvalues above max Marchenko Pastour limit
                    'shrink': Lopez de Prado's book shrinkage method
                    """,
            choices=statics.COVARIANCE_CHOICES,
        )
        parser.add_argument(
            "-de",
            "--d-ewma",
            type=float,
            default=self.params["smoothing_factor_ewma"]
            if "smoothing_factor_ewma" in self.params
            else 0.94,
            dest="smoothing_factor_ewma",
            help="Smoothing factor for ewma estimators",
        )
        parser.add_argument(
            "-vs",
            "--value-short",
            dest="short_allocation",
            help="Amount to allocate to portfolio in short positions",
            type=float,
            default=self.params["short_allocation"]
            if "short_allocation" in self.params
            else 0.0,
        )

        parser = self.po_parser(
            parser,
            rm=True,
            mt=True,
            ct=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
            v=True,
            name="MAXRET_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            table = True
            if "historic_period_sa" in vars(ns_parser):
                table = False

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_max_ret(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                risk_measure=ns_parser.risk_measure.lower(),
                risk_free_rate=ns_parser.risk_free,
                alpha=ns_parser.significance_level,
                target_return=ns_parser.target_return,
                target_risk=ns_parser.target_risk,
                mean=ns_parser.expected_return.lower(),
                covariance=ns_parser.covariance.lower(),
                d_ewma=ns_parser.smoothing_factor_ewma,
                value=ns_parser.long_allocation,
                value_short=ns_parser.short_allocation,
                table=table,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

            if table is False:
                weights_sa = optimizer_view.display_max_ret(
                    symbols=self.tickers,
                    interval=ns_parser.historic_period_sa,
                    start_date=ns_parser.start_period_sa,
                    end_date=ns_parser.end_period_sa,
                    log_returns=ns_parser.log_returns_sa,
                    freq=ns_parser.return_frequency_sa,
                    maxnan=ns_parser.max_nan_sa,
                    threshold=ns_parser.threshold_value_sa,
                    method=ns_parser.nan_fill_method_sa,
                    risk_measure=ns_parser.risk_measure_sa.lower(),
                    risk_free_rate=ns_parser.risk_free_sa,
                    alpha=ns_parser.significance_level_sa,
                    target_return=ns_parser.target_return_sa,
                    target_risk=ns_parser.target_risk_sa,
                    mean=ns_parser.expected_return_sa.lower(),
                    covariance=ns_parser.covariance_sa.lower(),
                    d_ewma=ns_parser.smoothing_factor_ewma_sa,
                    value=ns_parser.long_allocation_sa,
                    value_short=ns_parser.short_allocation_sa,
                    table=table,
                )

                console.print("")
                optimizer_view.display_weights_sa(
                    weights=weights, weights_sa=weights_sa
                )

                if not ns_parser.categories:
                    categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
                else:
                    categories = ns_parser.categories

                for category in categories:
                    optimizer_view.display_categories_sa(
                        weights=weights,
                        weights_sa=weights_sa,
                        categories=self.categories,
                        column=category,
                        title="Category - " + category.title(),
                    )

    @log_start_end(log=logger)
    def call_maxdiv(self, other_args: List[str]):
        """Process maxdiv command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="maxdiv",
            description="Maximizes the portfolio's diversification ratio",
        )
        parser.add_argument(
            "-cv",
            "--covariance",
            default=self.params["covariance"]
            if "covariance" in self.params
            else "hist",
            dest="covariance",
            help="""Method used to estimate covariance matrix. Possible values are
                    'hist': historical method
                    'ewma1': exponential weighted moving average with adjust=True
                    'ewma2': exponential weighted moving average with adjust=False
                    'ledoit': Ledoit and Wolf shrinkage method
                    'oas': oracle shrinkage method
                    'shrunk': scikit-learn shrunk method
                    'gl': graphical lasso method
                    'jlogo': j-logo covariance
                    'fixed': takes average of eigenvalues above max Marchenko Pastour limit
                    'spectral':  makes zero eigenvalues above max Marchenko Pastour limit
                    'shrink': Lopez de Prado's book shrinkage method
                    """,
            choices=statics.COVARIANCE_CHOICES,
        )
        parser.add_argument(
            "-de",
            "--d-ewma",
            type=float,
            default=self.params["smoothing_factor_ewma"]
            if "smoothing_factor_ewma" in self.params
            else 0.94,
            dest="smoothing_factor_ewma",
            help="Smoothing factor for ewma estimators",
        )
        parser.add_argument(
            "-vs",
            "--value-short",
            dest="short_allocation",
            help="Amount to allocate to portfolio in short positions",
            type=float,
            default=self.params["short_allocation"]
            if "short_allocation" in self.params
            else 0.0,
        )

        parser = self.po_parser(
            parser,
            mt=True,
            ct=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            v=True,
            name="MAXDIV_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            table = True
            if "historic_period_sa" in vars(ns_parser):
                table = False

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_max_div(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                covariance=ns_parser.covariance.lower(),
                d_ewma=ns_parser.smoothing_factor_ewma,
                value=ns_parser.long_allocation,
                value_short=ns_parser.short_allocation,
                table=table,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

            if table is False:
                weights_sa = optimizer_view.display_max_div(
                    symbols=self.tickers,
                    interval=ns_parser.historic_period_sa,
                    start_date=ns_parser.start_period_sa,
                    end_date=ns_parser.end_period_sa,
                    log_returns=ns_parser.log_returns_sa,
                    freq=ns_parser.return_frequency_sa,
                    maxnan=ns_parser.max_nan_sa,
                    threshold=ns_parser.threshold_value_sa,
                    method=ns_parser.nan_fill_method_sa,
                    covariance=ns_parser.covariance_sa.lower(),
                    d_ewma=ns_parser.smoothing_factor_ewma_sa,
                    value=ns_parser.long_allocation_sa,
                    value_short=ns_parser.short_allocation_sa,
                    table=table,
                )

                console.print("")
                optimizer_view.display_weights_sa(
                    weights=weights, weights_sa=weights_sa
                )

                if not ns_parser.categories:
                    categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
                else:
                    categories = ns_parser.categories

                for category in categories:
                    optimizer_view.display_categories_sa(
                        weights=weights,
                        weights_sa=weights_sa,
                        categories=self.categories,
                        column=category,
                        title="Category - " + category.title(),
                    )

    @log_start_end(log=logger)
    def call_maxdecorr(self, other_args: List[str]):
        """Process maxdecorr command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="maxdecorr",
            description="Maximizes the portfolio's decorrelation",
        )
        parser.add_argument(
            "-cv",
            "--covariance",
            default=self.params["covariance"]
            if "covariance" in self.params
            else "hist",
            dest="covariance",
            help="""Method used to estimate covariance matrix. Possible values are
                    'hist': historical method
                    'ewma1': exponential weighted moving average with adjust=True
                    'ewma2': exponential weighted moving average with adjust=False
                    'ledoit': Ledoit and Wolf shrinkage method
                    'oas': oracle shrinkage method
                    'shrunk': scikit-learn shrunk method
                    'gl': graphical lasso method
                    'jlogo': j-logo covariance
                    'fixed': takes average of eigenvalues above max Marchenko Pastour limit
                    'spectral':  makes zero eigenvalues above max Marchenko Pastour limit
                    'shrink': Lopez de Prado's book shrinkage method
                    """,
            choices=statics.COVARIANCE_CHOICES,
        )
        parser.add_argument(
            "-de",
            "--d-ewma",
            type=float,
            default=self.params["smoothing_factor_ewma"]
            if "smoothing_factor_ewma" in self.params
            else 0.94,
            dest="smoothing_factor_ewma",
            help="Smoothing factor for ewma estimators",
        )
        parser.add_argument(
            "-vs",
            "--value-short",
            dest="short_allocation",
            help="Amount to allocate to portfolio in short positions",
            type=float,
            default=self.params["short_allocation"]
            if "short_allocation" in self.params
            else 0.0,
        )

        parser = self.po_parser(
            parser,
            mt=True,
            ct=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            v=True,
            name="MAXDECORR_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            table = True
            if "historic_period_sa" in vars(ns_parser):
                table = False

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_max_decorr(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                covariance=ns_parser.covariance.lower(),
                d_ewma=ns_parser.smoothing_factor_ewma,
                value=ns_parser.long_allocation,
                value_short=ns_parser.short_allocation,
                table=table,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

            if table is False:
                weights_sa = optimizer_view.display_max_decorr(
                    symbols=self.tickers,
                    interval=ns_parser.historic_period_sa,
                    start_date=ns_parser.start_period_sa,
                    end_date=ns_parser.end_period_sa,
                    log_returns=ns_parser.log_returns_sa,
                    freq=ns_parser.return_frequency_sa,
                    maxnan=ns_parser.max_nan_sa,
                    threshold=ns_parser.threshold_value_sa,
                    method=ns_parser.nan_fill_method_sa,
                    covariance=ns_parser.covariance_sa.lower(),
                    d_ewma=ns_parser.smoothing_factor_ewma_sa,
                    value=ns_parser.long_allocation_sa,
                    value_short=ns_parser.short_allocation_sa,
                    table=table,
                )

                console.print("")
                optimizer_view.display_weights_sa(
                    weights=weights, weights_sa=weights_sa
                )

                if not ns_parser.categories:
                    categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
                else:
                    categories = ns_parser.categories

                for category in categories:
                    optimizer_view.display_categories_sa(
                        weights=weights,
                        weights_sa=weights_sa,
                        categories=self.categories,
                        column=category,
                        title="Category - " + category.title(),
                    )

    @log_start_end(log=logger)
    def call_blacklitterman(self, other_args: List[str]):
        """Process blacklitterman command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="blacklitterman",
            description="Optimize portfolio using Black Litterman estimates",
        )
        parser.add_argument(
            "-bm",
            "--benchmark",
            type=str,
            default=None,
            dest="benchmark",
            help="portfolio name from current portfolio list",
        )
        parser.add_argument(
            "-o",
            "--objective",
            default=self.params["objective"]
            if "objective" in self.params
            else "Sharpe",
            dest="objective",
            help="Objective function used to optimize the portfolio",
            choices=statics.OBJECTIVE_CHOICES,
        )
        parser.add_argument(
            "-pv",
            "--p-views",
            type=lambda s: [
                [float(item) for item in row.split(",")] for row in s.split(";")
            ],
            default=self.params["p_views"] if "p_views" in self.params else None,
            dest="p_views",
            help="matrix P of analyst views",
        )
        parser.add_argument(
            "-qv",
            "--q-views",
            type=lambda s: [float(item) for item in s.split(",")],
            default=self.params["q_views"] if "q_views" in self.params else None,
            dest="q_views",
            help="matrix Q of analyst views",
        )
        parser.add_argument(
            "-ra",
            "--risk-aversion",
            type=float,
            dest="risk_aversion",
            default=self.params["risk_aversion"]
            if "risk_aversion" in self.params
            else 1,
            help="Risk aversion parameter",
        )
        parser.add_argument(
            "-d",
            "--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(
            "-eq",
            "--equilibrium",
            action="store_true",
            default=self.params["equilibrium"]
            if "equilibrium" in self.params
            else True,
            dest="equilibrium",
            help="""If True excess returns are based on equilibrium market portfolio, if False
                excess returns are calculated as historical returns minus risk free rate.
                """,
        )
        parser.add_argument(
            "-op",
            "--optimize",
            action="store_false",
            default=self.params["optimize"] if "optimize" in self.params else True,
            dest="optimize",
            help="""If True Black Litterman estimates are used as inputs of mean variance model,
                if False returns equilibrium weights from Black Litterman model
                """,
        )
        parser.add_argument(
            "-vs",
            "--value-short",
            type=float,
            default=self.params["short_allocation"]
            if "short_allocation" in self.params
            else 0.0,
            dest="short_allocation",
            help="Amount to allocate to portfolio in short positions",
        )
        parser.add_argument(
            "--file",
            type=lambda s: s if s.endswith(".xlsx") or len(s) == 0 else s + ".xlsx",
            dest="file",
            default="",
            help="Upload an Excel file with views for Black Litterman model",
        )

        parser.add_argument(
            "--download",
            type=lambda s: s if s.endswith(".xlsx") or len(s) == 0 else s + ".xlsx",
            dest="download",
            default="",
            help="Create a template to design Black Litterman model views",
        )
        parser = self.po_parser(
            parser,
            mt=True,
            ct=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            v=True,
            name="BL_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            if len(ns_parser.download) > 0:
                file = (
                    USER_EXPORTS_DIRECTORY / "portfolio" / "views" / ns_parser.download
                )

                excel_model.excel_bl_views(file=file, stocks=self.tickers, n=1)
                return

            if ns_parser.file:
                excel_file = USER_PORTFOLIO_DATA_DIRECTORY / "views" / ns_parser.file
                p_views, q_views = excel_model.load_bl_views(excel_file=excel_file)
            else:
                p_views = ns_parser.p_views
                q_views = ns_parser.q_views

            if ns_parser.benchmark is None:
                benchmark = None
            else:
                benchmark = self.portfolios[ns_parser.benchmark.upper()]

            table = True
            if "historic_period_sa" in vars(ns_parser):
                table = False

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_black_litterman(
                symbols=self.tickers,
                p_views=p_views,
                q_views=q_views,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                benchmark=benchmark,
                objective=ns_parser.objective.lower(),
                risk_free_rate=ns_parser.risk_free,
                risk_aversion=ns_parser.risk_aversion,
                delta=ns_parser.delta,
                equilibrium=ns_parser.equilibrium,
                optimize=ns_parser.optimize,
                value=ns_parser.long_allocation,
                value_short=ns_parser.short_allocation,
                table=table,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

            if table is False:
                if ns_parser.file_sa:
                    excel_file = (
                        USER_PORTFOLIO_DATA_DIRECTORY / "views" / ns_parser.file_sa
                    )
                    p_views_sa, q_views_sa = excel_model.load_bl_views(
                        excel_file=excel_file
                    )
                else:
                    p_views_sa = ns_parser.p_views_sa
                    q_views_sa = ns_parser.q_views_sa

                weights_sa = optimizer_view.display_black_litterman(
                    symbols=self.tickers,
                    p_views=p_views_sa,
                    q_views=q_views_sa,
                    interval=ns_parser.historic_period_sa,
                    start_date=ns_parser.start_period_sa,
                    end_date=ns_parser.end_period_sa,
                    log_returns=ns_parser.log_returns_sa,
                    freq=ns_parser.return_frequency_sa,
                    maxnan=ns_parser.max_nan_sa,
                    threshold=ns_parser.threshold_value_sa,
                    method=ns_parser.nan_fill_method_sa,
                    benchmark=benchmark,
                    objective=ns_parser.objective_sa.lower(),
                    risk_free_rate=ns_parser.risk_free_sa,
                    risk_aversion=ns_parser.risk_aversion_sa,
                    delta=ns_parser.delta_sa,
                    equilibrium=ns_parser.equilibrium_sa,
                    optimize=ns_parser.optimize_sa,
                    value=ns_parser.long_allocation_sa,
                    value_short=ns_parser.short_allocation_sa,
                    table=table,
                )

                console.print("")
                optimizer_view.display_weights_sa(
                    weights=weights, weights_sa=weights_sa
                )

                if not ns_parser.categories:
                    categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
                else:
                    categories = ns_parser.categories

                for category in categories:
                    optimizer_view.display_categories_sa(
                        weights=weights,
                        weights_sa=weights_sa,
                        categories=self.categories,
                        column=category,
                        title="Category - " + category.title(),
                    )

    @log_start_end(log=logger)
    def call_ef(self, other_args):
        """Process ef command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="ef",
            description="""This function plots random portfolios based on their
                risk and returns and shows the efficient frontier.""",
        )
        parser.add_argument(
            "-vs",
            "--value-short",
            dest="short_allocation",
            help="Amount to allocate to portfolio in short positions",
            type=float,
            default=self.params["short_allocation"]
            if "short_allocation" in self.params
            else 0.0,
        )
        if other_args and "-" not in other_args[0][0]:
            other_args.insert(0, "-n")
        parser.add_argument(
            "-n",
            "--number-portfolios",
            default=self.params["amount_portfolios"]
            if "amount_portfolios" in self.params
            else 100,
            type=check_non_negative,
            dest="amount_portfolios",
            help="Number of portfolios to simulate",
        )
        parser.add_argument(
            "-se",
            "--seed",
            default=self.params["random_seed"] if "random_seed" in self.params else 123,
            type=check_non_negative,
            dest="random_seed",
            help="Seed used to generate random portfolios",
        )
        parser.add_argument(
            "-t",
            "--tangency",
            action="store_true",
            dest="tangency",
            default=self.params["tangency"] if "tangency" in self.params else False,
            help="Adds the optimal line with the risk-free asset",
        )
        parser.add_argument(
            "--no_plot",
            action="store_false",
            dest="plot_tickers",
            default=True,
            help="Whether or not to plot the tickers for the assets provided",
        )
        parser = self.po_parser(
            parser,
            rm=True,
            mt=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
            v=True,
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)

        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            optimizer_view.display_ef(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                risk_measure=ns_parser.risk_measure.lower(),
                risk_free_rate=ns_parser.risk_free,
                alpha=ns_parser.significance_level,
                value=ns_parser.long_allocation,
                value_short=ns_parser.short_allocation,
                n_portfolios=ns_parser.amount_portfolios,
                seed=ns_parser.random_seed,
                tangency=ns_parser.tangency,
                plot_tickers=ns_parser.plot_tickers,
            )

    @log_start_end(log=logger)
    def call_riskparity(self, other_args: List[str]):
        """Process riskparity command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="riskparity",
            description="""Build a risk parity portfolio based on risk
                budgeting approach""",
        )
        parser.add_argument(
            "-rm",
            "--risk-measure",
            default=self.params["risk_measure"]
            if "risk_measure" in self.params
            else "MV",
            dest="risk_measure",
            help="""Risk measure used to optimize the portfolio. Possible values are:
                    'MV' : Variance
                    'MAD' : Mean Absolute Deviation
                    'MSV' : Semi Variance (Variance of negative returns)
                    'FLPM' : First Lower Partial Moment
                    'SLPM' : Second Lower Partial Moment
                    'CVaR' : Conditional Value at Risk
                    'EVaR' : Entropic Value at Risk
                    'UCI' : Ulcer Index of uncompounded returns
                    'CDaR' : Conditional Drawdown at Risk of uncompounded returns
                    'EDaR' : Entropic Drawdown at Risk of uncompounded returns
                    """,
            choices=statics.RISK_PARITY_CHOICES,
            metavar="RISK-MEASURE",
        )
        parser.add_argument(
            "-rc",
            "--risk-cont",
            type=lambda s: [float(item) for item in s.split(",")],
            default=self.params["risk_contribution"]
            if "risk_contribution" in self.params
            else None,
            dest="risk_contribution",
            help="vector of risk contribution constraint",
        )
        parser.add_argument(
            "-tr",
            "--target-return",
            dest="target_return",
            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(
            "-de",
            "--d-ewma",
            type=float,
            default=self.params["smoothing_factor_ewma"]
            if "smoothing_factor_ewma" in self.params
            else 0.94,
            dest="smoothing_factor_ewma",
            help="Smoothing factor for ewma estimators",
        )

        parser = self.po_parser(
            parser,
            mt=True,
            ct=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
            v=True,
            name="RP_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            table = True
            if "historic_period_sa" in vars(ns_parser):
                table = False

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_risk_parity(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                risk_measure=ns_parser.risk_measure.lower(),
                risk_cont=ns_parser.risk_contribution,
                risk_free_rate=ns_parser.risk_free,
                alpha=ns_parser.significance_level,
                target_return=ns_parser.target_return,
                value=ns_parser.long_allocation,
                table=table,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

            if table is False:
                weights_sa = optimizer_view.display_risk_parity(
                    symbols=self.tickers,
                    interval=ns_parser.historic_period_sa,
                    start_date=ns_parser.start_period_sa,
                    end_date=ns_parser.end_period_sa,
                    log_returns=ns_parser.log_returns_sa,
                    freq=ns_parser.return_frequency_sa,
                    maxnan=ns_parser.max_nan_sa,
                    threshold=ns_parser.threshold_value_sa,
                    method=ns_parser.nan_fill_method_sa,
                    risk_measure=ns_parser.risk_measure_sa.lower(),
                    risk_cont=ns_parser.risk_contribution_sa,
                    risk_free_rate=ns_parser.risk_free_sa,
                    alpha=ns_parser.significance_level_sa,
                    target_return=ns_parser.target_return_sa,
                    value=ns_parser.long_allocation_sa,
                    table=table,
                )

                console.print("")
                optimizer_view.display_weights_sa(
                    weights=weights, weights_sa=weights_sa
                )

                if not ns_parser.categories:
                    categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
                else:
                    categories = ns_parser.categories

                for category in categories:
                    optimizer_view.display_categories_sa(
                        weights=weights,
                        weights_sa=weights_sa,
                        categories=self.categories,
                        column=category,
                        title="Category - " + category.title(),
                    )

    @log_start_end(log=logger)
    def call_relriskparity(self, other_args: List[str]):
        """Process relriskparity command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="relriskparity",
            description="""Build a relaxed risk parity portfolio based on
                least squares approach""",
        )
        parser.add_argument(
            "-ve",
            "--version",
            default=self.params["risk_parity_model"]
            if "risk_parity_model" in self.params
            else "A",
            dest="risk_parity_model",
            help="""version of relaxed risk parity model: Possible values are:
                'A': risk parity without regularization and penalization constraints
                'B': with regularization constraint but without penalization constraint
                'C': with regularization and penalization constraints""",
            choices=statics.REL_RISK_PARITY_CHOICES,
            metavar="VERSION",
        )
        parser.add_argument(
            "-rc",
            "--risk-cont",
            type=lambda s: [float(item) for item in s.split(",")],
            default=self.params["risk_contribution"]
            if "risk_contribution" in self.params
            else None,
            dest="risk_contribution",
            help="Vector of risk contribution constraints",
        )
        parser.add_argument(
            "-pf",
            "--penal-factor",
            type=float,
            dest="penal_factor",
            default=self.params["penal_factor"] if "penal_factor" in self.params else 1,
            help="""The penalization factor of penalization constraints. Only
            used with version 'C'.""",
        )
        parser.add_argument(
            "-tr",
            "--target-return",
            dest="target_return",
            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(
            "-de",
            "--d-ewma",
            type=float,
            default=self.params["smoothing_factor_ewma"]
            if "smoothing_factor_ewma" in self.params
            else 0.94,
            dest="smoothing_factor_ewma",
            help="Smoothing factor for ewma estimators",
        )

        parser = self.po_parser(
            parser,
            mt=True,
            ct=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            v=True,
            name="RRP_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            table = True
            if "historic_period_sa" in vars(ns_parser):
                table = False

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_rel_risk_parity(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                version=ns_parser.risk_parity_model,
                risk_cont=ns_parser.risk_contribution,
                penal_factor=ns_parser.penal_factor,
                target_return=ns_parser.target_return,
                d_ewma=ns_parser.smoothing_factor_ewma,
                value=ns_parser.long_allocation,
                table=table,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

            if table is False:
                weights_sa = optimizer_view.display_rel_risk_parity(
                    symbols=self.tickers,
                    interval=ns_parser.historic_period_sa,
                    start_date=ns_parser.start_period_sa,
                    end_date=ns_parser.end_period_sa,
                    log_returns=ns_parser.log_returns_sa,
                    freq=ns_parser.return_frequency_sa,
                    maxnan=ns_parser.max_nan_sa,
                    threshold=ns_parser.threshold_value_sa,
                    method=ns_parser.nan_fill_method_sa,
                    version=ns_parser.risk_parity_model_sa,
                    risk_cont=ns_parser.risk_contribution_sa,
                    penal_factor=ns_parser.penal_factor_sa,
                    target_return=ns_parser.target_return_sa,
                    d_ewma=ns_parser.smoothing_factor_ewma_sa,
                    value=ns_parser.long_allocation_sa,
                    table=table,
                )

                console.print("")
                optimizer_view.display_weights_sa(
                    weights=weights, weights_sa=weights_sa
                )

                if not ns_parser.categories:
                    categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
                else:
                    categories = ns_parser.categories

                for category in categories:
                    optimizer_view.display_categories_sa(
                        weights=weights,
                        weights_sa=weights_sa,
                        categories=self.categories,
                        column=category,
                        title="Category - " + category.title(),
                    )

    @log_start_end(log=logger)
    def call_hrp(self, other_args: List[str]):
        """Process hierarchical risk parity command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="hrp",
            description="Builds a hierarchical risk parity portfolio",
        )
        parser.add_argument(
            "-cd",
            "--codependence",
            default=self.params["co_dependence"]
            if "co_dependence" in self.params
            else "pearson",
            dest="co_dependence",
            help="""The codependence or similarity matrix used to build the
                distance metric and clusters. Possible values are:
                'pearson': pearson correlation matrix
                'spearman': spearman correlation matrix
                'abs_pearson': absolute value of pearson correlation matrix
                'abs_spearman': absolute value of spearman correlation matrix
                'distance': distance correlation matrix
                'mutual_info': mutual information codependence matrix
                'tail': tail index codependence matrix""",
            choices=statics.CODEPENDENCE_CHOICES,
        )
        parser.add_argument(
            "-cv",
            "--covariance",
            default=self.params["covariance"]
            if "covariance" in self.params
            else "hist",
            dest="covariance",
            help="""Method used to estimate covariance matrix. Possible values are
                    'hist': historical method
                    'ewma1': exponential weighted moving average with adjust=True
                    'ewma2': exponential weighted moving average with adjust=False
                    'ledoit': Ledoit and Wolf shrinkage method
                    'oas': oracle shrinkage method
                    'shrunk': scikit-learn shrunk method
                    'gl': graphical lasso method
                    'jlogo': j-logo covariance
                    'fixed': takes average of eigenvalues above max Marchenko Pastour limit
                    'spectral':  makes zero eigenvalues above max Marchenko Pastour limit
                    'shrink': Lopez de Prado's book shrinkage method
                    """,
            choices=statics.COVARIANCE_CHOICES,
        )
        parser.add_argument(
            "-rm",
            "--risk-measure",
            default=self.params["risk_measure"]
            if "risk_measure" in self.params
            else "MV",
            dest="risk_measure",
            help="""Risk measure used to optimize the portfolio. Possible values are:
                    'MV' : Variance
                    'MAD' : Mean Absolute Deviation
                    'GMD' : Gini Mean Difference
                    'MSV' : Semi Variance (Variance of negative returns)
                    'FLPM' : First Lower Partial Moment
                    'SLPM' : Second Lower Partial Moment
                    'VaR' : Value at Risk
                    'CVaR' : Conditional Value at Risk
                    'TG' : Tail Gini
                    'EVaR' : Entropic Value at Risk
                    'WR' : Worst Realization
                    'RG' : Range
                    'CVRG' : CVaR Range
                    'TGRG' : Tail Gini Range
                    'ADD' : Average Drawdown of uncompounded returns
                    'UCI' : Ulcer Index of uncompounded returns
                    'DaR' : Drawdown at Risk of uncompounded returns
                    'CDaR' : Conditional Drawdown at Risk of uncompounded returns
                    'EDaR' : Entropic Drawdown at Risk of uncompounded returns
                    'MDD' : Maximum Drawdown of uncompounded returns
                    'ADD_Rel' : Average Drawdown of compounded returns
                    'UCI_Rel' : Ulcer Index of compounded returns
                    'DaR_Rel' : Drawdown at Risk of compounded returns
                    'CDaR_Rel' : Conditional Drawdown at Risk of compounded returns
                    'EDaR_Rel' : Entropic Drawdown at Risk of compounded returns
                    'MDD_Rel' : Maximum Drawdown of compounded returns
                    """,
            choices=statics.HCP_CHOICES,
            metavar="RISK-MEASURE",
        )
        parser.add_argument(
            "-as",
            "--a-sim",
            type=int,
            default=self.params["cvar_simulations_losses"]
            if "cvar_simulations_losses" in self.params
            else 100,
            dest="cvar_simulations_losses",
            help="""Number of CVaRs used to approximate Tail Gini of losses.
                The default is 100""",
        )
        parser.add_argument(
            "-b",
            "--beta",
            type=float,
            default=self.params["cvar_significance"]
            if "cvar_significance" in self.params
            else None,
            dest="cvar_significance",
            help="""Significance level of CVaR and Tail Gini of gains. If
                empty it duplicates alpha""",
        )
        parser.add_argument(
            "-bs",
            "--b-sim",
            type=int,
            default=self.params["cvar_simulations_gains"]
            if "cvar_simulations_gains" in self.params
            else None,
            dest="cvar_simulations_gains",
            help="""Number of CVaRs used to approximate Tail Gini of gains.
                If empty it duplicates a_sim value""",
        )
        parser.add_argument(
            "-lk",
            "--linkage",
            default=self.params["linkage"] if "linkage" in self.params else "single",
            dest="linkage",
            help="Linkage method of hierarchical clustering",
            choices=statics.LINKAGE_CHOICES,
            metavar="LINKAGE",
        )
        parser.add_argument(
            "-k",
            type=int,
            default=self.params["amount_clusters"]
            if "amount_clusters" in self.params
            else None,
            dest="amount_clusters",
            help="Number of clusters specified in advance",
        )
        parser.add_argument(
            "-mk",
            "--max-k",
            type=int,
            default=self.params["max_clusters"]
            if "max_clusters" in self.params
            else 10,
            dest="max_clusters",
            help="""Max number of clusters used by the two difference gap
            statistic to find the optimal number of clusters. If k is
            empty this value is used""",
        )
        parser.add_argument(
            "-bi",
            "--bins-info",
            default=self.params["amount_bins"]
            if "amount_bins" in self.params
            else "KN",
            dest="amount_bins",
            help="Number of bins used to calculate the variation of information",
            choices=statics.BINS_CHOICES,
        )
        parser.add_argument(
            "-at",
            "--alpha-tail",
            type=float,
            default=self.params["alpha_tail"] if "alpha_tail" in self.params else 0.05,
            dest="alpha_tail",
            help="""Significance level for lower tail dependence index, only
            used when when codependence value is 'tail' """,
        )
        parser.add_argument(
            "-lo",
            "--leaf-order",
            default=self.params["leaf_order"] if "leaf_order" in self.params else True,
            dest="leaf_order",
            help="""Indicates if the cluster are ordered so that the distance
                between successive leaves is minimal""",
        )
        parser.add_argument(
            "-de",
            "--d-ewma",
            type=float,
            default=self.params["smoothing_factor_ewma"]
            if "smoothing_factor_ewma" in self.params
            else 0.94,
            dest="smoothing_factor_ewma",
            help="Smoothing factor for ewma estimators",
        )

        parser = self.po_parser(
            parser,
            mt=True,
            ct=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
            v=True,
            name="_HRP",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            table = True
            if "historic_period_sa" in vars(ns_parser):
                table = False

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_hrp(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                codependence=ns_parser.co_dependence.lower(),
                covariance=ns_parser.covariance.lower(),
                risk_measure=ns_parser.risk_measure.lower(),
                risk_free_rate=ns_parser.risk_free,
                alpha=ns_parser.significance_level,
                a_sim=ns_parser.cvar_simulations_losses,
                beta=ns_parser.cvar_significance,
                b_sim=ns_parser.cvar_simulations_gains,
                linkage=ns_parser.linkage.lower(),
                k=ns_parser.amount_clusters,
                max_k=ns_parser.max_clusters,
                bins_info=ns_parser.amount_bins.upper(),
                alpha_tail=ns_parser.alpha_tail,
                leaf_order=ns_parser.leaf_order,
                d_ewma=ns_parser.smoothing_factor_ewma,
                value=ns_parser.long_allocation,
                table=table,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

            if table is False:
                weights_sa = optimizer_view.display_hrp(
                    symbols=self.tickers,
                    interval=ns_parser.historic_period_sa,
                    start_date=ns_parser.start_period_sa,
                    end_date=ns_parser.end_period_sa,
                    log_returns=ns_parser.log_returns_sa,
                    freq=ns_parser.return_frequency_sa,
                    maxnan=ns_parser.max_nan_sa,
                    threshold=ns_parser.threshold_value_sa,
                    method=ns_parser.nan_fill_method_sa,
                    codependence=ns_parser.co_dependence_sa.lower(),
                    covariance=ns_parser.covariance_sa.lower(),
                    risk_measure=ns_parser.risk_measure_sa.lower(),
                    risk_free_rate=ns_parser.risk_free_sa,
                    alpha=ns_parser.significance_level_sa,
                    a_sim=ns_parser.cvar_simulations_losses_sa,
                    beta=ns_parser.cvar_significance_sa,
                    b_sim=ns_parser.cvar_simulations_gains_sa,
                    linkage=ns_parser.linkage_sa.lower(),
                    k=ns_parser.amount_clusters_sa,
                    max_k=ns_parser.max_clusters_sa,
                    bins_info=ns_parser.amount_bins_sa.upper(),
                    alpha_tail=ns_parser.alpha_tail_sa,
                    leaf_order=ns_parser.leaf_order_sa,
                    d_ewma=ns_parser.smoothing_factor_ewma_sa,
                    value=ns_parser.long_allocation_sa,
                    table=table,
                )

                console.print("")
                optimizer_view.display_weights_sa(
                    weights=weights, weights_sa=weights_sa
                )

                if not ns_parser.categories:
                    categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
                else:
                    categories = ns_parser.categories

                for category in categories:
                    optimizer_view.display_categories_sa(
                        weights=weights,
                        weights_sa=weights_sa,
                        categories=self.categories,
                        column=category,
                        title="Category - " + category.title(),
                    )

    @log_start_end(log=logger)
    def call_herc(self, other_args: List[str]):
        """Process hierarchical equal risk contribution command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="herc",
            description="Builds a hierarchical equal risk contribution portfolio",
        )
        parser.add_argument(
            "-cd",
            "--codependence",
            default="pearson",
            dest="co_dependence",
            help="""The codependence or similarity matrix used to build the
                distance metric and clusters. Possible values are:
                'pearson': pearson correlation matrix
                'spearman': spearman correlation matrix
                'abs_pearson': absolute value of pearson correlation matrix
                'abs_spearman': absolute value of spearman correlation matrix
                'distance': distance correlation matrix
                'mutual_info': mutual information codependence matrix
                'tail': tail index codependence matrix""",
            choices=statics.CODEPENDENCE_CHOICES,
        )
        parser.add_argument(
            "-cv",
            "--covariance",
            default=self.params["covariance"]
            if "covariance" in self.params
            else "hist",
            dest="covariance",
            help="""Method used to estimate covariance matrix. Possible values are
                    'hist': historical method
                    'ewma1': exponential weighted moving average with adjust=True
                    'ewma2': exponential weighted moving average with adjust=False
                    'ledoit': Ledoit and Wolf shrinkage method
                    'oas': oracle shrinkage method
                    'shrunk': scikit-learn shrunk method
                    'gl': graphical lasso method
                    'jlogo': j-logo covariance
                    'fixed': takes average of eigenvalues above max Marchenko Pastour limit
                    'spectral':  makes zero eigenvalues above max Marchenko Pastour limit
                    'shrink': Lopez de Prado's book shrinkage method
                    """,
            choices=statics.COVARIANCE_CHOICES,
        )
        parser.add_argument(
            "-rm",
            "--risk-measure",
            default=self.params["risk_measure"]
            if "risk_measure" in self.params
            else "MV",
            dest="risk_measure",
            help="""Risk measure used to optimize the portfolio. Possible values are:
                    'MV' : Variance
                    'MAD' : Mean Absolute Deviation
                    'GMD' : Gini Mean Difference
                    'MSV' : Semi Variance (Variance of negative returns)
                    'FLPM' : First Lower Partial Moment
                    'SLPM' : Second Lower Partial Moment
                    'VaR' : Value at Risk
                    'CVaR' : Conditional Value at Risk
                    'TG' : Tail Gini
                    'EVaR' : Entropic Value at Risk
                    'WR' : Worst Realization
                    'RG' : Range
                    'CVRG' : CVaR Range
                    'TGRG' : Tail Gini Range
                    'ADD' : Average Drawdown of uncompounded returns
                    'UCI' : Ulcer Index of uncompounded returns
                    'DaR' : Drawdown at Risk of uncompounded returns
                    'CDaR' : Conditional Drawdown at Risk of uncompounded returns
                    'EDaR' : Entropic Drawdown at Risk of uncompounded returns
                    'MDD' : Maximum Drawdown of uncompounded returns
                    'ADD_Rel' : Average Drawdown of compounded returns
                    'UCI_Rel' : Ulcer Index of compounded returns
                    'DaR_Rel' : Drawdown at Risk of compounded returns
                    'CDaR_Rel' : Conditional Drawdown at Risk of compounded returns
                    'EDaR_Rel' : Entropic Drawdown at Risk of compounded returns
                    'MDD_Rel' : Maximum Drawdown of compounded returns
                    """,
            choices=statics.HCP_CHOICES,
            metavar="RISK-MEASURE",
        )
        parser.add_argument(
            "-as",
            "--a-sim",
            type=int,
            default=self.params["cvar_simulations_losses"]
            if "cvar_simulations_losses" in self.params
            else 100,
            dest="cvar_simulations_losses",
            help="""Number of CVaRs used to approximate Tail Gini of losses.
                The default is 100""",
        )
        parser.add_argument(
            "-b",
            "--beta",
            type=float,
            default=self.params["cvar_significance"]
            if "cvar_significance" in self.params
            else None,
            dest="cvar_significance",
            help="""Significance level of CVaR and Tail Gini of gains. If
                empty it duplicates alpha""",
        )
        parser.add_argument(
            "-bs",
            "--b-sim",
            type=int,
            default=self.params["cvar_simulations_gains"]
            if "cvar_simulations_gains" in self.params
            else None,
            dest="cvar_simulations_gains",
            help="""Number of CVaRs used to approximate Tail Gini of gains.
                If empty it duplicates a_sim value""",
        )
        parser.add_argument(
            "-lk",
            "--linkage",
            default=self.params["linkage"] if "linkage" in self.params else "single",
            dest="linkage",
            help="Linkage method of hierarchical clustering",
            choices=statics.LINKAGE_CHOICES,
            metavar="LINKAGE",
        )
        parser.add_argument(
            "-k",
            type=int,
            default=self.params["amount_clusters"]
            if "amount_clusters" in self.params
            else None,
            dest="amount_clusters",
            help="Number of clusters specified in advance",
        )
        parser.add_argument(
            "-mk",
            "--max-k",
            type=int,
            default=self.params["max_clusters"]
            if "max_clusters" in self.params
            else 10,
            dest="max_clusters",
            help="""Max number of clusters used by the two difference gap
            statistic to find the optimal number of clusters. If k is
            empty this value is used""",
        )
        parser.add_argument(
            "-bi",
            "--bins-info",
            default=self.params["amount_bins"]
            if "amount_bins" in self.params
            else "KN",
            dest="amount_bins",
            help="Number of bins used to calculate the variation of information",
            choices=statics.BINS_CHOICES,
        )
        parser.add_argument(
            "-at",
            "--alpha-tail",
            type=float,
            default=self.params["alpha_tail"] if "alpha_tail" in self.params else 0.05,
            dest="alpha_tail",
            help="""Significance level for lower tail dependence index, only
            used when when codependence value is 'tail' """,
        )
        parser.add_argument(
            "-lo",
            "--leaf-order",
            default=self.params["leaf_order"] if "leaf_order" in self.params else True,
            dest="leaf_order",
            help="""Indicates if the cluster are ordered so that the distance
                between successive leaves is minimal""",
        )
        parser.add_argument(
            "-de",
            "--d-ewma",
            type=float,
            default=self.params["smoothing_factor_ewma"]
            if "smoothing_factor_ewma" in self.params
            else 0.94,
            dest="smoothing_factor_ewma",
            help="Smoothing factor for ewma estimators",
        )

        parser = self.po_parser(
            parser,
            mt=True,
            ct=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
            v=True,
            name="HERC_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            table = True
            if "historic_period_sa" in vars(ns_parser):
                table = False

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_herc(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                codependence=ns_parser.co_dependence.lower(),
                covariance=ns_parser.covariance.lower(),
                risk_measure=ns_parser.risk_measure.lower(),
                risk_free_rate=ns_parser.risk_free,
                alpha=ns_parser.significance_level,
                a_sim=ns_parser.cvar_simulations_losses,
                beta=ns_parser.cvar_significance,
                b_sim=ns_parser.cvar_simulations_gains,
                linkage=ns_parser.linkage.lower(),
                k=ns_parser.amount_clusters,
                max_k=ns_parser.max_clusters,
                bins_info=ns_parser.amount_bins.upper(),
                alpha_tail=ns_parser.alpha_tail,
                leaf_order=ns_parser.leaf_order,
                d_ewma=ns_parser.smoothing_factor_ewma,
                value=ns_parser.long_allocation,
                table=table,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

            if table is False:
                weights_sa = optimizer_view.display_herc(
                    symbols=self.tickers,
                    interval=ns_parser.historic_period_sa,
                    start_date=ns_parser.start_period_sa,
                    end_date=ns_parser.end_period_sa,
                    log_returns=ns_parser.log_returns_sa,
                    freq=ns_parser.return_frequency_sa,
                    maxnan=ns_parser.max_nan_sa,
                    threshold=ns_parser.threshold_value_sa,
                    method=ns_parser.nan_fill_method_sa,
                    codependence=ns_parser.co_dependence_sa.lower(),
                    covariance=ns_parser.covariance_sa.lower(),
                    risk_measure=ns_parser.risk_measure_sa.lower(),
                    risk_free_rate=ns_parser.risk_free_sa,
                    alpha=ns_parser.significance_level_sa,
                    a_sim=ns_parser.cvar_simulations_losses_sa,
                    beta=ns_parser.cvar_significance_sa,
                    b_sim=ns_parser.cvar_simulations_gains_sa,
                    linkage=ns_parser.linkage_sa.lower(),
                    k=ns_parser.amount_clusters_sa,
                    max_k=ns_parser.max_clusters_sa,
                    bins_info=ns_parser.amount_bins_sa.upper(),
                    alpha_tail=ns_parser.alpha_tail_sa,
                    leaf_order=ns_parser.leaf_order_sa,
                    d_ewma=ns_parser.smoothing_factor_ewma_sa,
                    value=ns_parser.long_allocation_sa,
                    table=table,
                )

                console.print("")
                optimizer_view.display_weights_sa(
                    weights=weights, weights_sa=weights_sa
                )

                if not ns_parser.categories:
                    categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
                else:
                    categories = ns_parser.categories

                for category in categories:
                    optimizer_view.display_categories_sa(
                        weights=weights,
                        weights_sa=weights_sa,
                        categories=self.categories,
                        column=category,
                        title="Category - " + category.title(),
                    )

    @log_start_end(log=logger)
    def call_nco(self, other_args: List[str]):
        """Process nested clustered optimization command"""
        parser = argparse.ArgumentParser(
            add_help=False,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            prog="nco",
            description="Builds a nested clustered optimization portfolio",
        )
        parser.add_argument(
            "-cd",
            "--codependence",
            default=self.params["co_dependence"]
            if "co_dependence" in self.params
            else "pearson",
            dest="co_dependence",
            help="""The codependence or similarity matrix used to build the
                distance metric and clusters. Possible values are:
                'pearson': pearson correlation matrix
                'spearman': spearman correlation matrix
                'abs_pearson': absolute value of pearson correlation matrix
                'abs_spearman': absolute value of spearman correlation matrix
                'distance': distance correlation matrix
                'mutual_info': mutual information codependence matrix
                'tail': tail index codependence matrix""",
            choices=statics.CODEPENDENCE_CHOICES,
        )
        parser.add_argument(
            "-cv",
            "--covariance",
            default=self.params["covariance"]
            if "covariance" in self.params
            else "hist",
            dest="covariance",
            help="""Method used to estimate covariance matrix. Possible values are
                    'hist': historical method
                    'ewma1': exponential weighted moving average with adjust=True
                    'ewma2': exponential weighted moving average with adjust=False
                    'ledoit': Ledoit and Wolf shrinkage method
                    'oas': oracle shrinkage method
                    'shrunk': scikit-learn shrunk method
                    'gl': graphical lasso method
                    'jlogo': j-logo covariance
                    'fixed': takes average of eigenvalues above max Marchenko Pastour limit
                    'spectral':  makes zero eigenvalues above max Marchenko Pastour limit
                    'shrink': Lopez de Prado's book shrinkage method
                    """,
            choices=statics.COVARIANCE_CHOICES,
        )
        parser.add_argument(
            "-o",
            "--objective",
            default=self.params["objective"]
            if "objective" in self.params
            else "MinRisk",
            dest="objective",
            help="Objective function used to optimize the portfolio",
            choices=statics.NCO_OBJECTIVE_CHOICES,
        )
        parser.add_argument(
            "-ra",
            "--risk-aversion",
            type=float,
            dest="risk_aversion",
            default=self.params["long_allocation"]
            if "long_allocation" in self.params
            else 1,
            help="Risk aversion parameter",
        )
        parser.add_argument(
            "-lk",
            "--linkage",
            default=self.params["linkage"] if "linkage" in self.params else "single",
            dest="linkage",
            help="Linkage method of hierarchical clustering",
            choices=statics.LINKAGE_CHOICES,
            metavar="LINKAGE",
        )
        parser.add_argument(
            "-k",
            type=int,
            default=self.params["amount_clusters"]
            if "amount_clusters" in self.params
            else None,
            dest="amount_clusters",
            help="Number of clusters specified in advance",
        )
        parser.add_argument(
            "-mk",
            "--max-k",
            type=int,
            default=self.params["max_clusters"]
            if "max_clusters" in self.params
            else 10,
            dest="max_clusters",
            help="""Max number of clusters used by the two difference gap
            statistic to find the optimal number of clusters. If k is
            empty this value is used""",
        )
        parser.add_argument(
            "-bi",
            "--bins-info",
            default=self.params["amount_bins"]
            if "amount_bins" in self.params
            else "KN",
            dest="amount_bins",
            help="Number of bins used to calculate the variation of information",
            choices=statics.BINS_CHOICES,
        )
        parser.add_argument(
            "-at",
            "--alpha-tail",
            type=float,
            default=self.params["alpha_tail"] if "alpha_tail" in self.params else 0.05,
            dest="alpha_tail",
            help="""Significance level for lower tail dependence index, only
            used when when codependence value is 'tail' """,
        )
        parser.add_argument(
            "-lo",
            "--leaf-order",
            action="store_true",
            default=self.params["leaf_order"] if "leaf_order" in self.params else True,
            dest="leaf_order",
            help="""indicates if the cluster are ordered so that the distance
                between successive leaves is minimal""",
        )
        parser.add_argument(
            "-de",
            "--d-ewma",
            type=float,
            default=self.params["smoothing_factor_ewma"]
            if "smoothing_factor_ewma" in self.params
            else 0.94,
            dest="smoothing_factor_ewma",
            help="Smoothing factor for ewma estimators",
        )

        parser = self.po_parser(
            parser,
            rm=True,
            mt=True,
            ct=True,
            p=True,
            s=True,
            e=True,
            lr=True,
            freq=True,
            mn=True,
            th=True,
            r=True,
            a=True,
            v=True,
            name="NCO_",
        )
        ns_parser = self.parse_known_args_and_warn(parser, other_args)
        if ns_parser:
            if len(self.tickers) < 2:
                console.print(
                    "Please have at least 2 loaded tickers to calculate weights.\n"
                )
                return

            table = True
            if "historic_period_sa" in vars(ns_parser):
                table = False

            console.print(
                "[yellow]Optimization can take time. Please be patient...\n[/yellow]"
            )

            weights = optimizer_view.display_nco(
                symbols=self.tickers,
                interval=ns_parser.historic_period,
                start_date=ns_parser.start_period,
                end_date=ns_parser.end_period,
                log_returns=ns_parser.log_returns,
                freq=ns_parser.return_frequency,
                maxnan=ns_parser.max_nan,
                threshold=ns_parser.threshold_value,
                method=ns_parser.nan_fill_method,
                codependence=ns_parser.co_dependence.lower(),
                covariance=ns_parser.covariance.lower(),
                objective=ns_parser.objective.lower(),
                risk_measure=ns_parser.risk_measure.lower(),
                risk_free_rate=ns_parser.risk_free,
                risk_aversion=ns_parser.risk_aversion,
                alpha=ns_parser.significance_level,
                linkage=ns_parser.linkage.lower(),
                k=ns_parser.amount_clusters,
                max_k=ns_parser.max_clusters,
                bins_info=ns_parser.amount_bins.upper(),
                alpha_tail=ns_parser.alpha_tail,
                leaf_order=ns_parser.leaf_order,
                d_ewma=ns_parser.smoothing_factor_ewma,
                value=ns_parser.long_allocation,
                table=table,
            )

            self.portfolios[ns_parser.name.upper()] = weights
            self.count += 1
            self.update_runtime_choices()

            if table is False:
                weights_sa = optimizer_view.display_nco(
                    symbols=self.tickers,
                    interval=ns_parser.historic_period_sa,
                    start_date=ns_parser.start_period_sa,
                    end_date=ns_parser.end_period_sa,
                    log_returns=ns_parser.log_returns_sa,
                    freq=ns_parser.return_frequency_sa,
                    maxnan=ns_parser.max_nan_sa,
                    threshold=ns_parser.threshold_value_sa,
                    method=ns_parser.nan_fill_method_sa,
                    codependence=ns_parser.co_dependence_sa.lower(),
                    covariance=ns_parser.covariance_sa.lower(),
                    objective=ns_parser.objective_sa.lower(),
                    risk_measure=ns_parser.risk_measure_sa.lower(),
                    risk_free_rate=ns_parser.risk_free_sa,
                    risk_aversion=ns_parser.risk_aversion_sa,
                    alpha=ns_parser.significance_level_sa,
                    linkage=ns_parser.linkage_sa.lower(),
                    k=ns_parser.amount_clusters_sa,
                    max_k=ns_parser.max_clusters_sa,
                    bins_info=ns_parser.amount_bins_sa.upper(),
                    alpha_tail=ns_parser.alpha_tail_sa,
                    leaf_order=ns_parser.leaf_order_sa,
                    d_ewma=ns_parser.smoothing_factor_ewma_sa,
                    value=ns_parser.long_allocation_sa,
                    table=table,
                )

                console.print("")
                optimizer_view.display_weights_sa(
                    weights=weights, weights_sa=weights_sa
                )

                if not ns_parser.categories:
                    categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
                else:
                    categories = ns_parser.categories

                for category in categories:
                    optimizer_view.display_categories_sa(
                        weights=weights,
                        weights_sa=weights_sa,
                        categories=self.categories,
                        column=category,
                        title="Category - " + category.title(),
                    )
