Portfolio Optimization Problem (POP) Example
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
<Axes: title={'center': 'Portfolio Data — Cumulative Returns'}, xlabel='Time Period', ylabel='Growth of 1 unit invested'>
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)
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')
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)
<Axes: title={'center': 'Portfolio Optimization — Selected Assets'}, xlabel='Time Period', ylabel='Growth of 1 unit invested'>