Skip to content

Dynamic Portfolio Optimization Example

Download Notebook


Portfolio optimization selects assets to balance return and risk. This dynamic variant extends classical Markowitz theory across multiple time periods and incorporates transaction costs when rebalancing portfolios.

In this formulation, asset selections are optimized jointly over all time steps. The model assumes that expected returns and risk estimates are known for each period, resulting in a globally optimal sequence of portfolio decisions.

This corresponds to an offline optimization setting (also referred to as perfect foresight), where future information is available during optimization. As such, the model provides a theoretical benchmark for multi-period portfolio allocation rather than a directly implementable trading strategy.

import getpass
import os
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.dynamic_portfolio_optimization import (
    DynamicPoCollection,
    DynamicPoData,
    DynamicPoFormulation,
    DynamicPoInstance,
)

load_dotenv()
if "LUNA_API_KEY" not in os.environ:
    os.environ["LUNA_API_KEY"] = getpass.getpass("Enter your Luna API key: ")

Download Historical Market Data

We start by selecting a set of assets and downloading historical price data.

From these prices, we compute daily returns, which will serve as the basis for estimating: - expected returns - risk (covariance)

tickers = ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA", "JPM", "JNJ", "V"]
end_date = datetime.now(tz=pytz.timezone("Europe/London"))
start_date = end_date - timedelta(days=180)

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

close = stock_data["Close"]
daily_returns = close.pct_change().dropna()
returns = daily_returns.values  # shape: (T, N)

From Static Returns to Time-Dependent Estimates

In a static portfolio model, we would compute a single: - expected return vector μ - covariance matrix Σ

However, in a dynamic setting, these quantities change over time.

To model this, we estimate: - μ[t]: expected returns at time step t - Σ[t]: covariance matrix at time step t

using a rolling window over past observations.

Rolling Window Estimation

We use a rolling window of fixed size to estimate statistics from recent data.

For each time step t: - we look at the last window observations - compute the average return (μ[t]) - compute the covariance matrix (Σ[t])

This means that each estimate is based only on past observations relative to t.

However, note that in the subsequent optimization step, all time steps are considered jointly. As a result, while the statistical estimates themselves do not use future data, the overall optimization still operates in an offline setting with access to all time steps simultaneously.

window = 10  # rolling window

data = DynamicPoData.from_returns(
    tickers=tickers,
    returns=returns,
    window=window,
    pick_n=3,
    risk_aversion=1.5,
    transaction_cost=0.01,
)
data = DynamicPoData(
    tickers=["AAPL", "GOOGL", "MSFT"],
    expected_returns=[[0.05, 0.08, 0.03], [0.06, 0.04, 0.07]],
    covariance_matrices=[
        np.array([[0.04, 0.01, 0.005], [0.01, 0.09, 0.02], [0.005, 0.02, 0.03]]),
        np.array([[0.05, 0.015, 0.01], [0.015, 0.07, 0.025], [0.01, 0.025, 0.04]]),
    ],
    n_assets=3,
    n_time_steps=2,
    pick_n=2,
    risk_aversion=1.0,
    transaction_cost=0.01,
)
print(data.to_string())

Plot Data

Visualize expected returns and risk across time periods.

data.plot()

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

Create Formulation

Optimize multi-period asset allocation balancing returns, risk, and transaction costs.

formulation = DynamicPoFormulation()
print(formulation.to_string(data))
Dynamic Portfolio Optimization Formulation:
  Assets: 8
  Time steps: 112
  Pick per period: 3
  Risk aversion: 1.5
  Transaction cost: 0.01

Decision Variables:
  x[t,i] in {0,1} for t = 0, ..., 111, i = 0, ..., 7
  x[t,i] = 1 if asset i is selected at time step t
  z[t,i] in {0,1} for t = 1, ..., 111, i = 0, ..., 7
  z[t,i] = 1 if asset i changes between t-1 and t
  Total: 1784 binary variables

Objective:
  maximize sum_t sum_i mu[t][i]*x[t,i]
           - risk_aversion * sum_t sum_{{i,j}} sigma[t][i][j]*x[t,i]*x[t,j]
           - transaction_cost * sum_{{t>0}} sum_i z[t,i]

Constraints:
  1. Pick exactly 3 assets per period (112 constraints):
     sum_i x[t,i] == 3  for all t = 0, ..., 111
  2. Transaction cost linearization (1776 constraints):
     z[t,i] >= x[t,i] - x[t-1,i]  for all t = 1, ..., 111, i = 0, ..., 7
     z[t,i] >= x[t-1,i] - x[t,i]  for all t = 1, ..., 111, i = 0, ..., 7

Create Instance

Combine data and formulation into a solvable instance.

instance = DynamicPoInstance(data=data, formulation=formulation)
print(instance.to_string())
Data:Dynamic Portfolio Optimization Data:
  Tickers: AAPL, MSFT, GOOGL, AMZN, TSLA, JPM, JNJ, V
  Number of assets: 8
  Time steps: 112
  Assets to select per period: 3
  Risk aversion: 1.5
  Transaction cost: 0.01
Formulation:Dynamic Portfolio Optimization Formulation:
  Assets: 8
  Time steps: 112
  Pick per period: 3
  Risk aversion: 1.5
  Transaction cost: 0.01

Decision Variables:
  x[t,i] in {0,1} for t = 0, ..., 111, i = 0, ..., 7
  x[t,i] = 1 if asset i is selected at time step t
  z[t,i] in {0,1} for t = 1, ..., 111, i = 0, ..., 7
  z[t,i] = 1 if asset i changes between t-1 and t
  Total: 1784 binary variables

Objective:
  maximize sum_t sum_i mu[t][i]*x[t,i]
           - risk_aversion * sum_t sum_{{i,j}} sigma[t][i][j]*x[t,i]*x[t,j]
           - transaction_cost * sum_{{t>0}} sum_i z[t,i]

Constraints:
  1. Pick exactly 3 assets per period (112 constraints):
     sum_i x[t,i] == 3  for all t = 0, ..., 111
  2. Transaction cost linearization (1776 constraints):
     z[t,i] >= x[t,i] - x[t-1,i]  for all t = 1, ..., 111, i = 0, ..., 7
     z[t,i] >= x[t-1,i] - x[t,i]  for all t = 1, ..., 111, i = 0, ..., 7

Formulate Model

Translate the instance into a mathematical optimization model.

model = instance.formulate()

Solve and Interpret

Solve the model with SCIP and interpret the raw result into a use-case-specific solution.

scip = SCIP()
job = scip.run(model)
sol = job.result()
uc_solution = instance.interpret(sol)
print(uc_solution.to_string())
/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')






2026-05-29 11:33:37 INFO     Sleeping for 5.0 seconds. Waiting and checking a function in a loop.


2026-05-29 11:33:43 INFO     Sleeping for 10.0 seconds. Waiting and checking a function in a loop.


2026-05-29 11:33:54 INFO     Sleeping for 15.0 seconds. Waiting and checking a function in a loop.




Dynamic Portfolio Optimization Solution:
  Status: VALID
  Period 0: TSLA, JNJ, V
  Period 1: TSLA, JNJ, V
  Period 2: TSLA, JNJ, V
  Period 3: TSLA, JNJ, V
  Period 4: TSLA, JNJ, V
  Period 5: TSLA, JNJ, V
  Period 6: TSLA, JNJ, V
  Period 7: TSLA, JNJ, V
  Period 8: TSLA, JNJ, V
  Period 9: MSFT, TSLA, V
  Period 10: MSFT, TSLA, V
  Period 11: MSFT, GOOGL, TSLA
  Period 12: MSFT, GOOGL, TSLA
  Period 13: MSFT, GOOGL, TSLA
  Period 14: MSFT, GOOGL, TSLA
  Period 15: MSFT, GOOGL, TSLA
  Period 16: MSFT, GOOGL, TSLA
  Period 17: MSFT, GOOGL, TSLA
  Period 18: MSFT, GOOGL, AMZN
  Period 19: MSFT, GOOGL, AMZN
  Period 20: MSFT, GOOGL, AMZN
  Period 21: MSFT, GOOGL, AMZN
  Period 22: MSFT, GOOGL, AMZN
  Period 23: MSFT, GOOGL, AMZN
  Period 24: MSFT, GOOGL, AMZN
  Period 25: MSFT, GOOGL, AMZN
  Period 26: MSFT, GOOGL, AMZN
  Period 27: MSFT, GOOGL, AMZN
  Period 28: MSFT, GOOGL, AMZN
  Period 29: MSFT, GOOGL, AMZN
  Period 30: MSFT, GOOGL, AMZN
  Period 31: MSFT, GOOGL, AMZN
  Period 32: AAPL, GOOGL, AMZN
  Period 33: AAPL, GOOGL, AMZN
  Period 34: AAPL, AMZN, TSLA
  Period 35: AAPL, AMZN, TSLA
  Period 36: AAPL, AMZN, TSLA
  Period 37: AAPL, AMZN, TSLA
  Period 38: AAPL, AMZN, TSLA
  Period 39: AAPL, AMZN, TSLA
  Period 40: AAPL, AMZN, TSLA
  Period 41: AAPL, AMZN, TSLA
  Period 42: AAPL, AMZN, TSLA
  Period 43: AAPL, AMZN, TSLA
  Period 44: AAPL, AMZN, TSLA
  Period 45: AAPL, AMZN, TSLA
  Period 46: AAPL, MSFT, AMZN
  Period 47: AAPL, MSFT, AMZN
  Period 48: AAPL, MSFT, AMZN
  Period 49: AAPL, MSFT, AMZN
  Period 50: AAPL, MSFT, AMZN
  Period 51: AAPL, MSFT, AMZN
  Period 52: MSFT, AMZN, JPM
  Period 53: MSFT, AMZN, JPM
  Period 54: MSFT, AMZN, JPM
  Period 55: MSFT, AMZN, JPM
  Period 56: MSFT, AMZN, JPM
  Period 57: MSFT, AMZN, JPM
  Period 58: MSFT, AMZN, JPM
  Period 59: MSFT, AMZN, JPM
  Period 60: MSFT, AMZN, JPM
  Period 61: MSFT, AMZN, JPM
  Period 62: MSFT, AMZN, JPM
  Period 63: MSFT, AMZN, JPM
  Period 64: MSFT, AMZN, TSLA
  Period 65: MSFT, AMZN, TSLA
  Period 66: MSFT, AMZN, TSLA
  Period 67: MSFT, AMZN, TSLA
  Period 68: MSFT, AMZN, TSLA
  Period 69: MSFT, AMZN, TSLA
  Period 70: MSFT, AMZN, TSLA
  Period 71: MSFT, AMZN, TSLA
  Period 72: MSFT, AMZN, TSLA
  Period 73: MSFT, AMZN, TSLA
  Period 74: MSFT, AMZN, TSLA
  Period 75: MSFT, AMZN, TSLA
  Period 76: MSFT, AMZN, TSLA
  Period 77: MSFT, GOOGL, TSLA
  Period 78: MSFT, GOOGL, TSLA
  Period 79: MSFT, GOOGL, TSLA
  Period 80: MSFT, GOOGL, TSLA
  Period 81: MSFT, GOOGL, TSLA
  Period 82: MSFT, GOOGL, JPM
  Period 83: MSFT, GOOGL, JPM
  Period 84: MSFT, GOOGL, JPM
  Period 85: MSFT, GOOGL, JPM
  Period 86: MSFT, GOOGL, JPM
  Period 87: MSFT, GOOGL, JPM
  Period 88: MSFT, GOOGL, JPM
  Period 89: MSFT, GOOGL, JPM
  Period 90: MSFT, GOOGL, JPM
  Period 91: MSFT, GOOGL, JPM
  Period 92: MSFT, GOOGL, JPM
  Period 93: AAPL, MSFT, GOOGL
  Period 94: AAPL, MSFT, GOOGL
  Period 95: AAPL, MSFT, GOOGL
  Period 96: AAPL, MSFT, GOOGL
  Period 97: AAPL, MSFT, GOOGL
  Period 98: AAPL, GOOGL, JNJ
  Period 99: AAPL, GOOGL, JNJ
  Period 100: AAPL, GOOGL, JNJ
  Period 101: AAPL, GOOGL, JNJ
  Period 102: AAPL, GOOGL, JNJ
  Period 103: AAPL, GOOGL, JNJ
  Period 104: AAPL, GOOGL, JNJ
  Period 105: AAPL, GOOGL, JNJ
  Period 106: AAPL, AMZN, JNJ
  Period 107: AAPL, AMZN, JNJ
  Period 108: AAPL, AMZN, JNJ
  Period 109: AAPL, AMZN, JNJ
  Period 110: AAPL, AMZN, JNJ
  Period 111: AAPL, AMZN, JNJ
  Portfolio Return:   1.605505
  Portfolio Risk:     0.132801
  Transaction Costs:  0.260000

Plot Solution

Visualize the optimal solution.

uc_solution.plot(data)

<Axes: title={'center': 'Dynamic Portfolio — Time-dependent Selection'}, xlabel='Time Step', ylabel='Growth of 1 unit invested'>
png

Collections

Generate a benchmark collection of random instances for batch processing.

collection = DynamicPoCollection.from_random(
    min_assets=3, max_assets=6, n_time_steps=2, pick_n=2, num_instances=1, seed=42
)
model = collection.instances[0].formulate()
print(model)
Model: dynamic_portfolio_optimization<dynamic_portfolio_optimization>
Maximize
  -0.00031845908234652613 * x_0_0 * x_0_1 - 0.005378824097872742 * x_0_0 * x_0_2
  - 0.003801733038977486 * x_0_1 * x_0_2 + 0.006563805484193941 * x_1_0 * x_1_1
  - 0.006438512615851617 * x_1_0 * x_1_2 - 0.006590377259410821 * x_1_1 * x_1_2
  + 0.03303333549964048 * x_0_0 + 0.013952958333736984 * x_0_1
  + 0.052749880094328724 * x_0_2 + 0.03631039904708676 * x_1_0
  + 0.021702789925954542 * x_1_1 + 0.04562590629288803 * x_1_2 - 0.01 * z_1_0
  - 0.01 * z_1_1 - 0.01 * z_1_2
Subject To
  pick_n_period_0: x_0_0 + x_0_1 + x_0_2 == 2
  pick_n_period_1: x_1_0 + x_1_1 + x_1_2 == 2
  tc_pos_1_0: x_0_0 - x_1_0 + z_1_0 >= 0
  tc_neg_1_0: -x_0_0 + x_1_0 + z_1_0 >= 0
  tc_pos_1_1: x_0_1 - x_1_1 + z_1_1 >= 0
  tc_neg_1_1: -x_0_1 + x_1_1 + z_1_1 >= 0
  tc_pos_1_2: x_0_2 - x_1_2 + z_1_2 >= 0
  tc_neg_1_2: -x_0_2 + x_1_2 + z_1_2 >= 0
Binary
  x_0_0 x_0_1 x_0_2 x_1_0 x_1_1 x_1_2 z_1_0 z_1_1 z_1_2