How to Optimize an S&P 500 Index Portfolio Using Python and Quantum Annealing

Multiverse Computing
6 min readJul 25, 2023

This new package makes it even easier for analysts to use Singularity and improve financial performance with quantum computing

We are adding a new feature to Singularity, powered by the foundational engine Singularity Optimization, to make it easier for “quants” to build the best-performing portfolios. The Portfolio Optimization Python Package v0.5 brings this popular programming language to our quantum software platform.

In this post, we’ll review how quantitative analysts can perform portfolio optimization with Python. The Python package for Singularity provides an easy method for programmers to perform portfolio optimization without needing to know the technical details of how a quantum computer works or how to access it. If you’re new to Singularity, our previous post on how to optimize a portfolio with Excel is a good starting point for learning about the platform.

Python Tutorial

The examples covered in this section use real market data for three of the largest market-cap stocks in the S&P 500 (SPX) index as of January 2023, namely Exxon Mobil Corporation (XOM), Johnson & Johnson (JNJ), and Apple Inc. (AAPL). The publicly available input data included below reflects the annual return for these assets across 2022 and the covariance over that period based on daily price changes. This simple example which considers three assets can easily be extended with the tool to much larger scenarios, such as all 500 stocks in the SPX or more.

Import the Singularity PO Library

After installation and authentication, you can import Singularity Portfolio Optimization in a straightforward and familiar way like this:

>>> import singularity.portfolio_optimization as po

Assets

The first step in defining an optimization job is to provide the assets. The assets can be defined in two ways:

  1. Number of assets

The following code will generate a list of 3 assets:

>>> assets = po.Assets(3)

2. Name of assets

The following code will generate a list of 3 assets with their names assigned to them. These names are for the purpose of making it easier to work with the assets. Asset names remain anonymous and are not sent to the server.

>>> asset_names = [“XOM”, “JNJ”, “AAPL”]

>>> assets = po.Assets(asset_names)

If you name your assets (as shown below), when defining the other data (returns, correlations, etc.) you can use a dictionary (dict) and set the value for each specific asset by name and the order does not matter. If you don’t name your assets (as shown in the full documentation), you must use list to set the value of each asset in order.

Market Model

The next step is to define a market model, capturing the details of the assets under consideration and their relationship to each other. A market model can be defined in two ways.

  1. By providing returns, correlations, and volatilities

>>> market = po.MarketModel(returns=returns, correlations=correlations, volatilities=volatilities)

2. By providing returns and covariances

>>> market = po.MarketModel(returns=returns, covariances=covariances)

The way these values can be defined are explained below.

Returns

The returns of the assets should be defined as a dict similar to the following code:

1 returns = {

2 “XOM”: 0.00757,

3 “JNJ”: 0.00509,

4 “AAPL”: 0.0283,

5 }

Correlations

The correlations of the assets should be defined as a dict similar to the following code:

1 correlations = {

2 (“XOM”, “XOM”): 1.0,

3 (“XOM”, “JNJ”): 0.0921,

4 (“XOM”, “AAPL”): 0.279,

5 (“JNJ”, “JNJ”): 1.0,

6 (“AAPL”, “JNJ”): 0.367,

7 (“AAPL”, “AAPL”): 1.0,

8 }

Volatilities

The volatilities of the assets should be defined as a dict similar to the following code:

1 volatilities = {

2 “XOM”: 0.022,

3 “JNJ”: 0.011,

4 “AAPL”: 0.0225,

5 }

Covariances

The covariances of the assets (used in place of correlations and volatilities) should be defined as a dict similar to the following code:

1 covariances = {

2 (“XOM”, “XOM”): 0.000484,

3 (“XOM”, “JNJ”): 2.24e-05,

4 (“XOM”, “AAPL”): 0.000138,

5 (“JNJ”, “JNJ”): 0.000122,

6 (“JNJ”, “AAPL”): 9.13e-05,

7 (“AAPL”, “AAPL”): 0.000508,

8 }

Investor Preferences

Next, define the investor preferences, capturing the specific interests of the investor trading in the market. An investor preferences object requires max investment per asset, investment bands, resolution, and either risk aversion or target volatility. You can provide optional constraints as well.

  1. Creating investor preferences using risk aversion

1 investor = po.InvestorPreferences(

2 risk_aversion=100.0,

3 max_investment_per_asset=0.5,

4 investment_bands=investment_bands,

5 resolution=0.01,

6 constraints=constraints,

7 )

2. Creating investor preferences using target volatility

1 investor = po.InvestorPreferences(

2 target_volatility=0.055,

3 max_investment_per_asset=0.6,

4 investment_bands=investment_bands,

5 resolution=0.01,

6 constraints=constraints,

7 )

Investment Bands

The investment bands of the assets should be defined as a dict containing only the assets that you want to put a band on (placing upper and lower bounds on the investment in specific assets). It should be similar to the following code:

1 investment_bands = {

2 “XOM”: (0.05, 0.2),

3 “JNJ”: (0.1, None),

4 }

Constraints

A constraint consists of a left-hand side, an operator, a right-hand side, and a name. The supported operators are ==, >=, and <=. The left-hand side should be an expression containing a linear combination of assets and the right-hand side can either be another linear expression involving the assets or a number. The left-hand expressions can be at most linear in the asset variables (no quadratic or higher-order terms).

The example below uses a case where two named assets together should be at least 50% of the portfolio:

The mathematical expression of the constraint is: assets[“AAPL”] + assets[“XOM”] >= 0.5

1 constraint1 = po.Constraint(

2 lhs=(assets[“AAPL”] + assets[“XOM”]),

3 operator=”>=”,

4 rhs=0.5,

5 name=”min_investment”,

6 )

After preparing each constraint you can build your constraints list and add it to your investor preferences. Similar to the code below:

>>> constraints = [constraint1, constraint2] # A constraints list consisting of two constraints

Portfolio Model

By having your assets, market_model, and investor_preferences ready, you can now define the portfolio model. The code below shows how it can be done.

1 portfolio_model = po.PortfolioModel(

2 assets=assets,

3 market_model=market,

4 investor_preferences=investor

5 )

Optimization

To run the optimization, run the optimize method of the portfolio model and provide the name of the solver.

>>> portfolio = portfolio_model.optimize(solver=”Multiverse_Hybrid”)

Depending on the portfolio model, the optimization might take anything from a few seconds to a few minutes.

For running an optimization job, a solver should be chosen from the three options in Singularity. The Multiverse Hybrid is a quantum-classical hybrid solver, combining the power of D-Wave’s quantum hardware with additional computations on classical hardware specifically suited to portfolio optimization problems. The D-Wave Leap Hybrid is a general-use quantum-classical hybrid solver offered by D-Wave Systems. The Classical is a purely classical approach made available for benchmarking purposes.

Result

After getting the result, you can use the portfolio, including accessing the optimal holdings or getting the portfolio metrics.

>>> portfolio.holdings

OrderedDict([(‘XOM’, 0.06), (‘JNJ’, 0.44), (‘AAPL’, 0.5)])

>>> portfolio.get_metrics()

Metrics(portfolio_return=0.0168438, portfolio_volatility=0.014212540941014032, portfolio_sharpe_ratio=1.1851364277440901)

Examples

Named Assets With Covariances

Example of a complete application of Singularity Portfolio Optimization which uses named assets with covariances:

1import logging

2import singularity.portfolio_optimization as po

3

4if __name__ == “__main__”:

5 # Enables logging.

6 po.add_logger(level=logging.INFO)

7

8 # Creates a list of assets for the given names.

9 asset_names = [“XOM”, “JNJ”, “AAPL”]

10 assets = po.Assets(asset_names)

11

12 # Specifies the return of each asset.

13 # Alternatively could use a list. Refer to the unnamed asset example.

14 returns: po.Returns = {

15 “XOM”: 0.00757,

16 “JNJ”: 0.00509,

17 “AAPL”: 0.0283,

18 }

19

20 # Specifies the covariances of the assets.

21 # Alternatively could use a list. Refer to the unnamed asset example.

22 covariances: po.Covariances = {

23 (“XOM”, “XOM”): 0.000484,

24 (“XOM”, “JNJ”): 2.24e-05,

25 (“XOM”, “AAPL”): 0.000138,

26 (“JNJ”, “JNJ”): 0.000122,

27 (“JNJ”, “AAPL”): 9.13e-05,

28 (“AAPL”, “AAPL”): 0.000508,

29 }

30

31 # Creates the market model.

32 market = po.MarketModel(returns=returns, covariances=covariances)

33

34 # Set the investment bands of the specified assets.

35 # Alternatively could use a list. Refer to the unnamed asset example.

36 investment_bands: po.InvestmentBands = {

37 “XOM”: (0.05, 0.2),

38 “JNJ”: (0.1, None),

39 }

40

41 # Creates the investor preferences.

42 investor = po.InvestorPreferences(

43 risk_aversion=100,

44 max_investment_per_asset=0.5,

45 investment_bands=investment_bands,

46 resolution=0.01,

47 )

48

49 # Creates the portfolio model.

50 portfolio_model = po.PortfolioModel(

51 assets=assets,

52 market_model=market,

53 investor_preferences=investor

54 )

55

56 # Runs the optimization with the classical solver.

57 portfolio = portfolio_model.optimize(solver=”Classical”)

58

59 # Uses portfolio.

60 # The ‘holdings’ would be a dictionary corresponding to each asset name.

61 print(portfolio)

62 print(portfolio.get_metrics())

--

--

Multiverse Computing

Multiverse uses quantum and quantum-inspired software to tackle complex problems in finance, energy and manufacturing to deliver value today.