Quantum Portfolio Optimizer: una Qiskit Function de Global Data Quantum
Consulta la referencia de la API
Las Qiskit Functions son una funcionalidad experimental disponible únicamente para usuarios del Plan Premium, Plan Flex y Plan On-Prem (a través de la API de IBM Quantum Platform) de IBM Quantum®. Se encuentran en estado de versión preliminar y pueden cambiar.
Descripción general
El Quantum Portfolio Optimizer es una Qiskit Function que aborda el problema de optimización dinámica de carteras, un problema estándar en finanzas que busca rebalancear inversiones periódicas en un conjunto de activos para maximizar rendimientos y minimizar riesgos. Al desplegar técnicas de optimización cuántica de vanguardia, esta función simplifica el proceso para que los usuarios, sin experiencia en computación cuántica, puedan beneficiarse de sus ventajas a la hora de encontrar trayectorias de inversión óptimas. Ideal para gestores de carteras, investigadores en finanzas cuantitativas e inversores individuales, esta herramienta permite realizar backtesting de estrategias de negociación en optimización de carteras.
Descripción de la función
La función Quantum Portfolio Optimizer utiliza el algoritmo Variational Quantum Eigensolver (VQE) para resolver un problema de Optimización Binaria Cuadrática sin Restricciones (QUBO), abordando problemas de optimización dinámica de carteras. Los usuarios solo necesitan proporcionar los datos de precios de los activos y definir la restricción de inversión; luego, la función ejecuta el proceso de optimización cuántica que devuelve un conjunto de trayectorias de inversión optimizadas.
El proceso consta de cuatro etapas principales. Primero, los datos de entrada se mapean a un problema compatible con la computación cuántica, construyendo el QUBO del problema de optimización dinámica de carteras y transformándolo en un operador cuántico (hamiltoniano de Ising). A continuación, el problema de entrada y el algoritmo VQE se adaptan para ejecutarse en hardware cuántico. El algoritmo VQE se ejecuta entonces en el hardware cuántico y, finalmente, los resultados se post-procesan para proporcionar las trayectorias de inversión óptimas. El sistema también incluye un post-procesamiento consciente del ruido (basado en SQD) para maximizar la calidad de la salida.
Esta Qiskit Function está basada en el artículo publicado por Global Data Quantum.
Comenzar
Autentícate con tu clave de API y selecciona la Qiskit Function de la siguiente manera. (Este fragmento asume que ya has guardado tu cuenta en tu entorno local.)
# Added by doQumentation — required packages for this notebook
!pip install -q pandas qiskit-ibm-catalog
from qiskit_ibm_catalog import QiskitFunctionsCatalog
catalog = QiskitFunctionsCatalog(channel="ibm_quantum_platform")
# Access function
dpo_solver = catalog.load("global-data-quantum/quantum-portfolio-optimizer")
Ejemplo: Optimización dinámica de carteras con siete activos
Este ejemplo demuestra cómo ejecutar la función de optimización dinámica de carteras (DPO) y ajustar su configuración para un rendimiento óptimo. Incluye pasos detallados para ajustar los parámetros y lograr los resultados deseados.
Este caso implica siete activos, cuatro pasos temporales y cuatro qubits de resolución, lo que resulta en un requisito total de 112 qubits.
1. Lee los activos incluidos en la cartera
Si todos los activos de la cartera están almacenados en una carpeta en una ruta específica, puedes cargarlos en un pandas.DataFrame y convertirlos a un objeto en formato dict usando la siguiente función.
import os
import glob
import pandas as pd
def read_and_join_csv(file_pattern):
"""
Reads multiple CSV files matching the file pattern and combines them into a single DataFrame.
Parameters:
file_pattern (str): The pattern to match CSV files.
Returns:
pd.DataFrame: Combined DataFrame with data from all CSV files.
"""
# Find all files matching the pattern
csv_files = glob.glob(file_pattern)
# Get the base file names without the .csv extension
file_names = [os.path.basename(f).replace(".csv", "") for f in csv_files]
# Read each CSV file into a DataFrame and set the first column as the index
df_list = [pd.read_csv(f).set_index("Unnamed: 0") for f in csv_files]
# Rename columns in each DataFrame to the base file names
for df, name in zip(df_list, file_names):
df.columns = [name]
# Combine all DataFrames into one by merging them side by side
combined_df = pd.concat(df_list, axis=1)
return combined_df
file_pattern = "route/to/folder/with/assets/data/*.csv"
assets = read_and_join_csv(file_pattern).to_dict()
Para este ejemplo, hemos utilizado los activos 8801.T, CLF, GBPJPY, ITX.MC, META, TMBMKDE-10Y y XS2239553048. La siguiente figura ilustra los datos utilizados en este ejemplo, mostrando la evolución diaria del precio de cierre de los activos del 1 de enero al 1 de septiembre de 2023.
En este ejemplo, para garantizar la uniformidad entre fechas, hemos rellenado los días no bursátiles con el precio de cierre de la fecha disponible anterior. Aplicamos este paso porque los activos seleccionados provienen de diferentes mercados con distintos días de negociación, lo que hace esencial estandarizar el conjunto de datos para mantener la consistencia.
2. Define el problema
Define las especificaciones del problema configurando los parámetros en el diccionario qubo_settings.
qubo_settings = {
"nt": 4,
"nq": 4,
"dt": 30,
"max_investment": 25,
"risk_aversion": 1000.0,
"transaction_fee": 0.01,
"restriction_coeff": 1.0,
}
3. Define la configuración del optimizador y del ansatz (Opcional)
Opcionalmente, define requisitos específicos para el proceso de optimización, incluida la selección del optimizador y sus parámetros, así como la especificación del primitivo y sus configuraciones.
Para el Tailored Ansatz, el tamaño de población elegido se basó en experimentos previos que muestran que este valor produce una optimización estable y eficiente.
En el caso del Real Amplitudes Ansatz, puedes seguir una relación lineal entre el population_size y el número de qubits en el circuito. Como regla general aproximada, se recomienda usar un mínimo de population_size ~ 0.8 * n_qubits para el ansatz real_amplitudes.
Se espera que el Optimized Real Amplitudes tenga un mejor rendimiento de optimización que el ansatz Real Amplitudes. Sin embargo, el número de variables a optimizar en este ansatz crece mucho más rápido que en el caso de Real Amplitudes (consulta el artículo). Por lo tanto, para problemas grandes, el Optimized Real Amplitudes requiere más ejecuciones de circuitos. Es probable que el Optimized Real Amplitudes sea útil para problemas de hasta 100 qubits, pero se recomienda ser cuidadoso al establecer los parámetros population_size. Como ejemplo de este escalado en population_size, la tabla anterior muestra que para un problema de 84 qubits, el Optimized Real Amplitudes requiere 120 de population_size, mientras que para un problema de 56 qubits, un population_size de 40 es suficiente.
optimizer_settings = {
"de_optimizer_settings": {
"num_generations": 20,
"population_size": 90,
"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,
},
}
También es posible elegir un ansatz específico. El siguiente código usa el ansatz 'Tailored'.
ansatz_settings = {
"ansatz": "tailored",
"multiple_passmanager": False,
}
4. Ejecuta el problema
dpo_job = dpo_solver.run(
assets=assets,
qubo_settings=qubo_settings,
optimizer_settings=optimizer_settings,
ansatz_settings=ansatz_settings,
backend_name="<backend name>",
previous_session_id=[],
apply_postprocess=True,
)
5. Recupera los resultados
La función devuelve un diccionario con las trayectorias de inversión ordenadas de menor a mayor según su valor de función objetivo (consulta la sección Salida de la referencia de la API). Este conjunto de resultados permite identificar la trayectoria con el menor coste y sus evaluaciones de inversión correspondientes. Además, permite analizar diferentes trayectorias, facilitando la selección de las que mejor se adaptan a necesidades u objetivos específicos. Esta flexibilidad garantiza que las decisiones puedan adaptarse a una variedad de preferencias o escenarios. Comienza presentando la estrategia resultante que logró el menor coste objetivo encontrado durante el proceso.
# Get the results of the job
dpo_result = dpo_job.result()
# Show the solution strategy
dpo_result["result"]
{'time_step_0': {'8801.T': 0.11764705882352941,
'ITX.MC': 0.20588235294117646,
'META': 0.38235294117647056,
'GBPJPY=X': 0.058823529411764705,
'TMBMKDE-10Y': 0.0,
'CLF': 0.058823529411764705,
'XS2239553048': 0.17647058823529413},
'time_step_1': {'8801.T': 0.11428571428571428,
'ITX.MC': 0.14285714285714285,
'META': 0.2,
'GBPJPY=X': 0.02857142857142857,
'TMBMKDE-10Y': 0.42857142857142855,
'CLF': 0.0,
'XS2239553048': 0.08571428571428572},
'time_step_2': {'8801.T': 0.0,
'ITX.MC': 0.09375,
'META': 0.3125,
'GBPJPY=X': 0.34375,
'TMBMKDE-10Y': 0.0,
'CLF': 0.0,
'XS2239553048': 0.25},
'time_step_3': {'8801.T': 0.3939393939393939,
'ITX.MC': 0.09090909090909091,
'META': 0.12121212121212122,
'GBPJPY=X': 0.18181818181818182,
'TMBMKDE-10Y': 0.0,
'CLF': 0.0,
'XS2239553048': 0.21212121212121213}}
A continuación, usando los metadatos, puedes acceder a los resultados de todas las estrategias muestreadas. Así puedes analizar con mayor detalle las trayectorias alternativas devueltas por el optimizador. Para ello, lee el diccionario almacenado en dpo_result['metadata']['all_samples_metrics'], que contiene no solo información adicional sobre la estrategia óptima, sino también detalles de las otras estrategias candidatas evaluadas durante la optimización.
El siguiente ejemplo muestra cómo leer esta información usando pandas para extraer métricas clave asociadas a la estrategia óptima. Estas incluyen la Desviación de Restricción, el Ratio de Sharpe y el rendimiento de inversión correspondiente.
# Convert metadata to a DataFrame
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']}")
Minimum Objective Cost Found: -3.78
Best Solution:
- Restriction Deviation: 40.0
- Sharpe Ratio: 24.82
- Return: 0.46
6. Análisis de rendimiento
Por último, analiza el rendimiento de tu aplicación de optimización. Específicamente, compara tus resultados, obtenidos en el ejemplo anterior, contra una línea base aleatoria para evaluar la efectividad de nuestro enfoque. Si el algoritmo cuántico produce de manera demostrable y consistente resultados con valores de coste más bajos, esto indica un proceso de optimización efectivo.
La figura presenta las distribuciones de probabilidad de los costes objetivo. Para generar estas distribuciones, toma la lista de costes objetivo del resultado de la función y cuenta las ocurrencias de cada valor de coste (valores redondeados al segundo decimal). Luego, actualiza la columna de conteos uniendo los conteos de valores redondeados idénticos. Ten en cuenta que, para una mejor comparación visual, los conteos de ocurrencias se han normalizado para que cada distribución se muestre entre 0 y 1.
Como se muestra en la figura (línea sólida azul), la distribución de costes para nuestro enfoque con Variational Quantum Eigensolver (post-procesado con SQD) está concentrada de forma marcada en valores de coste objetivo más bajos, lo que indica un buen rendimiento de optimización. En contraste, la línea base ruidosa exhibe una distribución más amplia, centrada en torno a valores de coste más altos. La línea vertical discontinua gris representa el valor medio de la distribución aleatoria, lo que resalta aún más la consistencia de la función al devolver estrategias de inversión optimizadas. Para una comparación adicional, la línea discontinua negra en la figura corresponde a la solución obtenida con el optimizador Gurobi (versión gratuita). Todos estos resultados se exploran con más detalle en los benchmarks a continuación para el ejemplo de "Activos Mixtos" evaluado con el ansatz "Tailored".
Benchmarks
Esta función se probó bajo diferentes configuraciones de qubits de resolución, circuitos ansatz y agrupaciones de activos de varios sectores: una mezcla de diferentes activos (Conjunto 1), derivados del petróleo (Conjunto 2) e IBEX35 (Conjunto 3). Consulta más detalles en la siguiente tabla.
| Conjunto | Fecha | Activos |
|---|---|---|
| Conjunto 1 | 01/01/2023 | 8801.T, CL=F, GBPJPY=X, ITX.MC, META, TMBMKDE-10Y, XS2239553048 |
| Conjunto 2 | 01/06/2023 | CL=F, BZ=F, HO=F, NG=F, XOM, RB=F, 2222.SR |
| Conjunto 3 | 01/11/2022 | ACS.MC, ITX.MC, FER.MC, ELE.MC, SCYR.MC, AENA.MC, AMS.MC |
Se utilizaron dos métricas clave para evaluar la calidad de la solución.
- El coste objetivo, que mide la eficiencia de optimización comparando el valor de la función de coste de cada experimento con los resultados de Gurobi (versión gratuita).
- El ratio de Sharpe, que captura el rendimiento ajustado al riesgo de cada cartera, ofreciendo información sobre el rendimiento financiero de las soluciones.
Juntas, estas métricas sirven de referencia tanto para los aspectos computacionales como financieros de las carteras generadas cuánticamente.
| Ejemplo | Qubits | Ansatz | Profundidad | Uso en Runtime (s) | Uso total (s) | Coste objetivo | Sharpe | Coste objetivo Gurobi | Sharpe Gurobi |
|---|---|---|---|---|---|---|---|---|---|
| Activos Mixtos (Conjunto 1, 4 pasos temporales, 4-bit) | 112 | Tailored | 83 | 12735 | 13095 | -3.78 | 24.82 | -4.25 | 24.71 |
| Activos Mixtos (Conjunto 1, 4 pasos temporales, 4-bit) | 112 | Real Amplitudes | 359 | 11739 | 11903 | -3.39 | 23.64 | -4.25 | 24.71 |
| Derivados del Petróleo (Conjunto 2, 4 pasos temporales, 3-bit) | 84 | Optimized Real Amplitudes | 78 | 6180 | 6350 | -3.73 | 19.13 | -4.19 | 21.71 |
| IBEX35 (Conjunto 3, 4 pasos temporales, 2-bit) | 56 | Optimized Real Amplitudes | 96 | 3314 | 3523 | -3.67 | 14.48 | -4.11 | 16.44 |
Los resultados muestran que el optimizador cuántico, con ansatzes específicos para el problema, identifica eficazmente estrategias de inversión eficientes en varios tipos de carteras.
A continuación detallamos tanto el tamaño de la población como el número de generaciones especificados en el diccionario optimizer_options. Todos los demás parámetros se establecieron con sus valores por defecto.
| Ejemplo | population_size | num_generations |
|---|---|---|
| Cartera de Activos Mixtos | 90 | 20 |
| Cartera de Activos Mixtos | 92 | 20 |
| Cartera de Derivados del Petróleo | 120 | 20 |
| Cartera IBEX35 | 40 | 20 |
El número de generaciones se fijó en 20, ya que se comprobó que este valor es suficiente para alcanzar la convergencia. Además, los valores por defecto de los parámetros internos del optimizador se dejaron sin cambios, ya que proporcionan consistentemente un buen rendimiento y están generalmente recomendados por la literatura y las directrices de implementación.
Obtener soporte
Si necesitas ayuda, puedes enviar un correo electrónico a qpo.support@globaldataquantum.com. En tu mensaje, proporciona el ID del trabajo de la función.
Próximos pasos
- Lee el artículo de investigación asociado.
- Consulta la referencia de la API de esta Qiskit Function.
- Solicita acceso a la función rellenando este formulario.
- Prueba el tutorial de Optimización Dinámica de Carteras.