Portfolio Optimization and Efficient Frontier¶
We will use object oriented programming to implement the portfolio optimization by constructing PortfolioOptimization class for portfolio simulation and efficient frontier.
Portfolio Simulation
: We will implement a Monte Carlo simulation to generate random portfolio weights on a larger scale and calculate the expected portfolio return, variance and sharpe ratio for every simulated allocation. We will then identify the portfolio with a highest return for per unit of risk.
Efficient Frontier
: The Efficient Frontier is formed by a set of portfolios offering the highest expected portfolio return for a certain volatility or offering the lowest volatility for a certain level of expected returns.
In [117]:
import numpy as np
import pandas as pd
from scipy.optimize import minimize
from sqlalchemy import create_engine, text
from typing import Optional
import plotly.graph_objects as go
# Set the display options
pd.set_option("display.max_colwidth", None)
pd.set_option("display.max_rows", None)
pd.set_option("display.max_columns", None)
In [118]:
class PortfolioOptimization:
"""A class for portfolio optimization using Modern Portfolio Theory."""
def __init__(
self,
risk_free_rate: float = 0.03,
weight_bounds: tuple = (-0.9, 0.9),
data: pd.DataFrame = None,
):
self.assets = data.columns
self.number_of_assets = len(data.columns)
self.risk_free_rate = risk_free_rate
self.weight_bounds = weight_bounds
self.returns = data.pct_change().dropna()
self.expected_returns = self.returns.mean().dropna().mul(252)
self.cov_matrix = self.returns.cov().dropna() * 252
def _portfolio_variance(self, weights: np.ndarray) -> float:
"""Calculate portfolio variance."""
return weights.T @ self.cov_matrix @ weights
def _portfolio_return(self, weights: np.ndarray) -> float:
"""Calculate portfolio expected return."""
return np.sum(weights * self.expected_returns)
def _negative_sharpe_ratio(self, weights: np.ndarray) -> float:
"""Calculate negative Sharpe ratio (for minimization)."""
ret = self._portfolio_return(weights)
std = np.sqrt(self._portfolio_variance(weights))
return -(ret - self.risk_free_rate) / std if std != 0 else None # negatif car on va utiliser minimize pour minimizer donc maximizer les returns
def _negative_return(self, weights: np.ndarray) -> float:
"""Calculate negative return (for minimization)."""
return -self._portfolio_return(weights)
def monte_carlo_simulation(self, number_of_portfolio: int = 5000):
results = []
for _ in range(number_of_portfolio):
weights = np.random.dirichlet(np.ones(self.number_of_assets), size=1).flatten()
# dirichlet distributions gives us a list of random weights so that the sum equal one
portfolio_return = self._portfolio_return(weights)
portfolio_risk = np.sqrt(self._portfolio_variance(weights))
sharpe_ratio = (
(portfolio_return - self.risk_free_rate) / portfolio_risk
if portfolio_risk != 0
else None
) # sharpe_ratio is what we want to be at maximize value
results.append(
{
"weights": dict(zip(self.assets, weights)),
"expected_return": portfolio_return,
"expected_risk": portfolio_risk,
"sharpe_ratio": sharpe_ratio,
}
)
return pd.DataFrame(results)
def optimize_portfolio(
self,
target_return: Optional[float] = None,
target_risk: Optional[float] = None,
maximize_sharpe: bool = False,
):
"""Optimize portfolio using scipy.optimize.minimize."""
if target_risk is not None and target_return is not None:
raise ValueError("Specify either target_return or target_risk, not both.")
initial_weights = np.ones(self.number_of_assets) / self.number_of_assets
bounds = tuple(
(self.weight_bounds[0], self.weight_bounds[1])
for _ in range(self.number_of_assets)
)
constraints = [{"type": "eq", "fun": lambda x: np.sum(x) - 1}]
if maximize_sharpe:
objective = self._negative_sharpe_ratio
elif target_risk is not None:
objective = self._negative_return
constraints.append(
{
"type": "eq",
"fun": lambda x: target_risk**2 - self._portfolio_variance(x),
}
)
else:
objective = self._portfolio_variance
if target_return is not None:
constraints.append(
{
"type": "eq",
"fun": lambda x: target_return - self._portfolio_return(x),
}
)
result = minimize(
objective,
initial_weights,
method="SLSQP",
bounds=bounds,
constraints=constraints,
)
portfolio_return = self._portfolio_return(result.x)
portfolio_risk = np.sqrt(self._portfolio_variance(result.x))
sharpe_ratio = (
(portfolio_return - self.risk_free_rate) / portfolio_risk
if portfolio_risk != 0
else 0
)
return {
"weights": dict(zip(self.assets, result.x)),
"expected_return": portfolio_return,
"expected_risk": portfolio_risk,
"sharpe_ratio": sharpe_ratio,
}
def print_portfolio(self, portfolio: dict):
"""
Print the portfolio details in a neat format without np.float64.
"""
print("Weights:")
for asset, weight in portfolio["weights"].items():
print(f" {asset}: {weight:.4f}")
print(f"Expected Return: {portfolio['expected_return']:.4f}")
print(f"Expected Risk: {portfolio['expected_risk']:.4f}")
print(f"Sharpe Ratio: {portfolio['sharpe_ratio']:.4f}")
def plot_simulation(self, temp: pd.DataFrame):
# Plot simulated portfolio
fig = go.Figure(
data=go.Scatter(
x=temp["expected_risk"],
y=temp["expected_return"],
mode="markers",
marker=dict(
color=temp["sharpe_ratio"],
colorbar=dict(
title="Sharpe Ratio",
tickvals=[temp["sharpe_ratio"].min(), temp["sharpe_ratio"].max()],
ticktext=[f"{temp['sharpe_ratio'].min():.2f}", f"{temp['sharpe_ratio'].max():.2f}"],
),
symbol="cross",
),
name="Simulated Portfolio",
text=temp["sharpe_ratio"],
hovertemplate="Sharpe Ratio: %{text}<br>Expected Volatility: %{x}<br>Expected Return: %{y}<extra></extra>",
showlegend=True,
)
)
# Plot max Sharpe ratio
max_sharpe_idx = temp.sharpe_ratio.idxmax()
fig.add_trace(
go.Scatter(
x=[temp.iloc[max_sharpe_idx]["expected_risk"]],
y=[temp.iloc[max_sharpe_idx]["expected_return"]],
mode="markers",
marker=dict(color="RoyalBlue", size=20, symbol="star"),
name="Max Sharpe",
text="Max Sharpe Portfolio",
hovertemplate="Max Sharpe Portfolio<br>Expected Volatility: %{x}<br>Expected Return: %{y}<extra></extra>",
)
)
# Update layout for title and labels
fig.update_layout(
title="Monte Carlo Simulated Portfolio",
xaxis_title="Expected Volatility",
yaxis_title="Expected Return",
width=800,
height=600,
showlegend=False,
)
# Show spikes on both axes
fig.update_xaxes(showspikes=True)
fig.update_yaxes(showspikes=True)
# Show the plot
fig.show()
def plot_efficient_frontier(self, opt_var, opt_sharpe, num_portfolios=100):
"""
Plot the efficient frontier with max Sharpe portfolio and min variance portfolio using Plotly go objects.
"""
# Results of efficient frontier
results = []
target_risks = np.linspace(0.10, 0.35, num=num_portfolios) # Risk range for efficient frontier
for target_risk in target_risks:
result = self.optimize_portfolio(target_risk=target_risk)
results.append(
{
"expected_return": result["expected_return"],
"expected_risk": result["expected_risk"],
"sharpe_ratio": result["sharpe_ratio"],
}
)
# Create a DataFrame of results
frontier = pd.DataFrame(results)
# Plot efficient frontier using lines
fig = go.Figure(
data=go.Scatter(
x=frontier["expected_risk"],
y=frontier["expected_return"],
# mode="lines",
mode='markers',
marker=dict(
color=frontier["sharpe_ratio"],
colorbar=dict(
title="Sharpe Ratio",
tickvals=[frontier["sharpe_ratio"].min(), frontier["sharpe_ratio"].max()],
ticktext=[f"{frontier['sharpe_ratio'].min():.2f}", f"{frontier['sharpe_ratio'].max():.2f}"],
),
symbol='cross'),
name="Efficient Frontier",
line=dict(color="green"),
hovertemplate="Expected Volatility: %{x}<br>Expected Return: %{y}<extra></extra>",
)
)
# Plot max Sharpe ratio
fig.add_trace(
go.Scatter(
x=[opt_sharpe["expected_risk"]],
y=[opt_sharpe["expected_return"]],
mode="markers",
marker=dict(color="red", size=20, symbol="star"),
name="Max Sharpe",
text="Max Sharpe Portfolio",
hovertemplate="Max Sharpe Portfolio<br>Expected Volatility: %{x}<br>Expected Return: %{y}<extra></extra>",
)
)
# Plot min variance portfolio
fig.add_trace(
go.Scatter(
x=[opt_var["expected_risk"]],
y=[opt_var["expected_return"]],
mode="markers",
marker=dict(color="green", size=20, symbol="star"),
name="Min Variance",
text="Min Variance Portfolio",
hovertemplate="Min Variance Portfolio<br>Expected Volatility: %{x}<br>Expected Return: %{y}<extra></extra>",
)
)
# Update layout for title and labels
fig.update_layout(
title="Efficient Frontier with Max Sharpe and Min Variance Portfolios",
xaxis_title="Expected Volatility (%)",
yaxis_title="Expected Return (%)",
width=800,
height=600,
showlegend=False,
)
# Show spikes on both axes
fig.update_xaxes(showspikes=True)
fig.update_yaxes(showspikes=True)
# Show the plot
fig.show()
In [119]:
engine = create_engine("sqlite:///Nifty50")
assets = sorted(["ICICIBANK.BO", "ITC.BO", "RELIANCE.BO", "TCS.BO", "ADANIENT.BO"])
# Query close prices
df = pd.DataFrame()
for asset in assets:
query = f"SELECT Date, Close FROM '{asset}'" #asset has to be a str
with engine.connect() as connection:
df1 = pd.read_sql_query(text(query), connection, index_col="Date")
df1.columns = [asset]
df = pd.concat([df, df1], axis=1)
In [120]:
optimizer = PortfolioOptimization(data=df)
/var/folders/9m/rnxpskm57q90my8vz9bwy32h0000gn/T/ipykernel_7756/2567568079.py:14: FutureWarning: The default fill_method='pad' in DataFrame.pct_change is deprecated and will be removed in a future version. Either fill in any non-leading NA values prior to calling pct_change or specify 'fill_method=None' to not fill NA values.
In [121]:
# Monte Carlo Simulation
mc = optimizer.monte_carlo_simulation(10000)
print("\nMonte Carlo Simulation - Maximize Sharpe Portfolio")
mc_portfolio = mc.iloc[mc.sharpe_ratio.idxmax()]
optimizer.print_portfolio(mc_portfolio)
Monte Carlo Simulation - Maximize Sharpe Portfolio Weights: ADANIENT.BO: 0.0017 ICICIBANK.BO: 0.7913 ITC.BO: 0.1513 RELIANCE.BO: 0.0330 TCS.BO: 0.0227 Expected Return: 0.1537 Expected Risk: 0.1746 Sharpe Ratio: 0.7081
In [122]:
# plot simulation
optimizer.plot_simulation(mc)
In [123]:
# Optimize Portfolio for different strategies
print("\nOptimal Portfolio - Minimize Variance")
mv = optimizer.optimize_portfolio(target_return=0.25)
optimizer.print_portfolio(mv)
Optimal Portfolio - Minimize Variance Weights: ADANIENT.BO: -0.2049 ICICIBANK.BO: 0.8138 ITC.BO: 0.3953 RELIANCE.BO: 0.1304 TCS.BO: -0.1346 Expected Return: 0.2500 Expected Risk: 0.1937 Sharpe Ratio: 1.1358
In [124]:
print("\nOptimal Portfolio - Maximize Return")
mr = optimizer.optimize_portfolio(target_risk=0.25)
optimizer.print_portfolio(mr)
Optimal Portfolio - Maximize Return Weights: ADANIENT.BO: -0.3350 ICICIBANK.BO: 0.9000 ITC.BO: 0.5891 RELIANCE.BO: 0.2497 TCS.BO: -0.4039 Expected Return: 0.3522 Expected Risk: 0.2500 Sharpe Ratio: 1.2887
In [125]:
print("\nOptimal Portfolio - Maximize Sharpe")
ms = optimizer.optimize_portfolio(maximize_sharpe=True)
optimizer.print_portfolio(ms)
Optimal Portfolio - Maximize Sharpe Weights: ADANIENT.BO: -0.4204 ICICIBANK.BO: 0.9000 ITC.BO: 0.7485 RELIANCE.BO: 0.3459 TCS.BO: -0.5741 Expected Return: 0.4117 Expected Risk: 0.2931 Sharpe Ratio: 1.3023
In [127]:
# plot efficient frontier
optimizer.plot_efficient_frontier(mv, ms)
/Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:439: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:493: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:439: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:493: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:439: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:493: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:439: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:493: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:439: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:493: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:435: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:439: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds /Users/armandcoiffe/Desktop/Lab3/.venv/lib/python3.10/site-packages/scipy/optimize/_slsqp_py.py:493: RuntimeWarning: Values in x were outside bounds during a minimize step, clipping to bounds
In [ ]: