示例#1
0
class MeanVarianceOptimisation:
    # pylint: disable=too-many-instance-attributes
    """
    This class implements some classic mean-variance optimisation techniques for calculating the efficient frontier solutions.
    With the help of quadratic optimisers, users can generate optimal portfolios for different objective functions. Currently
    solutions to the following portfolios can be generated:

        1. Inverse Variance
        2. Maximum Sharpe
        3. Minimum Volatility
        4. Efficient Risk
        5. Maximum Return - Minimum Volatility
        6. Efficient Return
        7. Maximum Diversification
        8. Maximum Decorrelation
        9. Custom Objective Function
    """
    def __init__(self, calculate_expected_returns='mean', risk_free_rate=0.03):
        """
        Constructor.

        :param calculate_expected_returns: (str) The method to use for calculation of expected returns.
                                                 Currently supports: ``mean``, ``exponential``.
        """

        self.weights = list()
        self.asset_names = None
        self.num_assets = None
        self.portfolio_risk = None
        self.portfolio_return = None
        self.portfolio_sharpe_ratio = None
        self.calculate_expected_returns = calculate_expected_returns
        self.returns_estimator = ReturnsEstimators()
        self.risk_estimators = RiskEstimators()
        self.weight_bounds = (0, 1)
        self.risk_free_rate = risk_free_rate

    def allocate(self,
                 asset_names=None,
                 asset_prices=None,
                 expected_asset_returns=None,
                 covariance_matrix=None,
                 solution='inverse_variance',
                 target_return=0.2,
                 target_risk=0.01,
                 risk_aversion=10,
                 weight_bounds=None):
        # pylint: disable=invalid-name, too-many-branches
        """
        Calculate the portfolio asset allocations using the method specified.

        :param asset_names: (list) A list of strings containing the asset names.
        :param asset_prices: (pd.DataFrame) A dataframe of historical asset prices (daily close).
        :param expected_asset_returns: (list/np.array/pd.dataframe) A list of mean stock returns (mu).
        :param covariance_matrix: (pd.DataFrame/numpy matrix) User supplied covariance matrix of asset returns (sigma).
        :param solution: (str) The type of solution/algorithm to use to calculate the weights.
                               Supported solution strings - ``inverse_variance``, ``min_volatility``, ``max_sharpe``,
                               ``efficient_risk``, ``max_return_min_volatility``, ``max_diversification``, ``efficient_return``
                               and ``max_decorrelation``.
        :param target_return: (float) Target return of the portfolio.
        :param target_risk: (float) Target risk of the portfolio.
        :param risk_aversion: (float) Quantifies the risk averse nature of the investor - a higher value means
                                      more risk averse and vice-versa.
        :param weight_bounds: (dict/tuple) Can be either a single tuple of upper and lower bounds
                                           for all portfolio weights or a list of strings with each string representing
                                           an inequality on the weights. For e.g. to bound the weight of the 3rd asset
                                           pass the following weight bounds: ['weights[2] <= 0.3', 'weights[2] >= 0.1'].
        """

        self._error_checks(asset_names, asset_prices, expected_asset_returns,
                           covariance_matrix, solution)

        # Weight bounds
        if weight_bounds is not None:
            self.weight_bounds = weight_bounds

        # Calculate the expected asset returns and covariance matrix if not given by the user
        expected_asset_returns, covariance = self._calculate_estimators(
            asset_prices, expected_asset_returns, covariance_matrix)

        if solution == 'inverse_variance':
            self._inverse_variance(covariance=covariance,
                                   expected_returns=expected_asset_returns)
        elif solution == 'min_volatility':
            self._min_volatility(covariance=covariance,
                                 expected_returns=expected_asset_returns)
        elif solution == 'max_return_min_volatility':
            self._max_return_min_volatility(
                covariance=covariance,
                expected_returns=expected_asset_returns,
                risk_aversion=risk_aversion)
        elif solution == 'max_sharpe':
            self._max_sharpe(covariance=covariance,
                             expected_returns=expected_asset_returns)
        elif solution == 'efficient_risk':
            self._min_volatility_for_target_return(
                covariance=covariance,
                expected_returns=expected_asset_returns,
                target_return=target_return)
        elif solution == 'efficient_return':
            self._max_return_for_target_risk(
                covariance=covariance,
                expected_returns=expected_asset_returns,
                target_risk=target_risk)
        elif solution == 'max_diversification':
            self._max_diversification(covariance=covariance,
                                      expected_returns=expected_asset_returns)
        else:
            self._max_decorrelation(covariance=covariance,
                                    expected_returns=expected_asset_returns)

        # Calculate the portfolio sharpe ratio
        self.portfolio_sharpe_ratio = (
            (self.portfolio_return - self.risk_free_rate) /
            (self.portfolio_risk**0.5))

        # Do some post-processing of the weights
        self._post_process_weights()

    def allocate_custom_objective(self,
                                  non_cvxpy_variables,
                                  cvxpy_variables,
                                  objective_function,
                                  constraints=None):
        # pylint: disable=eval-used, exec-used
        """
        Create a portfolio using custom objective and constraints.

        :param non_cvxpy_variables: (dict) A dictionary of variables to be used for providing the required input matrices and
                                           other inputs required by the user. The key of dictionary will be the variable name
                                           while the value can be anything ranging from a numpy matrix, list, dataframe or number.
        :param cvxpy_variables: (list) This is a list of cvxpy specific variables that will be initialised in the format required
                                       by cvxpy. For e.g. ["risk = cp.quad_form(weights, covariance)"] where you are initialising
                                       a variable named "risk" using cvxpy. Note that cvxpy is being imported as "cp", so be sure
                                       to refer to cvxpy as cp.
        :param custom_objective: (str)  A custom objective function. You need to write it in the form
                                        expected by cvxpy. The objective will be a single string, e.g. 'cp.Maximise(
                                        expected_asset_returns)'.
        :param constraints: (list) a list of strings containing the optimisation constraints. For e.g. ['weights >= 0', 'weights <= 1']
        """

        # Initialise the non-cvxpy variables
        locals_ptr = locals()
        for variable_name, variable_value in non_cvxpy_variables.items():
            exec(variable_name + " = None")
            locals_ptr[variable_name] = variable_value

        self.num_assets = locals_ptr['num_assets']
        self.asset_names = list(map(str, range(self.num_assets)))
        if 'asset_names' in locals_ptr:
            self.asset_names = locals_ptr['asset_names']

        # Optimisation weights
        weights = cp.Variable(self.num_assets)
        weights.value = np.array([1 / self.num_assets] * self.num_assets)

        # Initialise cvxpy specific variables
        for variable in cvxpy_variables:
            exec(variable)

        # Optimisation objective and constraints
        allocation_objective = eval(objective_function)
        allocation_constraints = []
        for constraint in constraints:
            allocation_constraints.append(eval(constraint))

        # Define and solve the problem
        problem = cp.Problem(objective=allocation_objective,
                             constraints=allocation_constraints)
        problem.solve(warm_start=True)
        if weights.value is None:
            raise ValueError('No optimal set of weights found.')
        self.weights = weights.value

        # Calculate portfolio metrics
        if 'risk' in locals_ptr:
            self.portfolio_risk = locals_ptr['risk'].value
        if 'portfolio_return' in locals_ptr:
            self.portfolio_return = locals_ptr['portfolio_return'].value

        # Do some post-processing of the weights
        self._post_process_weights()

    def get_portfolio_metrics(self):
        """
        Prints the portfolio metrics - return, risk and Sharpe Ratio.
        """

        print("Portfolio Return = %s" % self.portfolio_return)
        print("Portfolio Risk = %s" % self.portfolio_risk)
        print("Portfolio Sharpe Ratio = %s" % self.portfolio_sharpe_ratio)

    def plot_efficient_frontier(self,
                                covariance,
                                expected_asset_returns,
                                min_return=0,
                                max_return=0.4,
                                risk_free_rate=0.05):
        # pylint: disable=broad-except
        """
        Plot the Markowitz efficient frontier.

        :param covariance: (pd.DataFrame) Covariance dataframe of asset returns.
        :param expected_asset_returns: (list/np.array/pd.dataframe) A list of mean stock returns (mu).
        :param min_return: (float) Minimum target return.
        :param max_return: (float) Maximum target return.
        :param risk_free_rate: (float) The rate of return for a risk-free asset.
        """

        expected_returns = np.array(expected_asset_returns).reshape(
            (len(expected_asset_returns), 1))
        volatilities = []
        returns = []
        sharpe_ratios = []
        for portfolio_return in np.linspace(min_return, max_return, 100):
            try:
                self.allocate(covariance_matrix=covariance,
                              expected_asset_returns=expected_returns,
                              solution='efficient_risk',
                              target_return=portfolio_return)
                volatilities.append(self.portfolio_risk**0.5)
                returns.append(portfolio_return)
                sharpe_ratios.append((portfolio_return - risk_free_rate) /
                                     (self.portfolio_risk**0.5 + 1e-16))
            except Exception:
                continue
        max_sharpe_ratio_index = sharpe_ratios.index(max(sharpe_ratios))
        min_volatility_index = volatilities.index(min(volatilities))
        figure = plt.scatter(volatilities,
                             returns,
                             c=sharpe_ratios,
                             cmap='viridis')
        plt.colorbar(label='Sharpe Ratio')
        plt.scatter(volatilities[max_sharpe_ratio_index],
                    returns[max_sharpe_ratio_index],
                    marker='*',
                    color='g',
                    s=400,
                    label='Maximum Sharpe Ratio')
        plt.scatter(volatilities[min_volatility_index],
                    returns[min_volatility_index],
                    marker='*',
                    color='r',
                    s=400,
                    label='Minimum Volatility')
        plt.xlabel('Volatility')
        plt.ylabel('Return')
        plt.legend(loc='upper left')
        return figure

    def _error_checks(self,
                      asset_names,
                      asset_prices,
                      expected_asset_returns,
                      covariance_matrix,
                      solution=None):
        """
        Some initial error checks on the inputs.

        :param asset_names: (list) A list of strings containing the asset names.
        :param asset_prices: (pd.DataFrame) A dataframe of historical asset prices (daily close).
        :param expected_asset_returns: (list/np.array/pd.dataframe) A list of mean stock returns (mu).
        :param covariance_matrix: (pd.DataFrame/numpy matrix) User supplied covariance matrix of asset returns (sigma).
        :param solution: (str) The type of solution/algorithm to use to calculate the weights.
                               Currently supported solution strings - inverse_variance, min_volatility, max_sharpe,
                               efficient_risk, max_return_min_volatility, max_diversification, efficient_return
                               and max_decorrelation.
        """

        if asset_prices is None and (expected_asset_returns is None
                                     or covariance_matrix is None):
            raise ValueError(
                "You need to supply either raw prices or expected returns "
                "and a covariance matrix of asset returns")

        if asset_prices is not None:
            if not isinstance(asset_prices, pd.DataFrame):
                raise ValueError("Asset prices matrix must be a dataframe")
            if not isinstance(asset_prices.index, pd.DatetimeIndex):
                raise ValueError(
                    "Asset prices dataframe must be indexed by date.")

        if solution is not None and solution not in {
                "inverse_variance", "min_volatility", "max_sharpe",
                "efficient_risk", "max_return_min_volatility",
                "max_diversification", "efficient_return", "max_decorrelation"
        }:
            raise ValueError(
                "Unknown solution string specified. Supported solutions - "
                "inverse_variance, min_volatility, max_sharpe, efficient_risk"
                "max_return_min_volatility, max_diversification, efficient_return and max_decorrelation"
            )

        if asset_names is None:
            if asset_prices is not None:
                asset_names = asset_prices.columns
            elif covariance_matrix is not None and isinstance(
                    covariance_matrix, pd.DataFrame):
                asset_names = covariance_matrix.columns
            else:
                raise ValueError("Please provide a list of asset names")
        self.asset_names = asset_names
        self.num_assets = len(asset_names)

    def _calculate_estimators(self, asset_prices, expected_asset_returns,
                              covariance_matrix):
        """
        Calculate the expected returns and covariance matrix of assets in the portfolio.

        :param asset_prices: (pd.DataFrame) A dataframe of historical asset prices (daily close).
        :param expected_asset_returns: (list/np.array/pd.dataframe) A list of mean stock returns (mu).
        :param covariance_matrix: (pd.DataFrame/numpy matrix) User supplied covariance matrix of asset returns (sigma).
        :return: (np.array, pd.DataFrame) Expected asset returns and covariance matrix.
        """

        # Calculate the expected returns if the user does not supply any returns
        if expected_asset_returns is None:
            if self.calculate_expected_returns == "mean":
                expected_asset_returns = self.returns_estimator.calculate_mean_historical_returns(
                    asset_prices=asset_prices)
            elif self.calculate_expected_returns == "exponential":
                expected_asset_returns = self.returns_estimator.calculate_exponential_historical_returns(
                    asset_prices=asset_prices)
            else:
                raise ValueError(
                    "Unknown returns specified. Supported returns - mean, exponential"
                )
        expected_asset_returns = np.array(expected_asset_returns).reshape(
            (len(expected_asset_returns), 1))

        # Calculate covariance of returns or use the user specified covariance matrix
        if covariance_matrix is None:
            returns = self.returns_estimator.calculate_returns(
                asset_prices=asset_prices)
            covariance_matrix = returns.cov()
        cov = pd.DataFrame(covariance_matrix,
                           index=self.asset_names,
                           columns=self.asset_names)

        return expected_asset_returns, cov

    def _post_process_weights(self):
        """
        Check weights for very small numbers and numbers close to 1. A final post-processing of weights produced by the
        optimisation procedures.
        """

        # Round weights which are very very small negative numbers (e.g. -4.7e-16) to 0
        self.weights[self.weights < 0] = 0

        # If any of the weights is very close to one, we convert it to 1 and set the other asset weights to 0.
        if True in set(np.isclose(self.weights, 1)):
            almost_one_index = np.isclose(self.weights, 1)
            self.weights[almost_one_index] = 1
            self.weights[np.logical_not(almost_one_index)] = 0

        self.weights = pd.DataFrame(self.weights)
        self.weights.index = self.asset_names
        self.weights = self.weights.T

    def _inverse_variance(self, covariance, expected_returns):
        """
        Calculate weights using inverse-variance allocation.

        :param covariance: (pd.DataFrame) Covariance dataframe of asset returns.
        :param expected_returns: (list/np.array/pd.dataframe) A list of mean stock returns (mu).
        """

        ivp = 1. / np.diag(covariance)
        ivp /= ivp.sum()
        self.weights = ivp
        self.portfolio_risk = np.dot(self.weights,
                                     np.dot(covariance.values, self.weights.T))
        self.portfolio_return = np.dot(self.weights, expected_returns)[0]

    def _min_volatility(self, covariance, expected_returns):
        # pylint: disable=eval-used
        """
        Compute minimum volatility portfolio allocation.

        :param covariance: (pd.DataFrame) Covariance dataframe of asset returns.
        :param expected_returns: (list/np.array/pd.dataframe) A list of mean stock returns (mu).
        """

        weights = cp.Variable(self.num_assets)
        weights.value = np.array([1 / self.num_assets] * self.num_assets)
        risk = cp.quad_form(weights, covariance)
        portfolio_return = cp.matmul(weights, expected_returns)

        # Optimisation objective and constraints
        allocation_objective = cp.Minimize(0.5 * risk)
        allocation_constraints = [
            cp.sum(weights) == 1,
        ]
        if isinstance(self.weight_bounds, tuple):
            allocation_constraints.extend([
                weights >= self.weight_bounds[0],
                weights <= min(self.weight_bounds[1], 1)
            ])
        else:
            for inequality in self.weight_bounds:
                allocation_constraints.append(eval(inequality))

            # Add the hard-boundaries for weights.
            allocation_constraints.extend([weights <= 1, weights >= 0])

        # Define and solve the problem
        problem = cp.Problem(objective=allocation_objective,
                             constraints=allocation_constraints)
        problem.solve(warm_start=True)
        if weights.value is None:
            raise ValueError('No optimal set of weights found.')

        self.weights = weights.value
        self.portfolio_risk = risk.value
        self.portfolio_return = portfolio_return.value[0]

    def _max_return_min_volatility(self, covariance, expected_returns,
                                   risk_aversion):
        # pylint: disable=eval-used
        """
        Calculate maximum return-minimum volatility portfolio allocation.

        :param covariance: (pd.DataFrame) Covariance dataframe of asset returns.
        :param expected_returns: (list/np.array/pd.dataframe) A list of mean stock returns (mu).
        :param risk_aversion: (float) Quantifies the risk-averse nature of the investor - a higher value means
                           more risk averse and vice-versa.
        """

        weights = cp.Variable(self.num_assets)
        weights.value = np.array([1 / self.num_assets] * self.num_assets)
        portfolio_return = cp.matmul(weights, expected_returns)
        risk = cp.quad_form(weights, covariance)

        # Optimisation objective and constraints
        allocation_objective = cp.Minimize(risk_aversion * risk -
                                           portfolio_return)
        allocation_constraints = [cp.sum(weights) == 1]
        if isinstance(self.weight_bounds, tuple):
            allocation_constraints.extend([
                weights >= self.weight_bounds[0],
                weights <= min(self.weight_bounds[1], 1)
            ])
        else:
            for inequality in self.weight_bounds:
                allocation_constraints.append(eval(inequality))

            # Add the hard-boundaries for weights.
            allocation_constraints.extend([weights <= 1, weights >= 0])

        # Define and solve the problem
        problem = cp.Problem(objective=allocation_objective,
                             constraints=allocation_constraints)
        problem.solve(warm_start=True)
        if weights.value is None:
            raise ValueError('No optimal set of weights found.')

        self.weights = weights.value
        self.portfolio_risk = risk.value
        self.portfolio_return = portfolio_return.value[0]

    def _max_sharpe(self, covariance, expected_returns):
        # pylint: disable=invalid-name, eval-used
        """
        Compute maximum Sharpe portfolio allocation.

        :param covariance: (pd.DataFrame) Covariance dataframe of asset returns.
        :param expected_returns: (list/np.array/pd.dataframe) A list of mean stock returns (mu).
        """

        y = cp.Variable(self.num_assets)
        y.value = np.array([1 / self.num_assets] * self.num_assets)
        kappa = cp.Variable(1)
        risk = cp.quad_form(y, covariance)
        weights = y / kappa
        portfolio_return = cp.matmul(weights, expected_returns)

        # Optimisation objective and constraints
        allocation_objective = cp.Minimize(risk)
        allocation_constraints = [
            cp.sum((expected_returns - self.risk_free_rate).T @ y) == 1,
            cp.sum(y) == kappa, kappa >= 0
        ]
        if isinstance(self.weight_bounds, tuple):
            allocation_constraints.extend([
                y >= kappa * self.weight_bounds[0],
                y <= kappa * self.weight_bounds[1]
            ])
        else:
            for inequality in self.weight_bounds:
                allocation_constraints.append(eval(inequality))

            # Add the hard-boundaries for weights.
            allocation_constraints.extend([y <= kappa, y >= 0])

        # Define and solve the problem
        problem = cp.Problem(objective=allocation_objective,
                             constraints=allocation_constraints)
        problem.solve(warm_start=True)
        if y.value is None or kappa.value is None:
            raise ValueError('No optimal set of weights found.')

        self.weights = weights.value
        self.portfolio_risk = risk.value
        self.portfolio_return = portfolio_return.value[0]

    def _min_volatility_for_target_return(self, covariance, expected_returns,
                                          target_return):
        # pylint: disable=eval-used
        """
        Calculate minimum volatility portfolio for a given target return.

        :param covariance: (pd.DataFrame) Covariance dataframe of asset returns.
        :param expected_returns: (list/np.array/pd.dataframe) A list of mean stock returns (mu).
        :param target_return: (float) Target return of the portfolio.
        """

        weights = cp.Variable(self.num_assets)
        weights.value = np.array([1 / self.num_assets] * self.num_assets)
        risk = cp.quad_form(weights, covariance)
        portfolio_return = cp.matmul(weights, expected_returns)

        # Optimisation objective and constraints
        allocation_objective = cp.Minimize(risk)
        allocation_constraints = [
            cp.sum(weights) == 1,
            portfolio_return >= target_return,
        ]
        if isinstance(self.weight_bounds, tuple):
            allocation_constraints.extend([
                weights >= self.weight_bounds[0],
                weights <= min(self.weight_bounds[1], 1)
            ])
        else:
            for inequality in self.weight_bounds:
                allocation_constraints.append(eval(inequality))

            # Add the hard-boundaries for weights.
            allocation_constraints.extend([weights <= 1, weights >= 0])

        # Define and solve the problem
        problem = cp.Problem(objective=allocation_objective,
                             constraints=allocation_constraints)
        problem.solve()
        if weights.value is None:
            raise ValueError('No optimal set of weights found.')

        self.weights = weights.value
        self.portfolio_risk = risk.value
        self.portfolio_return = target_return

    def _max_return_for_target_risk(self, covariance, expected_returns,
                                    target_risk):
        # pylint: disable=eval-used
        """
        Calculate maximum return for a given target volatility/risk.

        :param covariance: (pd.DataFrame) Covariance dataframe of asset returns.
        :param expected_returns: (list/np.array/pd.dataframe) A list of mean stock returns (mu).
        :param target_risk: (float) Target risk of the portfolio.
        """

        weights = cp.Variable(self.num_assets)
        weights.value = np.array([1 / self.num_assets] * self.num_assets)
        portfolio_return = cp.matmul(weights, expected_returns)
        risk = cp.quad_form(weights, covariance)

        # Optimisation objective and constraints
        allocation_objective = cp.Maximize(portfolio_return)
        allocation_constraints = [cp.sum(weights) == 1, risk <= target_risk]
        if isinstance(self.weight_bounds, tuple):
            allocation_constraints.extend([
                weights >= self.weight_bounds[0],
                weights <= min(self.weight_bounds[1], 1)
            ])
        else:
            for inequality in self.weight_bounds:
                allocation_constraints.append(eval(inequality))

            # Add the hard-boundaries for weights.
            allocation_constraints.extend([weights <= 1, weights >= 0])

        # Define and solve the problem
        problem = cp.Problem(objective=allocation_objective,
                             constraints=allocation_constraints)
        problem.solve()
        if weights.value is None:
            raise ValueError('No optimal set of weights found.')

        self.weights = weights.value
        self.portfolio_risk = target_risk
        self.portfolio_return = portfolio_return.value[0]

    def _max_diversification(self, covariance, expected_returns):
        """
        Calculate the maximum diversified portfolio.

        :param covariance: (pd.DataFrame) Covariance dataframe of asset returns.
        :param expected_returns: (list/np.array/pd.dataframe) A list of mean stock returns (mu).
        """

        self._max_decorrelation(covariance, expected_returns)

        # Divide weights by individual asset volatilities
        self.weights /= np.diag(covariance)

        # Standardize weights
        self.weights /= np.sum(self.weights)

        portfolio_return = np.dot(expected_returns.T, self.weights)[0]
        risk = np.dot(self.weights, np.dot(covariance, self.weights.T))

        self.portfolio_risk = risk
        self.portfolio_return = portfolio_return

    def _max_decorrelation(self, covariance, expected_returns):
        # pylint: disable=eval-used
        """
        Calculate the maximum decorrelated portfolio.

        :param covariance: (pd.DataFrame) Covariance dataframe of asset returns.
        :param expected_returns: (list/np.array/pd.dataframe) A list of mean stock returns (mu).
        """

        weights = cp.Variable(self.num_assets)
        weights.value = np.array([1 / self.num_assets] * self.num_assets)
        risk = cp.quad_form(weights, covariance)
        portfolio_return = cp.matmul(weights, expected_returns)
        corr = self.risk_estimators.cov_to_corr(covariance)
        portfolio_correlation = cp.quad_form(weights, corr)

        # Optimisation objective and constraints
        allocation_objective = cp.Minimize(portfolio_correlation)
        allocation_constraints = [cp.sum(weights) == 1]
        if isinstance(self.weight_bounds, tuple):
            allocation_constraints.extend([
                weights >= self.weight_bounds[0],
                weights <= min(self.weight_bounds[1], 1)
            ])
        else:
            for inequality in self.weight_bounds:
                allocation_constraints.append(eval(inequality))

            # Add the hard-boundaries for weights.
            allocation_constraints.extend([weights <= 1, weights >= 0])

        # Define and solve the problem
        problem = cp.Problem(objective=allocation_objective,
                             constraints=allocation_constraints)
        problem.solve(warm_start=True)
        if weights.value is None:
            raise ValueError('No optimal set of weights found.')

        self.weights = weights.value
        self.portfolio_risk = risk.value
        self.portfolio_return = portfolio_return.value[0]
示例#2
0
class CriticalLineAlgorithm:
    # pylint: disable=too-many-instance-attributes
    """
    This class implements the famous Critical Line Algorithm (CLA) for mean-variance portfolio optimisation. It is reproduced with
    modification from the following paper: `D.H. Bailey and M.L. Prado “An Open-Source Implementation of the Critical- Line
    Algorithm for Portfolio Optimization”,Algorithms, 6 (2013), 169-196. <http://dx.doi.org/10.3390/a6010169>`_.

    The Critical Line Algorithm is a famous portfolio optimisation algorithm used for calculating the optimal allocation weights
    for a given portfolio. It solves the optimisation problem with optimisation constraints on each weight - lower and upper
    bounds on the weight value. This class can compute multiple types of solutions:

    1. CLA Turning Points
    2. Minimum Variance
    3. Maximum Sharpe
    4. Efficient Frontier Allocations
    """
    def __init__(self,
                 weight_bounds=(0, 1),
                 calculate_expected_returns="mean"):
        """
        Initialise the storage arrays and some preprocessing.

        :param weight_bounds: (tuple) A tuple specifying the lower and upper bound ranges for the portfolio weights.
        :param calculate_expected_returns: (str) The method to use for calculation of expected returns.
                                                 Currently supports ``mean`` and ``exponential``
        """

        self.weight_bounds = weight_bounds
        self.calculate_expected_returns = calculate_expected_returns
        self.weights = list()
        self.lambdas = list()
        self.gammas = list()
        self.free_weights = list()
        self.max_sharpe = None
        self.min_var = None
        self.efficient_frontier_means = None
        self.efficient_frontier_sigma = None
        self.returns_estimator = ReturnsEstimators()
        self.risk_estimators = RiskEstimators()

    def allocate(self,
                 asset_names=None,
                 asset_prices=None,
                 expected_asset_returns=None,
                 covariance_matrix=None,
                 solution="cla_turning_points",
                 resample_by=None):
        # pylint: disable=consider-using-enumerate,too-many-locals,too-many-branches,too-many-statements,bad-continuation
        """
        Calculate the portfolio asset allocations using the method specified.

        :param asset_names: (list) List of strings containing the asset names.
        :param asset_prices: (pd.Dataframe) Dataframe of historical asset prices (adj closed).
        :param expected_asset_returns: (list) List of mean asset returns (mu).
        :param covariance_matrix: (pd.Dataframe/numpy matrix) User supplied covariance matrix of asset returns.
        :param solution: (str) Specifies the type of solution to compute. Supported strings: ``cla_turning_points``, ``max_sharpe``,
                               ``min_volatility``, ``efficient_frontier``.
        :param resample_by: (str) Specifies how to resample the prices - weekly, daily, monthly etc.. Defaults to
                                  None for no resampling.
        """

        # Initial checks
        if asset_prices is None and (expected_asset_returns is None
                                     or covariance_matrix is None):
            raise ValueError(
                "You need to supply either raw prices or expected returns "
                "and a covariance matrix of asset returns")

        if asset_prices is not None:
            if not isinstance(asset_prices, pd.DataFrame):
                raise ValueError("Asset prices matrix must be a dataframe")
            if not isinstance(asset_prices.index, pd.DatetimeIndex):
                raise ValueError(
                    "Asset prices dataframe must be indexed by date.")

        if asset_names is None:
            if asset_prices is not None:
                asset_names = asset_prices.columns
            else:
                raise ValueError("Please provide a list of asset names")

        # Some initial steps before the algorithm runs
        cov_matrix, expected_returns, lower_bounds, upper_bounds = self._initialise(
            asset_prices=asset_prices,
            resample_by=resample_by,
            expected_asset_returns=expected_asset_returns,
            covariance_matrix=covariance_matrix)

        # Compute the turning points, free sets and weights
        free_weights, weights = self._init_algo(expected_returns, lower_bounds,
                                                upper_bounds)
        self.weights.append(np.copy(weights))  # store solution
        self.lambdas.append(None)
        self.gammas.append(None)
        self.free_weights.append(free_weights[:])
        while True:

            # 1) Bound one free weight
            lambda_in, i_in, bi_in = self._bound_free_weight(
                cov_matrix, expected_returns, lower_bounds, upper_bounds,
                free_weights)

            # 2) Free one bounded weight
            lambda_out, i_out = self._free_bound_weight(
                cov_matrix, expected_returns, free_weights)

            # 3) Compute minimum variance solution
            if (lambda_in is None or lambda_in < 0) and (lambda_out is None
                                                         or lambda_out < 0):
                self.lambdas.append(0)
                covar_f, covar_fb, mean_f, w_b = self._get_matrices(
                    cov_matrix, expected_returns, free_weights)
                covar_f_inv = np.linalg.inv(covar_f)
                mean_f = np.zeros(mean_f.shape)

            # 4) Decide whether to free a bounded weight or bound a free weight
            else:
                if self._infnone(lambda_in) > self._infnone(lambda_out):
                    self.lambdas.append(lambda_in)
                    free_weights.remove(i_in)
                    weights[i_in] = bi_in  # set value at the correct boundary
                else:
                    self.lambdas.append(lambda_out)
                    free_weights.append(i_out)
                covar_f, covar_fb, mean_f, w_b = self._get_matrices(
                    cov_matrix, expected_returns, free_weights)
                covar_f_inv = np.linalg.inv(covar_f)

            # 5) Compute solution vector
            w_f, gamma = self._compute_w(covar_f_inv, covar_fb, mean_f, w_b)
            for i in range(len(free_weights)):
                weights[free_weights[i]] = w_f[i]
            self.weights.append(np.copy(weights))  # store solution
            self.gammas.append(gamma)
            self.free_weights.append(free_weights[:])
            if self.lambdas[-1] == 0:
                break

        # 6) Purge turning points
        self._purge_num_err(lower_bounds, upper_bounds, 10e-10)
        self._purge_excess(expected_returns)

        # Compute the specified solution
        self._compute_solution(asset_names, solution, cov_matrix,
                               expected_returns)

    def _initialise(self, asset_prices, expected_asset_returns,
                    covariance_matrix, resample_by):
        # pylint: disable=invalid-name, too-many-branches, bad-continuation
        """
        Initialise covariances, upper-bounds, lower-bounds and storage buffers.

        :param asset_prices: (pd.Dataframe) Dataframe of asset prices indexed by date.
        :param expected_asset_returns: (list) A list of mean stock returns (mu).
        :param covariance_matrix: (pd.Dataframe) User supplied dataframe of asset returns indexed by date. Used for
                                                 calculation of covariance matrix.
        :param resample_by: (str) Specifies how to resample the prices - weekly, daily, monthly etc.. Defaults to
                                  'B' meaning daily business days which is equivalent to no resampling.
        :return: (Numpy matrix, Numpy array, Numpy array, Numpy array) Data matrices and bounds arrays.
        """

        # Calculate the returns if the user does not supply a returns matrix
        expected_returns = expected_asset_returns
        if expected_asset_returns is None:
            if self.calculate_expected_returns == "mean":
                expected_returns = self.returns_estimator.calculate_mean_historical_returns(
                    asset_prices=asset_prices, resample_by=resample_by)
            elif self.calculate_expected_returns == "exponential":
                expected_returns = self.returns_estimator.calculate_exponential_historical_returns(
                    asset_prices=asset_prices, resample_by=resample_by)
            else:
                raise ValueError(
                    "Unknown returns specified. Supported returns - mean, exponential"
                )
        expected_returns = np.array(expected_returns).reshape(
            (len(expected_returns), 1))
        if (expected_returns == np.ones(expected_returns.shape) *
                expected_returns.mean()).all():
            expected_returns[-1, 0] += 1e-5

        # Calculate the covariance matrix
        if covariance_matrix is None:
            returns = self.returns_estimator.calculate_returns(
                asset_prices=asset_prices, resample_by=resample_by)
            covariance_matrix = returns.cov()
        cov_matrix = np.asarray(covariance_matrix)

        # Intialise lower bounds
        if isinstance(self.weight_bounds[0], numbers.Real):
            lower_bounds = np.ones(
                expected_returns.shape) * self.weight_bounds[0]
        else:
            lower_bounds = np.array(
                self.weight_bounds[0]).astype(float).reshape(
                    expected_returns.shape)

        # Intialise upper bounds
        if isinstance(self.weight_bounds[1], numbers.Real):
            upper_bounds = np.ones(
                expected_returns.shape) * self.weight_bounds[1]
        else:
            upper_bounds = np.array(
                self.weight_bounds[1]).astype(float).reshape(
                    expected_returns.shape)

        return cov_matrix, expected_returns, lower_bounds, upper_bounds

    @staticmethod
    def _infnone(number):
        """
        Converts a Nonetype object to inf.

        :param number: (int/float/None) Number.
        :return: (float) -inf or number.
        """
        return float("-inf") if number is None else number

    @staticmethod
    def _init_algo(expected_returns, lower_bounds, upper_bounds):
        """
        Initial setting up of the algorithm. Calculates the first free weight of the first turning point.

        :param expected_asset_returns: (Numpy array) A list of mean stock returns (mu).
        :param lower_bounds: (Numpy array) An array containing lower bound values for the weights.
        :param upper_bounds: (Numpy array) An array containing upper bound values for the weights.
        :return: (list, list) asset index and the corresponding free weight value.
        """

        # Form structured array
        structured_array = np.zeros((expected_returns.shape[0]),
                                    dtype=[("id", int), ("mu", float)])
        expected_returns_ = [
            expected_returns[i][0] for i in range(expected_returns.shape[0])
        ]  # dump array into list

        # Fill structured array
        structured_array[:] = list(
            zip(list(range(expected_returns.shape[0])), expected_returns_))

        # Sort structured array based on increasing return value
        expected_returns = np.sort(structured_array, order="mu")

        # First free weight
        index, weights = expected_returns.shape[0], np.copy(lower_bounds)
        while np.sum(weights) < 1:
            index -= 1

            # Set weights one by one to the upper bounds
            weights[expected_returns[index][0]] = upper_bounds[
                expected_returns[index][0]]
        weights[expected_returns[index][0]] += 1 - np.sum(weights)
        return [expected_returns[index][0]], weights

    @staticmethod
    def _compute_bi(c_final, asset_bounds_i):
        """
        Calculates which bound value to assign to a bounded asset - lower bound or upper bound.

        :param c_final: (float) A value calculated using the covariance matrices of free weights.
                                Refer to https://pdfs.semanticscholar.org/4fb1/2c1129ba5389bafe47b03e595d098d0252b9.pdf
                                for more information.
        :param asset_bounds_i: (list) A list containing the lower and upper bound values for the ith weight.
        :return: (float) Bounded weight value.
        """

        if c_final > 0:
            return asset_bounds_i[1][0]
        return asset_bounds_i[0][0]

    def _compute_w(self, covar_f_inv, covar_fb, mean_f, w_b):
        """
        Compute the turning point associated with the current set of free weights F.

        :param covar_f_inv: (np.array) Inverse of covariance matrix of free assets.
        :param covar_fb: (np.array) Covariance matrix between free assets and bounded assets.
        :param mean_f: (np.array) Expected returns of free assets.
        :param w_b: (np.array) Bounded asset weight values.
        :return: (array, float) List of turning point weights and gamma value from the lagrange equation.
        """

        # Compute gamma
        ones_f = np.ones(mean_f.shape)
        g_1 = np.dot(np.dot(ones_f.T, covar_f_inv), mean_f)
        g_2 = np.dot(np.dot(ones_f.T, covar_f_inv), ones_f)
        if w_b is None:
            g_final, w_1 = float(-self.lambdas[-1] * g_1 / g_2 + 1 / g_2), 0
        else:
            ones_b = np.ones(w_b.shape)
            g_3 = np.dot(ones_b.T, w_b)
            g_4 = np.dot(covar_f_inv, covar_fb)
            w_1 = np.dot(g_4, w_b)
            g_4 = np.dot(ones_f.T, w_1)
            g_final = float(-self.lambdas[-1] * g_1 / g_2 +
                            (1 - g_3 + g_4) / g_2)

        # Compute weights
        w_2 = np.dot(covar_f_inv, ones_f)
        w_3 = np.dot(covar_f_inv, mean_f)
        free_asset_weights = -1 * w_1 + g_final * w_2 + self.lambdas[-1] * w_3
        return free_asset_weights, g_final

    def _compute_lambda(self, covar_f_inv, covar_fb, mean_f, w_b, asset_index,
                        b_i):
        """
        Calculate the lambda value in the lagrange optimsation equation.

        :param covar_f_inv: (np.array) Inverse of covariance matrix of free assets.
        :param covar_fb: (np.array) Covariance matrix between free assets and bounded assets.
        :param mean_f: (np.array) Expected returns of free assets.
        :param w_b: (np.array) Bounded asset weight values.
        :param asset_index: (int) Index of the asset in the portfolio.
        :param b_i: (list) List of upper and lower bounded weight values.
        :return: (float) Lambda value.
        """

        # Compute C
        ones_f = np.ones(mean_f.shape)
        c_1 = np.dot(np.dot(ones_f.T, covar_f_inv), ones_f)
        c_2 = np.dot(covar_f_inv, mean_f)
        c_3 = np.dot(np.dot(ones_f.T, covar_f_inv), mean_f)
        c_4 = np.dot(covar_f_inv, ones_f)
        c_final = -1 * c_1 * c_2[asset_index] + c_3 * c_4[asset_index]
        if c_final == 0:
            return None, None

        # Compute bi
        if isinstance(b_i, list):
            b_i = self._compute_bi(c_final, b_i)

        # Compute Lambda
        if w_b is None:

            # All free assets
            return float((c_4[asset_index] - c_1 * b_i) / c_final), b_i

        ones_b = np.ones(w_b.shape)
        l_1 = np.dot(ones_b.T, w_b)
        l_2 = np.dot(covar_f_inv, covar_fb)
        l_3 = np.dot(l_2, w_b)
        l_2 = np.dot(ones_f.T, l_3)
        lambda_value = float(((1 - l_1 + l_2) * c_4[asset_index] - c_1 *
                              (b_i + l_3[asset_index])) / c_final)
        return lambda_value, b_i

    def _get_matrices(self, cov_matrix, expected_returns, free_weights):
        """
        Calculate the required matrices between free and bounded assets.

        :param cov_matrix: (Numpy matrix) Covariance matrix of asset returns.
        :param expected_returns: (Numpy array) Array of mean asset returns (mu).
        :param free_weights: (list) List of free assets/weights.
        :return: (tuple of np.array matrices) The corresponding matrices.
        """

        covar_f = self._reduce_matrix(cov_matrix, free_weights, free_weights)
        mean_f = self._reduce_matrix(expected_returns, free_weights, [0])
        bounded_weights = self._get_bounded_weights(expected_returns,
                                                    free_weights)
        covar_fb = self._reduce_matrix(cov_matrix, free_weights,
                                       bounded_weights)
        w_b = self._reduce_matrix(self.weights[-1], bounded_weights, [0])
        return covar_f, covar_fb, mean_f, w_b

    def _get_bounded_weights(self, expected_returns, free_weights):
        """
        Compute the list of bounded assets.

        :param expected_returns: (Numpy array) Array of mean asset returns (mu).
        :param free_weights: (np.array) List of free weights/assets.
        :return: (np.array) List of bounded assets/weights.
        """

        return self._diff_lists(list(range(expected_returns.shape[0])),
                                free_weights)

    @staticmethod
    def _diff_lists(list_1, list_2):
        """
        Calculate the set difference between two lists.

        :param list_1: (list) A list of asset indices.
        :param list_2: (list) Another list of asset indices.
        :return: (list) Set difference between the two input lists.
        """

        return list(set(list_1) - set(list_2))

    @staticmethod
    def _reduce_matrix(matrix, row_indices, col_indices):
        """
        Reduce a matrix to the provided set of rows and columns.

        :param matrix: (np.array) A matrix whose subset of rows and columns we need.
        :param row_indices: (list) List of row indices for the matrix.
        :param col_indices: (list) List of column indices for the matrix.
        :return: (np.array) Subset of input matrix.
        """

        return matrix[np.ix_(row_indices, col_indices)]

    def _purge_num_err(self, lower_bounds, upper_bounds, tol):
        """
        Purge violations of inequality constraints (associated with ill-conditioned cov matrix).

        :param lower_bounds: (Numpy array) An array containing lower bound values for the weights.
        :param upper_bounds: (Numpy array) An array containing upper bound values for the weights.
        :param tol: (float) Tolerance level for purging.
        """

        index_1 = 0
        while True:
            flag = False
            if index_1 == len(self.weights):
                break
            if abs(sum(self.weights[index_1]) - 1) > tol:
                flag = True
            else:
                for index_2 in range(len(self.weights[index_1])):
                    if (self.weights[index_1][index_2] - lower_bounds[index_2]
                            < -tol or self.weights[index_1][index_2] -
                            upper_bounds[index_2] > tol):
                        flag = True
                        break
            if flag is True:
                del self.weights[index_1]
                del self.lambdas[index_1]
                del self.gammas[index_1]
                del self.free_weights[index_1]
            else:
                index_1 += 1

    def _purge_excess(self, expected_returns):
        """
        Remove violations of the convex hull.

        :param expected_returns: (Numpy array) Array of mean asset returns (mu).
        """

        index_1, repeat = 0, False
        while True:
            if repeat is False:
                index_1 += 1
            if index_1 >= len(self.weights) - 1:
                break
            weights = self.weights[index_1]
            mean = np.dot(weights.T, expected_returns)[0, 0]
            index_2, repeat = index_1 + 1, False
            while True:
                if index_2 == len(self.weights):
                    break
                weights = self.weights[index_2]
                mean_ = np.dot(weights.T, expected_returns)[0, 0]
                if mean < mean_:
                    del self.weights[index_1]
                    del self.lambdas[index_1]
                    del self.gammas[index_1]
                    del self.free_weights[index_1]
                    repeat = True
                    break
                index_2 += 1

    @staticmethod
    def _golden_section(obj, left, right, **kwargs):
        """
        Golden section method. Maximum if kargs['minimum']==False is passed.

        :param obj: (function) The objective function on which the extreme will be found.
        :param left: (float) The leftmost extreme of search.
        :param right: (float) The rightmost extreme of search.
        """

        tol, sign, args = 1.0e-9, -1, None
        args = kwargs.get("args", None)
        num_iterations = int(ceil(-2.078087 * log(tol / abs(right - left))))
        gs_ratio = 0.618033989
        complementary_gs_ratio = 1.0 - gs_ratio

        # Initialize
        x_1 = gs_ratio * left + complementary_gs_ratio * right
        x_2 = complementary_gs_ratio * left + gs_ratio * right
        f_1 = sign * obj(x_1, *args)
        f_2 = sign * obj(x_2, *args)

        # Loop
        for _ in range(num_iterations):
            if f_1 > f_2:
                left = x_1
                x_1 = x_2
                f_1 = f_2
                x_2 = complementary_gs_ratio * left + gs_ratio * right
                f_2 = sign * obj(x_2, *args)
            else:
                right = x_2
                x_2 = x_1
                f_2 = f_1
                x_1 = gs_ratio * left + complementary_gs_ratio * right
                f_1 = sign * obj(x_1, *args)

        if f_1 < f_2:
            return x_1, sign * f_1
        return x_2, sign * f_2

    @staticmethod
    def _eval_sr(alpha, cov_matrix, expected_returns, w_0, w_1):
        """
        Evaluate the sharpe ratio of the portfolio within the convex combination.

        :param alpha: (float) Convex combination value.
        :param cov_matrix: (Numpy matrix) Covariance matrix of asset returns.
        :param expected_returns: (Numpy array) Array of mean asset returns (mu).
        :param w_0: (list) First endpoint of convex combination of weights.
        :param w_1: (list) Second endpoint of convex combination of weights.
        :return:
        """

        weights = alpha * w_0 + (1 - alpha) * w_1
        returns = np.dot(weights.T, expected_returns)[0, 0]
        volatility = np.dot(np.dot(weights.T, cov_matrix), weights)[0, 0]**0.5
        return returns / volatility

    def _bound_free_weight(self, cov_matrix, expected_returns, lower_bounds,
                           upper_bounds, free_weights):
        """
        Add a free weight to list of bounded weights.

        :param cov_matrix: (Numpy matrix) Covariance matrix of asset returns.
        :param expected_returns: (Numpy array) Array of mean asset returns (mu).
        :param lower_bounds: (Numpy array) An array containing lower bound values for the weights.
        :param upper_bounds: (Numpy array) An array containing upper bound values for the weights.
        :param free_weights: (list) List of free-weight indices.
        :return: (float, int, int) Lambda value, index of free weight to be bounded, bound weight value.
        """

        lambda_in = None
        i_in = None
        bi_in = None
        if len(free_weights) > 1:
            covar_f, covar_fb, mean_f, w_b = self._get_matrices(
                cov_matrix, expected_returns, free_weights)
            covar_f_inv = np.linalg.inv(covar_f)

            j = 0
            for i in free_weights:
                lambda_i, b_i = self._compute_lambda(
                    covar_f_inv, covar_fb, mean_f, w_b, j,
                    [lower_bounds[i], upper_bounds[i]])
                if self._infnone(lambda_i) > self._infnone(lambda_in):
                    lambda_in, i_in, bi_in = lambda_i, i, b_i
                j += 1
        return lambda_in, i_in, bi_in

    def _free_bound_weight(self, cov_matrix, expected_returns, free_weights):
        """
        Add a bounded weight to list of free weights.

        :param cov_matrix: (Numpy matrix) Covariance matrix of asset returns.
        :param expected_returns: (Numpy array) Array of mean asset returns (mu).
        :param free_weights: (list) List of free-weight indices.
        :return: (float, int) Lambda value, index of the bounded weight to be made free.
        """

        lambda_out = None
        i_out = None
        if len(free_weights) < expected_returns.shape[0]:
            bounded_weight_indices = self._get_bounded_weights(
                expected_returns, free_weights)
            for i in bounded_weight_indices:
                covar_f, covar_fb, mean_f, w_b = self._get_matrices(
                    cov_matrix, expected_returns, free_weights + [i])
                covar_f_inv = np.linalg.inv(covar_f)
                lambda_i, _ = self._compute_lambda(
                    covar_f_inv,
                    covar_fb,
                    mean_f,
                    w_b,
                    mean_f.shape[0] - 1,
                    self.weights[-1][i],
                )
                if (self.lambdas[-1] is None or lambda_i < self.lambdas[-1]
                    ) and lambda_i > self._infnone(lambda_out):
                    lambda_out, i_out = lambda_i, i
        return lambda_out, i_out

    def _compute_solution(self, assets, solution, covariance_matrix,
                          expected_returns):
        """
        Compute the desired solution to the portfolio optimisation problem.

        :param assets: (list) A list of asset names.
        :param solution: (str) Specify the type of solution to compute. Options are: cla_turning_points, max_sharpe,
                               min_volatility, efficient_frontier.
        :param covariance_matrix: (Numpy matrix) Covariance matrix of asset returns.
        :param expected_returns: (Numpy array) Array of mean asset returns (mu).
        """

        if solution == "max_sharpe":
            self.max_sharpe, self.weights = self._max_sharpe(
                covariance_matrix, expected_returns)
            self.weights = pd.DataFrame(self.weights)
            self.weights.index = assets
            self.weights = self.weights.T
        elif solution == "min_volatility":
            self.min_var, self.weights = self._min_volatility(
                covariance_matrix)
            self.weights = pd.DataFrame(self.weights)
            self.weights.index = assets
            self.weights = self.weights.T
        elif solution == "efficient_frontier":
            self.efficient_frontier_means, self.efficient_frontier_sigma, self.weights = self._efficient_frontier(
                covariance_matrix, expected_returns)
            weights_copy = self.weights.copy()
            for i, turning_point in enumerate(weights_copy):
                self.weights[i] = turning_point.reshape(1, -1)[0]
            self.weights = pd.DataFrame(self.weights, columns=assets)
        elif solution == "cla_turning_points":
            # Reshape the weight matrix
            weights_copy = self.weights.copy()
            for i, turning_point in enumerate(weights_copy):
                self.weights[i] = turning_point.reshape(1, -1)[0]
            self.weights = pd.DataFrame(self.weights, columns=assets)
        else:
            raise ValueError(
                "Unknown solution string specified. Supported solutions - cla_turning_points, "
                "efficient_frontier, min_volatility, max_sharpe")

    def _max_sharpe(self, cov_matrix, expected_returns):
        """
        Compute the maximum sharpe portfolio allocation.

        :param cov_matrix: (Numpy matrix) Covariance matrix of asset returns.
        :param expected_returns: (Numpy array) Array of mean asset returns (mu).
        :return: (float, np.array) Tuple of max. sharpe value and the set of weight allocations.
        """

        # 1) Compute the local max SR portfolio between any two neighbor turning points
        w_sr, sharpe_ratios = [], []
        for i in range(len(self.weights) - 1):
            w_0 = np.copy(self.weights[i])
            w_1 = np.copy(self.weights[i + 1])
            kwargs = {
                "minimum": False,
                "args": (cov_matrix, expected_returns, w_0, w_1)
            }
            alpha, sharpe_ratio = self._golden_section(self._eval_sr, 0, 1,
                                                       **kwargs)
            w_sr.append(alpha * w_0 + (1 - alpha) * w_1)
            sharpe_ratios.append(sharpe_ratio)

        maximum_sharp_ratio = max(sharpe_ratios)
        weights_with_max_sharpe_ratio = w_sr[sharpe_ratios.index(
            maximum_sharp_ratio)]
        return maximum_sharp_ratio, weights_with_max_sharpe_ratio

    def _min_volatility(self, cov_matrix):
        """
        Compute minimum volatility portfolio allocation.

        :param cov_matrix: (Numpy matrix) Covariance matrix of asset returns.
        :return: (float, np.array) Tuple of minimum variance value and the set of weight allocations.
        """

        var = []
        for weights in self.weights:
            volatility = np.dot(np.dot(weights.T, cov_matrix), weights)
            var.append(volatility)
        min_var = min(var)
        return min_var**.5, self.weights[var.index(min_var)]

    def _efficient_frontier(self, cov_matrix, expected_returns, points=100):
        # pylint: disable=invalid-name
        """
        Compute the entire efficient frontier solution.

        :param cov_matrix: (Numpy matrix) Covariance matrix of asset returns.
        :param expected_returns: (Numpy array) Array of mean asset returns (mu).
        :param points: (int) Number of efficient frontier points to be calculated.
        :return: (tuple) Tuple of mean, variance amd weights of the frontier solutions.
        """

        means, sigma, weights = [], [], []

        # remove the 1, to avoid duplications
        partitions = np.linspace(0, 1, points // len(self.weights))[:-1]
        b = list(range(len(self.weights) - 1))
        for i in b:
            w_0, w_1 = self.weights[i], self.weights[i + 1]

            if i == b[-1]:
                # include the 1 in the last iteration
                partitions = np.linspace(0, 1, points // len(self.weights))

            for j in partitions:
                w = w_1 * j + (1 - j) * w_0
                weights.append(np.copy(w))
                means.append(np.dot(w.T, expected_returns)[0, 0])
                sigma.append(np.dot(np.dot(w.T, cov_matrix), w)[0, 0]**0.5)
        return means, sigma, weights