Skip to content

Portfolio Optimization Problem (POP) Example

Download Notebook


This notebook demonstrates how to use the Portfolio Optimization use case to select (without weight allocation) an optimal portfolio of assets that balances risk and return.

Overview

The Portfolio Optimization problem aims to select a subset of assets from a larger set to maximize expected returns while minimizing risk (variance). This implementation uses:

  • Mean-Variance Optimization: Based on Markowitz portfolio theory
  • Risk Aversion Parameter: Controls the trade-off between risk and return
  • Constraint: Select exactly N assets from the available set
from datetime import datetime, timedelta

import pytz
import yfinance as yf
from dotenv import load_dotenv
from luna_quantum.algorithms import SCIP

from luna_usecases.portfolio_optimization import (
    PopData,
    PopFormulation,
    PopInstance,
    PopStockCollection,
)

Option 1: Create Data from Real Market Data (Yahoo Finance)

Use yfinance to download real historical stock returns.

# Define portfolio of assets (stock tickers)
tickers = ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA", "JPM", "JNJ", "V"]

# Download historical data (last 6 months of daily data)
end_date = datetime.now(tz=pytz.timezone("Europe/London"))
start_date = end_date - timedelta(days=180)

data = yf.download(tickers, start=start_date, end=end_date, progress=False)

close = data["Close"]
# Calculate daily returns
daily_returns = close.pct_change().dropna()

# Convert to numpy array (assets as rows, time periods as columns)
returns_array = daily_returns.T.values

Create Portfolio Optimization Data

Create the optimization data from the downloaded returns.

data = PopData.from_returns(
    returns=returns_array,
    asset_names=tickers,
    pick_n=3,  # Select 3 assets from the 8 available
    risk_aversion=1.5,  # Moderate risk aversion
)

print(data.to_string())

Portfolio Optimization Data:
  Number of assets:      8
  Time periods:          122
  Assets to select:      3
  Risk aversion:         1.5

Notes:
  - Returns are per period (e.g. daily returns)
  - Expected return and risk are derived from this data
data.plot()

<Axes: title={'center': 'Portfolio Data — Cumulative Returns'}, xlabel='Time Period', ylabel='Growth of 1 unit invested'>
png

Create Formulation and Instance

Define the optimization formulation and create an instance.

formulation = PopFormulation()
print(formulation.to_string(data))


instance = PopInstance(data=data, formulation=formulation)
print(instance.to_string())
Portfolio Optimization Formulation:
  Number of assets: 8
  Assets to select (pick_n): 3
  Risk aversion (lambda): 1.5

Statistical Inputs:
  mu[i]: expected return of asset i (average historical return)
  sigma[i,j]: covariance between asset i and j
    - measures how asset returns move together
    - includes both variance (risk) and correlation effects

Decision Variables:
  x[i] in {0,1} for i = 0, ..., 7
  x[i] = 1 if asset i is selected, 0 otherwise
  Total: 8 binary variables

Objective:
  minimize:
    lambda * sum_{i,j} sigma_{ij} x_i x_j   (portfolio risk)
    - sum_i mu_i x_i                        (expected return)

Constraints:
  1. Select exactly 3 assets:
     sum_i x[i] == 3

Interpretation:
  - Selected assets are equally weighted in the final portfolio
  - The model trades off return vs. risk via the risk_aversion parameter
Data:Portfolio Optimization Data:
  Number of assets:      8
  Time periods:          122
  Assets to select:      3
  Risk aversion:         1.5

Notes:
  - Returns are per period (e.g. daily returns)
  - Expected return and risk are derived from this data
Formulation:Portfolio Optimization Formulation:
  Number of assets: 8
  Assets to select (pick_n): 3
  Risk aversion (lambda): 1.5

Statistical Inputs:
  mu[i]: expected return of asset i (average historical return)
  sigma[i,j]: covariance between asset i and j
    - measures how asset returns move together
    - includes both variance (risk) and correlation effects

Decision Variables:
  x[i] in {0,1} for i = 0, ..., 7
  x[i] = 1 if asset i is selected, 0 otherwise
  Total: 8 binary variables

Objective:
  minimize:
    lambda * sum_{i,j} sigma_{ij} x_i x_j   (portfolio risk)
    - sum_i mu_i x_i                        (expected return)

Constraints:
  1. Select exactly 3 assets:
     sum_i x[i] == 3

Interpretation:
  - Selected assets are equally weighted in the final portfolio
  - The model trades off return vs. risk via the risk_aversion parameter

Option 2: Use Pre-defined Test Instances

Alternatively, use the built-in test instances from the collection.

# Create a small test instance
test_po = PopStockCollection.create_custom_portfolio(min_n_assets=5, max_n_assets=10, pick_n=3, risk_aversion=1.5)
for inst in test_po.instances:
    m = inst.formulate()
    print(m)
Model: portfolio_optimization<portfolio_optimization>
Minimize
  -0.00007005129277249194 * x_0 * x_1 + 0.0002141207106220881 * x_0 * x_2
  + 0.00006256891516344802 * x_0 * x_3 - 0.00008294607484611005 * x_0 * x_4
  + 0.00004568855235897685 * x_1 * x_2 + 0.0000776717470700951 * x_1 * x_3
  - 0.00002372971246648473 * x_1 * x_4 + 0.00007374805142725301 * x_2 * x_3
  - 0.00021686803634787092 * x_2 * x_4 - 0.00008232909047782008 * x_3 * x_4
  - 0.0013933654905351803 * x_0 + 0.00035393366372625464 * x_1
  + 0.0003435989528868859 * x_2 - 0.0005737899564445878 * x_3
  + 0.0013539642390579274 * x_4
Subject To
  select_exactly_pick_n: x_0 + x_1 + x_2 + x_3 + x_4 == 3
Binary
  x_0 x_1 x_2 x_3 x_4
Model: portfolio_optimization<portfolio_optimization>
Minimize
  0.000288793633827604 * x_0 * x_1 + 0.00006745355688544893 * x_0 * x_2
  + 0.0003896040042550918 * x_0 * x_3 - 0.00002529032416634754 * x_0 * x_4
  - 0.00009471096857887104 * x_0 * x_5 + 0.00010701854956429189 * x_1 * x_2
  + 0.0004919204230093956 * x_1 * x_3 - 0.00003282367104373749 * x_1 * x_4
  - 0.000024462692566661027 * x_1 * x_5 - 0.0001901757518303625 * x_2 * x_3
  + 0.00021478868026746464 * x_2 * x_4 + 0.00031263308425092675 * x_2 * x_5
  - 0.0003568526556612377 * x_3 * x_4 - 0.00030288057692547755 * x_3 * x_5
  + 0.0003878832802159856 * x_4 * x_5 - 0.0009781569567766852 * x_0
  + 0.001848904765739451 * x_1 - 0.002615439159996225 * x_2
  + 0.0010713795100068093 * x_3 - 0.0005737899564445878 * x_4
  - 0.0003087344729023033 * x_5
Subject To
  select_exactly_pick_n: x_0 + x_1 + x_2 + x_3 + x_4 + x_5 == 3
Binary
  x_0 x_1 x_2 x_3 x_4 x_5
Model: portfolio_optimization<portfolio_optimization>
Minimize
  0.0003143781079224709 * x_0 * x_1 - 0.0002468913028975508 * x_0 * x_2
  + 0.00022072604855150323 * x_0 * x_3 + 0.000015147146420763097 * x_0 * x_4
  - 0.0000059804008688062285 * x_0 * x_5 - 0.00016131397958226514 * x_0 * x_6
  - 0.0003015442663943221 * x_1 * x_2 - 0.0001483955827771325 * x_1 * x_3
  + 0.00003059775727655324 * x_1 * x_4 - 0.00008177482187286778 * x_1 * x_5
  - 0.0002805889235511774 * x_1 * x_6 + 0.000003884738264362213 * x_2 * x_3
  - 0.00002245169311104586 * x_2 * x_4 - 0.0001710923104262905 * x_2 * x_5
  + 0.0001900042527718664 * x_2 * x_6 + 0.0001371591729303367 * x_3 * x_4
  + 0.00014388806675699842 * x_3 * x_5 - 0.00008624977348357396 * x_3 * x_6
  + 0.0003368161109466721 * x_4 * x_5 + 0.000045630152812134544 * x_4 * x_6
  + 0.0001606076128871305 * x_5 * x_6 - 0.0009781569567766852 * x_0
  - 0.0009365270131606782 * x_1 - 0.001393365582150953 * x_2
  + 0.0029240209107076896 * x_3 - 0.000376014561790893 * x_4
  + 0.00005069876494565137 * x_5 - 0.0013478788255324304 * x_6
Subject To
  select_exactly_pick_n: x_0 + x_1 + x_2 + x_3 + x_4 + x_5 + x_6 == 3
Binary
  x_0 x_1 x_2 x_3 x_4 x_5 x_6
Model: portfolio_optimization<portfolio_optimization>
Minimize
  0.00024500490046342057 * x_0 * x_1 - 0.00009967638131377373 * x_0 * x_2
  + 0.0001876817038335382 * x_0 * x_3 + 0.0001153306434786213 * x_0 * x_4
  + 0.00023258621465676937 * x_0 * x_5 + 0.0001874921367677998 * x_0 * x_6
  + 0.0003896040042550918 * x_0 * x_7 + 0.0002733631696753858 * x_1 * x_2
  + 0.00048154026743055776 * x_1 * x_3 + 0.0004693202455555315 * x_1 * x_4
  - 0.0001102401718740882 * x_1 * x_5 + 0.00038627932383728187 * x_1 * x_6
  + 0.0003856963471085928 * x_1 * x_7 + 0.00021198169146426017 * x_2 * x_3
  + 0.00034127891479333134 * x_2 * x_4 + 0.0000184618061932042 * x_2 * x_5
  + 0.00007441342536602399 * x_2 * x_6 + 0.00015702496851483573 * x_2 * x_7
  + 0.0002778597489529008 * x_3 * x_4 - 0.00004100814793403026 * x_3 * x_5
  + 0.0001858433603617501 * x_3 * x_6 + 0.0003729188589948585 * x_3 * x_7
  + 0.00010644905088478374 * x_4 * x_5 + 0.00023110142906880218 * x_4 * x_6
  - 0.000046170555568297516 * x_4 * x_7 + 0.00026606261159245595 * x_5 * x_6
  - 0.00005167105662169858 * x_5 * x_7 + 0.00023628462738465818 * x_6 * x_7
  - 0.0009781569567766852 * x_0 - 0.0031041332136953646 * x_1
  - 0.0013866772538180922 * x_2 - 0.0011969526027589379 * x_3
  - 0.0015383513409912225 * x_4 + 0.0009994238911455997 * x_5
  + 0.0005064232793915892 * x_6 + 0.0010713795100068093 * x_7
Subject To
  select_exactly_pick_n: x_0 + x_1 + x_2 + x_3 + x_4 + x_5 + x_6 + x_7 == 3
Binary
  x_0 x_1 x_2 x_3 x_4 x_5 x_6 x_7
Model: portfolio_optimization<portfolio_optimization>
Minimize
  -0.00011862207136277183 * x_0 * x_1 + 0.00023124512311329082 * x_0 * x_2
  + 0.00027004854700815584 * x_0 * x_3 + 0.000275408721960466 * x_0 * x_4
  + 0.0004923226740301233 * x_0 * x_5 + 0.000006861008216305009 * x_0 * x_6
  + 0.00018584333528561746 * x_0 * x_7 + 0.0004938778871002597 * x_0 * x_8
  - 0.000045033683884848234 * x_1 * x_2 - 0.0005017294627852867 * x_1 * x_3
  - 0.0001564785927530897 * x_1 * x_4 - 0.00014317890232537942 * x_1 * x_5
  + 0.00004950432375961038 * x_1 * x_6 - 0.0003916664108552906 * x_1 * x_7
  - 0.0003054217815321279 * x_1 * x_8 - 0.00025963335826658654 * x_2 * x_3
  + 0.00010701854956429189 * x_2 * x_4 + 0.00016925099255608113 * x_2 * x_5
  + 0.00031263309065432084 * x_2 * x_6 + 0.000049746501269186906 * x_2 * x_7
  + 0.0002142342895433665 * x_2 * x_8 + 0.0004455240576553478 * x_3 * x_4
  + 0.00007116948270260724 * x_3 * x_5 + 0.000059179054580349054 * x_3 * x_6
  + 0.0008789629753298557 * x_3 * x_7 + 0.0008062260013112449 * x_3 * x_8
  + 0.00041915040655568004 * x_4 * x_5 - 0.000024462681260302576 * x_4 * x_6
  + 0.000185893348132623 * x_4 * x_7 + 0.0004151105678280717 * x_4 * x_8
  - 0.00012863115130455939 * x_5 * x_6 + 0.00011024637754923027 * x_5 * x_7
  + 0.0005516203197597281 * x_5 * x_8 + 0.0002178612616019227 * x_6 * x_7
  - 0.000003662172341581033 * x_6 * x_8 + 0.0004806977550797823 * x_7 * x_8
  + 0.0005064232983835175 * x_0 - 0.0016778183383448844 * x_1
  - 0.002615439159996225 * x_2 - 0.006387912242497353 * x_3
  + 0.001848904765739451 * x_4 + 0.0013539643048737407 * x_5
  - 0.0003087345523058035 * x_6 - 0.0011969526027589379 * x_7
  - 0.001486690595403976 * x_8
Subject To
  select_exactly_pick_n: x_0 + x_1 + x_2 + x_3 + x_4 + x_5 + x_6 + x_7 + x_8 ==
    3
Binary
  x_0 x_1 x_2 x_3 x_4 x_5 x_6 x_7 x_8
Model: portfolio_optimization<portfolio_optimization>
Minimize
  0.0002724826443183653 * x_0 * x_1 + 0.00027014896728958253 * x_0 * x_2
  + 0.00033314292048447134 * x_0 * x_3 + 0.0003237452529613392 * x_0 * x_4
  - 0.000012574528229983428 * x_0 * x_5 + 0.00010672237136961267 * x_0 * x_6
  + 0.0000470934358294214 * x_0 * x_7 + 0.00023965601140498772 * x_0 * x_8
  + 0.00027005303743996364 * x_0 * x_9 - 0.00025963315802865407 * x_1 * x_2
  + 0.00020129886121610772 * x_1 * x_3 + 0.00010701842480934178 * x_1 * x_4
  + 0.000006864957289742185 * x_1 * x_5 + 0.00008101428297508365 * x_1 * x_6
  - 0.000044659351677702835 * x_1 * x_7 + 0.00007877061640523507 * x_1 * x_8
  + 0.00019199821188182402 * x_1 * x_9 + 0.0006509799361110132 * x_2 * x_3
  + 0.0004455240576553478 * x_2 * x_4 - 0.000015353997151927852 * x_2 * x_5
  - 0.00002084575872736639 * x_2 * x_6 - 0.00030196531562697634 * x_2 * x_7
  + 0.00014731587606211805 * x_2 * x_8 + 0.0001331613424043839 * x_2 * x_9
  + 0.0002889049761221794 * x_3 * x_4 - 0.00012516054485760337 * x_3 * x_5
  - 0.00009427378918262 * x_3 * x_6 - 0.00015272796048058766 * x_3 * x_7
  + 0.00022350976631476017 * x_3 * x_8 + 0.0002498514542131433 * x_3 * x_9
  + 0.0002811897140857944 * x_4 * x_5 + 0.00013917230937569007 * x_4 * x_6
  + 0.00035570810623085174 * x_4 * x_7 + 0.00023792705686878613 * x_4 * x_8
  + 0.00004033880560799488 * x_4 * x_9 - 0.00009616381073291466 * x_5 * x_6
  + 0.0006699891088188888 * x_5 * x_7 - 0.00005100007705151588 * x_5 * x_8
  - 0.00028610863393727147 * x_5 * x_9 - 0.000009761848750811637 * x_6 * x_7
  + 0.00015356055628644306 * x_6 * x_8 + 0.0001623016841272584 * x_6 * x_9
  + 0.0001371591729303367 * x_7 * x_8 - 0.00017882565742934187 * x_7 * x_9
  + 0.00017032509701286956 * x_8 * x_9 + 0.0010207098284724311 * x_0
  - 0.002615439220486818 * x_1 - 0.006387912242497353 * x_2
  - 0.002946142543459135 * x_3 + 0.001848904765739451 * x_4
  + 0.0013838216686388092 * x_5 + 0.00035393435421324427 * x_6
  + 0.0029240209107076896 * x_7 - 0.000376014561790893 * x_8
  - 0.0012221528964938423 * x_9
Subject To
  select_exactly_pick_n: x_0 + x_1 + x_2 + x_3 + x_4 + x_5 + x_6 + x_7 + x_8
    + x_9 == 3
Binary
  x_0 x_1 x_2 x_3 x_4 x_5 x_6 x_7 x_8 x_9

Formulate and Solve

Create the quantum optimization model and solve it.

# Formulate the model
model = instance.formulate()

load_dotenv()

solver = SCIP()
job = solver.run(model)
solution = job.result()

uc_solution = instance.interpret(solution)
/Users/maximilianjanetschek/PycharmProjects/luna-usecases/.venv/lib/python3.13/site-packages/rich/live.py:260:
UserWarning: install "ipywidgets" for Jupyter support
  warnings.warn('install "ipywidgets" for Jupyter support')








print(uc_solution.to_string())

Portfolio Optimization Solution:
  Status: VALID
  Selected Assets (3): MSFT, GOOGL, AMZN

Performance (per period):
  Expected Return:   0.001498
  Variance (Risk):   0.000120
  Volatility (Std):  0.010975
  Return/Risk Ratio: 0.136505

Optimization:
  Objective Value:   -0.002868
  (lower is better; combines risk and return via risk_aversion)
uc_solution.plot(data)

<Axes: title={'center': 'Portfolio Optimization — Selected Assets'}, xlabel='Time Period', ylabel='Growth of 1 unit invested'>
png