Back To Top

July 15, 2024

Multi-objective Portfolio Optimization

Portfolio optimization across multiple factors - Dividends, Returns, Volatility, CVaR, and more

Multi-objective Portfolio optimization  considers multiple objectives simultaneously, such as maximizing returns, minimizing risk, and enhancing diversification, to build a portfolio that aligns with the investor’s goals across multiple factors.

In this article, we’ll optimize a portfolio based on multiple objectives, such as dividend yields, maximise returns, maximise Sharpe ratio, and more.

We offer end-to-end implementation code using Google Colab Notebook. Additionally, Entreprenerdly’s automated portfolio optimization tool is available here.

The tool allows investors to directly implement various portfolio optimization techniques to best suit their individual risk preferences and return expectations.

Portfolio Optimization Multi-objective Implementation Code Entreprenerdly AVIF

We’ll discuss and implement the following concepts:

  • Defining Objectives Function
    • Returns, Risk, Dividends, etc
    • Retrieving weights
  • Parameters Grid Search
    • Rebalancing Periods
    • Best Possible Portfolio
  • Assessing Portfolio Performance
    • Performance Attribution
    • Allocation, Selection, Interaction Effect
    • Sector Allocation Risks
    • Rolling Performance Metrics

2. Multi-Objective Portfolio Optimization

2.1 Approach and Methodology

The methodology involves defining various objectives and constraints to optimize the portfolio. The core approach in optimizing a multi-objective portfolio revolves around balancing several investment goals simultaneously. 

1. Define Objectives and Constraints

Each objective represents a specific goal we want the portfolio to achieve, such as maximizing returns or minimizing risk. Constraints ensure the portfolio remains realistic and feasible, like ensuring the sum of asset weights equals one.

2. Formulate the Objective Function

We create a single objective function that incorporates all the individual objectives. The function quantifies how well the portfolio meets each goal. For instance, if we want to maximize returns and minimize risk, the objective function could look like this:

Objective Value Formula For Multi-objective Portfolio Optimization

Here, ∑(wi⋅μi) represents the portfolio’s expected return, and sqrt{wT⋅Σ⋅w} represents the portfolio’s risk.

3. Weighting the Objectives

Not all objectives are equally important. We assign weights to each objective based on their relative importance. This step helps prioritize certain goals over others to reflect the investor’s preferences.

4. Optimization Algorithm

We use Sequential Least Squares Programming (SLSQP) to find the optimal set of asset weights. The algorithm iteratively adjusts the weights to minimize or maximize the objective function while adhering to the constraints. The goal is to find the set of weights that best achieves the defined objectives.

5. Periodic Rebalancing

Portfolios are rebalanced periodically to ensure they stay aligned with the original objectives. During rebalancing, we adjust the asset weights to account for market changes and shifts in the investor’s goals.

2.2 The Objectives to Optimize

The following objectives can be customized by setting them to 'true' or 'false' in the implementation code, based on whether the user wants to include them in the optimization process.

Maximize Return

We aim to maximize the expected return of the portfolio by calculating the weighted sum of mean returns. The objective value is adjusted by subtracting this weighted sum:

Objective-Value Formula Minus for Multi-objective portfolio

where represents the weight of asset and is the expected return of asset .

Minimize Risk

To minimize risk, we consider the portfolio’s volatility (standard deviation). This is achieved by adding the portfolio’s standard deviation to the objective value:

Minimize Risk in Multi-Objective Portfolio Optimization

is the covariance matrix of asset returns.

Maximize Sharpe Ratio

The Sharpe ratio measures risk-adjusted return. We calculate it and subtract it from the objective value:

Minimize Sharpe Ratio Formula In Multi-objective Portfolio

where is the risk-free rate.

Maximize Diversification

Diversification is assessed by comparing the weighted sum of individual asset volatilities to the portfolio’s volatility. This ratio is subtracted from the objective value:

Diversification Ratio Formula in multi-objective portfolio optimization

Other measures, such as HHI (Herfindahl-Hirschman Index), could be used instead.

Minimize CVaR

Conditional Value at Risk (CVaR) represents the expected loss in the worst-case scenario. The objective value is increased by adding the 5th percentile of portfolio returns:

Minimize CVaR Formula in multi-objective portfolio optimization

Minimize Transaction Costs

To account for transaction costs, we assume a previous portfolio with equal weights. The costs are calculated and added to the objective value:

Minimize Transaction Costs in Multi-objective portfolio entreprenerdly

where is the previous weight of asset .

Maximize Dividend Yield

We maximize dividend yield by subtracting the weighted sum of dividend yields from the objective value:

Maximize Dividend Yield in Multi-objective Portfolio

Maximize Sortino Ratio

The Sortino ratio focuses on downside risk. It is calculated and subtracted from the objective value:

Maximize Sortino Ratio in Multi-objective Portfolio

Maximize Upside Potential Ratio

This ratio compares the mean of positive returns to downside risk. The ratio is subtracted from the objective value:

Maximize Upside Potential Ratio in Multi-objective Portfolio

Minimize Beta

Beta measures the portfolio’s volatility relative to the market. We add the portfolio beta to the objective value:

Minimize Portfolio Beta Formula in Multi-objective portfolio

2.4 Python Implementation

2.4.1 Core Functions 

Lets discuss now the essential functions required for multi-objective portfolio optimization using Python. These functions facilitate data download, metric calculations, optimization, and visualization:

  • Dynamic Data Alignment: In portfolio_beta, the function dynamically aligns the portfolio’s returns with the benchmark returns using align. This ensures both datasets are on the same date range, preventing misalignment issues that could skew results.

  • Handling Missing Data: The calculate_metrics function drops any rows with missing data (dropna). This is needed as financial data often contains gaps due to holidays.

  • Multi-Objective Optimization: The multi_objective_function allows the inclusion or exclusion of various objectives such as maximizing return, minimizing risk, or maximizing dividend yield. Each objective modifies the obj_value, balancing trade-offs according to user-defined priorities.

  • Periodic Rebalancing: The rebalance_portfolio function ensures that the portfolio stays aligned with the defined objectives by adjusting the weights periodically as market conditions change.

  • Realistic Constraints: The optimize_portfolio function includes constraints to ensure that the sum of weights equals 1 and that all weights are between 0 and 1. 

				
					import yfinance as yf
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.optimize import minimize
from pypfopt import risk_models, expected_returns
import json

# Function to download adjusted close prices for given tickers
def download_data(tickers, start, end):
    return yf.download(tickers, start=start, end=end)['Adj Close']

# Function to calculate returns, mean returns, and covariance matrix
def calculate_metrics(data):
    returns = data.pct_change().dropna()  # Calculate daily returns
    mean_returns = expected_returns.mean_historical_return(data)  # Calculate mean returns
    cov_matrix = risk_models.sample_cov(data)  # Calculate covariance matrix
    return returns, mean_returns, cov_matrix

# Function to get dividend yields for given tickers
def get_dividend_yields(tickers):
    dividend_yields = {}
    for ticker in tickers:
        stock = yf.Ticker(ticker)
        info = stock.info
        # Get dividend yield or set to 0 if not available
        dividend_yield = info.get('dividendYield') or info.get('trailingAnnualDividendYield', 0)
        if dividend_yield is None:
            dividend_yield = 0
        dividend_yields[ticker] = dividend_yield
    return pd.Series(dividend_yields)

# Function to calculate portfolio beta
def portfolio_beta(weights, returns, benchmark_returns):
    aligned_returns, aligned_benchmark = returns.align(benchmark_returns, join='inner', axis=0)
    cov_matrix = np.cov(aligned_returns.T, aligned_benchmark)
    beta = cov_matrix[:-1, -1] / np.var(aligned_benchmark)
    return np.sum(weights * beta)

# Function to calculate Sortino ratio
def sortino_ratio(weights, returns, risk_free_rate=0.01):
    portfolio_return = np.dot(weights, returns.mean())
    downside_returns = np.minimum(returns - risk_free_rate, 0)
    downside_deviation = np.sqrt(np.mean(downside_returns ** 2))
    return (portfolio_return - risk_free_rate) / downside_deviation

# Function to calculate Upside Potential Ratio
def upside_potential_ratio(weights, returns, risk_free_rate=0.01):
    portfolio_return = np.dot(weights, returns.mean())
    upside_returns = np.maximum(returns - risk_free_rate, 0)
    downside_returns = np.minimum(returns - risk_free_rate, 0)
    downside_deviation = np.sqrt(np.mean(downside_returns ** 2))
    upside_potential = np.mean(upside_returns)
    return upside_potential / downside_deviation

# Multi-objective function for portfolio optimization
def multi_objective_function(weights, mean_returns, cov_matrix, dividend_yields, benchmark_returns, risk_free_rate=0.01):
    obj_value = 0  # Initialize the objective value to 0
    if maximize_return:
        # If we want to maximize return, subtract the weighted sum of mean returns from the objective value
        obj_value -= np.dot(weights, mean_returns)  # Maximize return
    if minimize_risk:
        # If we want to minimize risk, add the portfolio volatility (standard deviation) to the objective value
        obj_value += np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))  # Minimize risk
    if maximize_sharpe:
        # If we want to maximize the Sharpe ratio, calculate it and subtract from the objective value
        portfolio_return = np.dot(weights, mean_returns)  # Expected portfolio return
        portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))  # Portfolio volatility
        sharpe_ratio = (portfolio_return - risk_free_rate) / portfolio_volatility  # Sharpe ratio calculation
        obj_value -= sharpe_ratio  # Maximize Sharpe ratio
    if maximize_diversification:
        # If we want to maximize diversification, calculate the diversification ratio and subtract it from the objective value
        w_vol = np.dot(weights.T, np.sqrt(np.diag(cov_matrix)))  # Weighted volatilities of individual assets
        p_vol = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))  # Portfolio volatility
        diversification_ratio = w_vol / p_vol  # Diversification ratio calculation
        obj_value -= diversification_ratio  # Maximize diversification
    if minimize_cvar:
        # If we want to minimize Conditional Value at Risk (CVaR), calculate it and add to the objective value
        cvar = np.percentile(np.dot(returns.values, weights), 5)  # CVaR calculation
        obj_value += cvar  # Minimize CVaR
    if minimize_transaction_costs:
        # If we want to minimize transaction costs, calculate them and add to the objective value
        prev_weights = np.ones(len(weights)) / len(weights)  # Assume equal weights previously
        transaction_costs = np.sum(np.abs(weights - prev_weights)) * 0.001  # Assume 0.1% cost for transactions
        obj_value += transaction_costs  # Minimize transaction costs
    if maximize_dividend_yield:
        # If we want to maximize dividend yield, subtract the weighted sum of dividend yields from the objective value
        obj_value -= np.dot(weights, dividend_yields)  # Maximize dividend yield
    if maximize_sortino:
        # If we want to maximize the Sortino ratio, calculate it and subtract from the objective value
        obj_value -= sortino_ratio(weights, returns, risk_free_rate)  # Maximize Sortino ratio
    if maximize_upside_potential:
        # If we want to maximize the Upside Potential Ratio, calculate it and subtract from the objective value
        obj_value -= upside_potential_ratio(weights, returns, risk_free_rate)  # Maximize Upside Potential Ratio
    if minimize_beta:
        # If we want to minimize beta, calculate it and add to the objective value
        obj_value += portfolio_beta(weights, returns, benchmark_returns)  # Minimize Beta
    return obj_value  # Return the final objective value


# Function to optimize portfolio based on given metrics
def optimize_portfolio(mean_returns, cov_matrix, dividend_yields, benchmark_returns):
    num_assets = len(mean_returns)
    init_guess = num_assets * [1. / num_assets,]  # Initial guess for weights
    constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1}, {'type': 'ineq', 'fun': lambda x: x})  # Constraints
    bounds = [(0, 1) for _ in range(num_assets)]  # Bounds for weights
    result = minimize(multi_objective_function, init_guess, args=(mean_returns, cov_matrix, dividend_yields, benchmark_returns),
                      method='SLSQP', bounds=bounds, constraints=constraints)
    return result.x

# Function to calculate cumulative returns
def calculate_cumulative_returns(returns, weights):
    portfolio_returns = returns.dot(weights)
    return (1 + portfolio_returns).cumprod()

# Function to plot cumulative returns
def plot_results(portfolio_cumulative, sp500_cumulative, equal_weighted_cumulative):
    plt.figure(figsize=(10, 6))
    plt.plot(portfolio_cumulative, label='Multi-Objective Optimization Portfolio')
    plt.plot(sp500_cumulative, label='S&P 500')
    plt.plot(equal_weighted_cumulative, label='Equal Weighted Portfolio')
    plt.xlabel('Date')
    plt.ylabel('Cumulative Returns')
    plt.title('Cumulative Returns Over Time')
    plt.legend()
    plt.show()

# Function to rebalance portfolio periodically
def rebalance_portfolio(data, tickers, rebalance_period):
    rebalanced_weights = []
    dates = pd.date_range(start=data.index[0], end=data.index[-1], freq=f'{rebalance_period}M')
    for date in dates:
        if date in data.index:
            subset_data = data[:date]  # Data up to the current date
            returns, mean_returns, cov_matrix = calculate_metrics(subset_data)
            benchmark_returns = benchmark_data[:date].pct_change().dropna()
            dividend_yields = get_dividend_yields(tickers)
            weights = optimize_portfolio(mean_returns, cov_matrix, dividend_yields, benchmark_returns)
            rebalanced_weights.append((date, weights))
    return rebalanced_weights
				
			

2.4.2 Main Execution

We execute now the main functions, considering the following steps:

  • Define Global Objectives: We set variables, to true or false, to outline the portfolio optimization goals.

  • Download and Calculate Metrics: Using download_data, we fetch historical prices for the selected tickers. Then, calculate_metrics computes daily returns, mean returns, and the covariance matrix. get_dividend_yields retrieves the dividend yields for each ticker.

  • Fetch Benchmark Data: We download historical data for the S&P 500 index (^GSPC) to serve as our benchmark for performance comparison.

  • Optimization and Rebalancing: Depending on the rebalancing setting, we either rebalance the portfolio periodically using rebalance_portfolio or optimize it once with optimize_portfolio.

  • Convert Weights to JSON: The final portfolio weights are converted to a JSON format for better readability and further use.

  • Calculate Cumulative Returns: We compute cumulative returns for the optimized portfolio, the S&P 500, and an equally weighted portfolio with calculate_cumulative_returns.

				
					# Define global variables for the objectives
maximize_return = True
minimize_risk = True
maximize_sharpe = False
maximize_diversification = True
minimize_cvar = False
minimize_transaction_costs = False
maximize_dividend_yield = True
maximize_sortino = False
maximize_upside_potential = False
minimize_beta = False
rebalance = True
rebalance_period = 6  # in months

# Main execution
tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'ASML']
start_date = '2015-01-01'
end_date = '2025-01-01'

# Download and calculate metrics
data = download_data(tickers, start_date, end_date)
returns, mean_returns, cov_matrix = calculate_metrics(data)
dividend_yields = get_dividend_yields(tickers)

# Benchmark data
benchmark_ticker = '^GSPC'
benchmark_data = download_data(benchmark_ticker, start_date, end_date)
benchmark_returns = benchmark_data.pct_change().dropna()

# Optimization and Rebalancing
if rebalance:
    rebalanced_weights = rebalance_portfolio(data, tickers, rebalance_period)
    final_weights = rebalanced_weights[-1][1]
else:
    final_weights = optimize_portfolio(mean_returns, cov_matrix, dividend_yields, benchmark_returns)

# Convert weights to JSON
weights_json = json.dumps(dict(zip(tickers, final_weights)), indent=4)
print(weights_json)

# Calculate cumulative returns
portfolio_cumulative = calculate_cumulative_returns(returns, final_weights)
sp500_cumulative = (1 + benchmark_returns).cumprod()
equal_weights = np.ones(len(tickers)) / len(tickers)
equal_weighted_cumulative = calculate_cumulative_returns(returns, equal_weights)

# Plot results
plot_results(portfolio_cumulative, sp500_cumulative, equal_weighted_cumulative)
				
			
				
					{
    "AAPL": 0.22729428549853123,
    "MSFT": 0.2663101158784816,
    "GOOGL": 0.2388010280751279,
    "AMZN": 0.09101806783984355,
    "ASML": 0.1765765027080158
}
				
			
1. Portfolio Performance Multi-objective Portfolio Optimization

Figure. 1: Cumulative Profit Plot for a Multi-objective Portfolio Against an Equally Weighted Portfolio and the SP&500 Benchmark.

Also worth reading:

Optimize Portfolio Performance with Risk Parity Rebalancing

Optimizing Portfolios With Hierarchical Risk Parity

Prev Post

U.S. Treasury Yield Curve as a Key Economic Indicator

Next Post

Estimating Conditional Probability of Price Movements

post-bars
Mail Icon

Newsletter

Get Every Weekly Update & Insights

[mc4wp_form id=]

Leave a Comment