Notebook: Financial Analytics

PADS - Programa Avançado em Data Science

Insper

Paloma Vaissman Uribe

Código
import yfinance as yf
import numpy as np
from scipy.stats import linregress
import pandas as pd
from scipy.optimize import minimize

We will start by picking up some data from Yahoo Finance APIs

Código
#getting historic stock data from yfinance
stocks_list = ['BBDC4.SA', 'ITUB4.SA','SANB4.SA', 'BBAS3.SA']
data = yf.download(stocks_list, period='5y')['Close']
data
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.

Código
daily_returns = data.pct_change()
daily_returns.head()
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

Define equal weights and calculate portfolio returns

Create a vector of equal weights for the stocks and use it to calculate the daily returns of the portfolio.

Código
num_stocks = len(daily_returns.columns)
weights = np.array([1/num_stocks] * num_stocks)

portfolio_returns = (daily_returns * weights).sum(axis=1)
portfolio_returns.head()
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

Código
weights
array([0.25, 0.25, 0.25, 0.25])

Calculate Cumulative Portfolio Returns

Compute the cumulative returns of the portfolio using the portfolio_returns DataFrame.

Código
cumulative_returns = (1 + portfolio_returns).cumprod() - 1
cumulative_returns.head()
0
Date
2020-11-24 0.000000
2020-11-25 -0.001544
2020-11-26 -0.012105
2020-11-27 -0.014740
2020-11-30 -0.039000

Obtain Market Data and Risk-Free Rate

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.

Código
ibovespa_data = yf.download('^BVSP', period='5y')['Close']
ibovespa_daily_returns = ibovespa_data.pct_change()
ibovespa_daily_returns.head()
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.

Código
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

Calculate Portfolio Volatility

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.

Código
portfolio_volatility_daily = portfolio_returns.std()
portfolio_volatility_annualized = portfolio_volatility_daily * np.sqrt(trading_days_per_year)

print(f"Annualized Portfolio Volatility: {portfolio_volatility_annualized:.2%}")
Annualized Portfolio Volatility: 22.92%

Calculate Sharpe Ratio

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.

Código
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

Calculate CAPM Beta

Subtask:

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.

Código
# 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

Optimization

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.

Código
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.

Set up Optimization Constraints and Bounds

Subtask:

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.

Código
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

Run Optimization to Maximize Sharpe Ratio

Subtask:

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.

Código
# 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'.

Extract and Display Optimal Portfolio Metrics

Subtask:

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.

Código
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
Código
np.round(optimal_weights,4)
array([0.3632, 0.    , 0.6368, 0.    ])
De volta ao topo