Portfolio Optimization

Mean-variance portfolio optimization with efficient frontier
Published

February 8, 2026

1 Problem Statement

A quantitative analyst needs to allocate capital across multiple assets to:

  • Maximize returns for a given risk level, or
  • Minimize risk for a given return target

This is the classic Markowitz mean-variance optimization problem.


2 Mathematical Formulation

Given: - \(\mu\) = vector of expected returns - \(\Sigma\) = covariance matrix of returns - \(w\) = portfolio weights

Minimize variance (risk):

\[ \min_w \quad w^T \Sigma w \]

Subject to:

\[ \begin{aligned} \mu^T w &\geq r_{target} & \text{(return target)} \\ \sum_i w_i &= 1 & \text{(fully invested)} \\ w_i &\geq 0 & \text{(no short selling)} \end{aligned} \]


3 Implementation

import numpy as np
from optyx import VectorVariable, Problem

# Asset data
assets = ["Tech", "Energy", "Finance", "Healthcare", "Consumer", "Utilities"]
n_assets = len(assets)

# Expected annual returns
expected_returns = np.array([0.12, 0.08, 0.10, 0.09, 0.07, 0.05])

# Covariance matrix (simplified)
cov_matrix = np.array([
    [0.040, 0.010, 0.015, 0.008, 0.005, 0.002],
    [0.010, 0.035, 0.012, 0.006, 0.004, 0.003],
    [0.015, 0.012, 0.030, 0.010, 0.006, 0.004],
    [0.008, 0.006, 0.010, 0.025, 0.008, 0.005],
    [0.005, 0.004, 0.006, 0.008, 0.020, 0.006],
    [0.002, 0.003, 0.004, 0.005, 0.006, 0.015],
])

print("Asset Universe:")
for i, asset in enumerate(assets):
    print(f"  {asset}: E[r]={expected_returns[i]:.1%}, σ={np.sqrt(cov_matrix[i,i]):.1%}")
Asset Universe:
  Tech: E[r]=12.0%, σ=20.0%
  Energy: E[r]=8.0%, σ=18.7%
  Finance: E[r]=10.0%, σ=17.3%
  Healthcare: E[r]=9.0%, σ=15.8%
  Consumer: E[r]=7.0%, σ=14.1%
  Utilities: E[r]=5.0%, σ=12.2%

4 Creating the Optimization Model

# Decision variables: portfolio weights
# Create a vector of variables w[0]...w[5]
weights = VectorVariable("w", n_assets, lb=0, ub=0.4)

# Portfolio return: vector dot product (returns @ weights)
portfolio_return = expected_returns @ weights

# Portfolio variance: math-like w · (Σw) = w.T @ Σ @ w
portfolio_variance = weights.dot(cov_matrix @ weights)

# Constraints
total_weight = sum(weights)

print(f"Variables: {n_assets}")
print(f"Objective: minimize portfolio variance")
Variables: 6
Objective: minimize portfolio variance

5 Solving for Minimum Variance Portfolio

target_return = 0.08  # 8% target

solution = (
    Problem("min_variance")
    .minimize(portfolio_variance)
    .subject_to(total_weight >= 0.99)
    .subject_to(total_weight <= 1.01)
    .subject_to(portfolio_return >= target_return)
    .solve()
)

print("=" * 50)
print("MINIMUM VARIANCE PORTFOLIO")
print(f"Target Return: {target_return:.1%}")
print("=" * 50)
print("\nOptimal Allocation:")

# Extract values using the vector helper
w_opt = weights.to_numpy(solution.values)

for i, asset in enumerate(assets):
    w = w_opt[i]
    if w > 0.01:
        print(f"  {asset:12s}: {w:6.1%}")

actual_return = w_opt @ expected_returns
actual_risk = np.sqrt(solution.objective_value)
print(f"\nPortfolio Metrics:")
print(f"  Expected Return: {actual_return:.2%}")
print(f"  Risk (std dev):  {actual_risk:.2%}")
print(f"  Sharpe Ratio:    {actual_return/actual_risk:.2f}")
==================================================
MINIMUM VARIANCE PORTFOLIO
Target Return: 8.0%
==================================================

Optimal Allocation:
  Tech        :  16.2%
  Energy      :  10.1%
  Finance     :  10.8%
  Healthcare  :  16.9%
  Consumer    :  19.9%
  Utilities   :  25.1%

Portfolio Metrics:
  Expected Return: 8.00%
  Risk (std dev):  9.57%
  Sharpe Ratio:    0.84

6 Efficient Frontier

The efficient frontier shows the best risk-return trade-offs:

def solve_for_return_target(target):
    """Solve minimum variance portfolio for given return target."""
    w = VectorVariable("w", n_assets, lb=0, ub=0.4)
    
    port_ret = expected_returns @ w
    port_var = w.dot(cov_matrix @ w)  # Math-like: w · (Σw)
    total = w.sum()
    
    sol = (
        Problem()
        .minimize(port_var)
        .subject_to(total >= 0.99)
        .subject_to(total <= 1.01)
        .subject_to(port_ret >= target)
        .solve()
    )
    
    if sol.status.name == "OPTIMAL":
        return np.sqrt(sol.objective_value), target
    return None, None

# Compute frontier
targets = np.linspace(0.05, 0.11, 13)
frontier_points = []

for t in targets:
    risk, ret = solve_for_return_target(t)
    if risk is not None:
        frontier_points.append((risk, ret))

print("\nEfficient Frontier:")
print("-" * 40)
print(f"{'Return':>10} | {'Risk':>10} | {'Sharpe':>10}")
print("-" * 40)
for risk, ret in frontier_points:
    sharpe = ret / risk if risk > 0 else 0
    print(f"{ret:>10.2%} | {risk:>10.2%} | {sharpe:>10.2f}")

Efficient Frontier:
----------------------------------------
    Return |       Risk |     Sharpe
----------------------------------------
     5.00% |      9.22% |       0.54
     5.50% |      9.22% |       0.60
     6.00% |      9.22% |       0.65
     6.50% |      9.22% |       0.70
     7.00% |      9.22% |       0.76
     7.50% |      9.29% |       0.81
     8.00% |      9.57% |       0.84
     8.50% |     10.06% |       0.84
     9.00% |     10.69% |       0.84
     9.50% |     11.46% |       0.83
    10.00% |     12.36% |       0.81
    10.50% |     13.51% |       0.78
    11.00% |     14.19% |       0.78

7 Visualizing the Frontier (ASCII)

# Simple ASCII visualization
print("\nEfficient Frontier (ASCII)")
print("Return ↑")
print("       |")

# Scale to 40 columns
risks = [p[0] for p in frontier_points]
returns = [p[1] for p in frontier_points]
min_risk, max_risk = min(risks), max(risks)

for i, (risk, ret) in enumerate(reversed(frontier_points)):
    # Map risk to column position (0-40)
    col = int((risk - min_risk) / (max_risk - min_risk + 0.001) * 40)
    line = " " * 7 + "|" + " " * col + "*"
    label = f" {ret:.1%}"
    print(line + label)

print("       |" + "-" * 45 + " Risk →")
print(f"       0    {min_risk:.1%}                        {max_risk:.1%}")

Efficient Frontier (ASCII)
Return ↑
       |
       |                                       * 11.0%
       |                                 * 10.5%
       |                        * 10.0%
       |                 * 9.5%
       |           * 9.0%
       |      * 8.5%
       |  * 8.0%
       |* 7.5%
       |* 7.0%
       |* 6.5%
       |* 6.0%
       |* 5.5%
       |* 5.0%
       |--------------------------------------------- Risk →
       0    9.2%                        14.2%

8 Rebalancing on Price Shock

What if Tech drops 20%?

# Simulate price shock
shocked_returns = expected_returns.copy()
shocked_returns[0] = 0.05  # Tech return drops

# Re-optimize
w_shocked = VectorVariable("w_shocked", n_assets, lb=0, ub=0.4)
port_ret = shocked_returns @ w_shocked
port_var = w_shocked.dot(cov_matrix @ w_shocked)  # Math-like: w · (Σw)

solution_shocked = (
    Problem()
    .minimize(port_var)
    .subject_to(w_shocked.sum() >= 0.99)
    .subject_to(w_shocked.sum() <= 1.01)
    .subject_to(port_ret >= 0.07)  # Lower target after shock
    .solve()
)

print("\nRebalanced Portfolio (after Tech shock):")
w_new_vals = solution_shocked[w_shocked]

for i, asset in enumerate(assets):
    w_old_val = w_opt[i]
    w_new_val = w_new_vals[i]
    if abs(w_old_val - w_new_val) > 0.01:
        print(f"  {asset:12s}: {w_old_val:6.1%}{w_new_val:6.1%}")

Rebalanced Portfolio (after Tech shock):
  Tech        :  16.2% →   4.5%
  Energy      :  10.1% →  13.4%
  Finance     :  10.8% →  12.1%
  Healthcare  :  16.9% →  15.7%
  Utilities   :  25.1% →  32.5%

9 Key Takeaways

  1. Natural formulation: The math translates directly to Optyx code
  2. Automatic gradients: No need to derive Jacobians of the covariance term
  3. Fast re-optimization: Rebalancing after shocks takes milliseconds
  4. Extensible: Easy to add sector constraints, transaction costs, etc.

10 Extensions

Try adding: - Sector exposure limits - Transaction costs - Minimum holding sizes - Cardinality constraints (max N assets)

See the Mining Fleet Dispatch example for another real-world application.