Skip to content

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:

python
# 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 N>1 stocks. Let w:=(w1,,wN)T represent the investment proportions in each asset, constrained by i=1Nwi=1. These are the investment weights.

Ri and Rp represent the return of asset i and the total portfolio return, respectively. Similarly, σi and σp denote the volatility of asset i and the portfolio, respectively.

Portfolio Return

The portfolio return is a weighted average of the individual asset returns:

Rp=i=1NwiRi=wTR,

where R:=(R1,,RN)T. For historical data, Rp uses past returns. For future investments, expected returns E, replace actual returns.

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 w1 and w2 are the weights and R1 and R2 are the returns of these assets, the portfolio's variance is expressed as:

σp2=Var(w1R1+w2R2)=w12Var(R1)+w22Var(R2)+2w1w2Cov(R1,R2)=w12σ12+w22σ22+2w1w2Cov(R1,R2),

where Cov(R1,R2)=E[(R1μ1)(R2μ2)] represents the covariance between the two assets, with μi and μj as their mean returns.

Defining the correlation coefficient between the assets as ρ1,2=Cov(R1,R2)σ1σ2, the portfolio's volatility for two assets can be simplified to:

σp=w12σ12+w22σ22+2w1w2σ1σ2ρ1,2.

It's worth mentioning that by employing matrix notation, we can succinctly express this volatility calculation.

The portfolio's volatility, σp, can be expressed as:

σp=(w1,w2)(σ12σ1σ2ρ12σ1σ2ρ21σ22)(w1w2)

This simplifies to:

σp=wTΣw

Where the covariance matrix, Σ, is defined as:

Σ:=(σ12σ1σ2ρ12σ1σ2ρ21σ22)

For a portfolio of N stocks, the covariance matrix Σ=[cij] is an N×N matrix where each element cij=σiσjρij represents the covariance between assets i and j, and cii=σi2 is the variance of asset i (the diagonal elements of the covariance matrix).

Extending this concept to matrix notation for compactness, the portfolio's volatility is:

σp=wTΣw.

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, ρ12. We begin by initializing 500 daily returns and the necessary parameters for our simulation:

python
# 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:

python
# 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 6 different correlation coefficients between the two assets and generate 20 portfoliosfor each correlation scenario using varying weight allocations:

python
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 w=(1,0), while blue indicates complete investment in the second asset w=(0,1).

The plots demonstrate that lower asset correlations generally offer a more favorable return-to-volatility ratio. Notably, in the case of ρ=1, it's theoretically possible to construct a portfolio yielding around 30% return with minimal volatility.

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

python
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

python
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 4000 random portfolio allocations. By evaluating each portfolio's return, volatility, and Sharpe Ratio, it facilitates the identification of optimal investment strategies based on comprehensive statistical analysis.

Visualizing Portfolios and the Efficient Frontier

To illustrate the distribution of portfolios and highlight the efficient frontier, we create a scatter plot:

python
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 4000 randomly generated portfolios based on their 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:

python
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.

python
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, shp

Strategies 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:

minimize12wTΣw,

subject to

{wT1=1,0w1.
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 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:

minimize12wTΣw,

subject to

{wTR=R0,wT1=1,0w1.

where R0 represents the predetermined level of expected return. As an illustration, assume the target total expected return R0=16%:

python
# 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., 16%). This portfolio is then positioned on the efficient frontier:

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 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 55% is not feasible since the highest achievable return is approximately 50% from Amazon. Attempting to set a target return of 40% would yield a portfolio heavily weighted towards the asset with the highest return:

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:

minimizeRprfσp=:SR

subject to the constraints:

{wT1=1,0w1.

Employing the Minimizer, calculating, visualizing the Optimal Portfolio and comparing with Monte Carlo Simulation Results:

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 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:

minimizeRprfσp=:SR

subject to

{12wTΣw=σ0,wT1=1,0w1.

where σ0 signifies a predetermined level of portfolio volatility. Consider, for instance, setting a total portfolio volatility target of σ0=20%:

python
# Define the target volatility for the portfolio
target_volatility = 0.2
python
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 1 (indicating full investment) and all weights are non-negative (indicating only buying). These conditions were built into the optimization problems solved so far.

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:

minimize12wTΣw,

only ensuring that the portfolio meets a desired return R0:

wTR=R0.

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:

L(w,λ):=12wTΣwλ(wTRR0),

Setting its partial derivatives to zero gives us:

{Lw=12(2Σw)λR=0,Lλ=wTR+R0=0.

From here, we find:

ΣwλR=0w=λΣ1R,

and

(λΣ1R)TR+R0=0λRTΣ1R=R0λ=R0RTΣ1R.

Given that the covariance matrix Σ is symmetric, its inverse Σ1 is also symmetric, meaning (Σ1)T=Σ1. By substituting λ back into the equation, we derive the analytical solution for the weights. Thus, the optimal weights are:

w=R0Σ1RRTΣ1R,

This formula provides a precise method to calculate the weights. It's important to note that without the normalization constraint (ensuring weights sum to 1), the resulting weights may not represent a fully invested portfolio. Also, it shows us the best weights to use for our assets to minimize volatility while achieving a set return, R0. Importantly, because we've removed the constraint that weights must sum to 1, these weights might not represent a fully invested portfolio and could include short selling.

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:

minimize12wTΣw,

subject to the constraints:

{wTR=R0,wT1=1.

In this scenario, short selling is allowed (weights can be negative), but the total invested capital equals one.

Formulating the Lagrangian:

L(w,λ):=12wTΣwλ(wTRR0)δ(wT11),

where λ and δ are Lagrange multipliers.

Setting the derivatives of the Lagrangian to zero yields:

{Lw=12(2Σw)λRδ1=0,Lλ=wTR+R0=0,Lλ=wT1+R0=0.

Solving for weights (w) from the first equation:

w=Σ1(λR+δ1),

Substituting w into the second and third equations and solving for λ and δ yields a system of linear equations, respectively:

{(Σ1(λR+δ1))TR=λRTΣ1R+δ1Σ1R=R0,(Σ1(λR+δ1))T1=λRTΣ11+δ1Σ11=1.

Defining constants A, B, and C for compact representation:

{A:=RTΣ1R,B:=1TΣ1RRTΣ11,C:=1TΣ11,

leads to:

{λA+δB=R0,λB+δC=1.

Solving this system gives λ and δ in terms of A, B, C, and R0. Substituting back into the weights formula provides:

w=f+R0g,

where:

f=1B2AC(BΣ1RAΣ11),g=1B2AC(BΣ11CΣ1R).

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:

RCML=Rf+σCMLRpRfσp,

where:

  • RCML and σCML denote the return and volatility of a portfolio composed of both risky assets and the risk-free asset.

  • Rp and σp represent the return and volatility of the purely risky asset portfolio.

  • Rf 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:

python
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:

python
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 Rf . This situation allows for the opportunity to minimize the volatility of the portfolio, which consists of a portion invested in the risky assets and the remainder in the risk-free asset. The goal is to minimize the volatility of the risky asset portion while achieving a target return R0:

Minimize:

minimize12wTΣw,

subject to

{wTR+(1wT1)Rf=R0,

Here, short selling of assets is allowed, and the entire capital is invested. The Lagrangian is defined as:

L(w,λ):=12wTΣwλ(wTR+(1wT1)RfR0),

and the partial derivatives are set to zero:

{Lw=12(2Σw)λR+λRf1=0,Lλ=wTR(1w1)Rf+R0=0,

From here, the optimal weights w can be calculated, and the allocation to the risk-free asset is determined by 1wT1.

NOTE

The process of determining optimal weights w for a portfolio that includes both risky assets and a risk-free asset involves several steps.

Initially, the equation w=λΣ1(RRf1) is derived from the first condition.

This expression is then used in the second condition to solve for λ, which yields λ=R0Rf(RRf1)TΣ1(RRf1).

Subsequently, λ is substituted back into the expression for w to find the optimal portfolio weights w, which are determined as w=R0Rf(RRf1)TΣ1(RRf1)Σ1(RRf1)=rΣ1(RRf1),

where r is a scaling factor defined by the portion of the portfolio's excess return over the risk-free rate to the variance of the excess returns.

Portfolio Return & Volatility

A portfolio constructed with these weights is expected to yield a return equivalent to the predetermined target return, R0. This is demonstrated as follows:

μp=wTR+(1wT1)Rf=r(RRf1)TΣ1R+Rfr(RRf1)TΣ1Rf1=r(RRf1)TΣ1(RRf1)=R0Rf+Rf=R0.

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:

σp2=wTΣw=(rΣ1(RRf1))TΣ(rΣ1(RRf1))=r2(RRf1)TΣ1ΣΣ1=Id(RRf1)=(R0Rf)2((RRf1)TΣ1(RRf1))2(RRf1)TΣ1(RRf1)

which simplifies to:

σp=(R0Rf)(RRf1)TΣ1(RRf1).

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 w are proportional to the vector Σ1(RRf1), where the proportionality constant is r.

This allows the formulation of weights for the portfolio with full allocation to risky assets wM as:

wM:=Σ1(RRf1)1TΣ1(RRf1),

Here, the denominator sums the weights in Σ1(RRf1), ensuring that the weights in wM sum to 1, representing a complete allocation to risky assets and none to the risk-free asset.

The return for such a portfolio is:

μM=wMTR=(RRf1)TΣ1R1TΣ1(RRf1),

And the volatility is:

σM2=wMTΣwM=(RRf1)TΣ1(RRf1)(1TΣ1(RRf1))2σM=(RRf1)TΣ1(RRf1)1TΣ1(RRf1).

The portfolio constituted solely of risky assets, with weights wM, is inherently a minimum volatility portfolio on the efficient frontier. It is also the portfolio with the highest Sharpe Ratio.

Considering two specific portfolios:

  • The portfolio comprising only the risk-free asset (σ,μ)=(0,Rf)
  • The portfolio comprising only risky assets (σ,μ)=(σM,μM)

The Capital Market Line (CML) is the line connecting these two portfolios, described by:

μ=Rf+μMRfσMσ,

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 μM and σM, indicating that it is the 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 w.

To demonstrate these concepts in Python:

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.

After earning certification from EDHEC Business School, I translated complex financial theories into practical Python modules, openly shared under the MIT License.