Why QUBO for portfolio selection?
Classical mean-variance optimization (Markowitz) assumes continuous weights. Real portfolios have discrete constraints: minimum lot sizes, maximum position counts (cardinality constraints), sector exposure limits, and integer share quantities. These constraints make the problem combinatorially hard and incompatible with quadratic programming solvers.
NEROX encodes discrete portfolio selection as a QUBO and solves it with GPU annealing, handling all integer and combinatorial constraints natively.
Mean-variance QUBO formulation
import nerox
import numpy as np
# Asset universe
n_assets = 100
n_select = 10 # exactly 10 assets in portfolio
# Historical return estimates and covariance matrix
mu = np.random.randn(n_assets) * 0.08 + 0.06 # expected annual return
Sigma = np.cov(np.random.randn(n_assets, 252)) # covariance from 252 trading days
# Risk aversion parameter (higher = more conservative)
risk_aversion = 2.0
client = nerox.Client()
job = client.optimize.portfolio(
expected_returns=mu,
covariance=Sigma,
risk_aversion=risk_aversion,
n_assets_to_select=n_select, # cardinality constraint
solver="gpu",
)
result = job.wait()
selected = np.where(result.solution)[0]
print(f"Selected assets: {selected}")
print(f"Portfolio return: {mu[selected].mean():.3f}")
print(f"Portfolio risk (std): {np.sqrt(result.objective):.4f}")Adding sector constraints
# Limit exposure to any single sector
sectors = np.array([0,0,0,1,1,1,2,2,2,...]) # sector label per asset
max_per_sector = 3
job = client.optimize.portfolio(
expected_returns=mu,
covariance=Sigma,
risk_aversion=risk_aversion,
n_assets_to_select=n_select,
sector_labels=sectors,
max_assets_per_sector=max_per_sector,
solver="gpu",
)Index tracking
Construct a sparse portfolio that closely tracks a benchmark index using a subset of assets. Supply the index weights and NEROX minimizes tracking error subject to a cardinality limit.
index_weights = np.ones(n_assets) / n_assets # equal-weight index
job = client.optimize.portfolio(
covariance=Sigma,
index_weights=index_weights,
n_assets_to_select=20, # track index with 20 stocks
objective="tracking_error",
solver="gpu",
)