Código
import yfinance as yf
import numpy as np
from scipy.stats import linregress
import pandas as pd
from scipy.optimize import minimizePADS - Programa Avançado em Data Science
Insper
Paloma Vaissman Uribe
We will start by picking up some data from Yahoo Finance APIs
| Ticker | BBAS3.SA | BBDC4.SA | ITUB4.SA | SANB4.SA |
|---|---|---|---|---|
| Date | ||||
| 2020-11-24 | 11.897976 | 14.993035 | 20.721100 | 14.784431 |
| 2020-11-25 | 11.864409 | 14.799624 | 20.476990 | 15.099722 |
| 2020-11-26 | 11.676400 | 14.559301 | 20.044577 | 15.264225 |
| 2020-11-27 | 11.619330 | 14.442082 | 20.198013 | 15.181971 |
| 2020-11-30 | 11.367538 | 14.242801 | 19.919031 | 14.434868 |
| ... | ... | ... | ... | ... |
| 2025-11-17 | 22.500000 | 19.320000 | 40.299999 | 17.270000 |
| 2025-11-18 | 21.879999 | 19.090000 | 40.099998 | 17.330000 |
| 2025-11-19 | 21.580000 | 18.900000 | 39.849998 | 17.129999 |
| 2025-11-21 | 22.000000 | 18.790001 | 39.970001 | 17.150000 |
| 2025-11-24 | 22.100000 | 18.809999 | 39.860001 | 17.250000 |
1247 rows × 4 columns
Calculate the daily percentage returns for each stock using the .pct_change() method on the data DataFrame and store the result in a new DataFrame called daily_returns.
| Ticker | BBAS3.SA | BBDC4.SA | ITUB4.SA | SANB4.SA |
|---|---|---|---|---|
| Date | ||||
| 2020-11-24 | NaN | NaN | NaN | NaN |
| 2020-11-25 | -0.002821 | -0.012900 | -0.011781 | 0.021326 |
| 2020-11-26 | -0.015846 | -0.016238 | -0.021117 | 0.010894 |
| 2020-11-27 | -0.004888 | -0.008051 | 0.007655 | -0.005389 |
| 2020-11-30 | -0.021670 | -0.013799 | -0.013812 | -0.049210 |
Create a vector of equal weights for the stocks and use it to calculate the daily returns of the portfolio.
| 0 | |
|---|---|
| Date | |
| 2020-11-24 | 0.000000 |
| 2020-11-25 | -0.001544 |
| 2020-11-26 | -0.010577 |
| 2020-11-27 | -0.002668 |
| 2020-11-30 | -0.024623 |
Compute the cumulative returns of the portfolio using the portfolio_returns DataFrame.
Fetch historical data for the Ibovespa index (market data) and define a suitable risk-free rate for the corresponding period. Ensure the data is aligned by date with the portfolio returns.
| Ticker | ^BVSP |
|---|---|
| Date | |
| 2020-11-24 | NaN |
| 2020-11-25 | 0.003161 |
| 2020-11-26 | 0.000854 |
| 2020-11-27 | 0.003157 |
| 2020-11-30 | -0.015257 |
Define the annual and daily risk-free rates and align the portfolio and Ibovespa daily returns to ensure they cover the same dates for subsequent calculations.
annual_risk_free_rate = 0.12 # change as you wish
trading_days_per_year = 252
daily_risk_free_rate = annual_risk_free_rate / trading_days_per_year
# Align data by dropping NaNs and ensuring common dates
# First, drop any initial NaNs from pct_change
portfolio_returns_cleaned = portfolio_returns.dropna()
ibovespa_daily_returns_cleaned = ibovespa_daily_returns.dropna()
# Ensure both series have the exact same dates after dropping NaNs
common_index = portfolio_returns_cleaned.index.intersection(ibovespa_daily_returns_cleaned.index)
portfolio_returns = portfolio_returns_cleaned.reindex(common_index)
ibovespa_daily_returns = ibovespa_daily_returns_cleaned.reindex(common_index)
print(f"Annual Risk-Free Rate: {annual_risk_free_rate:.2%}")
print(f"Daily Risk-Free Rate: {daily_risk_free_rate:.4%}")
print("\nAligned portfolio_returns head:")
print(portfolio_returns.head())
print("\nAligned ibovespa_daily_returns head:")
print(ibovespa_daily_returns.head())
print(f"\nLengths after alignment: Portfolio returns = {len(portfolio_returns)}, Ibovespa daily returns = {len(ibovespa_daily_returns)}")Annual Risk-Free Rate: 12.00%
Daily Risk-Free Rate: 0.0476%
Aligned portfolio_returns head:
Date
2020-11-25 -0.001544
2020-11-26 -0.010577
2020-11-27 -0.002668
2020-11-30 -0.024623
2020-12-01 0.060496
dtype: float64
Aligned ibovespa_daily_returns head:
Ticker ^BVSP
Date
2020-11-25 0.003161
2020-11-26 0.000854
2020-11-27 0.003157
2020-11-30 -0.015257
2020-12-01 0.022473
Lengths after alignment: Portfolio returns = 1246, Ibovespa daily returns = 1246
To calculate the portfolio historical volatility, I will first compute the daily standard deviation of the portfolio_returns Series. Then, I will annualize this daily volatility by multiplying it by the square root of trading_days_per_year.
Compute the Sharpe Ratio using the portfolio’s average daily return, the risk-free rate, and the portfolio’s volatility. Annualize the Sharpe Ratio.
Reasoning: To calculate the Sharpe Ratio, I will first calculate the average daily portfolio return, then compute the daily excess return by subtracting the daily risk-free rate. After that, I’ll calculate the daily Sharpe Ratio and finally annualize it and print the result.
average_daily_portfolio_return = portfolio_returns.mean()
daily_excess_return = average_daily_portfolio_return - daily_risk_free_rate
daily_sharpe_ratio = daily_excess_return / portfolio_volatility_daily
annualized_sharpe_ratio = daily_sharpe_ratio * np.sqrt(trading_days_per_year)
print(f"Annualized Sharpe Ratio: {annualized_sharpe_ratio:.2f}")Annualized Sharpe Ratio: 0.01
Perform a linear regression of the portfolio’s daily returns against the daily returns of the Ibovespa index to calculate the Beta coefficient.
Reasoning: The previous code failed because ‘pd’ was not defined. I need to import the pandas library to use pd.DataFrame for type checking. Also, based on the kernel state, ibovespa_daily_returns is already a pandas DataFrame, so I will ensure that it is correctly accessed as a Series for the linregress function.
# Perform linear regression
# Ensure ibovespa_daily_returns is a 1D array/series for linregress
# If it's a DataFrame with a single column, access the column
if isinstance(ibovespa_daily_returns, pd.DataFrame):
market_returns = ibovespa_daily_returns.iloc[:, 0] # Get the first (and likely only) column
else:
market_returns = ibovespa_daily_returns
slope, intercept, r_value, p_value, std_err = linregress(market_returns, portfolio_returns)
beta = slope
print(f"CAPM Beta: {beta:.2f}")CAPM Beta: 0.99
Define a Python function portfolio_performance that takes portfolio weights as input and calculates the annualized portfolio volatility and annualized Sharpe Ratio, using the daily_returns, trading_days_per_year, and daily_risk_free_rate variables from the current kernel state. The function should return both these metrics.
def portfolio_performance(weights):
# 2. Calculate portfolio_daily_returns
portfolio_daily_returns = (daily_returns * weights).sum(axis=1)
# 3. Calculate daily standard deviation
portfolio_volatility_daily = portfolio_daily_returns.std()
# 4. Annualize the portfolio volatility
annualized_volatility = portfolio_volatility_daily * np.sqrt(trading_days_per_year)
# 5. Calculate average daily portfolio return
average_daily_portfolio_return = portfolio_daily_returns.mean()
# 6. Compute daily excess return
daily_excess_return = average_daily_portfolio_return - daily_risk_free_rate
# 7. Calculate daily Sharpe Ratio
daily_sharpe_ratio = daily_excess_return / portfolio_volatility_daily
# 8. Annualize the daily Sharpe Ratio
annualized_sharpe_ratio = daily_sharpe_ratio * np.sqrt(trading_days_per_year)
# 9. Return both metrics
return annualized_volatility, annualized_sharpe_ratio
print("Function 'portfolio_performance' defined successfully.")Function 'portfolio_performance' defined successfully.
Define the constraints for the optimization (e.g., sum of weights equals 1) and the bounds for individual weights (e.g., between 0 and 1 for no short-selling).
Reasoning: I will define the bounds for each stock weight, ensuring they are between 0 and 1, and then define the equality constraint that the sum of all weights must equal 1, preparing these for use in an optimization function.
num_stocks = len(stocks_list)
# Define bounds for individual weights (no short-selling, no leverage)
bounds = tuple((0, 1) for _ in range(num_stocks))
# Define constraints: sum of weights must be 1
constraints = ({'type': 'eq', 'fun': lambda weights: np.sum(weights) - 1})
print(f"Number of stocks: {num_stocks}")
print(f"Bounds for each weight: {bounds[0]}")
print("Constraints defined: Sum of weights = 1")Number of stocks: 4
Bounds for each weight: (0, 1)
Constraints defined: Sum of weights = 1
Use a numerical optimization library (like scipy.optimize.minimize) to find the set of weights that maximizes the Sharpe Ratio, subject to the defined constraints and bounds.
Reasoning: I will import the minimize function, define the objective function neg_sharpe_ratio, set initial equal weights, and then run the optimization using scipy.optimize.minimize to find the weights that maximize the Sharpe Ratio, subject to the previously defined constraints and bounds. The result will be stored in optimized_result.
# Objective function to minimize (negative Sharpe Ratio)
def neg_sharpe_ratio(weights):
# `portfolio_performance` returns (annualized_volatility, annualized_sharpe_ratio)
volatility, sharpe_ratio = portfolio_performance(weights)
return -sharpe_ratio
# Initial guess (equal weights)
initial_weights = np.array([1/num_stocks] * num_stocks)
# Run the optimization
optimized_result = minimize(neg_sharpe_ratio, initial_weights, method='SLSQP', bounds=bounds, constraints=constraints)
print("Optimization complete. Optimized result stored in 'optimized_result'.")Optimization complete. Optimized result stored in 'optimized_result'.
Retrieve the optimal weights from the optimization result and calculate the corresponding optimal annualized portfolio return, annualized volatility, and annualized Sharpe Ratio. Then, display these optimized metrics.
Reasoning: First, I need to extract the optimal weights from the optimized_result object. Then, I will use these optimal weights to calculate the optimal annualized volatility and Sharpe Ratio using the previously defined portfolio_performance function. After that, I will calculate the optimal annualized portfolio return. Finally, I will display all these optimal metrics in a formatted way.
optimal_weights = optimized_result.x
# Calculate optimal annualized volatility and Sharpe Ratio using the portfolio_performance function
optimal_annualized_volatility, optimal_annualized_sharpe_ratio = portfolio_performance(optimal_weights)
# Calculate optimal annualized portfolio return
optimal_portfolio_daily_returns = (daily_returns * optimal_weights).sum(axis=1)
optimal_annualized_portfolio_return = optimal_portfolio_daily_returns.mean() * trading_days_per_year
print("--- Optimal Portfolio Metrics ---")
print(f"Optimal Weights: {np.round(optimal_weights, 4)}")
print(f"Optimal Annualized Portfolio Return: {optimal_annualized_portfolio_return:.2%}")
print(f"Optimal Annualized Volatility: {optimal_annualized_volatility:.2%}")
print(f"Optimal Annualized Sharpe Ratio: {optimal_annualized_sharpe_ratio:.2f}")--- Optimal Portfolio Metrics ---
Optimal Weights: [0.3632 0. 0.6368 0. ]
Optimal Annualized Portfolio Return: 16.43%
Optimal Annualized Volatility: 23.55%
Optimal Annualized Sharpe Ratio: 0.19