Skip to content

Portfolio Optimization

qufin provides both classical and quantum portfolio optimization methods that share a common interface.

Classical Methods

Mean-Variance (Markowitz)

Solves the classic Markowitz problem using CVXPY with support for cardinality constraints.

from qufin.portfolio.classical.mean_variance import mean_variance, Objective

result = mean_variance(
    mu, cov,
    objective=Objective.MIN_VARIANCE,  # or MAX_SHARPE, MAX_RETURN
    cardinality=10,     # max 10 assets
    long_only=True,     # no short selling
    max_weight=0.15,    # position limit
)

Objectives:

  • MIN_VARIANCE — Minimum variance portfolio
  • MAX_SHARPE — Maximum Sharpe ratio (Cornuejols-Tutuncu transformation)
  • MAX_RETURN — Maximum return subject to variance constraint

Black-Litterman

Combines market equilibrium with investor views.

from qufin.portfolio.classical.black_litterman import black_litterman

result = black_litterman(
    cov=cov,
    market_caps=caps,
    P=np.array([[1, -1, 0, 0, 0]]),  # Asset 0 outperforms Asset 1
    Q=np.array([0.02]),               # by 2%
)
# result.posterior_mu, result.posterior_cov

Hierarchical Risk Parity (HRP)

Machine-learning-based allocation using hierarchical clustering.

from qufin.portfolio.classical.hrp import hrp

result = hrp(returns, linkage_method="single")
# result.weights, result.cluster_order

Risk Parity

Equal risk contribution portfolio.

from qufin.portfolio.classical.risk_parity import risk_parity

result = risk_parity(cov, budget=None)  # None = equal budget
# result.weights, result.risk_contributions

Quantum Methods

QUBO Formulation

All quantum portfolio optimizers work on a QUBO (Quadratic Unconstrained Binary Optimization) matrix.

from qufin.portfolio.qubo import PortfolioQUBO

qubo = PortfolioQUBO(
    mu=mu, cov=cov,
    gamma=1.0,              # risk aversion
    cardinality=5,          # select 5 assets
    sector_map={0: [0,1,2], 1: [3,4]},
    sector_caps={0: 3, 1: 2},
    turnover_penalty=0.01,
    budget_penalty=1e4,
    encoding="one_hot",     # or "binary"
)

Q = qubo.build_matrix()  # (n_qubits x n_qubits) QUBO matrix

QAOA Portfolio Optimizer

from qufin.portfolio.optimizers.qaoa import QAOAPortfolio, QAOAConfig
from qufin.backends.qiskit_backend import QiskitAerBackend

config = QAOAConfig(
    p=3,                    # QAOA depth (layers)
    mixer="xy_ring",        # Hamming-weight-preserving mixer
    cardinality=5,          # must match QUBO cardinality
    cvar_alpha=0.5,         # CVaR tail optimization
    shots=8192,
    seed=42,
)

solver = QAOAPortfolio(qubo, config, QiskitAerBackend(seed=42))
result = solver.run()

Mixer options:

Mixer Preserves Hamming Weight Circuit Depth Best For
x No O(n) Unconstrained
xy_ring Yes O(n) Cardinality-constrained
xy_full Yes O(n^2) Small problems, better mixing
grover Yes O(n) Hard constraints

VQE Portfolio Optimizer

Hardware-efficient ansatz with CVaR objective.

from qufin.portfolio.optimizers.vqe import VQEPortfolio, VQEConfig

config = VQEConfig(
    reps=3,
    entanglement="linear",
    cvar_alpha=0.5,
    shots=8192,
)

solver = VQEPortfolio(qubo, config, backend)
result = solver.run()

Exhaustive Solver (Small Problems)

Brute-force optimal solution for validation (up to 20 qubits).

from qufin.portfolio.optimizers.exhaustive import exhaustive_solve

result = exhaustive_solve(qubo, return_all=True)
# result.best_bitstring, result.best_objective, result.all_objectives

Advanced Portfolio Methods

Multi-Period Optimization

Optimize across multiple time periods with turnover penalties:

from qufin.portfolio.optimizers.multi_period import (
    multi_period_optimize, MultiPeriodConfig,
)

config = MultiPeriodConfig(turnover_penalty=0.01, holding_cost=0.001)
result = multi_period_optimize(mu_series, cov_series, config=config)
# result.allocations, result.turnovers, result.total_objective

Factor Models (Fama-French)

Estimate factor exposures and decompose risk:

from qufin.portfolio.classical.factor_models import build_factor_model, risk_decomposition

model = build_factor_model(asset_returns, factor_returns, window=252)
# model.expected_returns, model.cov, model.exposures

decomp = risk_decomposition(weights, model.exposures, model.factor_cov)
print(f"Systematic: {decomp['systematic_pct']:.1%}")

Robust Portfolio (Worst-Case CVaR)

Optimize under parameter uncertainty:

from qufin.portfolio.optimizers.robust import RobustPortfolioOptimizer

optimizer = RobustPortfolioOptimizer(backend=backend)
result = optimizer.optimize(mu, cov, uncertainty_radius=0.1)

Szegedy Quantum Walk

Portfolio optimization via quantum walk on asset correlation graph:

from qufin.portfolio.optimizers.quantum_walk import SzegedyWalkOptimizer, SzegedyWalkConfig

config = SzegedyWalkConfig(n_steps=100, n_assets=15)
optimizer = SzegedyWalkOptimizer(backend=backend, config=config)
result = optimizer.optimize(mu, cov)

Sector Rotation

VQC-based regime detection for sector allocation:

from qufin.portfolio.sector_rotation import SectorRotator, RegimeDetector

detector = RegimeDetector(n_regimes=3)
rotator = SectorRotator(detector=detector)
allocation = rotator.allocate(market_data)

Comparison Example

from qufin.backtesting import BacktestEngine

engine = BacktestEngine(returns, train_window=252, test_window=63)

strategies = {
    "Min Variance": lambda mu, cov: mean_variance(mu, cov).weights,
    "Risk Parity": lambda mu, cov: risk_parity(cov).weights,
    "HRP": lambda mu, cov: hrp_from_cov(cov).weights,
    "Equal Weight": lambda mu, cov: np.ones(len(mu)) / len(mu),
}

results = engine.compare(strategies)
table = engine.comparison_table(results)