Fundamentals of Portfolio Optimization
The previous chapter measured investment returns and risk. We now ask how to combine assets: which allocations offer attractive expected returns for the amount of variability an investor accepts?
We begin with a single investment horizon and explicitly assumed expected returns, volatilities, and correlations. These inputs determine a set of feasible portfolios. Optimization then selects among them under constraints such as full investment and long-only positions. Later examples introduce historical estimates, target returns, short selling, and a risk-free asset.
Run this setup before working through the examples in order. The toolkit provides reusable portfolio calculations; each example makes the investment inputs and results visible.
import json
import numpy as np
import pandas as pd
import PortfolioOptimizationKit as pok Modern Portfolio Theory (MPT)
Introduction to MPT
Modern Portfolio Theory (MPT) compares portfolios using expected return and variance. Diversification works through the way asset returns move together, not simply through the number of assets held. Adding an asset can reduce portfolio risk, but an arbitrary mix need not be less volatile than every constituent.
For
The weights are chosen at the beginning of the period. Assume no external cash flows or transaction costs during that period. Define
The return and variance identities below require finite second moments, not Gaussian returns. Using only mean and variance as a selection criterion nevertheless leaves out other features discussed in Chapter 1.1, including tail shape and drawdown.
Portfolio Return
Suppose initial wealth is
Therefore the portfolio's realized return is:
Taking expectations, with weights fixed at the investment decision, gives:
This linearity does not require independence. For fixed weights and expected asset returns, changing correlation changes risk but not expected portfolio return.
Expected return is different from historical compounded growth. Weighting asset CAGRs generally gives neither the portfolio's CAGR nor a justified forecast. For a historical portfolio, first calculate each period's return using the weights held at that period's start, then compound the portfolio returns. Maintaining the same target weights across periods implies rebalancing; buy-and-hold weights generally drift.
Portfolio Volatility
Understanding Portfolio Volatility
Portfolio volatility is the standard deviation of
Calculating and Minimizing Volatility
For two assets, subtract the portfolio mean and expand the square:
The first two terms measure each asset's own variance contribution. The cross term captures how the two return deviations move together. For positive asset volatilities, write covariance in terms of correlation
Taking the square root gives:
For fixed positive weights and asset volatilities, reducing correlation reduces this variance. A weight of zero removes the corresponding asset and the covariance term.
In matrix form, the two-asset covariance matrix is:
For
A valid covariance matrix is symmetric and positive semidefinite: it cannot imply a negative variance for any portfolio. The matrix product expresses the same variance and covariance contributions as the two-asset expansion.
Efficient Frontiers
Understanding Efficient Frontiers
The feasible set contains the portfolios allowed by the investment constraints. A portfolio is mean–variance efficient if no other feasible portfolio offers at least as much expected return with no more volatility, with a strict improvement in at least one of the two measures.
The efficient frontier is the set of those non-dominated portfolios. It depends on the assumed expected returns, covariance matrix, and constraints. It describes model-based trade-offs rather than guaranteed future performance.
The global minimum-variance (GMV) portfolio has the lowest variance across the feasible set. In the usual non-degenerate case, the efficient frontier extends from GMV toward higher expected returns. Feasible portfolios on the lower-return branch can be dominated; plotting every allocation does not make every plotted point efficient.
Calculating Efficient Frontiers
One way to trace the minimum-variance boundary is to solve for the lowest variance at each target expected return
For fully invested, long-only weights, a feasible target must lie between the smallest and largest expected asset returns. The target-return boundary can include dominated portfolios; the efficient part is its upper, non-dominated portion. We will implement this constrained optimization later. First, two assets let us see the trade-off directly by varying their weights.
Examining Efficient Frontiers with Two-Asset Portfolios
Setting Up the Scenario
Use the following hypothetical one-year inputs. These are assumed expected returns and volatilities, so no historical estimation or additional annualization is needed.
| Asset | Expected annual return | Annual volatility |
|---|---|---|
| Asset 1 | 8% | 10% |
| Asset 2 | 12% | 20% |
expected_returns = np.array([0.08, 0.12])
volatilities = np.array([0.10, 0.20]) Worked Example: A 60/40 Allocation
Allocate 60% to Asset 1 and 40% to Asset 2, with correlation
Portfolio variance is:
Hence volatility is
weights = np.array([0.60, 0.40])
rho = 0.25
sigma_1, sigma_2 = volatilities
covariance = np.array([
[sigma_1 ** 2, rho * sigma_1 * sigma_2],
[rho * sigma_1 * sigma_2, sigma_2 ** 2],
])
portfolio_mean = pok.portfolio_return(weights, expected_returns)
portfolio_variance = weights @ covariance @ weights
portfolio_vol = pok.portfolio_volatility(weights, covariance)
print(f"Expected annual return: {portfolio_mean:.2%}")
print(f"Annual return variance: {portfolio_variance:.6f}")
print(f"Annual volatility: {portfolio_vol:.2%}") Try it: change rho to 0.0, then 1.0. The same allocation has volatility of 10% and 14%, respectively, while expected return remains 9.60%.
Finding the Two-Asset Minimum-Variance Weight
Write
Setting its derivative to zero gives:
and, when the denominator is positive:
For long-only investing, a solution outside
If both volatilities are equal and correlation is one, the denominator is zero: every allocation has the same variance. The experiment below displays the equally weighted allocation as one representative minimum-variance portfolio in that special case. If expected returns differ, the higher-return endpoint is the efficient choice.
Comparing Correlation Scenarios
Run the setup and asset-input cells first. We calculate 101 long-only allocations for each correlation, holding the expected returns and volatilities fixed. The black diamond marks the analytical GMV allocation. No random simulation is needed for this comparison.
import pandas as pd
import numpy as np
import PortfolioOptimizationKit as pok
import json
correlations = [1.0, 0.5, 0.25, 0.0, -0.5, -1.0]
weights_1 = np.linspace(0, 1, num=101)
weight_grid = np.column_stack([weights_1, 1 - weights_1])
sigma_1, sigma_2 = volatilities
plot_data = {}
gmv_rows = []
for k_rho, rho in enumerate(correlations):
cov_12 = rho * sigma_1 * sigma_2
covariance = np.array([[sigma_1 ** 2, cov_12], [cov_12, sigma_2 ** 2]])
portfolios = []
for weights in weight_grid:
portfolio_ret = pok.portfolio_return(weights, expected_returns)
portfolio_vol = pok.portfolio_volatility(weights, covariance)
portfolios.append([
float(portfolio_vol * 100),
float(portfolio_ret * 100),
float(weights[1]), # Asset 2's weight controls the point's color.
])
denominator = sigma_1 ** 2 + sigma_2 ** 2 - 2 * cov_12
gmv_weight_1 = np.clip((sigma_2 ** 2 - cov_12) / denominator, 0, 1) if denominator > 0 else 0.5
gmv_weights = np.array([gmv_weight_1, 1 - gmv_weight_1])
gmv_ret = pok.portfolio_return(gmv_weights, expected_returns)
gmv_vol = pok.portfolio_volatility(gmv_weights, covariance)
gmv_rows.append({
"Correlation": rho,
"Asset 1 weight (%)": gmv_weights[0] * 100,
"Asset 2 weight (%)": gmv_weights[1] * 100,
"Expected return (%)": gmv_ret * 100,
"Volatility (%)": gmv_vol * 100,
})
plot_data[f"ef_{k_rho}"] = {
"type": "scatter",
"yAxisName": "Expected annual return (%)",
"title": f"Feasible Portfolios: ρ = {rho:.2f}",
"series": {"Portfolios": portfolios, "GMV": [[float(gmv_vol * 100), float(gmv_ret * 100)]]},
"xAxis": {"type": "value", "name": "Annual volatility (%)", "min": 0},
"yAxis": {"type": "value", "name": "Expected annual return (%)", "scale": True},
"visualMap": {
"min": 0,
"max": 1,
"dimension": 2,
"seriesIndex": 0,
"inRange": {"color": ["#0000FF", "#00FF00"]},
},
}
gmv_comparison = pd.DataFrame(gmv_rows).set_index("Correlation")
print(gmv_comparison.to_string(float_format=lambda value: f"{value:.2f}"))
print("\n<ECHARTS_DATA>" + json.dumps(plot_data, allow_nan=False)) Analyzing Results and Implications
Each panel shows the feasible allocations, not only the efficient frontier. Blue denotes 100% Asset 1 and green denotes 100% Asset 2. Every panel has the same expected-return range, 8% to 12%, because correlation does not enter the expected-return formula.
For these inputs, Asset 2 has the higher expected return. Moving from the GMV point toward Asset 2 traces the efficient branch. Allocations on the lower-return branch are dominated: they accept more risk than GMV while offering less expected return. At
The limiting correlations make the diversification mechanism particularly clear:
- At
, long-only portfolio volatility is . GMV holds only the less volatile Asset 1. - At
, volatility is . Weights and exactly offset the random return deviations in this model. Expected return is approximately 9.33%, with zero theoretical variance.
The zero-variance result depends on exact negative correlation and the specified volatilities. Estimated market relationships will not generally sustain that exact offset. It illustrates a mathematical limiting case rather than an empirically guaranteed hedge.
Try it: compare the 60/40 allocation across the six panels. Its expected return stays at 9.60%, while volatility falls as correlation decreases. Then compare the black diamonds to see how the minimum-risk weights themselves change with correlation.
Real-World Case Study: Portfolio Optimization with U.S. Stocks
We now replace assumed moments with estimates from the bundled price history of Amazon (AMZN), Coca-Cola (KO), and Microsoft (MSFT). The question is: given these three assets and a chosen estimation sample, which fully invested, long-only allocations offer the best estimated mean–variance trade-offs?
This is an in-sample construction exercise, not an out-of-sample strategy backtest. The same history supplies the estimates used to select and describe the portfolios. The three companies are selected for illustration; the results do not establish that this universe or these weights would have been an investable choice known in advance.
Stock Selection and Data Retrieval
pok.get_stock_dynamic() loads a stored CSV snapshot rather than fetching live prices. The supplied file contains 3,519 daily price observations, from 10 January 2011 to 3 January 2025, for the three stocks.
The collection script uses Yahoo Finance's Close field, but the snapshot does not record an explicit corporate-action adjustment policy. We therefore analyze the supplied price series as an educational dataset. A total-return performance study would first establish how splits and distributions were handled.
import PortfolioOptimizationKit as pok
import pandas as pd
tickers = ["AMZN", "KO", "MSFT"]
stocks = pok.get_stock_dynamic()[tickers]
n_assets = len(tickers)
print(f"Price observations: {len(stocks)}")
print(f"Coverage: {stocks.index[0]:%Y-%m-%d} to {stocks.index[-1]:%Y-%m-%d}")
print(stocks.head().to_string(float_format=lambda value: f"{value:.2f}")) The loader preserves the CSV's price units and uses trading dates as its index. The collection script already rounded prices to two decimal places; the calculations below introduce no further rounding. Dividing all prices by a constant would leave returns unchanged, but rounding those rescaled prices could materially distort returns. Format displayed values instead of altering the inputs.
Calculating Returns, Volatility, and Portfolio Metrics
Compute daily simple returns
With
These are conventional annualized moments, not the realized compounded return and variance of a one-year holding period. The risk scaling assumes stable period variance and zero serial covariances. Expected-return estimation is particularly uncertain: historical arithmetic means are inputs to this exercise, not known future returns.
To visualize the opportunity set, generate 4,000 random allocations with non-negative weights summing to one. A Dirichlet distribution with all parameters equal to one samples uniformly over this simplex. This samples portfolio weights, not future market paths. A fixed seed makes the comparison reproducible.
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
# Run the price-data example first.
daily_returns = pok.compute_returns(stocks).dropna()
periods_per_year = 252
annual_returns = daily_returns.mean() * periods_per_year
cov_matrix = daily_returns.cov() # Covariance of daily returns, using n-1.
risk_free_rate = 0.0
asset_estimates = pd.DataFrame({
"Estimated annual mean (%)": annual_returns * 100,
"Annualized volatility (%)": pok.annualize_vol(daily_returns, periods_per_year) * 100,
"Historical CAGR (%)": pok.annualize_rets(daily_returns, periods_per_year) * 100,
})
print(asset_estimates.to_string(float_format=lambda value: f"{value:.2f}"))
num_portfolios = 4000
n_assets = stocks.shape[1]
rng = np.random.default_rng(42)
weight_samples = rng.dirichlet(np.ones(n_assets), size=num_portfolios)
portfolio_metrics = []
for weights in weight_samples:
portfolio_return = pok.portfolio_return(weights, annual_returns)
portfolio_volatility = pok.annualize_vol(
pok.portfolio_volatility(weights, cov_matrix), periods_per_year
)
sharpe_ratio = (portfolio_return - risk_free_rate) / portfolio_volatility
portfolio_metrics.append({
"return": portfolio_return,
"volatility": portfolio_volatility,
"sharpe": sharpe_ratio,
})
portfolios_df = pd.DataFrame(portfolio_metrics)
weights_df = pd.DataFrame(weight_samples, columns=stocks.columns)
print("\nFirst five allocations and metrics (weights and returns as decimals):")
print(pd.concat([weights_df.head(), portfolios_df.head()], axis=1).to_string(
float_format=lambda value: f"{value:.4f}"
)) The estimated annual arithmetic means are approximately 28.20% for AMZN, 9.33% for KO, and 24.61% for MSFT. Their historical CAGRs are different and are shown separately. Portfolio construction uses the arithmetic estimates, not the CAGR column.
We use a zero risk-free benchmark in this first comparison, so each estimated Sharpe ratio is annualized mean return divided by annualized volatility. A random sample can reveal the opportunity set, but its best point is only the best allocation among those drawn. It does not establish the mathematical optimum.
Visualizing Portfolios and the Efficient Frontier
Use the same estimated moments and sampled allocations to draw the efficient frontier. The toolkit first finds GMV, then solves minimum-variance problems for target expected returns from GMV's return up to the largest expected asset return. This selects the upper, non-dominated boundary rather than connecting the best random points.
Run the price and portfolio-metric examples first. Both chart axes are displayed in percent; the underlying calculations remain in decimal units.
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
df_frontier = pok.efficient_frontier(
50, daily_returns, cov_matrix, periods_per_year, risk_free_rate=risk_free_rate
)
scatter_data = portfolios_df[["volatility", "return", "sharpe"]].to_numpy().copy()
scatter_data[:, :2] *= 100
frontier_data = (df_frontier[["volatility", "return"]] * 100).values.tolist()
plot_data = {
"stockFrontier": {
"type": "scatter",
"title": "Estimated Long-Only Efficient Frontier",
"yAxisName": "Estimated annual return (%)",
"xAxis": {"type": "value", "name": "Annualized volatility (%)", "min": 0},
"yAxis": {"type": "value", "name": "Estimated annual return (%)", "scale": True},
"visualMap": {
"dimension": 2,
"seriesIndex": 0,
"min": float(portfolios_df["sharpe"].min()),
"max": float(portfolios_df["sharpe"].max()),
"inRange": {"color": ["#0000FF", "#00FF00"]},
},
"series": {"Portfolios": scatter_data.tolist(), "Frontier": frontier_data},
},
}
print("\n<ECHARTS_DATA>" + json.dumps(plot_data, allow_nan=False)) Each point represents an allocation evaluated under the same estimated moments. The color indicates its estimated Sharpe ratio. The frontier begins at GMV and traces increasing target expected returns. The lower-return portion of the minimum-variance boundary is excluded because it is dominated.
Try it: increase num_portfolios to 10000 in the preceding cell and rerun both examples. The cloud becomes denser, but the optimized frontier is determined by the estimated moments and constraints, not by the number of random allocations.
Identifying Key Portfolios: GMV and MSR
Now solve explicitly for the GMV portfolio and the maximum-Sharpe (MSR) portfolio under the same long-only constraints. GMV minimizes estimated variance; MSR maximizes estimated excess return per unit of volatility. The toolkit checks solver success and constraint satisfaction before returning weights.
Run the preceding examples first. This cell reuses their estimates, sampled portfolios, and chart data, so the comparison is like-for-like.
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
key_weights = {
"GMV": pok.minimize_volatility(annual_returns, cov_matrix),
"MSR": pok.maximize_sharpe_ratio(
annual_returns, cov_matrix, risk_free_rate, periods_per_year
),
}
key_metrics = {}
for name, weights in key_weights.items():
ret = pok.portfolio_return(weights, annual_returns)
vol = pok.annualize_vol(pok.portfolio_volatility(weights, cov_matrix), periods_per_year)
sharpe = (ret - risk_free_rate) / vol
key_metrics[name] = {"return": ret, "volatility": vol, "sharpe": sharpe}
plot_data["stockFrontier"]["series"][name] = [[float(vol * 100), float(ret * 100)]]
key_portfolios = pd.DataFrame.from_dict(key_metrics, orient="index")
print("Optimized weights (%):")
print((pd.DataFrame(key_weights, index=stocks.columns).T * 100).to_string(
float_format=lambda value: f"{value:.2f}"
))
metrics_display = key_portfolios.copy()
metrics_display[["return", "volatility"]] *= 100
metrics_display = metrics_display.rename(columns={
"return": "Estimated return (%)", "volatility": "Volatility (%)", "sharpe": "Sharpe"
})
print("\n" + metrics_display.to_string(float_format=lambda value: f"{value:.3f}"))
print(f"\nLowest sampled volatility: {portfolios_df['volatility'].min():.2%}")
print(f"Highest sampled Sharpe: {portfolios_df['sharpe'].max():.3f}")
print("\n<ECHARTS_DATA>" + json.dumps(plot_data, allow_nan=False)) The optimized GMV volatility should be no greater than the lowest sampled volatility, and optimized MSR should have a Sharpe ratio at least as high as the best sampled allocation, within numerical tolerance. The objective values are optimal for this estimated problem; they do not establish future performance.
The weights also reveal concentration. Inspect which stocks dominate each solution and how that relates to their estimated means, volatilities, and covariances. Try it: restrict the price history with .loc["2020":] in the loading cell and rerun the case study. Changes in the optimized weights illustrate estimation sensitivity rather than a newly proven investment opportunity.
Portfolio Feature Calculation Function
The toolkit already provides get_portfolio_features() to report these three metrics. As a simple benchmark, evaluate a portfolio holding one third in each stock. Supply annualized arithmetic expected returns, the daily covariance matrix, and 252 periods per year; the helper annualizes volatility once.
equal_weights = np.repeat(1 / n_assets, n_assets)
equal_weight_metrics = pok.get_portfolio_features(
equal_weights, annual_returns, cov_matrix, risk_free_rate, periods_per_year
) The equally weighted portfolio requires no expected-return optimization. Comparing it with GMV and MSR helps distinguish the benefit of the estimated allocation rule from the benefit of simply holding a diversified mix. Next we examine the optimization problems and their constraints in more detail.
Strategies for Optimizing Portfolios
We now examine the investment objective behind each allocation. Use the same estimates and sampled portfolios from the stock case study: annual_returns contains annualized arithmetic means, and cov_matrix contains daily covariances. In the mathematical programs below, write
Run the preceding case-study cells before these examples. Reusing one estimation sample makes the comparisons meaningful; changing the sample requires recalculating the inputs and portfolios together.
Seeking Minimum Volatility
The global minimum-variance portfolio answers: which feasible allocation has the least estimated return variability? It solves:
The factor
GMV depends on covariances, not expected returns. The toolkit accepts the return vector to identify the assets and support optional return constraints, but the unconstrained GMV objective does not use its values. With a positive semidefinite covariance matrix and these linear constraints, the variance minimization problem is convex.
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
gmv_weights = pok.minimize_volatility(annual_returns, cov_matrix)
gmv_ret, gmv_vol, gmv_sharpe = pok.get_portfolio_features(
gmv_weights, annual_returns, cov_matrix, risk_free_rate, periods_per_year
)
print("\nGMV weights (%):")
print(pd.Series(gmv_weights * 100, index=stocks.columns).to_string(
float_format=lambda value: f"{value:.2f}"
))
sample_min_vol = portfolios_df["volatility"].min()
print(f"\nLowest sampled volatility: {sample_min_vol:.4%}")
print(f"Optimized GMV volatility: {gmv_vol:.4%}")
frontier_data = (df_frontier[["volatility", "return"]] * 100).values.tolist()
strategy_plot = {
"portfolioStrategy": {
"type": "line",
"title": "Global Minimum-Variance Portfolio",
"xAxis": {"type": "value", "name": "Annualized volatility (%)", "min": 0},
"yAxis": {"type": "value", "name": "Estimated annual return (%)", "scale": True},
"series": {"Frontier": frontier_data, "GMV": [[gmv_vol * 100, gmv_ret * 100]]},
},
}
print("\n<ECHARTS_DATA>" + json.dumps(strategy_plot, allow_nan=False)) For the full snapshot, GMV has estimated annual return approximately 12.96% and volatility 16.25%. A dense random sample may get close, but its minimum is only a minimum over the allocations drawn. The optimizer solves the specified covariance problem; neither result establishes future realized risk.
Try it: evaluate pok.minimize_volatility(2 * annual_returns, cov_matrix). The GMV weights stay the same because the covariance estimate has not changed. Doubling expected returns changes the reported expected performance, not the variance-minimizing allocation.
Targeting Specific Returns with Minimized Volatility
An investor may require a particular expected return rather than the lowest possible variance. For an exact target
This is still a convex variance-minimization problem. The return constraint fixes an estimated mean, not a guaranteed realized outcome. Under the current long-only, fully invested constraints, feasibility requires:
For the supplied snapshot, that range is approximately 9.33% to 28.20% annually. Run the GMV example first, then set an annual target of 16%:
target_return = 0.16
print(f"Feasible expected-return range: {annual_returns.min():.2%} to {annual_returns.max():.2%}") The solver finds the least volatile portfolio meeting this target. Verify the reported estimated return and inspect the required allocation:
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
target_weights = pok.minimize_volatility(annual_returns, cov_matrix, target_return)
print(f"Target estimated annual return: {target_return:.2%}")
target_ret, target_vol, target_sharpe = pok.get_portfolio_features(
target_weights, annual_returns, cov_matrix, risk_free_rate, periods_per_year
)
print("\nTarget-return weights (%):")
print(pd.Series(target_weights * 100, index=stocks.columns).to_string(
float_format=lambda value: f"{value:.2f}"
))
strategy_plot["portfolioStrategy"]["title"] = f"Minimum Volatility at {target_return:.0%} Expected Return"
strategy_plot["portfolioStrategy"]["series"] = {
"Frontier": frontier_data,
"MinVolForTarget": [[target_vol * 100, target_ret * 100]],
}
print("\n<ECHARTS_DATA>" + json.dumps(strategy_plot, allow_nan=False)) At the 16% target, estimated annual volatility is approximately 16.87%, compared with GMV's 16.25%. The higher expected return requires moving away from the minimum-risk allocation.
An exact target below GMV's expected return can be feasible but dominated. If the objective were instead at least target_return parameter imposes equality.
Try it: set target_return = 0.10 and rerun the solution cell. It produces a feasible portfolio with approximately 16.83% volatility, but GMV offers both higher expected return and lower risk. Then try 0.40: it exceeds the current 28.20% maximum expected asset return, so the toolkit reports an infeasible target rather than returning misleading weights.
Maximizing the Sharpe Ratio
GMV minimizes risk, whereas maximum Sharpe seeks the greatest estimated excess return per unit of volatility. For an annual benchmark return
The numerator is expected excess return, not compounded growth. We retain the zero benchmark for the default example. With a nonzero benchmark, its annualization convention must match the arithmetic mean inputs.
The optimization is:
The numerical solver minimizes
Compare the optimized allocation with the actual best sampled portfolio from our case study. Recalculate the sampled Sharpe ratios using the chosen benchmark so that changing it keeps the comparison consistent.
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
risk_free_rate = 0.0
msr_weights = pok.maximize_sharpe_ratio(
annual_returns, cov_matrix, risk_free_rate, periods_per_year
)
ret_msr = pok.portfolio_return(msr_weights, annual_returns)
vol_msr = pok.annualize_vol(pok.portfolio_volatility(msr_weights, cov_matrix), periods_per_year)
shp_msr = (ret_msr - risk_free_rate) / vol_msr
sample_sharpes = (portfolios_df["return"] - risk_free_rate) / portfolios_df["volatility"]
best_sample_index = sample_sharpes.idxmax()
best_sample = portfolios_df.loc[best_sample_index]
msr_comparison = pd.DataFrame({
"Estimated return (%)": [best_sample["return"] * 100, ret_msr * 100],
"Volatility (%)": [best_sample["volatility"] * 100, vol_msr * 100],
"Sharpe": [sample_sharpes.loc[best_sample_index], shp_msr],
}, index=["Best sampled", "Optimized MSR"])
print(msr_comparison.to_string(float_format=lambda value: f"{value:.4f}"))
print("\nMSR weights (%):")
print(pd.Series(msr_weights * 100, index=stocks.columns).to_string(
float_format=lambda value: f"{value:.2f}"
))
strategy_plot["portfolioStrategy"]["title"] = f"Maximum Sharpe: Benchmark {risk_free_rate:.2%}"
strategy_plot["portfolioStrategy"]["series"] = {
"Frontier": frontier_data,
"MSR": [[vol_msr * 100, ret_msr * 100]],
}
print("\n<ECHARTS_DATA>" + json.dumps(strategy_plot, allow_nan=False)) With the full sample and zero benchmark, MSR has estimated annual return approximately 22.14%, volatility 21.23%, and Sharpe ratio 1.0425. Its estimated risk-adjusted return exceeds GMV's, but its volatility is also higher. A higher Sharpe ratio does not by itself establish that the investor should accept that risk level.
Try it: change risk_free_rate in this cell to 0.02. Both the optimization and sampled comparison use the new benchmark. Unlike GMV, the maximum-Sharpe weights generally depend on expected returns and the benchmark as well as covariance.
Achieving Maximum Sharpe Ratio with Set Volatility
Suppose the investor specifies an exact positive annual volatility
Variance has squared-return units: 20% annual volatility corresponds to annual variance
Unlike a variance cap, an exact variance equality is generally nonconvex: different allocations can meet the same risk target. The toolkit compares solutions from multiple starting allocations and verifies their constraints; this improves the numerical search but is not a general proof of global optimality.
Under these long-only constraints, attainable volatility ranges from GMV's volatility to the largest individual asset volatility. The upper bound follows from
For the current estimates, the feasible range is approximately 16.25% to 32.73% annually. Set the exact target to 20%:
target_volatility = 0.2
max_feasible_vol = pok.annualize_vol(daily_returns, periods_per_year).max()
print(f"Feasible annual volatility range: {gmv_vol:.2%} to {max_feasible_vol:.2%}") import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
weights_at_vol = pok.maximize_sharpe_ratio(
annual_returns, cov_matrix, risk_free_rate, periods_per_year,
target_volatility=target_volatility,
)
ret_at_vol, vol_at_vol, sharpe_at_vol = pok.get_portfolio_features(
weights_at_vol, annual_returns, cov_matrix, risk_free_rate, periods_per_year
)
print("\nExact-volatility weights (%):")
print(pd.Series(weights_at_vol * 100, index=stocks.columns).to_string(
float_format=lambda value: f"{value:.2f}"
))
print(f"\nTarget-constrained Sharpe: {sharpe_at_vol:.4f}")
print(f"Unconstrained MSR Sharpe: {shp_msr:.4f}")
strategy_plot["portfolioStrategy"]["title"] = f"Best Expected Return at {target_volatility:.0%} Volatility"
strategy_plot["portfolioStrategy"]["series"] = {
"Frontier": frontier_data,
"MaxSharpePort": [[vol_at_vol * 100, ret_at_vol * 100]],
}
print("\n<ECHARTS_DATA>" + json.dumps(strategy_plot, allow_nan=False)) With the zero benchmark, the 20% volatility portfolio has estimated annual return approximately 20.79% and Sharpe ratio 1.0394, slightly below the unrestricted MSR ratio of 1.0425. The extra constraint reduces the available choices; it cannot improve the maximum attainable Sharpe ratio under the same inputs.
An exact target is different from a risk limit. A volatility cap would use
Try it: set target_volatility = 0.15. This is below the current GMV volatility, so the toolkit rejects the target. Then try 0.25 and compare the allocation and Sharpe ratio with the unrestricted MSR solution.
Reflections on Portfolio Constraints
The preceding portfolios obeyed two separate restrictions: long-only holdings,
Weights are positions relative to initial equity. A negative weight represents a short position: borrowing an asset and selling it, with an obligation to return it later. Net risky exposure is
We retain annual expected returns
Short Selling & Flexible Weights: Finding Low Volatility Portfolio for a Set Return
First allow signed risky weights without imposing their sum. To account for the remaining capital, define a cash weight:
A positive cash weight means lending or holding cash; a negative one means borrowing. Assume both earn or cost zero interest here. The total portfolio then has expected return
For an exact expected-return target
The Lagrangian attaches a multiplier to the return constraint:
Because covariance is symmetric, the gradient of its half-quadratic form is
Hence
For a nonzero mean vector, the denominator
Worked example. Return to the chapter's two assets, with annual means 8% and 12%, volatilities 10% and 20%, and correlation 0.25:
At a 10% target,
The calculation below solves the linear system rather than explicitly constructing a matrix inverse. Run the chapter's imports first; this example supplies its own annual moments, distinct from the stock snapshot.
constraint_means = np.array([0.08, 0.12])
constraint_covariance = np.array([[0.01, 0.005], [0.005, 0.04]])
constraint_target = 0.10
return_direction = np.linalg.solve(constraint_covariance, constraint_means)
A = constraint_means @ return_direction
flex_weights = constraint_target * return_direction / A
cash_weight = 1 - flex_weights.sum()
flex_return = pok.portfolio_return(flex_weights, constraint_means)
flex_vol = pok.portfolio_volatility(flex_weights, constraint_covariance)
print(pd.Series(
np.append(flex_weights, cash_weight) * 100,
index=["Asset 1 (%)", "Asset 2 (%)", "Cash / borrowing (%)"],
).to_string(float_format=lambda value: f"{value:.2f}"))
print(f"Estimated annual return: {flex_return:.2%}")
print(f"Annual volatility: {flex_vol:.2%}")
print(f"Net risky exposure: {flex_weights.sum():.2%}") Annual volatility is approximately 11.11%. Neither risky position is short: allowing shorts does not require using them. The leverage here comes from the negative cash position. Try it: change constraint_target to 0.08. Risky weights scale down, and cash becomes positive at approximately 10.53%.
Short Selling & Normalized Weights: Minimum Volatility Portfolio Given a Fixed Return
Now restore
Introduce separate multipliers for expected return and budget:
The first-order conditions are:
The first condition gives
Substituting into the two constraints produces a two-equation system:
When asset means are not all identical,
Collecting the target-independent and target-dependent terms gives:
Here
Hand check. At the same 10% target, the two-asset constraints are
Restore constraint_target = 0.10 in the preceding cell and run it before this one. Both solutions then use the same target and moments; only the risky-asset budget restriction changes.
ones = np.ones(len(constraint_means))
budget_direction = np.linalg.solve(constraint_covariance, ones)
B = ones @ return_direction
C = ones @ budget_direction
determinant = A * C - B ** 2
f = (A * budget_direction - B * return_direction) / determinant
g = (C * return_direction - B * budget_direction) / determinant
budget_weights = f + constraint_target * g
budget_return = pok.portfolio_return(budget_weights, constraint_means)
budget_vol = pok.portfolio_volatility(budget_weights, constraint_covariance)
print(pd.Series(budget_weights * 100, index=["Asset 1 (%)", "Asset 2 (%)"])
.to_string(float_format=lambda value: f"{value:.2f}"))
print(f"Estimated annual return: {budget_return:.2%}")
print(f"Annual volatility: {budget_vol:.2%}")
print(f"Net risky exposure: {budget_weights.sum():.2%}")
print(f"Gross risky exposure: {np.abs(budget_weights).sum():.2%}") The added budget equality raises minimum volatility from 11.11% to 12.25% at this target. More generally, adding a restriction cannot lower the minimum variance when the remaining inputs and constraints are identical. Conversely, allowing shorts cannot increase the minimum relative to a feasible long-only solution with the same budget and target.
Try it: change constraint_target to 0.14 in the first cell and rerun both. The fully invested solution becomes −50% in Asset 1 and 150% in Asset 2, with 200% gross risky exposure and approximately 29.15% volatility. A 14% mean is unattainable by long-only combinations of assets with means 8% and 12%; short selling expands feasibility, accompanied here by substantially higher risk.
Finally, minimum variance at an exact return is not automatically mean–variance efficient. This signed-weight boundary has variance
Optimizing the Sharpe Ratio Portfolio with a Non-Zero Risk-Free Rate
A risk-free asset has a known return over the modeled holding period. Its variance and covariance with risky returns are zero; its correlation with them is undefined because its standard deviation is zero. A Treasury bill held to maturity can approximate a nominal risk-free payoff over a matching horizon, whereas a bond fund's market value fluctuates.
Continue with the stock case study's annualized arithmetic means and daily covariance. To use an effective annual cash rate
For a quoted 6% effective annual rate, the annualized arithmetic cash mean is approximately 5.8276%. Below,
Capital Market Line (CML) Essentials
Hold a fraction
For
The intercept is the cash return and the slope is the risky fund's Sharpe ratio. When positive excess return is attainable, choosing the feasible maximum-Sharpe fund gives the steepest line. This is the tangency construction; under long-only constraints, the optimal fund can lie on a boundary with some asset weights equal to zero.
Strictly, the Capital Market Line (CML) is the CAL through the market portfolio in CAPM equilibrium. Our three-stock estimated tangency portfolio is not an established market portfolio, so the examples label the plotted lines CAL.
: all cash; volatility is zero and the Sharpe ratio is undefined. : lend part of the capital and invest the remainder in the risky fund. : hold only the risky fund. : borrow cash to increase risky exposure. This part of the line requires borrowing at the assumed rate.
For
Hand check. If cash earns 2%, a risky fund has mean 10% and volatility 20%, and
Run the stock case study through the equally weighted benchmark first. This cell reuses its estimates and frontier, finds the long-only risky tangency fund, and invests half the capital in that fund:
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
quoted_cash_rate = 0.06
risky_fraction = 0.5
cash_period_rate = np.expm1(np.log1p(quoted_cash_rate) / periods_per_year)
cml_rate = periods_per_year * cash_period_rate
tangent_weights = pok.maximize_sharpe_ratio(
annual_returns, cov_matrix, cml_rate, periods_per_year
)
tangent_return = pok.portfolio_return(tangent_weights, annual_returns)
tangent_vol = pok.annualize_vol(pok.portfolio_volatility(tangent_weights, cov_matrix), periods_per_year)
tangent_sharpe = (tangent_return - cml_rate) / tangent_vol
combined_weights = risky_fraction * tangent_weights
combined_cash = 1 - risky_fraction
combined_return = cml_rate + risky_fraction * (tangent_return - cml_rate)
combined_vol = abs(risky_fraction) * tangent_vol
print(f"Effective annual cash rate: {quoted_cash_rate:.2%}")
print(f"Annualized arithmetic cash mean: {cml_rate:.4%}")
print(pd.DataFrame({
"Tangency (%)": np.append(tangent_weights, 0.0) * 100,
"Combined (%)": np.append(combined_weights, combined_cash) * 100,
}, index=list(stocks.columns) + ["Cash"]).to_string(float_format=lambda value: f"{value:.2f}"))
print(f"Tangency mean: {tangent_return:.2%}; volatility: {tangent_vol:.2%}; Sharpe: {tangent_sharpe:.4f}")
print(f"Combined mean: {combined_return:.2%}; volatility: {combined_vol:.2%}")
line_end = max(1.0, risky_fraction)
capital_line = [[0.0, cml_rate * 100],
[line_end * tangent_vol * 100,
(cml_rate + line_end * (tangent_return - cml_rate)) * 100]]
allocation_plot = {
"cml_plot": {
"type": "line",
"title": "Estimated Tangency Line: Long-Only Risky Assets",
"xAxis": {"type": "value", "name": "Annualized volatility (%)", "min": 0},
"yAxis": {"type": "value", "name": "Estimated annual return (%)", "scale": True},
"series": {
"Frontier": (df_frontier[["volatility", "return"]] * 100).values.tolist(),
"CAL": capital_line,
"MSR": [[tangent_vol * 100, tangent_return * 100]],
"Portfolios": [[combined_vol * 100, combined_return * 100]],
},
},
}
print("\n<ECHARTS_DATA>" + json.dumps(allocation_plot, allow_nan=False)) For the full snapshot, the long-only tangency fund holds approximately 38.41% AMZN, 0% KO, and 61.59% MSFT. Its estimated mean is 25.99%, volatility 25.32%, and Sharpe ratio 0.7964. A half-cash allocation has mean 15.91% and volatility 12.66%. The Portfolios point marks that selected allocation.
Try it: set risky_fraction to 0, 1, and 1.25. The last choice means borrowing 25% of equity; the chart extends the CAL accordingly. With borrowing prohibited, only the segment from cash to the tangency fund is available through this mix. Higher-risk efficient choices may instead remain on the risky-asset frontier.
Next compare MSR, GMV, and equal weighting at the same cash rate. Reuse the preceding cell and the stock case study's GMV and equal-weight metrics. GMV and equal weighting are useful benchmarks; equal weighting generally lies inside the feasible region rather than on its efficient boundary.
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
cml_comparison = pd.DataFrame([
[tangent_return, tangent_vol],
key_portfolios.loc["GMV", ["return", "volatility"]].tolist(),
list(equal_weight_metrics[:2]),
], index=["MSR", "GMV", "EWP"], columns=["return", "volatility"])
cml_comparison["sharpe"] = (cml_comparison["return"] - cml_rate) / cml_comparison["volatility"]
cml_display = cml_comparison.rename(columns={
"return": "Estimated return (%)", "volatility": "Volatility (%)", "sharpe": "Sharpe"
}).copy()
cml_display[["Estimated return (%)", "Volatility (%)"]] *= 100
print(cml_display.to_string(float_format=lambda value: f"{value:.4f}"))
gmv_risk_fraction = cml_comparison.loc["GMV", "volatility"] / tangent_vol
cal_return_at_gmv_risk = cml_rate + gmv_risk_fraction * (tangent_return - cml_rate)
print(f"\nCAL mean at GMV's volatility: {cal_return_at_gmv_risk:.2%}")
print(f"Required fraction in tangency fund: {gmv_risk_fraction:.2%}")
allocation_plot["cml_plot"]["title"] = "One Cash Rate: MSR, GMV, and Equal Weighting"
allocation_plot["cml_plot"]["series"] = {
"Frontier": (df_frontier[["volatility", "return"]] * 100).values.tolist(),
"CAL": capital_line,
**{name: [[row["volatility"] * 100, row["return"] * 100]]
for name, row in cml_comparison.iterrows()},
}
print("\n<ECHARTS_DATA>" + json.dumps(allocation_plot, allow_nan=False)) The cash-plus-tangency mix offers a higher estimated mean than GMV at GMV's volatility for these inputs. This is a comparison at equal modeled risk, not simply a comparison of two funds with different risk levels. At the tangency point, the line and risky frontier coincide. The improvement elsewhere depends on the cash and borrowing opportunities actually allowed.
Try it: change quoted_cash_rate to 0.02 in the first cell and rerun both. The risky frontier's return-volatility coordinates stay fixed because asset means and covariance have not changed. The cash intercept, Sharpe ratios, and tangency weights change.
Maximizing the Sharpe Ratio Portfolio with a Non-Zero Risk-Free Asset
Now derive the solution when short sales of risky assets are allowed, with unrestricted cash lending or borrowing at the same rate. This differs from the long-only risky fund above. Assume annual covariance
With cash weight
This is the earlier flexible-risky-exposure problem expressed in excess returns. Cash completes the total budget; the risky weights alone need not sum to one. Its Lagrangian and first-order conditions are:
The first equation gives
Strict convexity makes this the unique minimum-variance solution for the specified target.
Portfolio Return & Volatility
The constraint guarantees the estimated mean, not the realized return. Substitution verifies both moments:
The absolute value is essential: volatility cannot be negative. Above the cash mean, these portfolios have Sharpe ratio
Portfolio Weights for Full Allocation to Risky Assets (Maximum Sharpe Ratio)
Let
Its Sharpe ratio is
For
The analytical fund can contain negative weights. It matches the earlier long-only maximum-Sharpe solution only when its weights are also feasible under that restriction. Under the default cash rate, it shorts KO, so the two solutions should differ.
Run the first CAL cell before this example. We retain its converted cash rate and stock estimates, use annual covariance throughout, and target a 13% annualized arithmetic mean with signed risky positions:
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
signed_target = 0.13
annual_covariance = cov_matrix.to_numpy() * periods_per_year
excess_means = annual_returns.to_numpy() - cml_rate
signed_direction = np.linalg.solve(annual_covariance, excess_means)
excess_quadratic = excess_means @ signed_direction
signed_tangent_weights = signed_direction / signed_direction.sum()
signed_tangent_return = pok.portfolio_return(signed_tangent_weights, annual_returns)
signed_tangent_vol = pok.portfolio_volatility(signed_tangent_weights, annual_covariance)
signed_tangent_sharpe = (signed_tangent_return - cml_rate) / signed_tangent_vol
signed_weights = (signed_target - cml_rate) * signed_direction / excess_quadratic
signed_cash = 1 - signed_weights.sum()
signed_return = cml_rate + signed_weights @ excess_means
signed_vol = pok.portfolio_volatility(signed_weights, annual_covariance)
print(pd.DataFrame({
"Long-only tangency (%)": tangent_weights * 100,
"Signed tangency (%)": signed_tangent_weights * 100,
}, index=stocks.columns).to_string(float_format=lambda value: f"{value:.2f}"))
print(f"Tangency Sharpe: long-only {tangent_sharpe:.4f}; signed {signed_tangent_sharpe:.4f}")
print("\nTarget allocation (%):")
print(pd.Series(np.append(signed_weights, signed_cash) * 100,
index=list(stocks.columns) + ["Cash"])
.to_string(float_format=lambda value: f"{value:.2f}"))
print(f"Target mean: {signed_return:.2%}; volatility: {signed_vol:.2%}")
line_max_vol = max(signed_tangent_vol, signed_vol)
signed_plot = {
"cml_chart": {
"type": "line",
"title": "Tangency Lines: Different Risky-Asset Constraints",
"xAxis": {"type": "value", "name": "Annualized volatility (%)", "min": 0},
"yAxis": {"type": "value", "name": "Estimated annual return (%)", "scale": True},
"series": {
"Long-only CAL": [[0.0, cml_rate * 100],
[line_max_vol * 100, (cml_rate + tangent_sharpe * line_max_vol) * 100]],
"Signed CAL": [[0.0, cml_rate * 100],
[line_max_vol * 100, (cml_rate + np.sqrt(excess_quadratic) * line_max_vol) * 100]],
"MSR": [[signed_tangent_vol * 100, signed_tangent_return * 100]],
"MinVolForTarget": [[signed_vol * 100, signed_return * 100]],
},
},
}
print("\n<ECHARTS_DATA>" + json.dumps(signed_plot, allow_nan=False)) With the default inputs, the signed tangency fund holds approximately 44.82% AMZN, −22.37% KO, and 77.55% MSFT. Its Sharpe ratio is 0.8020, compared with 0.7964 under the long-only restriction. The 13% target allocation holds approximately 13.50% AMZN, −6.74% KO, 23.36% MSFT, and 69.88% cash, with 8.94% volatility. Here MSR marks the signed tangency fund and MinVolForTarget marks the cash-plus-risky target allocation. Both plotted rays assume borrowing is available where needed.
Try it: set signed_target = cml_rate; the target allocation becomes all cash. Then try 0.04: the target is below the cash mean, so the minimum-variance point lies below the upper line while its volatility stays positive. Finally, change quoted_cash_rate to 0.02 in the first CAL cell and rerun the examples. For this sample, the analytical tangency weights then become nonnegative and agree with the long-only solution within numerical tolerance.
The chapter's central distinction is between choosing the risky fund and choosing its scale. Expected means, covariance, the cash rate, and risky-asset constraints determine the fund; lending or borrowing changes total exposure. All of these conclusions describe the estimated model, whose inputs and financing assumptions must match the investment problem.
