This notebook studies a basic Markowitz portfolio optimization problem using daily stock price data. We start by downloading price series from Yahoo Finance, transform them into daily log returns, and then estimate annualized expected returns and an annualized covariance matrix.
The notebook answers three main questions. First, we determine the efficient portfolio that achieves an annual expected return of 8% when short-selling is allowed. Second, we compute and plot the set of frontier portfolios in the expected return--volatility space. Third, we repeat the frontier calculation under a no-short-sales constraint and compare the results with the unconstrained case.
The purpose of the analysis is to illustrate the trade-off between expected return and risk. The efficient frontier shows the minimum level of risk required to achieve different target returns, while the comparison between the short-sales and no-short-sales cases shows how investment constraints affect the feasible set of portfolios.
Downloading and Transforming the Data
The first step is to download stock price data using the yfinance package. In this notebook, the data are downloaded for the period from 2020-01-01 to 2023-01-01.
From the price series we compute daily log returns:
$$ r_{i,t} = \log\left(\frac{P_{i,t}}{P_{i,t-1}}\right), $$
where $P_{i,t}$ denotes the price of asset $i$ on trading day $t$. From these daily returns we estimate the annualized expected return vector and the annualized covariance matrix:
$$ \mu = 252 \cdot \bar r, \qquad \Sigma = 252 \cdot \widehat{\operatorname{Cov}}(r_t). $$
The factor 252 is used because there are approximately 252 trading days in a year. In the optimization problems below, $\mu$ contains the annualized expected returns of the assets, while $\Sigma$ determines the annualized risk of any portfolio.
Show code
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
from scipy.optimize import minimize
pd.set_option("display.float_format", lambda x: f"{x:.4f}")
# ------------------------------------------------------------
# SETTINGS
# ------------------------------------------------------------
# Assets used in the original assignment.
tickers = ["AAPL", "AMZN", "BRK-B", "DAX", "DOW", "NKE"]
start_date = "2020-01-01"
end_date = "2023-01-01"
trading_days = 252
# ------------------------------------------------------------
# DOWNLOAD PRICE DATA
# ------------------------------------------------------------
price_data = yf.download(
tickers,
start=start_date,
end=end_date,
auto_adjust=False,
progress=False
)
# With several tickers, yfinance returns a DataFrame with multi-level columns.
# For the portfolio calculation we only need closing prices.
if isinstance(price_data.columns, pd.MultiIndex):
raw_data = price_data["Close"].copy()
else:
raw_data = price_data[["Close"]].copy()
raw_data.columns = tickers
# Keep the downloaded tickers in the same order as the original ticker list.
available_tickers = [ticker for ticker in tickers if ticker in raw_data.columns]
missing_tickers = sorted(set(tickers) - set(available_tickers))
if missing_tickers:
print("Missing tickers from the downloaded data:", missing_tickers)
raw_data = raw_data[available_tickers]
raw_data = raw_data.dropna(how="all").ffill().dropna()
# ------------------------------------------------------------
# LOG RETURNS AND ANNUALIZED MOMENTS
# ------------------------------------------------------------
log_returns = np.log(raw_data / raw_data.shift(1)).dropna()
expected_returns = log_returns.mean() * trading_days
cov_matrix = log_returns.cov() * trading_days
summary_table = pd.DataFrame({
"Annualized expected return": expected_returns,
"Annualized volatility": np.sqrt(np.diag(cov_matrix))
})
print("First rows of the price data:")
display(raw_data.head())
print("Annualized return and risk measures:")
display(summary_table)
print("Annualized covariance matrix:")
display(cov_matrix)
First rows of the price data:
| Ticker | AAPL | AMZN | BRK-B | DAX | DOW | NKE |
|---|---|---|---|---|---|---|
| Date | ||||||
| 2020-01-02 | 75.0875 | 94.9005 | 228.3900 | 28.5000 | 53.7500 | 102.2000 |
| 2020-01-03 | 74.3575 | 93.7485 | 226.1800 | 27.9900 | 52.4200 | 101.9200 |
| 2020-01-06 | 74.9500 | 95.1440 | 226.9900 | 28.0050 | 52.2100 | 101.8300 |
| 2020-01-07 | 74.5975 | 95.3430 | 225.9200 | 27.9550 | 52.1900 | 101.7800 |
| 2020-01-08 | 75.7975 | 94.5985 | 225.9900 | 28.2600 | 52.7300 | 101.5500 |
Annualized return and risk measures:
| Annualized expected return | Annualized volatility | |
|---|---|---|
| Ticker | ||
| AAPL | 0.1830 | 0.3692 |
| AMZN | -0.0407 | 0.3910 |
| BRK-B | 0.1008 | 0.2560 |
| DAX | -0.0358 | 0.2844 |
| DOW | -0.0215 | 0.4377 |
| NKE | 0.0452 | 0.3772 |
Annualized covariance matrix:
| Ticker | AAPL | AMZN | BRK-B | DAX | DOW | NKE |
|---|---|---|---|---|---|---|
| Ticker | ||||||
| AAPL | 0.1363 | 0.0961 | 0.0565 | 0.0611 | 0.0701 | 0.0805 |
| AMZN | 0.0961 | 0.1529 | 0.0396 | 0.0503 | 0.0499 | 0.0711 |
| BRK-B | 0.0565 | 0.0396 | 0.0655 | 0.0523 | 0.0794 | 0.0568 |
| DAX | 0.0611 | 0.0503 | 0.0523 | 0.0809 | 0.0822 | 0.0668 |
| DOW | 0.0701 | 0.0499 | 0.0794 | 0.0822 | 0.1916 | 0.0820 |
| NKE | 0.0805 | 0.0711 | 0.0568 | 0.0668 | 0.0820 | 0.1423 |
Efficient Portfolio with an 8% Expected Return
In this part we find the portfolio that achieves an annual expected return of 8% with the lowest possible risk. Let $w=(w_1,\ldots,w_N)^{\top}$ denote the vector of portfolio weights, where $w_i$ is the weight assigned to asset $i$.
The expected return of the portfolio is
$$ \operatorname{E}(R_p) = w^{\top}\mu, $$
and its variance is
$$ \sigma_p^2 = w^{\top}\Sigma w. $$
The minimum-variance problem for the 8% target return is therefore
$$ \min_w ; w^{\top}\Sigma w $$
subject to
$$ w^{\top}\mu = 0.08, \qquad \mathbf{1}^{\top}w = 1. $$
In this block, short-selling is allowed. This means that portfolio weights may be negative. Economically, a negative weight represents a short position in the corresponding asset. Allowing short positions expands the feasible set of portfolios and may reduce the minimum achievable risk for a given target return.
Show code
def solve_min_variance_portfolio(target_return, allow_short=True):
# Solve the minimum-variance portfolio problem for a given target return.
mu = expected_returns.to_numpy()
sigma = cov_matrix.to_numpy()
asset_names = expected_returns.index
n_assets = len(asset_names)
def portfolio_variance(weights):
return float(weights.T @ sigma @ weights)
constraints = [
{
"type": "eq",
"fun": lambda weights: np.sum(weights) - 1
},
{
"type": "eq",
"fun": lambda weights: weights @ mu - target_return
}
]
bounds = None if allow_short else [(0, 1) for _ in range(n_assets)]
initial_weights = np.repeat(1 / n_assets, n_assets)
result = minimize(
portfolio_variance,
initial_weights,
method="SLSQP",
bounds=bounds,
constraints=constraints,
options={"ftol": 1e-12, "maxiter": 1000}
)
if not result.success:
raise RuntimeError(f"Optimization failed: {result.message}")
weights = pd.Series(result.x, index=asset_names, name="Weight")
portfolio_return = float(weights @ expected_returns)
portfolio_variance_value = portfolio_variance(weights.to_numpy())
portfolio_volatility = np.sqrt(portfolio_variance_value)
return {
"weights": weights,
"return": portfolio_return,
"variance": portfolio_variance_value,
"volatility": portfolio_volatility
}
# ------------------------------------------------------------
# EFFICIENT PORTFOLIO WITH AN 8% TARGET RETURN, SHORT-SELLING ALLOWED
# ------------------------------------------------------------
target_return = 0.08
portfolio_8_short = solve_min_variance_portfolio(
target_return=target_return,
allow_short=True
)
weights_8_short = portfolio_8_short["weights"].to_frame()
weights_8_short["Weight, %"] = 100 * weights_8_short["Weight"]
performance_8_short = pd.DataFrame({
"Measure": ["Target return", "Achieved expected return", "Variance", "Volatility"],
"Value": [
target_return,
portfolio_8_short["return"],
portfolio_8_short["variance"],
portfolio_8_short["volatility"]
]
})
print("Optimal portfolio weights for an 8% target return, with short-selling:")
display(weights_8_short)
print("Portfolio performance measures:")
display(performance_8_short)
Optimal portfolio weights for an 8% target return, with short-selling:
| Weight | Weight, % | |
|---|---|---|
| Ticker | ||
| AAPL | 0.0046 | 0.4576 |
| AMZN | 0.1082 | 10.8201 |
| BRK-B | 0.8704 | 87.0367 |
| DAX | 0.2367 | 23.6736 |
| DOW | -0.2137 | -21.3716 |
| NKE | -0.0062 | -0.6163 |
Portfolio performance measures:
| Measure | Value | |
|---|---|---|
| 0 | Target return | 0.0800 |
| 1 | Achieved expected return | 0.0800 |
| 2 | Variance | 0.0560 |
| 3 | Volatility | 0.2367 |
Frontier Portfolios in the Expected Return--Risk Space
The set of frontier portfolios is obtained by solving the same minimum-variance problem for many different target returns $r^*$ rather than for only one fixed target return.
For each target return, we solve
$$ \min_w ; w^{\top}\Sigma w $$
subject to
$$ w^{\top}\mu = r^*, \qquad \mathbf{1}^{\top}w = 1. $$
Each target return corresponds to a portfolio with the lowest possible variance among all portfolios that achieve that return. Plotting the resulting pairs $(\sigma_p,\operatorname{E}(R_p))$ gives the frontier in the expected return--volatility space.
When short-selling is allowed, the portfolio weights are not restricted by sign. This makes the feasible set larger, so the minimum variance for a given target return can be lower than under a no-short-sales constraint.
Show code
def build_frontier(target_returns, allow_short=True):
# Compute frontier portfolios over a grid of target returns.
rows = []
for target in target_returns:
try:
solution = solve_min_variance_portfolio(
target_return=target,
allow_short=allow_short
)
rows.append({
"target_return": target,
"expected_return": solution["return"],
"volatility": solution["volatility"],
"variance": solution["variance"]
})
except RuntimeError:
# Some targets may be infeasible, especially under no-short-sales constraints.
# Skipping infeasible points makes the function usable in both cases.
continue
return pd.DataFrame(rows)
# ------------------------------------------------------------
# FRONTIER PORTFOLIOS WITH SHORT-SELLING
# ------------------------------------------------------------
min_target = expected_returns.min() - 0.10
max_target = expected_returns.max() + 0.10
frontier_targets_short = np.linspace(min_target, max_target, 200)
frontier_short = build_frontier(
target_returns=frontier_targets_short,
allow_short=True
)
plt.figure(figsize=(8, 5))
plt.plot(
frontier_short["volatility"],
frontier_short["expected_return"],
label="Frontier with short-selling"
)
plt.scatter(
portfolio_8_short["volatility"],
portfolio_8_short["return"],
marker="o",
label="8% target return"
)
plt.axhline(target_return, linestyle="--", linewidth=1)
plt.xlabel("Annualized volatility")
plt.ylabel("Annualized expected return")
plt.title("Frontier portfolios with short-selling")
plt.grid(True, linestyle="--", alpha=0.6)
plt.legend()
plt.show()
Frontier Portfolios without Short-Selling
We now solve the same portfolio optimization problem, but we impose a no-short-sales constraint. Mathematically, this means that all portfolio weights must be nonnegative:
$$ w_i \geq 0, \qquad i = 1,\ldots,N. $$
The constrained optimization problem is
$$ \min_w ; w^{\top}\Sigma w $$
subject to
$$ w^{\top}\mu = r^*, \qquad \mathbf{1}^{\top}w = 1, \qquad w_i \geq 0 \quad \text{for all } i. $$
Under this restriction, the investor can only hold positive or zero positions in the assets. The feasible set is therefore smaller than in the unconstrained case. As a result, the same target return usually requires at least as much risk as in the short-selling case, and often more. In addition, without short-selling, feasible target returns must lie between the smallest and largest individual expected returns, because the portfolio return is a convex combination of the asset returns.
Show code
# ------------------------------------------------------------
# EFFICIENT PORTFOLIO WITH AN 8% TARGET RETURN, NO SHORT-SELLING
# ------------------------------------------------------------
portfolio_8_no_short = solve_min_variance_portfolio(
target_return=target_return,
allow_short=False
)
weights_8_no_short = portfolio_8_no_short["weights"].to_frame()
weights_8_no_short["Weight, %"] = 100 * weights_8_no_short["Weight"]
performance_8_no_short = pd.DataFrame({
"Measure": ["Target return", "Achieved expected return", "Variance", "Volatility"],
"Value": [
target_return,
portfolio_8_no_short["return"],
portfolio_8_no_short["variance"],
portfolio_8_no_short["volatility"]
]
})
print("Optimal portfolio weights for an 8% target return, without short-selling:")
display(weights_8_no_short)
print("Portfolio performance measures:")
display(performance_8_no_short)
# ------------------------------------------------------------
# FRONTIER PORTFOLIOS WITHOUT SHORT-SELLING
# ------------------------------------------------------------
frontier_targets_no_short = np.linspace(
expected_returns.min(),
expected_returns.max(),
200
)
frontier_no_short = build_frontier(
target_returns=frontier_targets_no_short,
allow_short=False
)
plt.figure(figsize=(8, 5))
plt.plot(
frontier_no_short["volatility"],
frontier_no_short["expected_return"],
label="Frontier without short-selling"
)
plt.scatter(
portfolio_8_no_short["volatility"],
portfolio_8_no_short["return"],
marker="o",
label="8% target return"
)
plt.axhline(target_return, linestyle="--", linewidth=1)
plt.xlabel("Annualized volatility")
plt.ylabel("Annualized expected return")
plt.title("Frontier portfolios without short-selling")
plt.ylim(expected_returns.min() - 0.02, expected_returns.max() + 0.02)
plt.grid(True, linestyle="--", alpha=0.6)
plt.legend()
plt.show()
Optimal portfolio weights for an 8% target return, without short-selling:
| Weight | Weight, % | |
|---|---|---|
| Ticker | ||
| AAPL | 0.0556 | 5.5579 |
| AMZN | 0.0981 | 9.8130 |
| BRK-B | 0.7623 | 76.2315 |
| DAX | 0.0840 | 8.3976 |
| DOW | 0.0000 | 0.0000 |
| NKE | 0.0000 | 0.0000 |
Portfolio performance measures:
| Measure | Value | |
|---|---|---|
| 0 | Target return | 0.0800 |
| 1 | Achieved expected return | 0.0800 |
| 2 | Variance | 0.0604 |
| 3 | Volatility | 0.2458 |
Show code
# ------------------------------------------------------------
# COMPARISON OF SHORT-SELLING AND NO-SHORT-SALES RESULTS
# ------------------------------------------------------------
comparison_table = pd.DataFrame({
"With short-selling": [
portfolio_8_short["return"],
portfolio_8_short["variance"],
portfolio_8_short["volatility"]
],
"Without short-selling": [
portfolio_8_no_short["return"],
portfolio_8_no_short["variance"],
portfolio_8_no_short["volatility"]
]
}, index=["Achieved expected return", "Variance", "Volatility"])
comparison_table["Difference, no-short minus short"] = (
comparison_table["Without short-selling"] - comparison_table["With short-selling"]
)
weights_comparison = pd.DataFrame({
"With short-selling": portfolio_8_short["weights"],
"Without short-selling": portfolio_8_no_short["weights"]
})
weights_comparison["Difference, no-short minus short"] = (
weights_comparison["Without short-selling"] - weights_comparison["With short-selling"]
)
print("Comparison of the portfolios with an 8% target return:")
display(comparison_table)
print("Comparison of portfolio weights:")
display(weights_comparison)
plt.figure(figsize=(8, 5))
plt.plot(
frontier_short["volatility"],
frontier_short["expected_return"],
label="With short-selling"
)
plt.plot(
frontier_no_short["volatility"],
frontier_no_short["expected_return"],
label="Without short-selling"
)
plt.scatter(
portfolio_8_short["volatility"],
portfolio_8_short["return"],
marker="o",
label="8%, with short-selling"
)
plt.scatter(
portfolio_8_no_short["volatility"],
portfolio_8_no_short["return"],
marker="x",
label="8%, without short-selling"
)
plt.axhline(target_return, linestyle="--", linewidth=1)
plt.xlabel("Annualized volatility")
plt.ylabel("Annualized expected return")
plt.title("Comparison of short-selling and no-short-sales frontiers")
plt.grid(True, linestyle="--", alpha=0.6)
plt.legend()
plt.show()
Comparison of the portfolios with an 8% target return:
| With short-selling | Without short-selling | Difference, no-short minus short | |
|---|---|---|---|
| Achieved expected return | 0.0800 | 0.0800 | 0.0000 |
| Variance | 0.0560 | 0.0604 | 0.0044 |
| Volatility | 0.2367 | 0.2458 | 0.0091 |
Comparison of portfolio weights:
| With short-selling | Without short-selling | Difference, no-short minus short | |
|---|---|---|---|
| Ticker | |||
| AAPL | 0.0046 | 0.0556 | 0.0510 |
| AMZN | 0.1082 | 0.0981 | -0.0101 |
| BRK-B | 0.8704 | 0.7623 | -0.1081 |
| DAX | 0.2367 | 0.0840 | -0.1528 |
| DOW | -0.2137 | 0.0000 | 0.2137 |
| NKE | -0.0062 | 0.0000 | 0.0062 |