• Finance

Citigroup: QAOA for Multi-Period Portfolio Optimization and Quantum PCA

Citigroup

Citigroup tested QAOA for multi-period portfolio optimization with transaction cost constraints and implemented quantum PCA using phase estimation, assessing resource requirements for practical quantum advantage in fixed income and equity portfolio management.

Key Outcome
QAOA was competitive with classical heuristics on 8-asset instances. Multi-period constraints significantly increased qubit requirements. Quantum PCA on a 4x4 covariance matrix reproduced classical PCA eigenvalues. Resource analysis showed full quantum advantage requires fault-tolerant hardware far from current capability. Citi published technical findings in partnership with IBM.

The Problem

Static mean-variance optimization selects the best portfolio for a single period. Real asset managers rebalance monthly or quarterly, incurring transaction costs each time. Multi-period portfolio optimization finds the sequence of portfolios over T periods that maximizes total return net of transaction costs, a problem that grows combinatorially with T, N (assets), and position constraints.

Citigroup’s quant division explored two applications: QAOA for multi-period optimization with explicit transaction cost penalties, and quantum PCA for extracting risk factors from the covariance matrix of asset returns.

Multi-Period QUBO with Transaction Costs

Binary variable x_{t,i} = 1 means asset i is held in period t. The QUBO objective minimizes negative return plus risk minus transaction cost penalties for position changes between periods.

import numpy as np
from qiskit_optimization import QuadraticProgram
from qiskit_optimization.converters import QuadraticProgramToQubo
from qiskit_algorithms import QAOA
from qiskit_algorithms.optimizers import COBYLA
from qiskit.primitives import Sampler
from qiskit_optimization.algorithms import MinimumEigenOptimizer
from itertools import combinations

n_assets, n_periods, n_select = 8, 2, 3
txn_cost, risk_aversion = 0.01, 2.0

np.random.seed(7)
returns = np.random.normal(0.08, 0.15, (n_periods, n_assets))
cov = np.eye(n_assets) * 0.02 + np.random.rand(n_assets, n_assets) * 0.005
cov = (cov + cov.T) / 2

qp = QuadraticProgram(name="MultiPeriodPortfolio")
for t in range(n_periods):
    for i in range(n_assets):
        qp.binary_var(name=f"x_{t}_{i}")

linear_terms, quadratic_terms = {}, {}

for t in range(n_periods):
    for i in range(n_assets):
        linear_terms[f"x_{t}_{i}"] = (
            linear_terms.get(f"x_{t}_{i}", 0) - returns[t, i]
        )
    for i in range(n_assets):
        for j in range(i, n_assets):
            vi, vj = f"x_{t}_{i}", f"x_{t}_{j}"
            coeff = risk_aversion * cov[i, j] * (1 if i == j else 2)
            key = (vi, vj) if i != j else (vi, vi)
            quadratic_terms[key] = quadratic_terms.get(key, 0) + coeff

# XOR penalty for transaction costs: x_curr + x_prev - 2 * x_curr * x_prev
for t in range(1, n_periods):
    for i in range(n_assets):
        vp, vc = f"x_{t-1}_{i}", f"x_{t}_{i}"
        linear_terms[vc] = linear_terms.get(vc, 0) + txn_cost
        linear_terms[vp] = linear_terms.get(vp, 0) + txn_cost
        quadratic_terms[(vp, vc)] = (
            quadratic_terms.get((vp, vc), 0) - 2 * txn_cost
        )

qp.minimize(linear=linear_terms, quadratic=quadratic_terms)
for t in range(n_periods):
    qp.linear_constraint(
        linear={f"x_{t}_{i}": 1 for i in range(n_assets)},
        sense="==", rhs=n_select, name=f"card_{t}"
    )

qubo = QuadraticProgramToQubo().convert(qp)
print(f"QUBO variables: {qubo.get_num_vars()}")

qaoa = QAOA(sampler=Sampler(), optimizer=COBYLA(maxiter=400), reps=2)
result = MinimumEigenOptimizer(qaoa).solve(qubo)
print(f"QAOA objective: {result.fval:.4f}")

Quantum PCA via Phase Estimation

Quantum PCA uses quantum phase estimation to extract eigenvalues of the return covariance matrix, potentially exponentially faster than classical PCA given quantum-accessible data.

from qiskit import QuantumCircuit
from qiskit.circuit.library import PhaseEstimation
from qiskit.quantum_info import Operator
from qiskit_aer import AerSimulator
from scipy.linalg import expm

sigma = np.array([
    [0.040, 0.012, 0.008, 0.005],
    [0.012, 0.025, 0.006, 0.003],
    [0.008, 0.006, 0.030, 0.009],
    [0.005, 0.003, 0.009, 0.020],
])
print("Classical eigenvalues:", np.round(np.linalg.eigvalsh(sigma)[::-1], 5))

sigma_norm = sigma / np.trace(sigma)
t = np.pi
unitary_op = Operator(expm(1j * t * sigma_norm))
n_eval, n_state = 4, 2
qpe_circuit = QuantumCircuit(n_eval + n_state, n_eval)
qpe_circuit.compose(
    PhaseEstimation(num_evaluation_qubits=n_eval, unitary=unitary_op),
    inplace=True
)
qpe_circuit.measure(range(n_eval), range(n_eval))

counts = AerSimulator().run(qpe_circuit, shots=4096).result().get_counts()
phases = {}
for bits, cnt in counts.items():
    ev = int(bits, 2) / (2 ** n_eval) / t * np.trace(sigma)
    phases[ev] = phases.get(ev, 0) + cnt

for ev, cnt in sorted(phases.items(), key=lambda x: -x[1])[:4]:
    print(f"  {ev:.5f}  ({cnt} shots)")

Results

QAOA at p=2 found portfolios within 5-8% of brute-force optimal on 8-asset, 2-period instances. Multi-period constraints added ~30% more QUBO variables, pushing qubit counts to 18-22. QPE reproduced the two largest classical eigenvalues accurately. The critical barrier for quantum PCA at scale is quantum RAM: loading a 500-asset covariance matrix requires hardware that does not yet exist, eliminating the exponential speedup. Citi flagged the transaction cost QUBO formulation as the primary reusable contribution.

Learn more: Qiskit Reference