Qiskit Aqua: Financial Portfolio Optimization

The latest version of this notebook is available on https://github.com/Qiskit/qiskit-tutorial.


Contributors

Stefan Worner[1], Daniel Egger[1], Shaohan Hu[1], Stephen Wood[1], Marco Pistoia[1]

Affliation

  • [1]IBMQ

Introduction

This tutorial shows how to solve the following mean-variance portfolio optimization problem for $n$ assets:

$\begin{aligned} \min_{x \in \{0, 1\}^n} q x^T \Sigma x - \mu^T x\\ \text{subject to: } 1^T x = B \end{aligned}$

where we use the following notation:

  • $x \in \{0, 1\}^n$ denotes the vector of binary decision variables, which indicate which assets to pick ($x[i] = 1$) and which not to pick ($x[i] = 0$),
  • $\mu \in \mathbb{R}^n$ defines the expected returns for the assets,
  • $\Sigma \in \mathbb{R}^{n \times n}$ specifies the covariances between the assets,
  • $q > 0$ controls the risk appetite of the decision maker,
  • and $B$ denotes the budget, i.e. the number of assets to be selected out of $n$.

We assume the following simplifications:

  • all assets have the same price (normalized to 1),
  • the full budget $B$ has to be spent, i.e. one has to select exactly $B$ assets.

The equality constraint $1^T x = B$ is mapped to a penalty term $(1^T x - B)^2$ which is scaled by a parameter and subtracted from the objective function. The resulting problem can be mapped to a Hamiltonian whose groundstate corresponds to the optimal solution. This notebook shows how to use the Variational Quantum Eigensolver (VQE) or the Quantum Approximate Optimization Algorithm (QAOA) to find the optimal solution for a given set of parameters.


In [1]:
from qiskit_aqua import Operator, run_algorithm
from qiskit_aqua.input import get_input_instance
from qiskit_aqua.translators.ising import portfolio
import numpy as np

[Optional] Setup token to run the experiment on a real device

If you would like to run the experiement on a real device, you need to setup your account first.

Note: If you do not store your token yet, use IBMQ.save_accounts() to store it first.


In [2]:
from qiskit import IBMQ
IBMQ.load_accounts()

Here an Operator instance is created for our Hamiltonian. In this case the paulis are from an Ising Hamiltonian translated from the portfolio problem. We use a random portfolio problem for this notebook.


In [3]:
# set number of assets (= number of qubits)
num_assets = 4
# get random expected return vector (mu) and covariance matrix (sigma)
mu, sigma = portfolio.random_model(num_assets, seed=42)

q = 0.5 # set risk factor
budget = int(num_assets / 2) # set budget
penalty = num_assets # set parameter to scale the budget penalty term

qubitOp, offset = portfolio.get_portfolio_qubitops(mu, sigma, q, budget, penalty)
algo_input = get_input_instance('EnergyInput')
algo_input.qubit_op = qubitOp

We define some utility methods to print the results in a nice format.


In [4]:
def index_to_selection(i, num_assets):
    s = "{0:b}".format(i).rjust(num_assets)
    x = np.array([1 if s[i]=='1' else 0 for i in reversed(range(num_assets))])
    return x

def print_result(result):
    selection = portfolio.sample_most_likely(result['eigvecs'][0])
    value = portfolio.portfolio_value(selection, mu, sigma, q, budget, penalty)
    print('Optimal: selection {}, value {:.4f}'.format(selection, value))

    probabilities = np.abs(result['eigvecs'][0])**2
    i_sorted = reversed(np.argsort(probabilities))
    print('\n----------------- Full result ---------------------')
    print('selection\tvalue\t\tprobability')
    print('---------------------------------------------------')
    for i in i_sorted:
        x = index_to_selection(i, num_assets)
        value = portfolio.portfolio_value(x, mu, sigma, q, budget, penalty)    
        probability = probabilities[i]
        print('%10s\t%.4f\t\t%.4f' %(x, value, probability))

ExactEigensolver (as a classical reference)

Lets solve the problem. First classically...

We can now use the Operator we built above without regard to the specifics of how it was created. To run an algorithm we need to prepare a configuration params dictionary. We set the algorithm for the ExactEigensolver so we can have a classical reference. The problem is set for 'ising'. Backend is not required since this is computed classically not using quantum computation. The params, along with the algo input containing the operator, are now passed to the algorithm to be run. The result is returned as a dictionary.


In [5]:
algorithm_cfg = {
    'name': 'ExactEigensolver'
}

params = {
    'problem': {'name': 'ising'},
    'algorithm': algorithm_cfg
}
result = run_algorithm(params, algo_input)
print_result(result)


Optimal: selection [0 0 1 1], value -0.7012

----------------- Full result ---------------------
selection	value		probability
---------------------------------------------------
 [0 0 1 1]	-0.7012		1.0000
 [1 1 1 1]	15.6136		0.0000
 [0 1 1 1]	4.9012		0.0000
 [1 0 1 1]	3.0617		0.0000
 [1 1 0 1]	4.6445		0.0000
 [0 1 0 1]	2.1421		0.0000
 [1 0 0 1]	-0.4158		0.0000
 [0 0 0 1]	4.0314		0.0000
 [1 1 1 0]	2.6688		0.0000
 [0 1 1 0]	-0.5149		0.0000
 [1 0 1 0]	-0.2876		0.0000
 [0 0 1 0]	3.4782		0.0000
 [1 1 0 0]	-0.5110		0.0000
 [0 1 0 0]	4.5153		0.0000
 [1 0 0 0]	4.0242		0.0000
 [0 0 0 0]	16.0000		0.0000

Solution using VQE

We can now use the Variational Quantum Eigensolver (VQE) to solve the problem. We will specify the optimizer and variational form to be used.

Note: You can switch to different backends by providing the name of backend.


In [6]:
algorithm_cfg = {
    'name': 'VQE',
    'operator_mode': 'matrix'
}

optimizer_cfg = {
    'name': 'COBYLA',
    'maxiter': 25000
}

var_form_cfg = {
    'name': 'RYRZ',
    'depth': 3,
    'entanglement': 'full'
}

params = {
    'problem': {'name': 'ising', 'random_seed': 50},
    'algorithm': algorithm_cfg,
    'optimizer': optimizer_cfg,
    'variational_form': var_form_cfg,
    'backend': {'name': 'statevector_simulator'}
}
result = run_algorithm(params, algo_input)
print_result(result)


Optimal: selection [0 0 1 1], value -0.7012

----------------- Full result ---------------------
selection	value		probability
---------------------------------------------------
 [0 0 1 1]	-0.7012		1.0000
 [0 1 1 0]	-0.5149		0.0000
 [1 0 1 0]	-0.2876		0.0000
 [1 0 1 1]	3.0617		0.0000
 [1 1 1 0]	2.6688		0.0000
 [0 0 1 0]	3.4782		0.0000
 [0 0 0 1]	4.0314		0.0000
 [1 0 0 1]	-0.4158		0.0000
 [1 1 1 1]	15.6136		0.0000
 [0 1 1 1]	4.9012		0.0000
 [0 1 0 1]	2.1421		0.0000
 [0 0 0 0]	16.0000		0.0000
 [1 1 0 0]	-0.5110		0.0000
 [1 0 0 0]	4.0242		0.0000
 [0 1 0 0]	4.5153		0.0000
 [1 1 0 1]	4.6445		0.0000

Solution using QAOA

We also show here a result using the Quantum Approximate Optimization Algorithm (QAOA). This is another variational algorithm and it uses an internal variational form that is created based on the problem.


In [7]:
algorithm_cfg = {
    'name': 'QAOA.Variational',
    'p': 4,
    'operator_mode': 'matrix'
}

optimizer_cfg = {
    'name': 'COBYLA',
    'maxiter': 2500
}

params = {
    'problem': {'name': 'ising', 'random_seed': 50},
    'algorithm': algorithm_cfg,
    'optimizer': optimizer_cfg,
    'backend': {'name': 'statevector_simulator'}
}
result = run_algorithm(params, algo_input)
print_result(result)


Optimal: selection [0 0 1 1], value -0.7012

----------------- Full result ---------------------
selection	value		probability
---------------------------------------------------
 [0 0 1 1]	-0.7012		0.1956
 [1 1 0 0]	-0.5110		0.1838
 [1 0 0 1]	-0.4158		0.1825
 [0 1 1 0]	-0.5149		0.1811
 [1 0 1 0]	-0.2876		0.1636
 [0 1 0 1]	2.1421		0.0713
 [1 1 1 0]	2.6688		0.0115
 [1 0 1 1]	3.0617		0.0050
 [0 0 1 0]	3.4782		0.0016
 [0 0 0 1]	4.0314		0.0012
 [1 0 0 0]	4.0242		0.0008
 [0 1 1 1]	4.9012		0.0008
 [0 1 0 0]	4.5153		0.0005
 [1 1 0 1]	4.6445		0.0005
 [1 1 1 1]	15.6136		0.0001
 [0 0 0 0]	16.0000		0.0000