Intermediate

Maximizing Portfolio Value

Imagine you're interested in creating a portfolio of assets from the S&P500 index. Your primary goal is to ensure the security of your investment by minimizing the risk of potential losses. At the same time, you aim to achieve a certain minimum portfolio value to make the investment worthwhile. Additionally, you have a specific number of assets in mind that you intend to include in your portfolio.

As you explore our collection of pre-implemented Use Cases, you discover that we've already developed a solution tailored to your requirements. To define the problem, you need historical data on the returns of the candidate assets over a specified time period.

Let's assume you've prepared a CSV file containing the assets from the S&P500 index that you're considering for purchase. In this example, let's consider a scenario where you want to create a portfolio consisting of 2 out of 5 assets from the S&P500 index and achieve a target return that is higher or equal to the 75th percentile of all returns. Your data might look something like this:

DateAAPLAMZNGOOGLMETAMSFT
2023-05-02-0.0061910.015483-0.017537-0.016202-0.000491
2023-05-03-0.0064670.0001930.000855-0.009238-0.003307
2023-05-04-0.0099130.003377-0.006830-0.0148080.003318
2023-05-050.0469270.0159620.008406-0.0031690.017157
2023-05-08-0.0004030.0016090.0208390.002105-0.006438
2023-05-09-0.0099710.007465-0.0038970.000429-0.005346
2023-05-100.0104210.0334830.040987-0.0012430.017296
2023-05-110.0010950.0180600.0431320.011627-0.007044
2023-05-12-0.005418-0.0171150.008064-0.008397-0.003676
2023-05-15-0.0028970.008525-0.0085100.0215990.001586
2023-05-160.0000000.0197840.025749-0.0001670.007368
2023-05-170.0036030.0185190.0111290.0153670.009452
2023-05-180.0136660.0229440.0164680.0179800.014395
2023-05-190.000628-0.016081-0.000570-0.004902-0.000565
2023-05-22-0.005481-0.0106670.0186540.0109100.008921
2023-05-23-0.015155-0.000174-0.019912-0.006363-0.018432
2023-05-240.0016320.015306-0.0135440.010011-0.004472
2023-05-250.006692-0.0149890.0213400.0139640.038458
2023-05-260.0141050.0444350.0091510.0370020.021386

These data show the change in the value of these 5 assets from May 2nd 2023 until May 26th 2023 for the top 5 assets from the S&P500 index. You can simply read in this data from a CSV file for further usage within Luna.

# You can use pandas to read a CSV file
import pandas as pd

# Read the CSV file containing the assets you want to choose from
assets = pd.read_csv('portfolio.csv')

Creating a Portfolio Optimization Instance

To optimize the selection of assets for your portfolio, you can use our LunaSolve service. To do this, you need to create a portfolio optimization (PO) instance, which requires your data to be formatted in a way suitable for optimization algorithms. In the case of portfolio optimization, we've already established a mathematical format for this purpose. Specifically, we use a Quadratic Unconstrained Binary Optimization (QUBO) problem formulation.

For more details on this formulation, you can refer to our Portfolio Optimization Use Case. There, you'll find information about the problem's mathematical representation. Note that we require a two-dimensional list as input data. Therefore, let's start by transforming your CSV file into the correct format: a list of lists containing the time series of returns per asset.

# Numpy allows for easier modification of the data
import numpy as np

# Remove the Date column from the data
assets_no_date = assets.iloc[:, 1:]

# Convert the data to a numpy array
returns_np = np.array(assets_no_date)

# Switch axes to retrieve the time series of each asset
returns_np = np.transpose(returns_np)

# Convert the returns to a list of lists
returns = returns_np.tolist()

Now we only need to define our target return and the number of assets we want to buy.

# Define the minimum return you want to achieve, at least the 75% quartile of all returns
target_return = np.percentile(returns, 75)

# Define the number of assets you want to buy
n_assets = 2

Next, we can send your portfolio data to Luna and create the optimization instance.

# Load the luna packages
from luna_sdk import LunaSolve
from luna_sdk.schemas.use_cases import PortfolioOptimization

# Create a Luna object and set your credentials
luna = LunaSolve(email="YOUREMAIL", password="YOURPASSWORD")

# Define that we want to create an instance of a PO with the corresponding data
use_case = PortfolioOptimization(returns=returns, R=target_return, n=n_assets)

# Create a PO instance and retrieve the corresponding ID
created_optimization = luna.optimization.create_from_use_case(
    name="a name", use_case=use_case
)

This completes the process of creating a PO instance through Luna. We are now ready to move on to the selection of an algorithm to solve our optimization problem.

Solving the problem instance

Luna offers a diverse range of pre-implemented algorithms that are designed to tackle various types of optimization problems. You have the option to manually choose an algorithm that suits your needs or use our Recommendation Engine to suggest the most suitable algorithm. In this tutorial, let's assume that you'd like to apply a solver developed by us — the hybrid QAGA+ solver. This evolutionary algorithm combines classical hardware with Quantum Annealers from D-Wave Systems.

QAGA+ is a highly configurable algorithm with numerous hyperparameters that can be adjusted to enhance performance across different optimization problems. For simplicity, we will only configure the three main parameters used in evolutionary algorithms in this tutorial:

  • p_size: The population size, determining how many individuals remain at the end of each iteration.
  • mut_rate: The mutation rate, indicating the likelihood of each individual undergoing mutation.
  • rec_rate: The recommendation rate, determining the number of mates each individual is paired with in each iteration.

In practical scenarios, we recommend reviewing the complete list of available parameters and adjusting them based on your specific needs when selecting your algorithm. While default parameters are configured to function effectively across a wide range of applications, they might not always yield optimal performance.

Now, let's proceed with the necessary steps to solve the PO instance using QAGA+. Firstly, we need to establish our access token for the hardware provided by D-Wave Systems (this is only necessary once). If you don't possess such a token, you can obtain one directly from D-Wave, or you can reach out to us for assistance and questions about the process.

# Load the necessary package to set a QPU token
from luna_sdk.schemas.qpu_token import QpuToken

# Set your token to access the Quantum Annealer from D-Wave Systems
qpu_token: = luna.qpu_token.create_qpu_token(
    name='TOKENNAME', provider="dwave", token="<TOKEN>"
)

Next, we can configure the solver and its corresponding parameters to proceed with solving the optimization problem using LunaSolve:

from luna_sdk.schemas.enums.solver import SolverEnum

# Define QAGA+ as our solver
solver = "QAGA+"

# Define the three parameters for QAGA+ we want to set
params = {
    'p_size': 40,
    'mut_rate': 0.3,
    'rec_rate': 2
}

# Solve the PO instance using the QAGA+ algorithms and retrieve a solution url
solution = luna.solution.create(
    optimization_id=created_optimization.id, 
    solver=solver, 
    solver_parameters=params
)

Given that QAGA+ involves computations on real quantum hardware from D-Wave Systems, the execution might require some time. To ensure that you don't have to wait for the code to finish executing, we have designed the process to be asynchronous. This means that when you request a solution, LunaSolve takes care of all the necessary steps in the background, allowing you the freedom to engage in other tasks while the solution is being computed. You can simply return when the execution is complete.

There are two ways to retrieve your solution:

  1. Raw Solution: This corresponds to the solution derived from the mathematical model of the problem. In our scenario, where we're dealing with QUBOs, this will be a binary vector.

  2. Transformed Solution: In this case, the raw solution, represented in mathematical format, has been translated back into the initial problem domain. For our portfolio optimization, this means obtaining the list of assets that should be purchased.

To begin, let's examine the raw solution:

# After the execution of your algorithm has been finished, retrieve your raw solution
solution = luna.solution.get(
    solution_id=solution.id
)

# Print the raw solution
print(solution)

Output from raw solution

{
    "id": "64b6aafa40fce273e81a83c2",
    "status": "ready",
    "solution": {
        "samples": [
            [
                False,
                True,
                False,
                True,
                False,
                False,
                True,
                False,
                False,
                True,
                True,
                True,
                False,
                False,
                False,
                True,
                False,
                False,
                False,
                True,
                False,
                False,
                True,
                False,
                True,
                False,
                False,
                True,
                False,
                True,
                False,
            ]
        ],
        "energies": [-4993926.178292751],
        "solver": "QAGA",
        "params": {
            "p_size": 40,
            "p_max": 62,
            "p_increase": 8,
            "add_random_percent": 0.013139542716724795,
            "mut_rate": 0.3,
            "num_sweeps": 7,
            "num_sweeps_inc_max": 170,
            "num_sweeps_inc": 1.2371683676197422,
            "beta_range": [31.146886766221193, 15.703883087993717],
            "rec_rate": 2,
            "rec_method": "random_crossover",
            "multiple_select": False,
        },
        "runtime": {"total": 0.008881330490112305, "overhead": None, "qpu": None},
        "metadata": {
            "beta_range": [2.7922498552367933e-10, 0.003573423576559224],
            "beta_schedule_type": "geometric",
        },
    },
}

However, the raw output provided by these algorithms might not be particularly informative, especially when working with our pre-implemented problem formulations. Instead, let's focus on the transformed solution, which is more aligned with the initial problem formulation and easier to interpret in the context of our use case.

Luna only retains the indices of each asset, not their names, but you can easily access the names in your original data.

representation = luna.solution.get_use_case_representation(solution.id)

# Get the names of your initial assets
asset_names = assets.columns.tolist()[1:]

# Retrieve the names of the assets you should buy from the solution
assets_to_buy = [asset_names[i] for i in representation.results[0]]

# Print the assets to buy
print(assets_to_buy)

Assets to buy

['AAPL', 'META']

The

And there you have it – we've reached the end of this tutorial! With Luna, you have the capability to efficiently solve optimization problems and apply a wide range of algorithms. You're also free to configure each step in the process according to your needs. If you're looking for more inspiration, there's a whole library of Use Cases and Algorithms available for exploration. Alternatively, you're welcome to explore our Expert Tutorials, which provide more in-depth insights. For instance, you can delve into solving your own unique optimization problems using Luna's capabilities.

Was this page helpful?