Mean-variance portfolio optimization with covariance using w.dot(Σ @ w)
Published
August 24, 2026
1 Introduction
This tutorial builds a complete Markowitz mean-variance portfolio optimization model using Optyx. You’ll see how VectorVariable, math-like w.dot(Σ @ w) syntax, and Parameter work together for realistic portfolio problems.
/tmp/ipykernel_2973/547607387.py:8: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
.solve()
The efficient frontier shows the best risk-return tradeoff. Let’s trace it by varying the target return:
from optyx import VectorVariable, Problemdef solve_for_target(target_return):"""Solve minimum variance for a given target return.""" w = VectorVariable("w", n_assets, lb=0, ub=1) port_ret = expected_returns @ w port_var = w.dot(covariance @ w) # wᵀΣw sol = ( Problem() .minimize(port_var) .subject_to(w.sum().eq(1)) .subject_to(port_ret >= target_return) .solve() )if sol.status.value =="optimal": weights = np.array([sol[f'w[{i}]'] for i inrange(n_assets)]) ret = expected_returns @ weights vol = np.sqrt(weights @ covariance @ weights)return ret, vol, weightsreturnNone# Trace the frontiertargets = np.linspace(0.05, 0.14, 20)frontier = []for target in targets: result = solve_for_target(target)if result: frontier.append(result)# Display resultsprint("Efficient Frontier (Risk-Return Tradeoff):")print("-"*50)print(f"{'Return':>10}{'Volatility':>12}{'Sharpe':>10}")print("-"*50)for ret, vol, _ in frontier[::3]: # Show every 3rd point sharpe = (ret -0.02) / volprint(f"{ret:>10.2%}{vol:>12.2%}{sharpe:>10.3f}")
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 3.29e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 2.13e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 1.75e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 2.91e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 1.85e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 2.82e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 2.53e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 2.48e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/1106969790.py:14: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 2.66e-05). Retrying with trust-constr method for more robust optimization.
.solve()
7 Using Parameters for Fast Re-solves
When exploring scenarios, rebuilding the problem each time is wasteful. Use Parameter to update values without recompilation:
from optyx import VectorVariable, Parameter, Problem# Create problem oncew = VectorVariable("w", n_assets, lb=0, ub=1)target_param = Parameter("target", value=0.08)port_ret = expected_returns @ wport_var = w.dot(covariance @ w) # wᵀΣwproblem = ( Problem("parametric_portfolio") .minimize(port_var) .subject_to(w.sum().eq(1)) .subject_to(port_ret >= target_param))# Solve for different targets (much faster after first solve)import timetargets = [0.06, 0.08, 0.10, 0.12]results = []print("Parametric Solves:")print("-"*50)for i, target inenumerate(targets): target_param.set(target) start = time.perf_counter() sol = problem.solve() elapsed = time.perf_counter() - start weights = np.array([sol[f'w[{i}]'] for i inrange(n_assets)]) vol = np.sqrt(weights @ covariance @ weights) solve_type ="cold"if i ==0else"warm"print(f"Target {target:.0%}: vol={vol:.2%}, time={elapsed*1000:.2f}ms ({solve_type})")
/tmp/ipykernel_2973/4020890516.py:29: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
sol = problem.solve()
/tmp/ipykernel_2973/4020890516.py:29: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 1.50e-04). Retrying with trust-constr method for more robust optimization.
sol = problem.solve()
Target 12%: vol=7.20%, time=286.82ms (warm)
TipParameter Performance
After the first solve, subsequent solves with different parameter values reuse the compiled problem structure. This can be 10-100x faster for complex problems.
8 Real-World Constraints
8.1 Sector Exposure Limits
Limit exposure to any single sector:
# Sector definitions (3 assets each in Tech, Energy, Finance; 1 in Other)sectors = {"Tech": [0, 1, 2],"Energy": [3, 4, 5],"Finance": [6, 7, 8],"Other": [9]}w = VectorVariable("w", n_assets, lb=0, ub=1)port_var = w.dot(covariance @ w) # wᵀΣwport_ret = expected_returns @ wproblem = ( Problem("sector_constrained") .minimize(port_var) .subject_to(w.sum().eq(1)) .subject_to(port_ret >=0.09))# Sector limits: max 40% in any sectorfor sector_name, indices in sectors.items(): sector_weight =sum(w[i] for i in indices) problem = problem.subject_to(sector_weight <=0.40)solution = problem.solve()# Display by sectorprint("Sector-Constrained Portfolio:")print("-"*50)for sector_name, indices in sectors.items(): sector_total =sum(solution[f'w[{i}]'] for i in indices)print(f"{sector_name}: {sector_total:.1%}")
/tmp/ipykernel_2973/1750883322.py:25: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 1.87e-04). Retrying with trust-constr method for more robust optimization.
solution = problem.solve()
# Maximum 20% in any single asset, minimum 2% if investedw = VectorVariable("w", n_assets, lb=0, ub=0.20)# Note: minimum position constraints require binary variables or careful formulation# For simplicity, we just use upper bounds heresolution = ( Problem("position_limited") .minimize(w.dot(covariance @ w)) # wᵀΣw .subject_to(w.sum().eq(1)) .subject_to(expected_returns @ w >=0.08) .solve())print("Position-Limited Portfolio:")print("-"*40)weights = np.array([solution[f'w[{i}]'] for i inrange(n_assets)])for i inrange(n_assets):if weights[i] >0.01:print(f" {asset_names[i]}: {weights[i]:.1%}")print(f"\nMax position: {weights.max():.1%}")print(f"Positions > 1%: {np.sum(weights >0.01)}")
/tmp/ipykernel_2973/1078884120.py:12: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.55e-04). Retrying with trust-constr method for more robust optimization.
.solve()
9 Maximum Sharpe Ratio Portfolio
The tangency portfolio maximizes risk-adjusted return. This is a more complex problem requiring a different formulation:
from optyx import VectorVariable, Problem# Risk-free raterf =0.02# For max Sharpe, we minimize variance for unit excess return# This is equivalent to the tangency portfoliow = VectorVariable("w", n_assets, lb=0) # No upper bound initially# Excess return must equal 1 (we'll rescale later)excess_returns = expected_returns - rfport_excess = excess_returns @ wsolution = ( Problem("max_sharpe") .minimize(w.dot(covariance @ w)) # wᵀΣw .subject_to(port_excess.eq(1)) # Normalize excess return to 1 .solve())# Rescale to sum to 1raw_weights = np.array([solution[f'w[{i}]'] for i inrange(n_assets)])tangency_weights = raw_weights / raw_weights.sum()# Compute metricstang_return = expected_returns @ tangency_weightstang_vol = np.sqrt(tangency_weights @ covariance @ tangency_weights)tang_sharpe = (tang_return - rf) / tang_volprint("="*60)print("MAXIMUM SHARPE RATIO (TANGENCY) PORTFOLIO")print("="*60)print(f"Expected Return: {tang_return:.2%}")print(f"Volatility: {tang_vol:.2%}")print(f"Sharpe Ratio: {tang_sharpe:.3f}")print()print("Allocation:")for i inrange(n_assets):if tangency_weights[i] >0.01:print(f" {asset_names[i]}: {tangency_weights[i]:.1%}")
/tmp/ipykernel_2973/4187109632.py:18: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 1.03e-04). Retrying with trust-constr method for more robust optimization.
.solve()
/tmp/ipykernel_2973/220360586.py:15: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
.solve()
Portfolio Comparison:
============================================================
Portfolio Return Vol Sharpe
------------------------------------------------------------
Equal Weight 10.00% 8.01% 0.999
Min Variance (8% target) 121.53% 70.43% 1.697
Max Sharpe 11.29% 6.54% 1.420
Min Variance 10.26% 6.17% 1.339
11 Performance: VectorVariable vs Loop-Based
Let’s compare the performance of VectorVariable approach vs traditional loops:
import timedef old_style_portfolio(n, returns, cov, target):"""Build portfolio with loop-based variables."""from optyx import Variable, Problem# Create variables one by one w = [Variable(f"w_{i}", lb=0, ub=1) for i inrange(n)]# Portfolio return via loop port_ret =sum(w[i] * returns[i] for i inrange(n))# Portfolio variance via double loop (O(n²)) port_var =sum( w[i] * cov[i, j] * w[j] for i inrange(n) for j inrange(n) )# Budget constraint via loop budget =sum(w) problem = ( Problem() .minimize(port_var) .subject_to(budget.eq(1)) .subject_to(port_ret >= target) )return problem.solve()def new_style_portfolio(n, returns, cov, target):"""Build portfolio with VectorVariable."""from optyx import VectorVariable, Problem w = VectorVariable("w", n, lb=0, ub=1) port_ret = returns @ w port_var = w.dot(cov @ w) # wᵀΣw problem = ( Problem() .minimize(port_var) .subject_to(w.sum().eq(1)) .subject_to(port_ret >= target) )return problem.solve()# Benchmarkn =10print("Build + Solve Time Comparison (10 assets):")print("-"*50)# Old stylestart = time.perf_counter()sol_old = old_style_portfolio(n, expected_returns, covariance, 0.08)old_time = time.perf_counter() - start# New stylestart = time.perf_counter()sol_new = new_style_portfolio(n, expected_returns, covariance, 0.08)new_time = time.perf_counter() - startprint(f"Loop-based: {old_time*1000:.2f} ms")print(f"VectorVariable: {new_time*1000:.2f} ms")print(f"Speedup: {old_time/new_time:.1f}x")
Build + Solve Time Comparison (10 assets):
--------------------------------------------------
/tmp/ipykernel_2973/1842296654.py:30: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
return problem.solve()
/tmp/ipykernel_2973/1842296654.py:48: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 7.50e-04). Retrying with trust-constr method for more robust optimization.
return problem.solve()
Loop-based: 543.42 ms
VectorVariable: 356.51 ms
Speedup: 1.5x
12 Summary
This tutorial demonstrated:
Feature
Benefit
VectorVariable
Clean syntax for many variables
w.dot(Σ @ w)
Math-like quadratic form with O(1) gradient
Parameter
Fast re-solves for scenarios
Vector operations
No explicit loops needed
Key takeaways:
Use math-like syntaxw.dot(Σ @ w) for variance—reads as wᵀΣw and computes gradients analytically
Use Parameter when exploring different target returns or risk tolerances
Vector syntax makes constraints like w.sum().eq(1) readable and maintainable