Skip to main content
IBM Quantum Platform

Otimização dinâmica de carteiras com o Portfolio Optimizer da Global Data Quantum

Nota

As funções do Qiskit são um recurso experimental disponível apenas para usuários dos planos IBM Quantum® Premium Plan, Flex Plan e On-Prem (via IBM Quantum Platform API). Eles estão no status de versão prévia e estão sujeitos a alterações.

Estimativa de uso: Aproximadamente 55 minutos em um processador Heron r2. (OBSERVAÇÃO: essa é apenas uma estimativa. O tempo de execução real pode variar)


Segundo plano

O problema de otimização dinâmica de portfólio tem como objetivo encontrar a estratégia de investimento ideal em vários períodos de tempo para maximizar o retorno esperado do portfólio e minimizar os riscos, geralmente sob certas restrições, como orçamento, custos de transação ou aversão ao risco. Diferentemente da otimização de portfólio padrão, que considera um único momento para reequilibrar o portfólio, a versão dinâmica leva em conta a natureza evolutiva dos ativos e adapta os investimentos com base nas mudanças no desempenho dos ativos ao longo do tempo.

Este tutorial demonstra como executar a otimização dinâmica de portfólio usando a função Qiskit do Quantum Portfolio Optimizer. Especificamente, ilustramos como usar essa função de aplicativo para resolver um problema de alocação de investimentos em várias etapas de tempo.

A abordagem envolve a formulação da otimização do portfólio como um problema de Otimização Binária Quadrática Sem Restrições (QUBO) com múltiplos objetivos. Especificamente, formulamos a função QUBO OO para otimizar simultaneamente quatro objetivos diferentes:

  • Maximizar a função de retorno FF
  • Minimizar o risco do investimento RR
  • Minimizar os custos de transação CC
  • Cumprir as restrições de investimento, formuladas em um termo adicional para minimizar PP.

Resumindo, para atingir esses objetivos, formulamos a função QUBO como O=F+γ2R+C+ρP,O = -F + \frac{\gamma}{2} R + C + \rho P, em que γ\gamma é o coeficiente de aversão ao risco e ρ\rho é o coeficiente de reforço de restrições (multiplicador de Lagrange). A formulação explícita pode ser encontrada na Eq. (15) de nosso manuscrito [1].

Resolvemos usando um método híbrido quântico-clássico baseado no Variational Quantum Eigensolver (VQE). Nessa configuração, o circuito quântico estima a função de custo, enquanto a otimização clássica é realizada usando o algoritmo de evolução diferencial, permitindo a navegação eficiente do cenário de soluções. O número de qubits necessários depende de três fatores principais: o número de ativos na, o número de períodos de tempo nt e a resolução de bits usada para representar o investimento nq. Especificamente, o número mínimo de qubits em nosso problema é na*nt*nq.

Neste tutorial, vamos nos concentrar na otimização de um portfólio regional com base no índice espanhol IBEX 35. Especificamente, usamos um portfólio de sete ativos, conforme indicado na tabela abaixo:

Portfólio IBEX 35
ACS.MC
ITX.MC
FER.MC
ELE.MC
SCYR.MC
AENA.MC
AMS.MC

Rebalanceamos nosso portfólio em quatro etapas, cada uma separada por um intervalo de 30 dias a partir de 1º de novembro de 2022. Cada variável de investimento é codificada usando dois bits. Isso resulta em um problema que requer 56 qubits para ser resolvido.

Usamos o ansatz Optimized Real Amplitudes, uma adaptação personalizada e eficiente em termos de hardware do ansatz Real Amplitudes padrão, especificamente adaptado para melhorar o desempenho desse tipo de problema de otimização financeira.

A execução quântica é realizada no backend ibm_torino . Para obter uma explicação detalhada da formulação do problema, da metodologia e da avaliação de desempenho, consulte o manuscrito publicado [1].


Requisitos

!pip install qiskit-ibm-catalog
!pip install pandas
!pip install matplotlib
!pip install yfinance

Instalação

Para usar o Quantum Portfolio Optimizer, selecione a função por meio do Catálogo de Funções do Qiskit. Você precisa de uma conta IBM Quantum Premium Plan ou Flex Plan com uma licença da Global Data Quantum para executar essa função.

Primeiro, faça a autenticação com sua chave de API. Em seguida, carregue a função desejada do Qiskit Functions Catalog. Aqui, você está acessando a quantum_portfolio_optimizer função do catálogo usando a QiskitFunctionsCatalog classe. Esta função permite-nos utilizar o solucionador Quantum Portfolio Optimization predefinido.

from qiskit_ibm_catalog import QiskitFunctionsCatalog

catalog = QiskitFunctionsCatalog(
    channel="ibm_quantum_platform",
    instance="INSTANCE_CRN",
    # For `token`, use the 44-character API_KEY you created
    # and saved from the IBM Quantum Platform Home dashboard
    token="YOUR_API_KEY",
)

# Access function
dpo_solver = catalog.load("global-data-quantum/quantum-portfolio-optimizer")

Passo 1: Leia a carteira de entrada

Nessa etapa, carregamos os dados históricos dos sete ativos selecionados do índice IBEX 35, especificamente de 1º de novembro de 2022 a 1º de abril de 2023.

Obtemos os dados usando a API do Yahoo Finance, com foco nos preços de fechamento. Os dados são então processados para garantir que todos os ativos tenham o mesmo número de dias com dados. Todos os dados ausentes (dias não comerciais) são tratados adequadamente, garantindo que todos os ativos estejam alinhados nas mesmas datas.

Os dados são estruturados em um site DataFrame com formatação consistente em todos os ativos.

import yfinance as yf
import pandas as pd

# List of IBEX 35 symbols
symbols = [
    "ACS.MC",
    "ITX.MC",
    "FER.MC",
    "ELE.MC",
    "SCYR.MC",
    "AENA.MC",
    "AMS.MC",
]

start_date = "2022-11-01"
end_date = "2023-4-01"

series_list = []
symbol_names = [symbol.replace(".", "_") for symbol in symbols]

# Create a full date index including weekends
full_index = pd.date_range(start=start_date, end=end_date, freq="D")

for symbol, name in zip(symbols, symbol_names):
    print(f"Downloading data for {symbol}...")
    data = yf.download(symbol, start=start_date, end=end_date)["Close"]
    data.name = name

    # Reindex to include weekends
    data = data.reindex(full_index)

    # Fill missing values (for example, weekends or holidays)
    # by forward/backward fill
    data.ffill(inplace=True)
    data.bfill(inplace=True)

    series_list.append(data)

# Combine all series into a single DataFrame
assets = pd.concat(series_list, axis=1)

# Convert index to string for consistency
assets.index = assets.index.astype(str)

Output:

[*********************100%***********************]  1 of 1 completed

[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
Downloading data for ACS.MC...
Downloading data for ITX.MC...
Downloading data for FER.MC...
Downloading data for ELE.MC...
Downloading data for SCYR.MC...
Downloading data for AENA.MC...
Downloading data for AMS.MC...
Aviso

Recomendamos executar a seguinte célula para verificar se os dados foram baixados corretamente da API do Yahoo Finance. Se o download falhar, uma notificação indicará que os dados serão recuperados de um banco de dados online alternativo.

if (
    assets.isna().values.any()
):  # If has nan values, load the data from the fallback online database
    print(
        "WARNING: Asset data format is invalid or incomplete. "
        "Loading data from the remote backup database."
    )

    url = "https://raw.githubusercontent.com/Global-Data-Quantum/PortfolioData/1f3ebb95fdef245014a04d4273f688a2951e0061/data/qpo_tutorial_ibex.csv"
    assets = pd.read_csv(url, index_col=0)

Etapa 2: Defina os dados do problema

Os parâmetros necessários para definir o problema QUBO são configurados no dicionário qubo_settings . Definimos o número de etapas de tempo (nt), o número de bits para especificação do investimento (nq) e a janela de tempo para cada etapa de tempo (dt). Além disso, definimos o investimento máximo por ativo, o coeficiente de aversão ao risco, a taxa de transação e o coeficiente de restrição (consulte nosso artigo para obter detalhes sobre a formulação do problema). Essas configurações nos permitem adaptar o problema QUBO ao cenário de investimento específico.

qubo_settings = {
    "nt": 4,
    "nq": 2,
    "dt": 30,
    # maximum investment per asset is 2**nq/max_investment = 80%
    "max_investment": 5,
    "risk_aversion": 1000.0,
    "transaction_fee": 0.01,
    "restriction_coeff": 1.0,
}

O optimizer_settings dicionário configura o processo de otimização, incluindo parâmetros como num_generations para o número de iterações e population_size para o número de soluções candidatas por geração. Outras configurações controlam aspectos como a taxa de recombinação, tarefas paralelas, tamanho do lote e intervalo de mutação. Além disso, as configurações básicas, como estimator_shots, estimator_precision, e sampler_shots, definem as configurações do Estimador e do Amostrador quânticos para o processo de otimização.

optimizer_settings = {
    "de_optimizer_settings": {
        "num_generations": 20,
        "population_size": 40,
        "recombination": 0.4,
        "max_parallel_jobs": 5,
        "max_batchsize": 4,
        "mutation_range": [0.0, 0.25],
    },
    "optimizer": "differential_evolution",
    "primitive_settings": {
        "estimator_shots": 25_000,
        "estimator_precision": None,
        "sampler_shots": 100_000,
    },
}
Note

O número total de circuitos depende dos parâmetros do site optimizer_settings e é calculado como (num_generations + 1) * population_size.

O ansatz_settings dicionário configura o ansatz do circuito quântico. O ansatz parâmetro especifica o uso da "optimized_real_amplitudes" abordagem, que é uma abordagem eficiente em termos de hardware projetada para problemas de otimização financeira. A multiple_passmanager opção, que permite o uso de vários gerenciadores de passagem (incluindo o gerenciador de passagem local padrão do Qiskit e o serviço transpiler com tecnologia de IA do Qiskit) para melhorar o desempenho e a eficiência da otimização do circuito, está atualmente definida como falsa porque esse recurso está temporariamente desativado.

ansatz_settings = {
    "ansatz": "optimized_real_amplitudes",
    "multiple_passmanager": False,
}

Por fim, executamos a otimização executando a função dpo_solver.run() , passando as entradas preparadas. Isso inclui o dicionário de dados de ativos (assets), a configuração do QUBO (qubo_settings), os parâmetros de otimização (optimizer_settings) e as configurações de ansatz do circuito quântico (ansatz_settings). Além disso, especificamos os detalhes da execução, como o backend, e se devemos aplicar o pós-processamento aos resultados. Isso inicia o processo de otimização dinâmica do portfólio no backend quântico selecionado.

dpo_job = dpo_solver.run(
    assets=assets.to_dict(),  # Insert assets as a dictionary
    qubo_settings=qubo_settings,
    optimizer_settings=optimizer_settings,
    ansatz_settings=ansatz_settings,
    backend_name="ibm_torino",
    previous_session_id=[],
    apply_postprocess=True,
)

Etapa 3: Analise os resultados da otimização

Nesta seção, extraímos e exibimos a solução com o menor custo objetivo dos resultados da otimização. Juntamente com o custo objetivo mínimo, também apresentamos as principais métricas associadas à solução associada, incluindo o desvio de restrição, o índice de Sharpe e o retorno do investimento.

# Get the results of the job
dpo_result = dpo_job.result()

# Show the solution strategy
dpo_result["result"]

Output:

{'time_step_0': {'ACS.MC': 0.11764705882352941,
  'ITX.MC': 0.20588235294117646,
  'FER.MC': 0.38235294117647056,
  'ELE.MC': 0.058823529411764705,
  'SCYR.MC': 0.0,
  'AENA.MC': 0.058823529411764705,
  'AMS.MC': 0.17647058823529413},
 'time_step_1': {'ACS.MC': 0.11428571428571428,
  'ITX.MC': 0.14285714285714285,
  'FER.MC': 0.2,
  'ELE.MC': 0.02857142857142857,
  'SCYR.MC': 0.42857142857142855,
  'AENA.MC': 0.0,
  'AMS.MC': 0.08571428571428572},
 'time_step_2': {'ACS.MC': 0.0,
  'ITX.MC': 0.09375,
  'FER.MC': 0.3125,
  'ELE.MC': 0.34375,
  'SCYR.MC': 0.0,
  'AENA.MC': 0.0,
  'AMS.MC': 0.25},
 'time_step_3': {'ACS.MC': 0.3939393939393939,
  'ITX.MC': 0.09090909090909091,
  'FER.MC': 0.12121212121212122,
  'ELE.MC': 0.18181818181818182,
  'SCYR.MC': 0.0,
  'AENA.MC': 0.0,
  'AMS.MC': 0.21212121212121213}}
import pandas as pd

# Get results from the job
dpo_result = dpo_job.result()

# Convert metadata to a DataFrame, excluding 'session_id'
df = pd.DataFrame(dpo_result["metadata"]["all_samples_metrics"])

# Find the minimum objective cost
min_cost = df["objective_costs"].min()
print(f"Minimum Objective Cost Found: {min_cost:.2f}")

# Extract the row with the lowest cost
best_row = df[df["objective_costs"] == min_cost].iloc[0]

# Display the results associated with the best solution
print("Best Solution:")
print(f"  - Restriction Deviation: {best_row['rest_breaches']}%")
print(f"  - Sharpe Ratio: {best_row['sharpe_ratios']:.2f}")
print(f"  - Return: {best_row['returns']:.2f}")

Output:

Minimum Objective Cost Found: -3.67
Best Solution:
  - Restriction Deviation: 40.0%
  - Sharpe Ratio: 14.54
  - Return: 0.28

O código a seguir mostra como visualizar e comparar a distribuição de custos de um algoritmo de otimização com uma distribuição de amostragem aleatória. Da mesma forma, exploramos o panorama da função objetivo QUBO (que pode ser carregada a partir da saída da função) avaliando-a com investimentos aleatórios. Traçamos ambas as distribuições normalizadas em amplitude para facilitar a comparação de como o processo de otimização difere da amostragem aleatória em termos de custo. Além disso, o resultado obtido com o DOCPlex é incluído como uma linha de referência vertical tracejada para servir como referência clássica. Usamos a versão gratuita do DOCPlex — a biblioteca IBM® de código aberto para otimização matemática em Python — para resolver o mesmo problema de forma clássica.

import matplotlib.pyplot as plt
from matplotlib.ticker import MultipleLocator
import matplotlib.patheffects as patheffects


def plot_normalized(dpo_x, dpo_y_normalized, random_x, random_y_normalized):
    """
    Plots normalized results for two sampling results.

    Parameters:
        dpo_x (array-like): X-values for the VQE Post-processed curve.
        dpo_y_normalized (array-like): Y-values (normalized) for the
        VQE Post-processed curve.
        random_x (array-like): X-values for the Noise (Random) curve.
        random_y_normalized (array-like): Y-values (normalized) for the
        Noise (Random) curve.
    """
    plt.figure(figsize=(6, 3))
    plt.tick_params(axis="both", which="major", labelsize=12)

    # Define custom colors
    colors = ["#4823E8", "#9AA4AD"]

    # Plot DPO results
    (line1,) = plt.plot(
        dpo_x, dpo_y_normalized, label="VQE Postprocessed", color=colors[0]
    )
    line1.set_path_effects(
        [patheffects.withStroke(linewidth=3, foreground="white")]
    )

    # Plot Random results
    (line2,) = plt.plot(
        random_x, random_y_normalized, label="Noise (Random)", color=colors[1]
    )
    line2.set_path_effects(
        [patheffects.withStroke(linewidth=3, foreground="white")]
    )

    # Set X-axis ticks to increment by 5 units
    plt.gca().xaxis.set_major_locator(MultipleLocator(5))

    # Axis labels and legend
    plt.xlabel("Objective cost", fontsize=14)
    plt.ylabel("Normalized Counts", fontsize=14)

    # Add DOCPLEX reference line
    plt.axvline(
        x=-4.11, color="black", linestyle="--", linewidth=1, label="DOCPlex"
    )  # DOCPlex value
    plt.ylim(bottom=0)

    plt.legend()

    # Adjust layout
    plt.tight_layout()
    plt.show()
import numpy as np
from collections import defaultdict

# ================================
# STEP 1: DPO COST DISTRIBUTION
# ================================

# Extract data from DPO results
counts_list = dpo_result["metadata"]["all_samples_metrics"][
    "counts"
]  # List of how many times each solution occurred
cost_list = dpo_result["metadata"]["all_samples_metrics"][
    "objective_costs"
]  # List of corresponding objective function values (costs)

# Round costs to one decimal and accumulate counts for each unique cost
dpo_counter = defaultdict(int)
for cost, count in zip(cost_list, counts_list):
    rounded_cost = round(cost, 1)
    dpo_counter[rounded_cost] += count

# Prepare data for plotting
dpo_x = sorted(dpo_counter.keys())  # Sorted list of cost values
dpo_y = [dpo_counter[c] for c in dpo_x]  # Corresponding counts

# Normalize the counts to the range [0, 1] for better comparison
dpo_min = min(dpo_y)
dpo_max = max(dpo_y)
dpo_y_normalized = [
    (count - dpo_min) / (dpo_max - dpo_min) for count in dpo_y
]

# ================================
# STEP 2: RANDOM COST DISTRIBUTION
# ================================

# Read the QUBO matrix
qubo = np.array(dpo_result["metadata"]["qubo"])

bitstring_length = qubo.shape[0]
num_random_samples = 100_000  # Number of random samples to generate
random_cost_counter = defaultdict(int)

# Generate random bitstrings and calculate their cost
for _ in range(num_random_samples):
    x = np.random.randint(0, 2, size=bitstring_length)
    cost = float(x @ qubo @ x.T)
    rounded_cost = round(cost, 1)
    random_cost_counter[rounded_cost] += 1

# Prepare random data for plotting
random_x = sorted(random_cost_counter.keys())
random_y = [random_cost_counter[c] for c in random_x]

# Normalize the random cost distribution
random_min = min(random_y)
random_max = max(random_y)
random_y_normalized = [
    (count - random_min) / (random_max - random_min) for count in random_y
]

# ================================
# STEP 3: PLOTTING
# ================================

plot_normalized(dpo_x, dpo_y_normalized, random_x, random_y_normalized)

Output:

Output of the previous code cell

O gráfico mostra como o otimizador de portfólio quântico retorna consistentemente estratégias de investimento otimizadas.


Referências

[1] Nodar, Álvaro, Irene De León, Danel Arias, Ernesto Mamedaliev, María Esperanza Molina, Manuel Martín-Cordero, Senaida Hernández-Santana et al. "Dimensionamento do solucionador variacional de eigenvalores quânticos para o método dinâmico de Portfolio Optimization " arXiv pré-impressão arXiv:2412.19150 (2024).


Pesquisa tutorial

Reserve um minuto para fazer comentários sobre este tutorial. Suas percepções nos ajudarão a melhorar nossas ofertas de conteúdo e a experiência do usuário. Link para a pesquisa

Esta página foi útil?
Relate um bug, erro de digitação ou solicite conteúdo no GitHub.