Source code for qiskit_finance.applications.optimization.portfolio_optimization

# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2018, 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""An application class for a portfolio optimization problem."""
from typing import List, Tuple, Union, Optional

import numpy as np
from docplex.mp.advmodel import AdvModel

from qiskit_optimization.algorithms import OptimizationResult
from qiskit_optimization.applications import OptimizationApplication
from qiskit_optimization.problems import QuadraticProgram
from qiskit_optimization.translators import from_docplex_mp
from qiskit_finance.exceptions import QiskitFinanceError


[docs]class PortfolioOptimization(OptimizationApplication): """Optimization application for the "portfolio optimization" [1] problem. References: [1]: "Portfolio optimization", https://en.wikipedia.org/wiki/Portfolio_optimization """ def __init__( self, expected_returns: np.ndarray, covariances: np.ndarray, risk_factor: float, budget: int, bounds: Optional[List[Tuple[int, int]]] = None, ) -> None: """ Args: expected_returns: The expected returns for the assets. covariances: The covariances between the assets. risk_factor: The risk appetite of the decision maker. budget: The budget, i.e. the number of assets to be selected. bounds: The list of tuples for the lower bounds and the upper bounds of each variable. e.g. [(lower bound1, upper bound1), (lower bound2, upper bound2), ...]. Default is None which means all the variables are binary variables. """ self._expected_returns = expected_returns self._covariances = covariances self._risk_factor = risk_factor self._budget = budget self._bounds = bounds self._check_compatibility(bounds)
[docs] def to_quadratic_program(self) -> QuadraticProgram: """Convert a portfolio optimization problem instance into a :class:`~qiskit_optimization.QuadraticProgram`. Returns: The :class:`~qiskit_optimization.QuadraticProgram` created from the portfolio optimization problem instance. """ self._check_compatibility(self._bounds) num_assets = len(self._expected_returns) mdl = AdvModel(name="Portfolio optimization") if self.bounds: x = [ mdl.integer_var(lb=self.bounds[i][0], ub=self.bounds[i][1], name=f"x_{i}") for i in range(num_assets) ] else: x = [mdl.binary_var(name=f"x_{i}") for i in range(num_assets)] quad = mdl.quad_matrix_sum(self._covariances, x) linear = np.dot(self._expected_returns, x) mdl.minimize(self._risk_factor * quad - linear) mdl.add_constraint(mdl.sum(x[i] for i in range(num_assets)) == self._budget) op = from_docplex_mp(mdl) return op
[docs] def portfolio_expected_value(self, result: Union[OptimizationResult, np.ndarray]) -> float: """Returns the portfolio expected value based on the result. Args: result: The calculated result of the problem Returns: The portfolio expected value """ x = self._result_to_x(result) return np.dot(self._expected_returns, x)
[docs] def portfolio_variance(self, result: Union[OptimizationResult, np.ndarray]) -> float: """Returns the portfolio variance based on the result Args: result: The calculated result of the problem Returns: The portfolio variance """ x = self._result_to_x(result) return np.dot(x, np.dot(self._covariances, x))
[docs] def interpret(self, result: Union[OptimizationResult, np.ndarray]) -> List[int]: """Interpret a result as a list of asset indices Args: result: The calculated result of the problem Returns: The list of asset indices whose corresponding variable is 1 """ x = self._result_to_x(result) return [i for i, x_i in enumerate(x) if x_i]
def _check_compatibility(self, bounds) -> None: """Check the compatibility of given variables""" if len(self._expected_returns) != len(self._covariances) or not all( len(self._expected_returns) == len(row) for row in self._covariances ): raise QiskitFinanceError( "The sizes of expected_returns and covariances do not match. ", f"expected_returns: {self._expected_returns}, covariances: {self._covariances}.", ) if bounds is not None: if ( not isinstance(bounds, list) or not all(isinstance(lb_, int) for lb_, _ in bounds) or not all(isinstance(ub_, int) for _, ub_ in bounds) ): raise QiskitFinanceError( f"The bounds must be a list of tuples of integers. {bounds}", ) if any(ub_ < lb_ for lb_, ub_ in bounds): raise QiskitFinanceError( "The upper bound of each variable, in the list of bounds, must be greater ", f"than or equal to the lower bound. {bounds}", ) if len(bounds) != len(self._expected_returns): raise QiskitFinanceError( f"The lengths of the bounds, {len(self._bounds)}, do not match to ", f"the number of types of assets, {len(self._expected_returns)}.", ) @property def expected_returns(self) -> np.ndarray: """Getter of expected_returns Returns: The expected returns for the assets. """ return self._expected_returns @expected_returns.setter def expected_returns(self, expected_returns: np.ndarray) -> None: """Setter of expected_returns Args: expected_returns: The expected returns for the assets. """ self._expected_returns = expected_returns @property def covariances(self) -> np.ndarray: """Getter of covariances Returns: The covariances between the assets. """ return self._covariances @covariances.setter def covariances(self, covariances: np.ndarray) -> None: """Setter of covariances Args: covariances: The covariances between the assets. """ self._covariances = covariances @property def risk_factor(self) -> float: """Getter of risk_factor Returns: The risk appetite of the decision maker. """ return self._risk_factor @risk_factor.setter def risk_factor(self, risk_factor: float) -> None: """Setter of risk_factor Args: risk_factor: The risk appetite of the decision maker. """ self._risk_factor = risk_factor @property def budget(self) -> int: """Getter of budget Returns: The budget, i.e. the number of assets to be selected. """ return self._budget @budget.setter def budget(self, budget: int) -> None: """Setter of budget Args: budget: The budget, i.e. the number of assets to be selected. """ self._budget = budget @property def bounds(self) -> List[Tuple[int, int]]: """Getter of the lower bounds and upper bounds of each selectable assets. Returns: The lower bounds and upper bounds of each assets selectable """ return self._bounds @bounds.setter def bounds(self, bounds: List[Tuple[int, int]]) -> None: """Setter of the lower bounds and upper bounds of each selectable assets. Args: bounds: The lower bounds and upper bounds of each assets selectable """ self._check_compatibility(bounds) # check compatibility before setting bounds self._bounds = bounds