Porfolio Optimization with Multiple Risk Strategies in Python with AMPL#

Notebook_4_Porfolio_Optimization_Risk_Strategies.ipynb Open In Colab Kaggle Gradient Open In SageMaker Studio Lab Hits

Description: This notebook evaluates three distinct risk-based portfolio strategies: Semivariance Optimization, Conditional Value-at-Risk (CVaR) Optimization, and Conditional Drawdown-at-Risk (CDaR) Optimization.

Tags: finance, portfolio-optimization, cvar, cdar, semivariance

Notebook author: Mukeshwaran Baskaran <mukesh96official@gmail.com>

Introduction - Semivariance, CVaR & CDaR#

This project focuses on portfolio optimization, a critical aspect of investment management that aims to construct portfolios offering the best possible return for a given level of risk. By leveraging advanced optimization techniques, it evaluates three distinct risk-based portfolio strategies: Semivariance Optimization, Conditional Value-at-Risk (CVaR) Optimization, and Conditional Drawdown-at-Risk (CDaR) Optimization. These methods are designed to address different aspects of risk, such as downside volatility, tail risk, and drawdowns, providing investors with a robust framework to manage their investments effectively.

The analysis begins by calculating expected returns and historical performance data for a selection of assets. It then constructs optimal portfolios using each of the three strategies, balancing the trade-off between risk and return. The performance of these portfolios is evaluated using key metrics such as annualized return, volatility, Sharpe ratio, and maximum drawdown. Visualizations are provided to help investors understand the allocation of assets in each portfolio, compare cumulative returns over time, and assess the risk-return profiles of the strategies.

This project is particularly useful for investors, portfolio managers, and financial analysts seeking to enhance their understanding of risk-based portfolio construction and improve decision-making in asset allocation. It combines theoretical insights with practical implementation, offering a comprehensive toolkit for modern portfolio management.

Relevant Books for Further Reading#

  1. “Modern Portfolio Theory and Investment Analysis” by Edwin J. Elton, Martin J. Gruber, Stephen J. Brown, and William N. Goetzmann

    • A classic text that provides a thorough introduction to portfolio theory, including risk measures and optimization techniques.

  2. “Portfolio Optimization and Performance Analysis” by Jean-Luc Prigent

    • This book delves into advanced portfolio optimization methods, including CVaR and CDaR, with a focus on practical applications.

  3. “Risk Management and Financial Institutions” by John C. Hull

    • A comprehensive guide to risk management concepts, including Value-at-Risk (VaR) and Conditional Value-at-Risk (CVaR), and their applications in finance.

  4. “The Intelligent Asset Allocator: How to Build Your Portfolio to Maximize Returns and Minimize Risk” by William J. Bernstein

    • A practical guide to asset allocation and portfolio construction, emphasizing the importance of risk management.

  5. “Quantitative Equity Portfolio Management: Modern Techniques and Applications” by Edward E. Qian, Ronald H. Hua, and Eric H. Sorensen

    • This book explores quantitative methods for portfolio management, including optimization and risk modeling.

  6. “Financial Risk Forecasting: The Theory and Practice of Forecasting Market Risk with Implementation in R and Python” by Jon Danielsson

    • A hands-on guide to forecasting financial risk, with practical examples and code implementations.

These resources provide a solid foundation for understanding the theoretical and practical aspects of portfolio optimization and risk management, complementing the insights gained from this project.


# Install dependencies
%pip install amplpy yfinance matplotlib pandas numpy -q
# Google Colab & Kaggle integration
from amplpy import AMPL, ampl_notebook

ampl = ampl_notebook(
    modules=["gurobi"],  # modules to install
    license_uuid="default",  # license to use
)  # instantiate AMPL object and register magics

Implementation#

Import libraries#

import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt

Efficient-semivariance optimization in AMPL#

def efficient_semivariance(
    expected_returns, returns, target_return=None, benchmark=0, weight_bounds=(0, 1)
):
    """Perform efficient-semivariance optimization using AMPL."""
    ampl = AMPL()

    # Extract dimensions
    tickers = list(expected_returns.index)
    n_assets = len(tickers)
    n_periods = len(returns)

    # Define the AMPL model for Efficient Semivariance
    ampl.eval(
        r"""
        set ASSETS ordered;
        set PERIODS ordered;

        param mu{ASSETS};
        param returns{PERIODS, ASSETS};
        param benchmark;
        param target_return;
        param lb;
        param ub;

        var w{ASSETS} >= lb <= ub;
        var n{PERIODS} >= 0;
        var p{PERIODS} >= 0;

        minimize semivariance:
            sum{t in PERIODS} n[t] / card(PERIODS);

        subject to excess_returns{t in PERIODS}:
            sum{i in ASSETS} returns[t,i] * w[i] - benchmark - p[t] + n[t] = 0;

        subject to return_target:
            sum{i in ASSETS} mu[i] * w[i] >= target_return;

        subject to weights_sum:
            sum{i in ASSETS} w[i] = 1;
        """
    )

    # Set data in AMPL
    ampl.set["ASSETS"] = tickers
    ampl.set["PERIODS"] = list(range(n_periods))

    ampl.param["mu"] = {ticker: expected_returns[ticker] for ticker in tickers}
    ampl.param["returns"] = {
        (t, ticker): returns.iloc[t][ticker]
        for t in range(n_periods)
        for ticker in tickers
    }
    ampl.param["benchmark"] = benchmark
    ampl.param["target_return"] = target_return if target_return is not None else 0
    ampl.param["lb"] = weight_bounds[0]
    ampl.param["ub"] = weight_bounds[1]

    # Solve the optimization problem
    ampl.solve(solver="gurobi", gurobi_options="outlev=1")

    # Check if the problem was solved successfully
    assert ampl.solve_result == "solved", ampl.solve_result

    # Extract weights
    weights = ampl.var["w"].to_dict()

    return weights

Efficient-CVaR optimization using AMPL#

def efficient_cvar(
    expected_returns, returns, target_return=None, beta=0.95, weight_bounds=(0, 1)
):
    """Perform efficient-CVaR optimization using AMPL."""
    ampl = AMPL()

    # Extract dimensions
    tickers = list(expected_returns.index)
    n_periods = len(returns)

    # Define the AMPL model for Efficient CVaR
    ampl.eval(
        r"""
        set ASSETS ordered;
        set PERIODS ordered;

        param mu{ASSETS};
        param returns{PERIODS, ASSETS};
        param beta;
        param target_return;
        param lb;
        param ub;

        var w{ASSETS} >= lb <= ub;
        var alpha;
        var u{PERIODS} >= 0;

        minimize cvar:
            alpha + (1/(card(PERIODS)*(1-beta))) * sum{t in PERIODS} u[t];

        subject to cvar_constraint{t in PERIODS}:
            u[t] >= -sum{i in ASSETS} returns[t,i] * w[i] - alpha;

        subject to return_target:
            sum{i in ASSETS} mu[i] * w[i] >= target_return;

        subject to weights_sum:
            sum{i in ASSETS} w[i] = 1;
        """
    )

    # Set data in AMPL
    ampl.set["ASSETS"] = tickers
    ampl.set["PERIODS"] = list(range(n_periods))

    ampl.param["mu"] = {ticker: expected_returns[ticker] for ticker in tickers}
    ampl.param["returns"] = {
        (t, ticker): returns.iloc[t][ticker]
        for t in range(n_periods)
        for ticker in tickers
    }
    ampl.param["beta"] = beta
    ampl.param["target_return"] = target_return if target_return is not None else 0
    ampl.param["lb"] = weight_bounds[0]
    ampl.param["ub"] = weight_bounds[1]

    # Solve the optimization problem
    ampl.solve(solver="gurobi", gurobi_options="outlev=1")

    # Check if the problem was solved successfully
    assert ampl.solve_result == "solved", ampl.solve_result

    # Extract weights
    weights = ampl.var["w"].to_dict()

    return weights

Efficient-CDaR optimization using AMPL#

def efficient_cdar(
    expected_returns,
    cumulative_returns,
    target_return=None,
    beta=0.95,
    weight_bounds=(0, 1),
):
    """Perform efficient-CDaR optimization using AMPL."""
    ampl = AMPL()

    # Extract dimensions
    tickers = list(expected_returns.index)
    n_periods = len(cumulative_returns)

    # Define the AMPL model for Efficient CDaR
    ampl.eval(
        r"""
        set ASSETS ordered;
        set PERIODS ordered;
        set PERIODS_MINUS_FIRST = PERIODS diff {first(PERIODS)};

        param mu{ASSETS};
        param returns{PERIODS, ASSETS}; # Cumulative returns
        param beta;
        param target_return;
        param lb;
        param ub;

        var w{ASSETS} >= lb <= ub;
        var alpha;
        var u{PERIODS} >= 0;
        var z{PERIODS_MINUS_FIRST} >= 0;

        minimize cdar:
            alpha + (1/(card(PERIODS_MINUS_FIRST)*(1-beta))) * sum{t in PERIODS_MINUS_FIRST} z[t];

        subject to drawdown_def{t in PERIODS_MINUS_FIRST}:
            z[t] >= u[t] - alpha;

        subject to peak_constraint{t in PERIODS: ord(t) > 1}:
            u[t] >= u[prev(t)] - sum{i in ASSETS} (returns[t,i] - returns[prev(t),i]) * w[i];

        subject to initial_wealth:
            u[first(PERIODS)] = 0;

        subject to return_target:
            sum{i in ASSETS} mu[i] * w[i] >= target_return;

        subject to weights_sum:
            sum{i in ASSETS} w[i] = 1;
        """
    )

    # Set data in AMPL
    ampl.set["ASSETS"] = tickers
    ampl.set["PERIODS"] = list(range(n_periods))

    ampl.param["mu"] = {ticker: expected_returns[ticker] for ticker in tickers}
    ampl.param["returns"] = {
        (t, ticker): cumulative_returns.iloc[t][ticker]
        for t in range(n_periods)
        for ticker in tickers
    }
    ampl.param["beta"] = beta
    ampl.param["target_return"] = target_return if target_return is not None else 0
    ampl.param["lb"] = weight_bounds[0]
    ampl.param["ub"] = weight_bounds[1]

    # Solve the optimization problem
    ampl.solve(solver="gurobi", gurobi_options="outlev=1")

    # Check if the problem was solved successfully
    assert ampl.solve_result == "solved", ampl.solve_result

    # Extract weights
    weights = ampl.var["w"].to_dict()

    return weights

Calculate performance metrics for a given portfolio#

def calculate_performance_metrics(weights, returns, risk_free_rate=0.02):
    """Calculate performance metrics for a given portfolio."""
    portfolio_returns = (returns * pd.Series(weights)).sum(axis=1)

    # Annualized return
    annualized_return = portfolio_returns.mean() * 252

    # Annualized volatility
    annualized_volatility = portfolio_returns.std() * np.sqrt(252)

    # Sharpe ratio
    sharpe_ratio = (annualized_return - risk_free_rate) / annualized_volatility

    # Maximum drawdown
    cumulative_returns = (1 + portfolio_returns).cumprod()
    peak = cumulative_returns.cummax()
    drawdown = (cumulative_returns - peak) / peak
    max_drawdown = drawdown.min()

    return {
        "Annualized Return": annualized_return,
        "Annualized Volatility": annualized_volatility,
        "Sharpe Ratio": sharpe_ratio,
        "Max Drawdown": max_drawdown,
    }

Calculate expected returns from price data#

def calculate_expected_returns(prices, method="mean_historical"):
    """Calculate expected returns from price data"""
    returns = prices.pct_change().dropna()

    if method == "mean_historical":
        mu = returns.mean()
    else:
        # Could implement other methods here
        mu = returns.mean()

    return mu

Plotting functions#

def plot_portfolio_weights(weights_dict, title):
    """Plot portfolio weights as a pie chart"""
    labels = list(weights_dict.keys())
    sizes = list(weights_dict.values())

    # Filter out tiny allocations for better visualization
    threshold = 0.01
    other_sum = sum([size for i, size in enumerate(sizes) if size < threshold])
    filtered_labels = [label for i, label in enumerate(labels) if sizes[i] >= threshold]
    filtered_sizes = [size for size in sizes if size >= threshold]

    if other_sum > 0:
        filtered_labels.append("Other")
        filtered_sizes.append(other_sum)

    # Improved visualization
    plt.figure(figsize=(12, 7))
    plt.pie(
        filtered_sizes,
        labels=filtered_labels,
        autopct="%1.1f%%",
        startangle=90,
        colors=plt.cm.Paired.colors,
    )
    plt.axis("equal")  # Equal aspect ratio ensures that pie is drawn as a circle.
    plt.title(title, fontsize=16, weight="bold")
    plt.show()


def plot_cumulative_returns(returns_dict, title):
    """Plot cumulative returns for multiple portfolios."""
    plt.figure(figsize=(14, 7))
    for label, returns in returns_dict.items():
        cumulative_returns = (1 + returns).cumprod() - 1
        plt.plot(cumulative_returns, label=label)

    plt.title(title, fontsize=16, weight="bold")
    plt.xlabel("Date")
    plt.ylabel("Cumulative Returns")
    plt.legend()
    plt.grid(True)
    plt.show()


def plot_performance_metrics(metrics_dict):
    """Plot performance metrics as a bar plot."""
    metrics_df = pd.DataFrame(metrics_dict).T
    metrics_df.plot(kind="bar", figsize=(14, 8), colormap="Paired")
    plt.title("Portfolio Performance Metrics", fontsize=16, weight="bold")
    plt.xlabel("Portfolio")
    plt.ylabel("Value")
    plt.xticks(rotation=45)
    plt.grid(True)
    plt.show()

Execution#

Download historical data#

# Define tickers and date range
tickers = ["AAPL", "MSFT", "AMZN", "GOOGL", "META", "TSLA", "NVDA"]
start_date = "2020-01-01"
end_date = "2022-01-01"  # Use future date to get data up to today

print(f"Fetching data for {len(tickers)} stocks from {start_date} to today...")

# Download historical data
data = yf.download(tickers, start=start_date, end=end_date)["Close"]
Fetching data for 7 stocks from 2020-01-01 to today...
YF.download() has changed argument auto_adjust default to True
[*********************100%***********************]  7 of 7 completed

Calculate returns#

returns = data.pct_change().dropna()
print(f"Data fetched successfully. {len(returns)} days of returns.")
Data fetched successfully. 504 days of returns.

Calculate expected returns (annualized)#

mu = calculate_expected_returns(data) * 252  # Annualized return

Calculate cumulative returns for CDaR#

cumulative_returns = (1 + returns).cumprod() - 1

Set target return (annualized)#

target_return = 0.15  # 15% annual return

Run Efficient Semivariance optimization#

print("\nRunning Efficient Semivariance optimization...")
sv_weights = efficient_semivariance(
    mu, returns, target_return=target_return / 252, weight_bounds=(0, 1)
)
Running Efficient Semivariance optimization...
Gurobi 12.0.1: Set parameter LogToConsole to value 1
  tech:outlev = 1

AMPL MP initial flat model has 1015 variables (0 integer, 0 binary);
Objectives: 1 linear; 
Constraints:  506 linear;

AMPL MP final model has 1015 variables (0 integer, 0 binary);
Objectives: 1 linear; 
Constraints:  506 linear;


Set parameter InfUnbdInfo to value 1
Gurobi Optimizer version 12.0.1 build v12.0.1rc0 (linux64 - "Ubuntu 22.04.4 LTS")

CPU model: Intel(R) Xeon(R) CPU @ 2.20GHz, instruction set [SSE2|AVX|AVX2]
Thread count: 1 physical cores, 2 logical processors, using up to 2 threads

Non-default parameters:
InfUnbdInfo  1

Optimize a model with 506 rows, 1015 columns and 4546 nonzeros
Model fingerprint: 0xe5982b6e
Coefficient statistics:
  Matrix range     [3e-05, 2e+00]
  Objective range  [2e-03, 2e-03]
  Bounds range     [1e+00, 1e+00]
  RHS range        [6e-04, 1e+00]
Presolve removed 161 rows and 665 columns
Presolve time: 0.01s
Presolved: 345 rows, 350 columns, 2754 nonzeros

Iteration    Objective       Primal Inf.    Dual Inf.      Time
       0    3.3955925e-03   1.657223e+00   0.000000e+00      0s
     214    5.6558402e-03   0.000000e+00   0.000000e+00      0s

Solved in 214 iterations and 0.03 seconds (0.01 work units)
Optimal objective  5.655840154e-03
Gurobi 12.0.1: optimal solution; objective 0.005655840154
214 simplex iterations

Run Efficient CVaR optimization#

print("\nRunning Efficient CVaR optimization...")
cvar_weights = efficient_cvar(
    mu, returns, target_return=target_return / 252, beta=0.95, weight_bounds=(0, 1)
)
Running Efficient CVaR optimization...
Gurobi 12.0.1: Set parameter LogToConsole to value 1
  tech:outlev = 1

AMPL MP initial flat model has 512 variables (0 integer, 0 binary);
Objectives: 1 linear; 
Constraints:  506 linear;

AMPL MP final model has 512 variables (0 integer, 0 binary);
Objectives: 1 linear; 
Constraints:  506 linear;


Set parameter InfUnbdInfo to value 1
Gurobi Optimizer version 12.0.1 build v12.0.1rc0 (linux64 - "Ubuntu 22.04.4 LTS")

CPU model: Intel(R) Xeon(R) CPU @ 2.20GHz, instruction set [SSE2|AVX|AVX2]
Thread count: 1 physical cores, 2 logical processors, using up to 2 threads

Non-default parameters:
InfUnbdInfo  1

Optimize a model with 506 rows, 512 columns and 4546 nonzeros
Model fingerprint: 0xbdcb107b
Coefficient statistics:
  Matrix range     [3e-05, 2e+00]
  Objective range  [4e-02, 1e+00]
  Bounds range     [1e+00, 1e+00]
  RHS range        [6e-04, 1e+00]
Presolve time: 0.01s
Presolved: 506 rows, 512 columns, 4546 nonzeros

Iteration    Objective       Primal Inf.    Dual Inf.      Time
       0      handle free variables                          0s
      86    4.1895690e-02   0.000000e+00   0.000000e+00      0s

Solved in 86 iterations and 0.01 seconds (0.00 work units)
Optimal objective  4.189568970e-02
Gurobi 12.0.1: optimal solution; objective 0.0418956897
86 simplex iterations

Run Efficient CDaR optimization#

print("\nRunning Efficient CDaR optimization...")
cdar_weights = efficient_cdar(
    mu,
    cumulative_returns,
    target_return=target_return / 252,
    beta=0.95,
    weight_bounds=(0, 1),
)
Running Efficient CDaR optimization...
Gurobi 12.0.1: Set parameter LogToConsole to value 1
  tech:outlev = 1

AMPL MP initial flat model has 1014 variables (0 integer, 0 binary);
Objectives: 1 linear; 
Constraints:  1007 linear;

AMPL MP final model has 1014 variables (0 integer, 0 binary);
Objectives: 1 linear; 
Constraints:  1007 linear;


Set parameter InfUnbdInfo to value 1
Gurobi Optimizer version 12.0.1 build v12.0.1rc0 (linux64 - "Ubuntu 22.04.4 LTS")

CPU model: Intel(R) Xeon(R) CPU @ 2.20GHz, instruction set [SSE2|AVX|AVX2]
Thread count: 1 physical cores, 2 logical processors, using up to 2 threads

Non-default parameters:
InfUnbdInfo  1

Optimize a model with 1007 rows, 1014 columns and 6037 nonzeros
Model fingerprint: 0xcd316ba1
Coefficient statistics:
  Matrix range     [5e-05, 2e+00]
  Objective range  [4e-02, 1e+00]
  Bounds range     [1e+00, 1e+00]
  RHS range        [6e-04, 1e+00]
Presolve removed 87 rows and 64 columns
Presolve time: 0.08s
Presolved: 920 rows, 950 columns, 5934 nonzeros

Iteration    Objective       Primal Inf.    Dual Inf.      Time
       0      handle free variables                          0s
     492    2.2061872e-01   0.000000e+00   0.000000e+00      0s

Solved in 492 iterations and 0.11 seconds (0.05 work units)
Optimal objective  2.206187180e-01
Gurobi 12.0.1: optimal solution; objective 0.220618718
492 simplex iterations

Display results#

print("\nEfficient Semivariance Weights:")
for ticker, weight in sv_weights.items():
    print(f"{ticker}: {weight:.4f}")

print("\nEfficient CVaR Weights:")
for ticker, weight in cvar_weights.items():
    print(f"{ticker}: {weight:.4f}")

print("\nEfficient CDaR Weights:")
for ticker, weight in cdar_weights.items():
    print(f"{ticker}: {weight:.4f}")
Efficient Semivariance Weights:
AAPL: 0.0425
AMZN: 0.2776
GOOGL: 0.4547
META: 0.0000
MSFT: 0.2015
NVDA: 0.0000
TSLA: 0.0237

Efficient CVaR Weights:
AAPL: 0.0000
AMZN: 0.7275
GOOGL: 0.2711
META: 0.0000
MSFT: 0.0014
NVDA: 0.0000
TSLA: 0.0000

Efficient CDaR Weights:
AAPL: 0.0000
AMZN: 0.4449
GOOGL: 0.0000
META: 0.0000
MSFT: 0.5551
NVDA: 0.0000
TSLA: 0.0000

Calculate portfolio returns#

# Calculate portfolio returns
sv_returns = (returns * pd.Series(sv_weights)).sum(axis=1)
cvar_returns = (returns * pd.Series(cvar_weights)).sum(axis=1)
cdar_returns = (returns * pd.Series(cdar_weights)).sum(axis=1)

Calculate performance metrics#

# Calculate performance metrics
sv_metrics = calculate_performance_metrics(sv_weights, returns)
cvar_metrics = calculate_performance_metrics(cvar_weights, returns)
cdar_metrics = calculate_performance_metrics(cdar_weights, returns)

Plot results#

# Plot results
plot_portfolio_weights(sv_weights, "Efficient Semivariance Portfolio Allocation")
plot_portfolio_weights(cvar_weights, "Efficient CVaR Portfolio Allocation")
plot_portfolio_weights(cdar_weights, "Efficient CDaR Portfolio Allocation")
../_images/7450fd7b45156e62b66b6ec001826116b093eb5c752b751913087ddabeb083dc.png ../_images/91a0de6ac4cbb630e9e511e91a717fce5ac11c54082fdca5fb6d702818ec9135.png ../_images/b012056e43ae5b9566025dc45c0cbe921d634940cc9f446e65b522e62c7959d3.png

Plot cumulative returns#

# Plot cumulative returns
returns_dict = {"Semivariance": sv_returns, "CVaR": cvar_returns, "CDaR": cdar_returns}
plot_cumulative_returns(returns_dict, "Cumulative Returns of Portfolios")
../_images/481d26b55fb0ecbcda40468ad39bd39364ab880448f5da975351f7d678db7064.png

Plot performance metrics#

# Plot performance metrics
metrics_dict = {"Semivariance": sv_metrics, "CVaR": cvar_metrics, "CDaR": cdar_metrics}
plot_performance_metrics(metrics_dict)
../_images/cece64cf0b11b187ca7e982fabb791b24e8256b5a70526c9ed28d917ee791e88.png