Fundamentals of Portfolio Optimization
This document provides a comprehensive exploration of portfolio optimization, presenting a systematic approach to maximizing returns for a given level of risk. It covers Modern Portfolio Theory (MPT), efficient frontier construction, risk and return calculations, and various strategies for achieving optimal portfolios, including techniques for minimizing volatility, targeting specific returns, and maximizing the Sharpe Ratio. The content is enhanced with Python examples, offering practical insights into implementing these strategies.
Additionally, it delves into portfolio constraints like short selling and risk-free asset integration, presenting advanced concepts such as the Capital Market Line (CML) and portfolio weight calculations for different investment scenarios.
Let's start by uploading the necessary modules, including the custom-developed Python kit module PortfolioOptimizationKit:
# Standard library imports
import json
from datetime import datetime
# Third-party library imports
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy.stats
from scipy.optimize import minimize
from tabulate import tabulate
# Custom module import
import PortfolioOptimizationKit as pok
try:
# Test basic operations with each module
# pandas: Data manipulation and analysis
df = pd.DataFrame({'test': [1, 2, 3]})
# numpy: Numerical operations
array = np.array([1, 2, 3])
# scipy.stats: Statistical functions
percentile = scipy.stats.norm.ppf(0.95)
# datetime: Handling date and time
current_time = datetime.now()
# PortfolioOptimizationKit: Custom portfolio optimization functions
ffme_returns = pok.get_ffme_returns()
# tabulate: Creating formatted tables
tabulated = tabulate([['Alice', 24], ['Bob', 19]], headers=['Name', 'Age'])
print("All modules loaded successfully!")
except Exception as e:
error_message = f"Error loading modules: {str(e)}"
print(error_message) Modern Portfolio Theory (MPT)
Introduction to MPT
Modern Portfolio Theory (MPT) is a mathematical framework for constructing a portfolio of assets to maximize expected return for a given level of risk. It's based on the principle of diversification, suggesting that a mixed variety of investments yields less risk than any single investment.
Efficient Frontiers
Understanding Efficient Frontiers
In MPT, the efficient frontier is a graph showing the best possible return for a given level of risk. Introduced by Harry Markowitz in 1952, it represents portfolios that optimize the expected return for a given standard deviation (risk).
The goal is to allocate investment across multiple assets, determining the optimal percentage for each to maximize returns for a specific risk level.
Calculating Efficient Frontiers
Assume we have
Portfolio Return
The portfolio return is a weighted average of the individual asset returns:
where
Portfolio Volatility
Understanding Portfolio Volatility
Portfolio volatility is the standard deviation of the weighted sum of asset returns.
Calculating and Minimizing Volatility
Considering an example with just two assets, where
where
Defining the correlation coefficient between the assets as
It's worth mentioning that by employing matrix notation, we can succinctly express this volatility calculation.
The portfolio's volatility,
This simplifies to:
Where the covariance matrix,
For a portfolio of
Extending this concept to matrix notation for compactness, the portfolio's volatility is:
Examining Efficient Frontiers with Two-Asset Portfolios
Setting Up the Scenario
In this section, we present a hypothetical example to analyze the efficient frontier generated by various two-asset portfolios with different correlation coefficients,
# Number of simulated returns
nret = 500
# Number of trading days in a year
periods_per_year = 252
# Risk-free rate for Sharpe ratio calculation
risk_free_rate = 0.0 Next, we define the mean returns and volatilities for our two hypothetical assets:
# Mean returns for Asset 1 and Asset 2
mean_1 = 0.001019
mean_2 = 0.001249
# Volatilities for Asset 1 and Asset 2
vol_1 = 0.016317
vol_2 = 0.019129 We then specify
import pandas as pd
import numpy as np
import PortfolioOptimizationKit as pok
import json
try:
# # Define simulation parameters
# nret = 500 # Number of simulated daily returns
# periods_per_year = 252 # Number of trading days in a year
# risk_free_rate = 0.0 # Assumed risk-free rate for Sharpe ratio calculations
# # Define mean returns for Asset 1 and Asset 2
# mean_1, mean_2 = 0.001019, 0.001249
# # Define volatilities for Asset 1 and Asset 2
# vol_1, vol_2 = 0.016317, 0.019129
# Generate a range of correlation coefficients from 1 to -1
rhos = np.linspace(1, -1, num=6)
# Define the number of weight allocations per correlation scenario
nweig = 20
# Generate weight allocations for Asset 1 (w1) and Asset 2 (w2)
w1 = np.linspace(0, 1, num=nweig)
w2 = 1 - w1
ww = pd.DataFrame([w1, w2]).T # DataFrame containing weight pairs
# Set seed for reproducibility of random returns
np.random.seed(1)
# Initialize dictionary to store plotting data for each correlation
plot_data = {}
# Iterate over each correlation coefficient to generate portfolios
for k_rho, rho in enumerate(rhos):
# Initialize a DataFrame to store portfolio metrics
portfolio = pd.DataFrame(columns=["return", "volatility", "sharpe ratio"])
# Calculate covariance between Asset 1 and Asset 2
cov_ij = rho * vol_1 * vol_2
cov_rets = pd.DataFrame([[vol_1**2, cov_ij], [cov_ij, vol_2**2]])
# Generate random daily returns based on the covariance matrix
daily_rets = pd.DataFrame(np.random.multivariate_normal(
mean=[mean_1, mean_2],
cov=cov_rets.values,
size=nret
))
# Initialize list to store portfolio metrics for plotting
portfolios = []
# Generate portfolios with different weight allocations
for i in range(ww.shape[0]):
weights = ww.loc[i] # Extract weight pair
# Annualize the daily returns
ann_rets = pok.annualize_rets(daily_rets, periods_per_year)
# Calculate the portfolio's annualized return
portfolio_ret = pok.portfolio_return(weights, ann_rets)
# Calculate the portfolio's volatility
portfolio_vol = pok.portfolio_volatility(weights, cov_rets)
portfolio_vol = pok.annualize_vol(portfolio_vol, periods_per_year)
# Calculate the Sharpe Ratio for the portfolio
portfolio_spr = pok.sharpe_ratio(
portfolio_ret,
risk_free_rate,
periods_per_year,
v=portfolio_vol
)
# Append the portfolio metrics to the list
portfolios.append([
float(portfolio_vol * 100), # Volatility in percentage
float(portfolio_ret * 100), # Return in percentage
float(w2[i]) # Weight of Asset 2 for color mapping
])
# Configure plotting parameters for the current correlation
plot_data[f"ef_{k_rho}"] = {
"type": "scatter",
"yAxisName": "Return (%)",
"title": f"Correlation ρ: {rho:.2f}",
"series": {
"Portfolios": portfolios
},
"xAxis": {
"type": "value",
"name": "Volatility (%)",
"min": 0,
"max": 32
},
"yAxis": {
"type": "value",
"min": 0,
"max": 95
},
"visualMap": {
"min": 0,
"max": 1,
"dimension": 2,
"inRange": {
"color": ["#0000FF", "#00FF00"] # Blue to Green
# Alternatively, uncomment the line below for Purple to Orange
# "color": ["#800080", "#FFA500"] # Purple to Orange
}
}
}
# Graph the efficient frontier analysis results
print("\n<ECHARTS_DATA>" + json.dumps(plot_data))
except Exception as e:
# Display error message if any exception occurs during execution
print(f"Error: {str(e)}") Analyzing Results and Implications
Each point on the plots represents a specific portfolio's return and volatility pair for a given correlation. The color gradient reflects the weight allocation: green signifies a portfolio weighted entirely towards the first asset
The plots demonstrate that lower asset correlations generally offer a more favorable return-to-volatility ratio. Notably, in the case of
Real-World Case Study: Portfolio Optimization with U.S. Stocks
In this section, we delve into the time series data of selected U.S. stocks and demonstrate effective methodologies for constructing optimized investment portfolios.
Stock Selection and Data Retrieval
import PortfolioOptimizationKit as pok
import pandas as pd
# Define the stock tickers and compute the number of assets
tickers = ['AMZN', 'KO', 'MSFT']
n_assets = len(tickers)
# Retrieve stock price data using a custom function from PortfolioOptimizationKit
stocks = pok.get_stock_dynamic()
# (Optional) Round the stock price data to two decimal places for improved readability
stocks = stocks.round(2)
# Display the first few rows of the loaded stock data in Markdown table format
print("Loaded stock data:")
print(stocks.head().to_markdown(index=False))
print("\nData shape:", stocks.shape) Calculating Returns, Volatility, and Portfolio Metrics
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
# Step 1: Load the stock price data using the custom function
# stocks = pok.get_stock_dynamic()
# Step 2: Calculate daily returns and annualize them
daily_returns = pok.compute_returns(stocks)
annual_returns = pok.annualize_rets(daily_returns, periods_per_year=252)
# Step 3: Compute statistical metrics for daily returns
mean_returns = daily_returns.mean() # Mean daily returns for each asset
std_returns = daily_returns.std() # Standard deviation of daily returns
cov_matrix = daily_returns.cov() # Covariance matrix of daily returns
# Step 4: Define parameters for portfolio simulation
periods_per_year = 252 # Number of trading days in a year
num_portfolios = 4000 # Total number of portfolios to simulate
risk_free_rate = 0.0 # Assumed risk-free rate for Sharpe ratio calculations
# Determine the number of assets based on the stock data
n_assets = stocks.shape[1]
# Step 5: Generate random portfolios and calculate their metrics
portfolio_metrics = [] # Initialize a list to store portfolio metrics
for _ in range(num_portfolios):
# Generate random weights for each asset
weights = np.random.random(n_assets)
weights /= np.sum(weights) # Normalize weights to ensure they sum to 1
# Calculate the portfolio's annualized return
portfolio_return = pok.portfolio_return(weights, annual_returns)
# Calculate the portfolio's volatility (annualized standard deviation)
portfolio_volatility = pok.portfolio_volatility(weights, cov_matrix)
portfolio_volatility = pok.annualize_vol(portfolio_volatility, periods_per_year)
# Calculate the portfolio's Sharpe Ratio
sharpe_ratio = pok.sharpe_ratio(
portfolio_return,
risk_free_rate,
periods_per_year,
v=portfolio_volatility
)
# Append the calculated metrics and weights to the list
portfolio_metrics.append({
"return": portfolio_return,
"volatility": portfolio_volatility,
"sharpe_ratio": sharpe_ratio,
# Assign weights to each asset; extend as needed for more assets
"w1": weights[0],
"w2": weights[1],
"w3": weights[2] if n_assets >= 3 else None
})
# Step 6: Convert the list of portfolio metrics into a Pandas DataFrame for analysis
portfolios_df = pd.DataFrame(portfolio_metrics)
print("Sample of generated portfolios:")
print(portfolios_df.head()) This Python script utilizes Monte Carlo simulation to generate
Visualizing Portfolios and the Efficient Frontier
To illustrate the distribution of portfolios and highlight the efficient frontier, we create a scatter plot:
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
def build_portfolio_frontier_json():
"""
Generates a comprehensive set of random portfolios, constructs the efficient frontier,
and outputs a JSON structure compatible with ECharts for visualization.
This function performs the following steps:
1. Loads stock price data.
2. Computes daily and annualized returns.
3. Generates a specified number of random portfolios.
4. Constructs the efficient frontier based on the generated portfolios.
5. Prepares scatter and line data for visualization.
6. Compiles all data into a JSON format suitable for ECharts.
"""
# Step 1: Load Stock Price Data
stocks = pok.get_stock_dynamic()
# Step 2: Calculate Daily Returns and Annualize Them
daily_rets = pok.compute_returns(stocks) # Compute daily returns for each stock
ann_rets = pok.annualize_rets(daily_rets, 252) # Annualize the daily returns assuming 252 trading days
cov_rets = daily_rets.cov() # Calculate the covariance matrix of daily returns
# Step 3: Initialize Portfolio Simulation Parameters
n_assets = stocks.shape[1] # Number of assets based on the loaded stock data
num_portfolios = 4000 # Total number of portfolios to simulate
periods_per_year = 252 # Number of trading days in a year
risk_free_rate = 0.0 # Assumed risk-free rate for Sharpe Ratio calculations
# Step 4: Generate Random Portfolios and Compute Metrics
all_portfolios = [] # Initialize a list to store portfolio metrics
for _ in range(num_portfolios):
weights = np.random.random(n_assets) # Generate random weights for each asset
weights /= weights.sum() # Normalize weights to ensure they sum to 1
# Calculate portfolio return using the weighted sum of annual returns
port_ret = pok.portfolio_return(weights, ann_rets)
# Calculate portfolio volatility using the weighted covariance matrix
port_vol = pok.portfolio_volatility(weights, cov_rets)
port_vol = pok.annualize_vol(port_vol, periods_per_year) # Annualize the volatility
# Calculate the Sharpe Ratio for the portfolio
port_spr = pok.sharpe_ratio(port_ret, risk_free_rate, periods_per_year, v=port_vol)
# Append the calculated metrics to the portfolio list
all_portfolios.append({
"return": float(port_ret), # Portfolio's expected annual return
"volatility": float(port_vol), # Portfolio's annualized volatility
"sharpe": float(port_spr) # Portfolio's Sharpe Ratio
})
# Convert the list of portfolios into a Pandas DataFrame for easier manipulation
portfolios = pd.DataFrame(all_portfolios)
# Step 5: Construct the Efficient Frontier DataFrame
df_frontier = pok.efficient_frontier(50, daily_rets, cov_rets, periods_per_year)
# 'df_frontier' contains columns like ["volatility", "return"] representing points on the efficient frontier
# Step 6: Prepare Scatter Data for the Portfolios
# Each portfolio is represented as a list containing [volatility, return, sharpe_ratio]
scatter_data = []
for _, row in portfolios.iterrows():
scatter_data.append([
row["volatility"], # X-axis value: Portfolio volatility (%)
row["return"], # Y-axis value: Portfolio return (%)
row["sharpe"] # Color dimension: Sharpe Ratio for visual mapping
])
# Step 7: Prepare Line Data for the Efficient Frontier
# Each point on the efficient frontier is represented as a list containing [volatility, return]
line_data = []
for _, row in df_frontier.iterrows():
line_data.append([
row["volatility"], # X-axis value: Frontier point volatility (%)
row["return"] # Y-axis value: Frontier point return (%)
])
# Define a unique key for the chart configuration
chart_key = "ef_plot"
# Determine the minimum and maximum Sharpe Ratios among all portfolios for color scaling
sharpe_min = float(portfolios["sharpe"].min())
sharpe_max = float(portfolios["sharpe"].max())
# Calculate maximum volatility and return to set dynamic axis limits
vol_max = float(portfolios["volatility"].max())
ret_max = float(portfolios["return"].max())
# Compile the plotting data into a dictionary structured for ECharts
plot_data = {
chart_key: {
"type": "multi",
"title": "Port. & Efficient Frontier",
"yAxisName": "Return (%)",
"xAxis": {
"type": "value",
"name": "Volatility (%)",
"min": 0.15,
"max": vol_max
},
"yAxis": {
"type": "value",
"min": 0.05,
"max": ret_max
},
# Configure visualMap to color-code portfolios based on Sharpe Ratio
"visualMap": {
"dimension": 2,
"min": sharpe_min, # Minimum Sharpe Ratio for the color scale
"max": sharpe_max, # Maximum Sharpe Ratio for the color scale
"inRange": {
"color": ["#0000FF", "#00FF00"] # Blue to Green
# Alternatively, uncomment the line below for Purple to Orange
# "color": ["#800080", "#FFA500"] # Purple to Orange
}
},
"series": {
"Portfolios": scatter_data, # Scatter plot data for all portfolios
"Frontier": line_data # Line plot data for the efficient frontier
}
}
}
# Step 8: Output the Plot Data in JSON Format for ECharts Integration
print("\n<ECHARTS_DATA>" + json.dumps(plot_data))
if __name__ == "__main__":
build_portfolio_frontier_json() The resulting scatter plot visualizes the distribution of returns and volatility. Each portfolio is color-coded according to its Sharpe Ratio, allowing for quick identification of portfolios that offer the best risk-adjusted returns. The efficient frontier line overlays the scatter plot, representing the optimal balance between risk and return.
Identifying Key Portfolios: GMV and MSR
We identify the Global Minimum Volatility (GMV) portfolio and the Maximum Sharpe Ratio (MSR) portfolio to highlight optimal investment strategies:
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
from tabulate import tabulate
def build_portfolios_with_gmv_msr():
"""
Generates random portfolios, identifies GMV & MSR, constructs the efficient frontier,
and outputs a JSON structure compatible with ECharts for visualization, including
the Global Minimum Volatility (GMV) and Maximum Sharpe Ratio (MSR) portfolios.
Key points:
- 'series' remains a DICTIONARY of sub-keys: "Portfolios", "Frontier", "GMV", "MSR".
- We insert them in an order so ECharts sees "Portfolios" as index=0, "Frontier"=1, ...
- 'visualMap.seriesIndex = 0' => Only color the Portfolios sub-series by Sharpe.
"""
# 1) Load data
stocks = pok.get_stock_dynamic()
daily_rets = pok.compute_returns(stocks)
ann_rets = pok.annualize_rets(daily_rets, 252)
cov_rets = daily_rets.cov()
# Parameters
n_assets = stocks.shape[1]
num_portfolios = 4000
periods_per_year = 252
risk_free_rate = 0.0
# 2) Generate random portfolios
all_portfolios = []
for _ in range(num_portfolios):
w = np.random.random(n_assets)
w /= w.sum()
ret = pok.portfolio_return(w, ann_rets)
vol = pok.portfolio_volatility(w, cov_rets)
vol = pok.annualize_vol(vol, periods_per_year)
shp = pok.sharpe_ratio(ret, risk_free_rate, periods_per_year, v=vol)
all_portfolios.append({
"return": float(ret),
"volatility": float(vol),
"sharpe": float(shp)
})
portfolios = pd.DataFrame(all_portfolios)
# 3) Identify GMV & MSR
gmv_port = portfolios.iloc[portfolios["volatility"].idxmin()]
msr_port = portfolios.iloc[portfolios["sharpe"].idxmax()]
# 4) Efficient Frontier
df_frontier = pok.efficient_frontier(50, daily_rets, cov_rets, periods_per_year)
# Prepare Data
# Portfolios => [volatility, return, sharpe]
scatter_data = portfolios[["volatility","return","sharpe"]].values.tolist()
# Frontier => [volatility, return]
line_data = df_frontier[["volatility","return"]].values.tolist()
# GMV & MSR => single points with Sharpe Ratio
gmv_data = [[gmv_port["volatility"], gmv_port["return"], gmv_port["sharpe"]]]
msr_data = [[msr_port["volatility"], msr_port["return"], msr_port["sharpe"]]]
# Compute axis bounds
sharpe_min = float(portfolios["sharpe"].min())
sharpe_max = float(portfolios["sharpe"].max())
vol_max = float(portfolios["volatility"].max())
ret_max = float(portfolios["return"].max())
# 5) Insert sub-keys in EXACT order so "Portfolios" is index=0:
# Python 3.7+ respects insertion order
series_dict = {}
series_dict["Portfolios"] = scatter_data # index=0 => color-coded
series_dict["Frontier"] = line_data # index=1 => line
series_dict["GMV"] = gmv_data # index=2 => custom color
series_dict["MSR"] = msr_data # index=3 => custom color
plot_data = {
"ef_plot": {
"type": "multi",
"title": "GVM & MSR",
"legend": { "data": ["Portfolios","Frontier","GMV","MSR"] },
"yAxisName": "Return (%)",
"xAxis": {
"type": "value",
"name": "Volatility (%)",
"min": 0.15,
"max": vol_max * 1.1
},
"yAxis": {
"type": "value",
"name": "Return (%)",
"min": 0.05,
"max": ret_max * 1.1
},
# Apply color gradient only to sub-series index 0 => "Portfolios"
"visualMap": {
"dimension": 2,
"min": sharpe_min,
"max": sharpe_max,
"seriesIndex": 0, # sub-series 0 is 'Portfolios'
"inRange": {
"color": ["#0000FF","#00FF00"] # Blue->Green
}
},
"series": series_dict
}
}
# 6) Print for OutputDisplay.vue
print("\n<ECHARTS_DATA>" + json.dumps(plot_data))
# 7) Summaries
summary_data = [
["GMV Portfolio",
f"{gmv_port['return']*100:.2f}%",
f"{gmv_port['volatility']*100:.2f}%",
f"{gmv_port['sharpe']:.2f}"],
["MSR Portfolio",
f"{msr_port['return']*100:.2f}%",
f"{msr_port['volatility']*100:.2f}%",
f"{msr_port['sharpe']:.2f}"]
]
headers = ["Portfolio","Return","Volatility","Sharpe Ratio"]
print(tabulate(summary_data, headers=headers, tablefmt="github"))
if __name__ == "__main__":
build_portfolios_with_gmv_msr() This script identifies and highlights the Global Minimum Volatility (GMV) portfolio, which offers the lowest possible risk, and the Maximum Sharpe Ratio (MSR) portfolio, which provides the best risk-adjusted return. These portfolios exemplify the advantages of diversification and strategic asset allocation in optimizing investment performance.
Portfolio Feature Calculation Function
We can now integrate the following function, part of the PortfolioOptimizationKit.py module, computes and displays key metrics for a portfolio based on specified weights, returns, covariance matrix, risk-free rate, and the number of trading periods per year. It calculates the portfolio's volatility, return, and Sharpe ratio.
def get_portfolio_features(weights, rets, covmat, risk_free_rate, periods_per_year):
"""
Calculate and print portfolio return, volatility, and Sharpe ratio.
Parameters:
- weights: Array of asset weights in the portfolio.
- rets: Annualized returns for each asset.
- covmat: Covariance matrix of asset returns.
- risk_free_rate: Risk-free rate for Sharpe ratio calculation.
- periods_per_year: Number of periods in a year (trading days).
Returns:
Tuple of (return, volatility, sharpe ratio) for the portfolio.
"""
# Calculate portfolio volatility
vol = pok.portfolio_volatility(weights, covmat)
vol = pok.annualize_vol(vol, periods_per_year)
# Calculate portfolio return
ret = pok.portfolio_return(weights, rets)
# Calculate portfolio Sharpe ratio
shp = pok.sharpe_ratio(ret, risk_free_rate, periods_per_year, v=vol)
# Display the calculated metrics
print("Portfolio return: {:.2f}%" .format(ret*100))
print("Portfolio volatility: {:.2f}%" .format(vol*100))
print("Portfolio Sharpe ratio: {:.2f}" .format(shp))
return ret, vol, shpStrategies for Optimizing Portfolios
Seeking Minimum Volatility
Instead of generating a multitude of portfolios, we approach the identification of optimal portfolios on the efficient frontier by solving a minimization problem. Specifically, to locate the portfolio with the lowest volatility on the efficient frontier, we address the following optimization challenge:
Minimize:
subject to
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
from tabulate import tabulate
def get_portfolio_features(weights, rets, covmat, risk_free_rate, periods_per_year):
"""
Calculate portfolio return, volatility, and Sharpe ratio.
"""
vol = pok.portfolio_volatility(weights, covmat)
vol = pok.annualize_vol(vol, periods_per_year)
ret = pok.portfolio_return(weights, rets)
shp = pok.sharpe_ratio(ret, risk_free_rate, periods_per_year, v=vol)
return (ret, vol, shp)
def build_gmv_comparison():
"""
1) Load data
2) Approach A: Monte Carlo to find random GMV
3) Approach B: Minimize volatility to find GMV
4) Print them side by side in tabulate
5) (Optional) Output ECharts data for the second approach
"""
# ========== 1) Load Data ==========
stocks = pok.get_stock_dynamic()
daily_rets = pok.compute_returns(stocks)
periods_per_year = 252
risk_free_rate = 0.0
ann_rets = pok.annualize_rets(daily_rets, periods_per_year)
cov_rets = daily_rets.cov()
# ========== 2) Approach A: Monte Carlo GMV ==========
num_portfolios = 4000
best_vol = float('inf')
best_weights = None
for _ in range(num_portfolios):
w = np.random.random(len(stocks.columns))
w /= w.sum()
# Calculate volatility
vol = pok.portfolio_volatility(w, cov_rets)
vol = pok.annualize_vol(vol, periods_per_year)
if vol < best_vol:
best_vol = vol
best_weights = w
mc_ret, mc_vol, mc_shp = get_portfolio_features(best_weights, ann_rets, cov_rets, risk_free_rate, periods_per_year)
# ========== 3) Approach B: Minimization GMV ==========
opt_weights = pok.minimize_volatility(ann_rets, cov_rets)
min_ret, min_vol, min_shp = get_portfolio_features(opt_weights, ann_rets, cov_rets, risk_free_rate, periods_per_year)
table_data = [
["Monte carlo",
f"{mc_ret*100:.2f}%",
f"{mc_vol*100:.2f}%",
f"{mc_shp:.2f}"],
["Minimization problem",
f"{min_ret*100:.2f}%",
f"{min_vol*100:.2f}%",
f"{min_shp:.2f}"]
]
table_headers = ["Method", "Return", "Volatility", "Sharpe Ratio"]
print("GMV portfolio:\n")
print(tabulate(table_data, headers=table_headers, tablefmt="github"))
print()
df_frontier = pok.efficient_frontier(50, daily_rets, cov_rets, periods_per_year)
frontier_data = []
for _, row in df_frontier.iterrows():
frontier_data.append([row["volatility"], row["return"]])
gmv_point = [[min_vol, min_ret]]
# Build ECharts JSON
echarts_data = {
"gmv_plot": {
"type": "multi",
"title": "Min-Vol (Convex Optim)",
"yAxisName": "Return (%)",
"xAxis": {
"type": "value",
"name": "Volatility (%)",
"min": 0.15,
"max": 0.3
},
"yAxis": {
"type": "value",
"min": 0,
"max": 0.3
},
"series": {
"Frontier": frontier_data,
"GMV": gmv_point
}
}
}
# Print for your Vue/ECharts
print("<ECHARTS_DATA>" + json.dumps(echarts_data))
if __name__ == "__main__":
build_gmv_comparison() This script determines the portfolio with the lowest volatility, contrasts its performance with results from a Monte Carlo simulation, and visualizes the efficient frontier alongside the selected optimal portfolio. It exemplifies the real-world use of optimization techniques in managing investment portfolios.
Targeting Specific Returns with Minimized Volatility
To identify the portfolio on the efficient frontier with minimum volatility for a predetermined level of return, an additional constraint is introduced into the minimization problem:
subject to
where
# Set the target return for the portfolio
target_return = 0.16 The minimization can then be executed with the constraint set for the specified target return.
Next, the volatility of the portfolio constructed with these optimal weights is calculated. The corresponding return is verified to ensure it matches the chosen target return (i.e.,
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
from tabulate import tabulate
def get_portfolio_features(weights, rets, covmat, risk_free_rate, periods_per_year):
"""
Calculate portfolio return, volatility, and Sharpe ratio.
"""
vol = pok.portfolio_volatility(weights, covmat)
vol = pok.annualize_vol(vol, periods_per_year)
ret = pok.portfolio_return(weights, rets)
shp = pok.sharpe_ratio(ret, risk_free_rate, periods_per_year, v=vol)
return (ret, vol, shp)
def build_portfolio_for_target_return(target_return=0.2):
"""
1) Solve for minimum volatility subject to target_return.
2) Print results in tabulate form.
3) Output ECharts JSON with a diamond marker for 'MinVolForTarget'.
"""
# ========== 1) LOAD DATA ==========
stocks = pok.get_stock_dynamic()
daily_rets = pok.compute_returns(stocks)
periods_per_year = 252
risk_free_rate = 0.0
ann_rets = pok.annualize_rets(daily_rets, periods_per_year)
cov_rets = daily_rets.cov()
# ========== 2) MIN-VOL FOR GIVEN TARGET RETURN ==========
print(f"\nMinimizing volatility for target return = {target_return*100:.1f}%\n")
optimal_weights = pok.minimize_volatility(ann_rets, cov_rets, target_return)
print("Optimal weights (Target Return):")
for i, col in enumerate(stocks.columns):
print(f" {col:5s}: {optimal_weights[i]*100:.2f}%")
ret, vol, shp = get_portfolio_features(optimal_weights, ann_rets, cov_rets, risk_free_rate, periods_per_year)
# Tabulate
summary_data = [
[f"Min-Vol @ {target_return*100:.1f}%", f"{ret*100:.2f}%", f"{vol*100:.2f}%", f"{shp:.2f}"]
]
headers = ["Portfolio", "Return", "Volatility", "Sharpe Ratio"]
print("\n**Minimum Volatility Portfolio for Target Return**")
print(tabulate(summary_data, headers=headers, tablefmt="github"))
# ========== 3) ECHARTS JSON OUTPUT ==========
# Efficient frontier line
df_frontier = pok.efficient_frontier(50, daily_rets, cov_rets, periods_per_year)
frontier_data = [[r["volatility"], r["return"]] for _, r in df_frontier.iterrows()]
# One-point series for the min-vol portfolio => [vol, ret]
min_vol_point = [[vol, ret]]
# Create an ECharts-compatible structure
plot_data = {
"target_portfolio": {
"type": "multi",
"title": f"Min-Vol @ {target_return*100:.1f}%",
"yAxisName": "Return (%)",
"xAxis": {
"type": "value",
"name": "Volatility (%)",
"min": 0.15,
"max": 0.35
},
"yAxis": {
"type": "value",
"min": 0,
"max": 0.3
},
"series": {
# 'Frontier' => line
"Frontier": frontier_data,
# 'MinVolForTarget' => single diamond
"MinVolForTarget": min_vol_point
}
}
}
# Print for your Vue/ECharts component
print("\n<ECHARTS_DATA>" + json.dumps(plot_data))
if __name__ == "__main__":
build_portfolio_for_target_return(target_return) It's important to note that the return of a portfolio will be confined between the minimum and maximum returns of the assets composing the portfolio. For instance, with the same three companies, achieving a return of
Maximizing the Sharpe Ratio
This section delves into the methodology of locating the portfolio along the efficient frontier that yields the maximum Sharpe ratio. In essence, this portfolio offers the highest return per unit of risk.
Understanding the Sharpe Ratio Maximization:
The Sharpe ratio is a critical metric, quantifying the return earned above the risk-free rate per unit of volatility. While the scipy library provides a method for minimization, it doesn't offer a direct function for maximization. However, one can achieve the maximization of the Sharpe ratio by minimizing its negative value. Thus, the optimization problem transforms as follows:
Minimize the negative Sharpe ratio:
subject to the constraints:
Employing the Minimizer, calculating, visualizing the Optimal Portfolio and comparing with Monte Carlo Simulation Results:
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
from tabulate import tabulate
def get_portfolio_features(weights, rets, covmat, risk_free_rate, periods_per_year):
"""
Calculate portfolio return, volatility, and Sharpe ratio.
"""
vol = pok.portfolio_volatility(weights, covmat)
vol = pok.annualize_vol(vol, periods_per_year)
ret = pok.portfolio_return(weights, rets)
shp = pok.sharpe_ratio(ret, risk_free_rate, periods_per_year, v=vol)
return (ret, vol, shp)
def build_max_sharpe_ratio_portfolio():
"""
1) Load data
2) Maximize Sharpe ratio
3) Compare with Monte Carlo-based (high_sharpe_portfolio)
4) Print results in tabulate form
5) Output ECharts JSON (frontier line + MSR point)
"""
# 1) LOAD DATA
stocks = pok.get_stock_dynamic()
daily_rets = pok.compute_returns(stocks)
periods_per_year = 252
risk_free_rate = 0.0
ann_rets = pok.annualize_rets(daily_rets, periods_per_year)
cov_rets = daily_rets.cov()
# 2) MAXIMIZE SHARPE RATIO
msr_weights = pok.maximize_sharpe_ratio(ann_rets, cov_rets, risk_free_rate, periods_per_year)
print("Optimal weights for Maximum Sharpe Ratio Portfolio:")
for i, col in enumerate(stocks.columns):
print(f" {col:5s}: {msr_weights[i]*100:.2f}%")
ret_msr, vol_msr, shp_msr = get_portfolio_features(
msr_weights, ann_rets, cov_rets, risk_free_rate, periods_per_year
)
# 3) COMPARE WITH MONTE CARLO
# Suppose you already have 'high_sharpe_portfolio' from a Monte Carlo approach
# shaped like (return, volatility, sharpe). This is just a placeholder:
high_sharpe_portfolio = (0.185, 0.25, 0.74) # Example from your simulation
# 4) PRINT RESULTS IN TABULATE
comparison_data = [
["Monte Carlo Simulation",
f"{high_sharpe_portfolio[0]*100:.2f}%",
f"{high_sharpe_portfolio[1]*100:.2f}%",
f"{high_sharpe_portfolio[2]:.2f}"],
["Optimization",
f"{ret_msr*100:.2f}%",
f"{vol_msr*100:.2f}%",
f"{shp_msr:.2f}"]
]
headers = ["Method", "Return", "Volatility", "Sharpe Ratio"]
print("\nMaximum Sharpe Ratio (MSR) portfolio comparison:\n")
print(tabulate(comparison_data, headers=headers, tablefmt="github"))
# 5) BUILD ECHARTS JSON
# a) Frontier data => line
# b) 'MSR' => single scatter point (red pin)
df_frontier = pok.efficient_frontier(50, daily_rets, cov_rets, periods_per_year)
frontier_data = [[row["volatility"], row["return"]] for _, row in df_frontier.iterrows()]
# Single point => [vol_msr, ret_msr]
msr_point = [[vol_msr, ret_msr]]
# ECharts JSON structure
plot_data = {
"msr_portfolio": {
"type": "multi",
"title": f"Max SR Portolio @ {shp_msr:.2f}",
"yAxisName": "Return (%)",
"xAxis": {
"type": "value",
"name": "Volatility (%)",
"min": 0.1,
"max": 0.3
},
"yAxis": {
"type": "value"
},
"series": {
# Frontier => line
"Frontier": frontier_data,
# MSR => single point
"MSR": msr_point
}
}
}
# Print JSON for Vue/ECharts
print("\n<ECHARTS_DATA>" + json.dumps(plot_data))
if __name__ == "__main__":
build_max_sharpe_ratio_portfolio() Achieving Maximum Sharpe Ratio with Set Volatility
To pinpoint the portfolio on the efficient frontier with the highest Sharpe ratio at a certain volatility level, one must incorporate an additional constraint into the optimization problem:
subject to
where
# Define the target volatility for the portfolio
target_volatility = 0.2 import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
def get_portfolio_features(weights, rets, covmat, risk_free_rate, periods_per_year):
"""
Given a set of portfolio weights and market data, compute:
- vol: annualized volatility
- ret: annualized return
- shp: Sharpe ratio
"""
# 1) Calculate daily portfolio volatility and then annualize it
vol = pok.portfolio_volatility(weights, covmat)
vol = pok.annualize_vol(vol, periods_per_year)
# 2) Calculate annualized portfolio return
ret = pok.portfolio_return(weights, rets)
# 3) Compute the Sharpe Ratio
shp = pok.sharpe_ratio(ret, risk_free_rate, periods_per_year, v=vol)
return (ret, vol, shp)
def build_max_sharpe_portfolio_at_vol(target_volatility=0.2):
"""
1) Loads CSV-based market data for (e.g.) AMZN, KO, MSFT.
2) Computes the weights that achieve the maximum Sharpe Ratio at the specified target volatility.
3) Prints the optimal weights plus summary metrics (Return, Vol, Sharpe).
4) Builds and prints an ECharts JSON to show:
- Efficient Frontier (a line)
- The single 'MaxSharpePort' point at the chosen volatility.
"""
# ========== 1) LOAD MARKET DATA ==========
# Load daily stock data and compute daily returns
stocks = pok.get_stock_dynamic() # Custom function to retrieve stock data
daily_rets = pok.compute_returns(stocks)
# Annualize returns and compute covariance
periods_per_year = 252
risk_free_rate = 0.0
ann_rets = pok.annualize_rets(daily_rets, periods_per_year)
cov_rets = daily_rets.cov()
# ========== 2) OPTIMIZE PORTFOLIO AT TARGET VOLATILITY ==========
# Hypothetical function name: 'maximize_shape_ratio'
# Adapt the name if your library differs
optimal_weights = pok.maximize_sharpe_ratio(
ann_rets, cov_rets, risk_free_rate, periods_per_year, target_volatility
)
# Print the optimal weights for each stock
print("Optimal weights (Target Volatility = {:.1f}%):".format(target_volatility * 100))
print(" AMZN: {:.2f}%".format(optimal_weights[0]*100))
print(" KO: {:.2f}%".format(optimal_weights[1]*100))
print(" MSFT: {:.2f}%".format(optimal_weights[2]*100))
# ========== 3) RETRIEVE PORTFOLIO METRICS ==========
# Return (ret), Volatility (vol), Sharpe Ratio (shp)
ret, vol, shp = get_portfolio_features(
optimal_weights, ann_rets, cov_rets, risk_free_rate, periods_per_year
)
# Print portfolio summary
print("\nMaximum Sharpe Ratio Portfolio at Target Volatility = {:.1f}%".format(target_volatility * 100))
print(" Return: {:.2f}%".format(ret * 100))
print(" Volatility: {:.2f}%".format(vol * 100))
print(" Sharpe Ratio {:.2f}".format(shp))
# ========== 4) EFFICIENT FRONTIER & SINGLE POINT FOR ECHARTS ==========
# 4a) Retrieve the efficient frontier
df_frontier = pok.efficient_frontier(50, daily_rets, cov_rets, periods_per_year)
# Convert each frontier row to [vol, ret]
frontier_data = df_frontier[["volatility", "return"]].values.tolist()
# 4b) Single point => [vol, ret] for the chosen portfolio
best_portfolio_point = [[vol, ret]]
# ========== 5) DYNAMIC AXIS RANGE ==========
# Determine min/max from the frontier plus this single point
min_vol = df_frontier["volatility"].min()
max_vol = df_frontier["volatility"].max()
min_ret = df_frontier["return"].min()
max_ret = df_frontier["return"].max()
# Check if our single point extends the frontier's bounds
if vol < min_vol: min_vol = vol
if vol > max_vol: max_vol = vol
if ret < min_ret: min_ret = ret
if ret > max_ret: max_ret = ret
# Add a small buffer
x_min = max(0.0, min_vol * 0.9)
x_max = max_vol * 1.1
y_min = min_ret * 0.9
y_max = max_ret * 1.1
# ========== 6) BUILD THE ECHARTS-FRIENDLY JSON ==========
plot_data = {
"max_sharpe_vol": {
"type": "multi",
"title": "MSR@{:.1f}% Target Vol".format(target_volatility*100, shp),
"yAxisName": "Return (%)",
# Axis config ensures we include both the frontier & single point
"xAxis": {
"type": "value",
"name": "Volatility (%)",
"min": x_min,
"max": x_max
},
"yAxis": {
"type": "value",
"min": y_min,
"max": y_max
},
# 'series' dictionary => sub-keys for your Vue/ECharts code
"series": {
# Frontier => line
"Frontier": frontier_data,
# Single best portfolio => scatter point
"MaxSharpePort": best_portfolio_point
}
}
}
# Print JSON after <ECHARTS_DATA> so Vue can parse it
print("\n<ECHARTS_DATA>" + json.dumps(plot_data))
if __name__ == "__main__":
build_max_sharpe_portfolio_at_vol(target_volatility) This approach systematically calculates the portfolio with the highest Sharpe ratio for a given level of volatility, illustrating its efficiency through visualization on the efficient frontier. The code demonstrates a methodical application of optimization techniques in portfolio management, focusing on risk-adjusted returns.
Reflections on Portofolio Constraints
Until now, the focus has been on investing all capital while only buying assets. This is known as a long-only strategy, where the sum of the weights of assets is
However, one might choose not to invest all their capital or even engage in short selling (selling assets one doesn't own but borrows instead).
Short Selling & Flexible Weights: Finding Low Volatility Portfolio for a Set Return
The optimization problem can be simplified by removing the constraints for positive weights and full investment:
only ensuring that the portfolio meets a desired return
In this scenario, short selling is allowed, and there's no need to use all the available capital.
To solve this, Lagrange multipliers come in handy. The Lagrangian for this problem is:
Setting its partial derivatives to zero gives us:
From here, we find:
and
Given that the covariance matrix
This formula provides a precise method to calculate the weights. It's important to note that without the normalization constraint (ensuring weights sum to
Short Selling & Normalized Weights: Minimum Volatility Portfolio Given a Fixed Return
This section explores finding the optimal portfolio weights to minimize volatility, given a fixed return, while allowing for short selling but ensuring all capital is invested. Unlike previous constraints, here weights don't need to be positive, and they sum up to one.
Minimization problem setup:
subject to the constraints:
In this scenario, short selling is allowed (weights can be negative), but the total invested capital equals one.
Formulating the Lagrangian:
where
Setting the derivatives of the Lagrangian to zero yields:
Solving for weights (
Substituting
Defining constants
leads to:
Solving this system gives
where:
This analytical solution provides the optimal weights without the constraint of positive weights and ensures all capital is invested. The solution allows for short selling but ensures the total invested capital sums to one. The formula showcases the relationship between the desired return level and the resulting optimal portfolio weights.
Optimizing the Sharpe Ratio Portfolio with a Non-Zero Risk-Free Rate
Understanding that a risk-free asset is an idealized asset with a guaranteed return, typically represented by short-term government securities like US treasury bills due to their stable interest rate and very low default risk. These assets exhibit no volatility and don't correlate with risky assets, meaning their inclusion in a portfolio linearly affects the return relative to the change in risk.
Capital Market Line (CML) Essentials
The introduction of a risk-free asset into the portfolio landscape creates the Capital Market Line (CML), a concept central to portfolio optimization. The CML is characterized by:
Tangency with the Efficient Frontier: It touches the efficient frontier precisely at the portfolio of risky assets that yields the maximum Sharpe ratio. This portfolio is purely comprised of risky assets with no allocation to the risk-free asset.Vertical Intercept as the Risk-Free Rate: The point where the CML intercepts the y-axis represents a portfolio entirely invested in the risk-free asset, indicating its return rate.Combinations of Risk-Free and Risky Assets: Points along the CML indicate portfolios blending the risk-free asset with the tangency portfolio of risky assets. The exact mix varies, moving from all risk-free to all risky as you move up the line.Linear Relationship between Risk and Return: The CML showcases how adding the risk-free asset to a portfolio linearly affects its return relative to its risk.
The CML is mathematically described by the equation:
where:
and denote the return and volatility of a portfolio composed of both risky assets and the risk-free asset. and represent the return and volatility of the purely risky asset portfolio. is the risk-free rate.
This equation helps determine the expected return of a mixed portfolio for a given level of risk, illustrating the trade-off and potential gains from diversifying into risk-free assets.
The next step is to visualize the efficient frontier alongside the Capital Market Line (CML), which reflects the introduction of the risk-free asset into the investment opportunities:
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
def get_portfolio_features(weights, rets, covmat, risk_free_rate, periods_per_year):
"""
Calculate annualized return, volatility, and Sharpe ratio for given portfolio weights.
"""
vol = pok.portfolio_volatility(weights, covmat)
vol = pok.annualize_vol(vol, periods_per_year)
ret = pok.portfolio_return(weights, rets)
shp = pok.sharpe_ratio(ret, risk_free_rate, periods_per_year, v=vol)
return (ret, vol, shp)
def build_cml():
"""
1) Load daily stock data (AMZN, KO, MSFT).
2) Compute the max Sharpe ratio portfolio for a given risk-free rate.
3) Retrieve the efficient frontier plus a possible cml_df from `pok.efficient_frontier(...)`.
4) If cml_df is non-empty, use its points for the CML line. Else, build a fallback
two-point CML from (0, risk_free_rate) to (vol_sharpe, ret_sharpe).
5) Print an ECharts JSON with sub-series:
- "Frontier" => line
- "CML" => line (dotted in Vue)
- "MaxSharpePort" => single scatter point
"""
# ========== 1) LOAD MARKET DATA ==========
risk_free_rate = 0.06 # e.g., 6%
periods_per_year = 252
# Load daily stock data and compute daily returns
stocks = pok.get_stock_dynamic()
daily_rets = pok.compute_returns(stocks)
# Number of frontier points
n_portfolios = 40
# Annualize returns and compute covariance
ann_rets = pok.annualize_rets(daily_rets, periods_per_year)
cov_rets = daily_rets.cov()
# ========== 2) MAXIMUM SHARPE PORTFOLIO ==========
# We find the weights that maximize Sharpe ratio at the specified Rf
optimal_weights = pok.maximize_sharpe_ratio(
ann_rets, cov_rets, risk_free_rate, periods_per_year
)
# Display the weights for each asset
print("Optimal weights for the Maximum Sharpe Ratio portfolio:")
print(" AMZN: {:.2f}%".format(optimal_weights[0]*100))
print(" KO: {:.2f}%".format(optimal_weights[1]*100))
print(" MSFT: {:.2f}%".format(optimal_weights[2]*100))
# Retrieve the portfolio's metrics
ret, vol, shp = get_portfolio_features(
optimal_weights, ann_rets, cov_rets, risk_free_rate, periods_per_year
)
print("\nMax Sharpe Portfolio => Return: {:.2f}%, Vol: {:.2f}%, Sharpe: {:.2f}".format(
ret*100, vol*100, shp
))
# ========== 3) EFFICIENT FRONTIER & POSSIBLE CML ==========
try:
df_frontier, cml_df = pok.efficient_frontier(
n_portfolios,
daily_rets,
cov_rets,
periods_per_year,
risk_free_rate=risk_free_rate,
cml=True
)
except ValueError:
# or if your kit doesn't do that, fallback
df_frontier = pok.efficient_frontier(
n_portfolios,
daily_rets,
cov_rets,
periods_per_year,
risk_free_rate=risk_free_rate,
cml=True
)
cml_df = None
frontier_data = df_frontier[["volatility","return"]].values.tolist()
# ========== 4) MERGE/CREATE CML DATA ==========
# If cml_df is not None and has >=2 rows => use it
if (cml_df is not None) and (len(cml_df) >= 2):
# We'll parse its [vol, ret] columns
cml_data = cml_df[["volatility","return"]].values.tolist()
else:
# Otherwise, build a fallback 2-point line from (0, risk_free_rate) => (vol, ret)
cml_data = [
[0.0, risk_free_rate],
[vol, ret]
]
# Single point => Max Sharpe portfolio => [vol, ret]
msr_point = [[vol, ret]]
# ========== 5) DYNAMIC AXIS RANGE ==========
min_vol = df_frontier["volatility"].min()
max_vol = df_frontier["volatility"].max()
min_ret = df_frontier["return"].min()
max_ret = df_frontier["return"].max()
# Incorporate the single MSR portfolio
if vol < min_vol: min_vol = vol
if vol > max_vol: max_vol = vol
if ret < min_ret: min_ret = ret
if ret > max_ret: max_ret = ret
# Add 10% buffer
x_min = max(0.0, min_vol * 0.9)
x_max = max_vol * 1.1
y_min = min_ret * 0.9
y_max = max_ret * 1.1
# ========== 6) BUILD ECHARTS JSON ==========
# We'll label this chart key "cml_plot"
plot_data = {
"cml_plot": {
"type": "multi",
"title": "MSR @ {:.2f}% RF".format(risk_free_rate*100),
"yAxisName": "Return (%)",
"xAxis": {
"type": "value",
"name": "Volatility (%)",
"min": x_min,
"max": x_max
},
"yAxis": {
"type": "value",
"min": y_min,
"max": y_max
},
"series": {
"Frontier": frontier_data, # entire frontier line
"CML": cml_data, # line => dotted in Vue
"MaxSharpePort": msr_point # single scatter point
}
}
}
# ========== 7) PRINT ECHARTS JSON FOR VUE ==========
print("\n<ECHARTS_DATA>" + json.dumps(plot_data))
if __name__ == "__main__":
build_cml() This graph illustrates how the inclusion of a risk-free asset expands the range of available return-volatility combinations. The CML represents a new set of optimal portfolios, offering higher returns for the same level of risk compared to the original efficient frontier.
Additionally, it's possible to plot various significant portfolios on the efficient frontier:
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
def get_portfolio_features(weights, rets, covmat, risk_free_rate, periods_per_year):
"""
Calculate annualized return, volatility, and Sharpe ratio for given portfolio weights.
"""
vol = pok.portfolio_volatility(weights, covmat)
vol = pok.annualize_vol(vol, periods_per_year)
ret = pok.portfolio_return(weights, rets)
shp = pok.sharpe_ratio(ret, risk_free_rate, periods_per_year, v=vol)
return (ret, vol, shp)
def cml():
"""
1) Set risk-free rate (e.g., 0.05 => 5%).
2) Call pok.efficient_frontier(...) with hsr, cml, mvp, ewp, iplot=False.
3) Gather sub-series:
- 'Frontier' => line
- 'CML' => line
- 'MaxSharpe' => scatter
- 'GMV' => scatter
- 'EWP' => scatter
4) Print ECharts JSON => <ECHARTS_DATA>...
"""
# -- 1) CONFIG & DATA --
risk_free_rate = 0.05
n_portfolios = 90
periods_per_year= 252
stocks = pok.get_stock_dynamic()
daily_rets= pok.compute_returns(stocks)
ann_rets = pok.annualize_rets(daily_rets, periods_per_year)
cov_rets = daily_rets.cov()
# -- 2) CALL EFFICIENT FRONTIER (NO MATPLOTLIB) --
df_frontier, cml_df = pok.efficient_frontier(
n_portfolios,
daily_rets,
cov_rets,
periods_per_year,
risk_free_rate=risk_free_rate,
iplot=False,
hsr=True,
cml=True,
mvp=True,
ewp=True
)
# Frontier => [ [vol, ret], ... ]
frontier_data = df_frontier[["volatility","return"]].values.tolist()
# If cml_df has >=2 rows => parse them, else fallback
if cml_df is not None and len(cml_df) >= 2:
cml_data = cml_df[["volatility","return"]].values.tolist()
else:
# fallback => re-compute tangency or just 2 points
w_msr = pok.maximize_sharpe_ratio(ann_rets, cov_rets, risk_free_rate, periods_per_year)
ret_msr, vol_msr, shp_msr = get_portfolio_features(w_msr, ann_rets, cov_rets, risk_free_rate, periods_per_year)
cml_data = [
[0.0, risk_free_rate],
[vol_msr, ret_msr]
]
# Single MSR scatter => last row of cml or fallback
if cml_df is not None and len(cml_df) >= 2:
vol_msr = cml_df["volatility"].iloc[-1]
ret_msr = cml_df["return"].iloc[-1]
else:
vol_msr, ret_msr = cml_data[-1]
msr_data = [[vol_msr, ret_msr]]
# GMV => re-calc
w_gmv = pok.minimize_volatility(ann_rets, cov_rets)
ret_gmv, vol_gmv, shp_gmv = get_portfolio_features(w_gmv, ann_rets, cov_rets, risk_free_rate, periods_per_year)
gmv_data = [[vol_gmv, ret_gmv]]
# EWP => re-calc
w_ew = np.repeat(1.0 / ann_rets.shape[0], ann_rets.shape[0])
ret_ew, vol_ew, shp_ew = get_portfolio_features(w_ew, ann_rets, cov_rets, risk_free_rate, periods_per_year)
ewp_data = [[vol_ew, ret_ew]]
# -- 3) DETERMINE AXIS BOUNDS --
min_vol = df_frontier["volatility"].min()
max_vol = df_frontier["volatility"].max()
min_ret = df_frontier["return"].min()
max_ret = df_frontier["return"].max()
# incorporate msr, gmv, ewp
for (vx, rx) in [(vol_msr, ret_msr), (vol_gmv, ret_gmv), (vol_ew, ret_ew)]:
if vx < min_vol: min_vol = vx
if vx > max_vol: max_vol = vx
if rx < min_ret: min_ret = rx
if rx > max_ret: max_ret = rx
x_min = max(0.0, min_vol * 0.9)
x_max = max_vol * 1.1
y_min = min_ret * 0.9
y_max = max_ret * 1.1
# -- 4) BUILD ECHARTS JSON --
# Replace "multi" => "line" or remove it entirely
plot_data = {
"eff_plot": {
"type": "line", # <= changed from "multi" to "line"
"title": f"Efficient Frontier (RF={risk_free_rate*100:.2f}%)",
"yAxisName": "Return (%)",
"xAxis": {
"type": "value",
"name": "Volatility (%)",
"min": x_min,
"max": x_max
},
"yAxis": {
"type": "value",
"min": y_min,
"max": y_max
},
"series": {
"Frontier": frontier_data,
"CML": cml_data,
"MSR": msr_data,
"GMV": gmv_data,
"EWP": ewp_data
}
}
}
# -- 5) PRINT --
print("\n<ECHARTS_DATA>" + json.dumps(plot_data))
if __name__ == "__main__":
build_cml() In this script, the efficient frontier is plotted, highlighting various portfolios such as the highest Sharpe ratio portfolio, the minimum volatility portfolio, and the equally weighted portfolio. The graph and the data provide insights into the potential risk-return profiles, assisting investors in making informed decisions based on their risk preferences and investment goals.
Maximizing the Sharpe Ratio Portfolio with a Non-Zero Risk-Free Asset
Consider a scenario where, in addition to a set of risky assets, there's also a risk-free asset with zero volatility and a return equal to the risk-free rate
Minimize:
subject to
Here, short selling of assets is allowed, and the entire capital is invested. The Lagrangian is defined as:
and the partial derivatives are set to zero:
From here, the optimal weights
NOTE
The process of determining optimal weights
Initially, the equation
This expression is then used in the second condition to solve for
Subsequently,
where
Portfolio Return & Volatility
A portfolio constructed with these weights is expected to yield a return equivalent to the predetermined target return,
This equation illustrates that the portfolio's return aligns with the specified target return when the optimal weights are applied.
The portfolio's volatility is determined by:
which simplifies to:
Portfolio Weights for Full Allocation to Risky Assets (Maximum Sharpe Ratio)
In the presence of a risk-free asset, an investor might choose to allocate all available capital to risky assets. In this scenario, the optimal weights can be derived by normalizing the efficient weights previously calculated. Specifically, the optimal weights
This allows the formulation of weights for the portfolio with full allocation to risky assets
Here, the denominator sums the weights in
The return for such a portfolio is:
And the volatility is:
The portfolio constituted solely of risky assets, with weights
Considering two specific portfolios:
- The portfolio comprising only the risk-free asset
- The portfolio comprising only risky assets
The Capital Market Line (CML) is the line connecting these two portfolios, described by:
This line represents the range of portfolios that can be formed by varying allocations between the risk-free asset and the risky assets. The slope of the CML is equivalent to the Sharpe Ratio of the portfolio with Maximum Sharpe Ratio portfolio.
Therefore, any point on the CML represents an investment in both the risk-free asset and the risky assets, with weights given by
To demonstrate these concepts in Python:
import PortfolioOptimizationKit as pok
import pandas as pd
import numpy as np
import json
from tabulate import tabulate
def get_portfolio_features(weights, rets, covmat, risk_free_rate, periods_per_year):
"""
Calculate annualized return, volatility, and Sharpe ratio for given portfolio weights.
"""
vol = pok.portfolio_volatility(weights, covmat)
vol = pok.annualize_vol(vol, periods_per_year)
ret = pok.portfolio_return(weights, rets)
shp = pok.sharpe_ratio(ret, risk_free_rate, periods_per_year, v=vol)
return (ret, vol, shp)
def build_cml():
"""
1) Load data, compute MSR portfolio & verify formulas.
2) Build a target-return portfolio and the CML line.
3) Output ECharts JSON for sub-series:
- CML => line
- MSR => single point
- Target => single point
Also uses `tabulate` to print tables in a clean format.
"""
# ~~~~~~~~~~~~~ 1) BASIC SETUP & DATA ~~~~~~~~~~~~~
periods_per_year = 252
risk_free_rate = 0.06
target_ret = 0.13
# Load data
stocks = pok.get_stock_dynamic()
daily_rets = pok.compute_returns(stocks)
ann_rets = pok.annualize_rets(daily_rets, periods_per_year)
cov_rets = daily_rets.cov()
print(f"Risk-free rate: {risk_free_rate:.2f}")
print("\nAnnualized returns:")
df_ann_rets = pd.DataFrame(ann_rets, columns=["Ann. Return"])
print(tabulate(df_ann_rets, headers="keys", tablefmt="github"))
print("\nCovariance:")
print(tabulate(cov_rets, headers="keys", tablefmt="github"), "\n")
# ~~~~~~~~~~~~~ 2) MAXIMUM SHARPE RATIO PORTFOLIO ~~~~~~~~~~~~~
optimal_weights = pok.maximize_sharpe_ratio(ann_rets, cov_rets, risk_free_rate, periods_per_year)
ret_msr, vol_msr, shp_msr = get_portfolio_features(optimal_weights, ann_rets, cov_rets, risk_free_rate, periods_per_year)
print("Optimal weights for Maximum Sharpe Ratio Portfolio:")
w_dict = {
"AMZN": optimal_weights[0]*100,
"KO": optimal_weights[1]*100,
"MSFT": optimal_weights[2]*100
}
df_w = pd.DataFrame.from_dict(w_dict, orient="index", columns=["Weight (%)"])
print(tabulate(df_w, headers="keys", tablefmt="github"))
print(f"\nMSR => Return: {ret_msr:.4f}, Vol: {vol_msr:.4f}, Sharpe: {shp_msr:.4f}")
# ~~~~~~~~~~~~~ 3) Formula Check for MSR Weights (w_M) ~~~~~~~~~~~~~
invcov = pok.inverse_df(cov_rets)
ones = np.repeat(1.0, len(ann_rets))
r_rf = ann_rets - risk_free_rate * ones
w_M = np.dot(invcov, r_rf) / np.dot(ones, np.dot(invcov, r_rf))
mu_M = pok.portfolio_return(w_M, ann_rets)
sigma_M = pok.annualize_vol(pok.portfolio_volatility(w_M, cov_rets), periods_per_year)
print("\nWeights for full allocation to risky assets (w_M):")
wM_df = pd.DataFrame(w_M, index=ann_rets.index, columns=["Weight"])
print(tabulate(wM_df, headers="keys", tablefmt="github"))
print(f"Return (mu_M) = {mu_M:.4f}, Volatility (sigma_M) = {sigma_M:.4f}\n")
# ~~~~~~~~~~~~~ 4) Combined Portfolio for target_ret (13%) ~~~~~~~~~~~~~
# w* = (target_ret - rf)/[ones' * invcov*(Ri-Rf)] * invcov*(Ri-Rf), plus remainder in Rf
wstar = (target_ret - risk_free_rate)/np.dot(r_rf, np.dot(invcov, r_rf)) * np.dot(invcov, r_rf)
# Combine with Rf as the last "asset"
wstar_full = np.append(wstar, 1 - wstar.sum())
print("Weights for combined investment (w^*):")
# We'll label them [AMZN, KO, MSFT, "Rf"]
combined_labels = list(ann_rets.index) + ["Rf"]
wstar_df = pd.DataFrame(wstar_full, index=combined_labels, columns=["Weight"])
print(tabulate(wstar_df, headers="keys", tablefmt="github"))
# Build a Series for Rf, then concat
rf_series = pd.Series([risk_free_rate], index=["Rf"])
rets_plus_rf = pd.concat([ann_rets, rf_series])
mu_p = pok.portfolio_return(wstar_full, rets_plus_rf)
sigma_p = pok.annualize_vol(pok.portfolio_volatility(wstar, cov_rets), periods_per_year)
print(f"\nCombined portfolio => Return (mu_p): {mu_p:.4f}, Volatility (sigma_p): {sigma_p:.4f}")
# ~~~~~~~~~~~~~ 5) Building the CML Points ~~~~~~~~~~~~~
n_cml = 20
target_ret_vec = np.linspace(target_ret, mu_M, n_cml)
cml_points = []
for tr in target_ret_vec:
wstar_i = (tr - risk_free_rate)/np.dot(r_rf, np.dot(invcov, r_rf)) * np.dot(invcov, r_rf)
# weights + remainder in Rf
wstar_i_full = np.append(wstar_i, 1 - wstar_i.sum())
# we need ann_rets + Rf to evaluate
mu_i = pok.portfolio_return(wstar_i_full, rets_plus_rf)
sigma_i = pok.annualize_vol(pok.portfolio_volatility(wstar_i, cov_rets), periods_per_year)
cml_points.append([sigma_i, mu_i])
# ~~~~~~~~~~~~~ 6) Building ECharts Output ~~~~~~~~~~~~~
# We'll create sub-series => "CML" => line of cml_points,
# "MSR" => single scatter => (vol_msr, ret_msr),
# "Target" => single scatter => (sigma_p, mu_p).
# You can also add "Frontier" if you want.
msr_data = [[vol_msr, ret_msr]]
target_data = [[sigma_p, mu_p]]
# Axis range
all_vols = [pt[0] for pt in cml_points] + [vol_msr, sigma_p]
all_rets = [pt[1] for pt in cml_points] + [ret_msr, mu_p]
min_vol = min(all_vols)
max_vol = max(all_vols)
min_ret_ = min(all_rets)
max_ret_ = max(all_rets)
x_min = max(0.0, min_vol * 0.9)
x_max = max_vol * 1.1
y_min = min_ret_ * 0.9
y_max = max_ret_ * 1.1
# No top-level "type", so each sub-series can be line or scatter
plot_data = {
"cml_chart": {
"title": f"CML & MSR (Rf={risk_free_rate*100:.2f}%)",
"yAxisName": "Return (%)",
"xAxis": {
"type": "value",
"name": "Volatility (%)",
"min": x_min,
"max": x_max
},
"yAxis": {
"type": "value",
"min": y_min,
"max": y_max
},
"series": {
"CML": cml_points, # line => many [vol, ret]
"MSR": msr_data, # single scatter => (vol_msr, ret_msr)
"Target": target_data # single scatter => (sigma_p, mu_p)
}
}
}
# ~~~~~~~~~~~~~ 7) Print ECharts JSON ~~~~~~~~~~~~~
print("\n<ECHARTS_DATA>" + json.dumps(plot_data))
if __name__ == "__main__":
build_cml() It combines theoretical explanations with Python code to demonstrate the calculation of portfolio weights for full allocation to risky assets and how these portfolios align with the Capital Market Line.
