QuantBrasil

Comparando os Resultados da Estratégia de IFR2 nos Ativos do Ibovespa

Andressa Quintanilha
Por Andressa Quintanilha
04 março, 2021
Compartilhar:

O post de hoje é a continuação do artigo anterior, onde testamos a estratégia de IFR2 nos papeis mais fortes do Ibovespa, em uma janela de 100 dias, nos últimos 6 anos.

Nessa abordagem, a cada operação nós selecionávamos um novo ativo. Ao final, percebemos que um dos pontos fracos dessa estratégia é que acabávamos deixando de utilizar ativos que não apresentavam altas expressivas, mas que performariam bem no IFR2 (ex: EQTL3).

Para fins comparativos, no backtest de hoje nós testaremos a estratégia para todos os ativos que compõem a carteira do Ibovespa, no mesmo intervalo de tempo. Assim, poderemos ranquear os ativos de acordo com os melhores resultados e visualizar onde a estratégia anterior se encaixa.

Baixando os ativos que compõem a carteira do Ibovespa

Após baixar as bibliotecas necessárias, nós utilizaremos a mesma função get_ibov_tickers(), elaborada no último post.

OBS: Vamos remover ASAI3.SA da lista, uma vez que o ativo compõe o Ibovespa porém foi criado apenas em Março/2021.

# %%capture means we suppress the output
%%capture

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
!pip install yfinance
import yfinance as yf
def get_ibov_tickers():
    url = "http://bvmf.bmfbovespa.com.br/indices/ResumoCarteiraTeorica.aspx?Indice=IBOV&idioma=pt-br"
    html = pd.read_html(url, decimal=",", thousands=".", index_col="Código")[0][:-1]
    tickers = (html.index + ".SA").to_list()
    return tickers

tickers = get_ibov_tickers()

# Remove ticker without data
tickers.remove("ASAI3.SA")

Faremos o download das colunas com o preço de abertura, fechamento e máxima, no mesmo intervalo de tempo (Janeiro/2015 - Dezembro/2020).

start = "2015-01-01"
end = "2020-12-30"
df = yf.download(tickers=tickers, start=start, end=end).copy()[["Open", "High", "Close"]]

df.head()

[*********************100%***********************] 81 of 81 completed

Open...Close
ABEV3.SAAZUL4.SAB3SA3.SABBAS3.SABBDC3.SABBDC4.SABBSE3.SABEEF3.SABPAC11.SABRAP4.SA...TAEE11.SATIMS3.SATOTS3.SAUGPA3.SAUSIM5.SAVALE3.SAVIVT3.SAVVAR3.SAWEGE3.SAYDUQ3.SA
Date
2015-01-0216.139999NaN9.8123.43000015.96930816.48244331.8500009.744451NaN14.06...18.85000011.7411.91070225.3300004.7921.28000137.8200006.8011.84615322.049999
2015-01-0515.910000NaN9.4322.58000015.79470315.98915530.3400009.301968NaN13.36...18.78000111.4611.54473124.6550014.5020.95999937.0700006.8011.92692320.730000
2015-01-0615.730000NaN9.2022.23000016.02121716.32117529.4500018.938149NaN13.51...18.80999911.4410.82277024.6900014.7221.79999936.1500026.8011.75000019.520000
2015-01-0716.340000NaN9.4022.94000116.46009117.04687730.8300008.800488NaN14.11...18.84000011.4810.74624825.3600015.0022.60000037.3899997.3611.61538418.910000
2015-01-0816.430000NaN9.8923.77000016.99334517.54965030.5900009.124975NaN14.67...19.02000012.0010.99577425.1000004.7522.84000038.9100007.3611.81153819.500000

5 rows × 243 columns


Também transformaremos a indexação em apenas um nível, para simplificar o acesso às colunas.

df.columns = [" ".join(col).strip() for col in df.columns.values]
df.head()
Open ABEV3.SAOpen AZUL4.SAOpen B3SA3.SAOpen BBAS3.SAOpen BBDC3.SAOpen BBDC4.SAOpen BBSE3.SAOpen BEEF3.SAOpen BPAC11.SAOpen BRAP4.SA...Close TAEE11.SAClose TIMS3.SAClose TOTS3.SAClose UGPA3.SAClose USIM5.SAClose VALE3.SAClose VIVT3.SAClose VVAR3.SAClose WEGE3.SAClose YDUQ3.SA
Date
2015-01-0216.139999NaN9.8123.43000015.96930816.48244331.8500009.744451NaN14.06...18.85000011.7411.91070225.3300004.7921.28000137.8200006.8011.84615322.049999
2015-01-0515.910000NaN9.4322.58000015.79470315.98915530.3400009.301968NaN13.36...18.78000111.4611.54473124.6550014.5020.95999937.0700006.8011.92692320.730000
2015-01-0615.730000NaN9.2022.23000016.02121716.32117529.4500018.938149NaN13.51...18.80999911.4410.82277024.6900014.7221.79999936.1500026.8011.75000019.520000
2015-01-0716.340000NaN9.4022.94000116.46009117.04687730.8300008.800488NaN14.11...18.84000011.4810.74624825.3600015.0022.60000037.3899997.3611.61538418.910000
2015-01-0816.430000NaN9.8923.77000016.99334517.54965030.5900009.124975NaN14.67...19.02000012.0010.99577425.1000004.7522.84000038.9100007.3611.81153819.500000

5 rows × 243 columns

Calculando o IFR2 e os pontos de entrada e saída para cada ativo

Como de praxe, utilizaremos a função rsi() para calcular os valores de IFR para 2 períodos e strategy_points() para calcular os preços de compra e venda da estratégia. Lembrando que utilizaremos a estratégia original, caracterizada por pontos de entrada em valores de IFR2 abaixo ou iguais a 30 e pontos de saída na máxima dos dois dias anteriores.

def rsi(data, column, window=2):   
    
    #data = data.copy()
    
    # Establish gains and losses for each day
    data["Variation"] = data[column].diff()
    data = data[1:]
    data["Gain"] = np.where(data["Variation"] > 0, data["Variation"], 0)
    data["Loss"] = np.where(data["Variation"] < 0, data["Variation"], 0)

    # Calculate simple averages so we can initialize the classic averages
    simple_avg_gain = data["Gain"].rolling(window).mean()
    simple_avg_loss = data["Loss"].abs().rolling(window).mean()
    classic_avg_gain = simple_avg_gain.copy()
    classic_avg_loss = simple_avg_loss.copy()

    for i in range(window, len(classic_avg_gain)):
        classic_avg_gain[i] = (classic_avg_gain[i - 1] * (window - 1) + data["Gain"].iloc[i]) / window
        classic_avg_loss[i] = (classic_avg_loss[i - 1] * (window - 1) + data["Loss"].abs().iloc[i]) / window
    
    # Calculate the RSI
    RS = classic_avg_gain / classic_avg_loss
    RSI = 100 - (100 / (1 + RS))
    
    return RSI
def strategy_points(data, rsi_parameter_entry=30):
    data["Target1"] = data["High"].shift(1)
    data["Target2"] = data["High"].shift(2)
    data["Target"] = data[["Target1", "Target2"]].max(axis=1)
  
    # We don't need them anymore
    data.drop(columns=["Target1", "Target2"], inplace=True)

    # Define exact buy price
    data["Buy Price"] = np.where(data["IFR2"] <= rsi_parameter_entry, data["Close"], np.nan)

    # Define exact sell price
    data["Sell Price"] = np.where(
        data["High"] > data['Target'],
        np.where(data['Open'] > data['Target'], data['Open'], data['Target']),
        np.nan)
    return data 

Assim como foi feito no artigo anterior, vamos isolar as colunas de cada ativo em dataframes, armazenando-os no dicionário dict_of_df, através do loop abaixo. Desse modo, cada ativo (representando as chaves) terá seu próprio dataframe (representando os valores correspondentes).

Antes de utilizarmos as funções acima, temos que excluir valores NaN a fim de evitar possíveis erros em nossos códigos mais para frente. Faremos isso através da função .dropna().

dict_of_df = {}

for ticker in tickers:
    # Isolate the columns (open, high, close) of each asset into a dataframe
    # and store it in a dictionary 'dict_of_df' 
    data = df[
        [("Open " + ticker), 
         ("High " + ticker), 
         ("Close " + ticker)]].copy()
    
    # Rename the columns
    data.rename(columns={
        ("Open " + ticker): 'Open', 
        ("High " + ticker): 'High', 
        ("Close " + ticker): 'Close'}, inplace=True)
    
    data.dropna(inplace=True)
    
    # Calculate IFR2 and add it to a new column
    data["IFR2"] = rsi(data=data, column="Close")
    
    # Calculate purchase and sale prices
    data = strategy_points(data)
    
    dict_of_df[ticker] = data

Realizando o backtest

Utilizaremos a estratégia com um stop de 7 dias, uma vez que esta foi a que apresentou melhores resultados no último backtest.

As funções a seguir foram desenvolvidas ao longo dessa série de artigos. Se você ainda não as conhece, não deixe de conferir os posts anteriores!

import math

# Create a function to round any number to the smalles multiple of 100
def round_down(x):
    return int(math.floor(x / 100.0)) * 100

def algorithm_with_stop(data, max_days, initial_capital=10000):
    
    # List with the total capital after every operation
    total_capital = [initial_capital]  
    
    # List with profits for every operation. We initialize with 0 so 
    # both lists have the same size
    all_profits = [0] 

    days_in_operation = 0

    ongoing = False 

    for i in range(0,len(data)):
        
        if ongoing == True:
            
            days_in_operation += 1
            
            # If any of the following conditions are met, the operation will end
            if days_in_operation == max_days or ~(np.isnan(data['Sell Price'][i])):
                
                # Define exit point and total profit
                exit = np.where(
                    ~(np.isnan(data['Sell Price'][i])), 
                    data['Sell Price'][i], 
                    data['Close'][i])
                
                profit = shares * (exit - entry)

                # Append profit to list and create a new entry with the capital
                # after the operation is complete
                all_profits += [profit]
                current_capital = total_capital[-1]
                total_capital += [current_capital + profit]

                ongoing = False
                
        else:
            if ~(np.isnan(data['Buy Price'][i])):
                entry = data['Buy Price'][i]
                shares = round_down(initial_capital / entry)
                days_in_operation = 0
                ongoing = True
    
    return all_profits, total_capital
def get_drawdown(data, column = "Close"):
    data["Max"] = data[column].cummax()
    data["Delta"] = data['Max'] - data[column]
    data["Drawdown"] = 100 * (data["Delta"] / data["Max"])
    max_drawdown = data["Drawdown"].max()
    
    return max_drawdown
def strategy_test(all_profits, total_capital):
    num_operations = (len(all_profits) - 1)
    gains = sum(x >= 0 for x in all_profits)
    pct_gains = 100 * (gains / num_operations)
    losses = num_operations - gains
    pct_losses = 100 - pct_gains
    total_profit = sum(all_profits)
    pct_profit = (total_profit / total_capital[0]) * 100

    # Compute drawdown
    total_capital = pd.DataFrame(data=total_capital, columns=["total_capital"])
    drawdown = get_drawdown(data=total_capital, column="total_capital")
    
    profit_per_operation = pct_profit / num_operations

    return {
      "num_operations": num_operations,
      "gains": gains ,
      "pct_gains": pct_gains.round(),
      "losses": losses,
      "pct_losses": pct_losses.round(), 
      "total_profit": total_profit,
      "pct_profit": pct_profit,
      "profit_per_operation": profit_per_operation, 
      "drawdown": drawdown
    }

Como o objetivo é realizar o backtest para cada ativo do Ibovespa, iteraremos sobre a lista de tickers, afim de utilizar as funções que rodam o algoritmo (algorithm_with_stop) e a estatítica (strategy_test) para cada um deles. Os resultados desta última serão armazenado no dicionário statistics.

statistics = {}

for ticker in tickers:
    data = dict_of_df[ticker]
    all_profits, total_capital = algorithm_with_stop(data=data, max_days=7)
    statistics[ticker] = strategy_test(all_profits, total_capital)

Vamos transformar statistics em um dataframe para uma melhor visualização dos dados.

statistics = pd.DataFrame.from_dict(statistics, orient="index").round(2)
statistics
num_operationsgainspct_gainslossespct_lossestotal_profitpct_profitprofit_per_operationdrawdown
ABEV3.SA16712474.04326.04899.0048.990.2926.27
AZUL4.SA1027069.03231.01869.0018.690.1852.24
B3SA3.SA16713178.03622.015232.00152.320.9111.07
BBAS3.SA17311667.05733.04111.0041.110.2428.28
BBDC3.SA16610966.05734.039.750.400.0039.73
..............................
VALE3.SA16210766.05534.07176.0071.760.4462.17
VIVT3.SA18413976.04524.014010.00140.100.7616.21
VVAR3.SA15410166.05334.08767.0087.670.5768.31
WEGE3.SA16412174.04326.011089.16110.890.6815.44
YDUQ3.SA16711468.05332.05165.0151.650.3155.73

81 rows × 9 columns


Agora já podemos incluir a estatística da estratégia do último artigo nessa mesma tabela. Iremos chamá-la de IBOV Portfolio Strategy.

# Values were generated by running a prior backtest
statistics.loc["IBOV Portfolio Strategy"] = [72, 52, 72.0, 20, 28.0, 5842.15, 58.42, 0.81, 26.2]
statistics.tail()
num_operationsgainspct_gainslossespct_lossestotal_profitpct_profitprofit_per_operationdrawdown
VIVT3.SA184.0139.076.045.024.014010.00140.100.7616.21
VVAR3.SA154.0101.066.053.034.08767.0087.670.5768.31
WEGE3.SA164.0121.074.043.026.011089.16110.890.6815.44
YDUQ3.SA167.0114.068.053.032.05165.0151.650.3155.73
IBOV Portfolio Strategy72.052.072.020.028.05842.1558.420.8126.20

Plotando as informações mais relevantes

Uma vez que já temos o backtest para todos os ativos, vamos plotar o lucro por operação e o drawdown. Para isso, vamos primeiro ordenar nosso dataframe de acordo com essas colunas e separá-los em tabelas diferentes.

Faremos isso através da função .sort_values() do pandas, que recebe como argumento a coluna com os valores no qual se deseja ordenar o dataframe inteiro. Ao final, teremos uma tabela ordenada pelo lucro por operação (sorted_by_profit_per_operation) e outra pelo drawdown (sorted_by_drawdown).

A função pd.set_option() nos permite alterar o máximo de linhas a serem exibidas.

sorted_by_profit_per_operation = statistics.sort_values("profit_per_operation", ascending=False)
pd.set_option("display.max_rows", 100)
sorted_by_profit_per_operation
num_operationsgainspct_gainslossespct_lossestotal_profitpct_profitprofit_per_operationdrawdown
PCAR3.SA104.089.086.015.014.015244.00152.441.4733.19
NTCO3.SA31.024.077.07.023.04434.4744.341.4325.43
LCAM3.SA158.0121.077.037.023.018439.99184.401.1720.66
ENGI11.SA154.0122.079.032.021.014843.48148.430.966.86
TOTS3.SA173.0131.076.042.024.016145.82161.460.9317.03
B3SA3.SA167.0131.078.036.022.015232.00152.320.9111.07
EQTL3.SA168.0126.075.042.025.015238.00152.380.919.34
CPLE6.SA177.0129.073.048.027.016035.00160.350.9126.36
LREN3.SA167.0123.074.044.026.013965.04139.650.8411.48
IBOV Portfolio Strategy72.052.072.020.028.05842.1558.420.8126.20
FLRY3.SA171.0129.075.042.025.013455.00134.550.7912.47
VIVT3.SA184.0139.076.045.024.014010.00140.100.7616.21
SULA11.SA172.0125.073.047.027.013128.92131.290.7614.78
CRFB3.SA100.077.077.023.023.07641.0076.410.7610.65
PRIO3.SA161.0119.074.042.026.011902.10119.020.7438.29
ENBR3.SA165.0122.074.043.026.012107.73121.080.7311.01
PETR3.SA160.0108.068.052.032.011203.00112.030.7022.67
RENT3.SA165.0125.076.040.024.011538.69115.390.7022.12
TIMS3.SA164.0117.071.047.029.011319.99113.200.6911.46
PETR4.SA157.0106.068.051.032.010836.01108.360.6925.84
WEGE3.SA164.0121.074.043.026.011089.16110.890.6815.44
CCRO3.SA180.0125.069.055.031.012192.00121.920.6812.23
MRVE3.SA165.0118.072.047.028.011264.92112.650.6814.12
JBSS3.SA171.0128.075.043.025.011541.99115.420.6731.33
MRFG3.SA183.0134.073.049.027.012266.00122.660.6724.31
CYRE3.SA175.0129.074.046.026.011699.99117.000.6736.97
SANB11.SA172.0132.077.040.023.011216.00112.160.6512.35
ENEV3.SA154.0111.072.043.028.09901.3299.010.6436.18
MULT3.SA173.0129.075.044.025.010856.67108.570.6315.66
RADL3.SA160.0118.074.042.026.09945.4099.450.6211.44
TAEE11.SA173.0132.076.041.024.010732.00107.320.627.09
GNDI3.SA72.060.083.012.017.04376.0943.760.6121.21
BRAP4.SA160.0115.072.045.028.09444.0094.440.5970.12
VVAR3.SA154.0101.066.053.034.08767.0087.670.5768.31
BPAC11.SA100.070.070.030.030.05397.0053.970.5427.71
ECOR3.SA172.0116.067.056.033.08557.0085.570.5034.92
EGIE3.SA174.0128.074.046.026.08487.8084.880.496.82
VALE3.SA162.0107.066.055.034.07176.0071.760.4462.17
HAPV3.SA67.045.067.022.033.02928.6029.290.4418.57
EZTC3.SA162.0115.071.047.029.07136.7471.370.4428.52
BBSE3.SA175.0117.067.058.033.07238.0072.380.4117.42
CPFE3.SA156.0117.075.039.025.06024.0260.240.3926.44
LAME4.SA164.0113.069.051.031.06449.9264.500.3928.49
BRML3.SA175.0124.071.051.029.06571.8165.720.3825.24
BTOW3.SA165.0103.062.062.038.05706.0957.060.3550.66
ITUB4.SA172.0120.070.052.030.05863.3158.630.3416.88
YDUQ3.SA167.0114.068.053.032.05165.0151.650.3155.73
CMIG4.SA177.0118.067.059.033.05508.2055.080.3147.85
ELET3.SA171.0113.066.058.034.05304.2253.040.3134.27
BBDC4.SA167.0112.067.055.033.04875.7648.760.2919.99
SBSP3.SA157.0109.069.048.031.04586.0045.860.2921.94
ABEV3.SA167.0124.074.043.026.04899.0048.990.2926.27
KLBN11.SA169.0119.070.050.030.04853.0048.530.2920.51
CSAN3.SA162.0113.070.049.030.04492.0044.920.2824.38
MGLU3.SA146.097.066.049.034.04077.4040.770.28110.56
UGPA3.SA171.0119.070.052.030.04600.4946.000.2731.53
IGTA3.SA161.0111.069.050.031.04420.0044.200.2724.66
BBAS3.SA173.0116.067.057.033.04111.0041.110.2428.28
JHSF3.SA168.0110.065.058.035.04011.0040.110.2434.64
ITSA4.SA176.0122.069.054.031.04233.6342.340.2424.24
HYPE3.SA164.0115.070.049.030.03675.0036.750.2228.21
GGBR4.SA166.0109.066.057.034.03557.0035.570.2147.41
AZUL4.SA102.070.069.032.031.01869.0018.690.1852.24
BRKM5.SA169.0116.069.053.031.02787.0027.870.1640.24
EMBR3.SA171.0115.067.056.033.02070.0120.700.1231.46
QUAL3.SA163.0111.068.052.032.01773.0017.730.1147.47
RAIL3.SA158.0108.068.050.032.0994.659.950.06103.31
ELET6.SA161.098.061.063.039.0528.005.280.0335.75
BBDC3.SA166.0109.066.057.034.039.750.400.0039.73
GOAU4.SA167.0117.070.050.030.054.990.550.00141.33
BEEF3.SA168.0111.066.057.034.0-261.94-2.62-0.0250.04
CVCB3.SA169.0116.069.053.031.0-278.32-2.78-0.0257.04
CSNA3.SA161.0100.062.061.038.0-703.00-7.03-0.0480.61
USIM5.SA159.0106.067.053.033.0-794.00-7.94-0.0599.44
CIEL3.SA181.0117.065.064.035.0-1530.71-15.31-0.0875.26
SUZB3.SA82.057.070.025.030.0-1084.00-10.84-0.1343.75
BRDT3.SA75.049.065.026.035.0-1424.00-14.24-0.1938.78
COGN3.SA170.0104.061.066.039.0-3530.00-35.30-0.2161.17
HGTX3.SA93.058.062.035.038.0-2112.99-21.13-0.2383.84
BRFS3.SA169.0106.063.063.037.0-4649.99-46.50-0.2874.57
GOLL4.SA170.0112.066.058.034.0-5540.00-55.40-0.33135.63
IRBR3.SA93.067.072.026.028.0-4434.52-44.35-0.4879.71
sorted_by_drawdown = statistics.sort_values("drawdown")
pd.set_option("display.max_rows", 100)
sorted_by_drawdown
num_operationsgainspct_gainslossespct_lossestotal_profitpct_profitprofit_per_operationdrawdown
EGIE3.SA174.0128.074.046.026.08487.8084.880.496.82
ENGI11.SA154.0122.079.032.021.014843.48148.430.966.86
TAEE11.SA173.0132.076.041.024.010732.00107.320.627.09
EQTL3.SA168.0126.075.042.025.015238.00152.380.919.34
CRFB3.SA100.077.077.023.023.07641.0076.410.7610.65
ENBR3.SA165.0122.074.043.026.012107.73121.080.7311.01
B3SA3.SA167.0131.078.036.022.015232.00152.320.9111.07
RADL3.SA160.0118.074.042.026.09945.4099.450.6211.44
TIMS3.SA164.0117.071.047.029.011319.99113.200.6911.46
LREN3.SA167.0123.074.044.026.013965.04139.650.8411.48
CCRO3.SA180.0125.069.055.031.012192.00121.920.6812.23
SANB11.SA172.0132.077.040.023.011216.00112.160.6512.35
FLRY3.SA171.0129.075.042.025.013455.00134.550.7912.47
MRVE3.SA165.0118.072.047.028.011264.92112.650.6814.12
SULA11.SA172.0125.073.047.027.013128.92131.290.7614.78
WEGE3.SA164.0121.074.043.026.011089.16110.890.6815.44
MULT3.SA173.0129.075.044.025.010856.67108.570.6315.66
VIVT3.SA184.0139.076.045.024.014010.00140.100.7616.21
ITUB4.SA172.0120.070.052.030.05863.3158.630.3416.88
TOTS3.SA173.0131.076.042.024.016145.82161.460.9317.03
BBSE3.SA175.0117.067.058.033.07238.0072.380.4117.42
HAPV3.SA67.045.067.022.033.02928.6029.290.4418.57
BBDC4.SA167.0112.067.055.033.04875.7648.760.2919.99
KLBN11.SA169.0119.070.050.030.04853.0048.530.2920.51
LCAM3.SA158.0121.077.037.023.018439.99184.401.1720.66
GNDI3.SA72.060.083.012.017.04376.0943.760.6121.21
SBSP3.SA157.0109.069.048.031.04586.0045.860.2921.94
RENT3.SA165.0125.076.040.024.011538.69115.390.7022.12
PETR3.SA160.0108.068.052.032.011203.00112.030.7022.67
ITSA4.SA176.0122.069.054.031.04233.6342.340.2424.24
MRFG3.SA183.0134.073.049.027.012266.00122.660.6724.31
CSAN3.SA162.0113.070.049.030.04492.0044.920.2824.38
IGTA3.SA161.0111.069.050.031.04420.0044.200.2724.66
BRML3.SA175.0124.071.051.029.06571.8165.720.3825.24
NTCO3.SA31.024.077.07.023.04434.4744.341.4325.43
PETR4.SA157.0106.068.051.032.010836.01108.360.6925.84
IBOV Portfolio Strategy72.052.072.020.028.05842.1558.420.8126.20
ABEV3.SA167.0124.074.043.026.04899.0048.990.2926.27
CPLE6.SA177.0129.073.048.027.016035.00160.350.9126.36
CPFE3.SA156.0117.075.039.025.06024.0260.240.3926.44
BPAC11.SA100.070.070.030.030.05397.0053.970.5427.71
HYPE3.SA164.0115.070.049.030.03675.0036.750.2228.21
BBAS3.SA173.0116.067.057.033.04111.0041.110.2428.28
LAME4.SA164.0113.069.051.031.06449.9264.500.3928.49
EZTC3.SA162.0115.071.047.029.07136.7471.370.4428.52
JBSS3.SA171.0128.075.043.025.011541.99115.420.6731.33
EMBR3.SA171.0115.067.056.033.02070.0120.700.1231.46
UGPA3.SA171.0119.070.052.030.04600.4946.000.2731.53
PCAR3.SA104.089.086.015.014.015244.00152.441.4733.19
ELET3.SA171.0113.066.058.034.05304.2253.040.3134.27
JHSF3.SA168.0110.065.058.035.04011.0040.110.2434.64
ECOR3.SA172.0116.067.056.033.08557.0085.570.5034.92
ELET6.SA161.098.061.063.039.0528.005.280.0335.75
ENEV3.SA154.0111.072.043.028.09901.3299.010.6436.18
CYRE3.SA175.0129.074.046.026.011699.99117.000.6736.97
PRIO3.SA161.0119.074.042.026.011902.10119.020.7438.29
BRDT3.SA75.049.065.026.035.0-1424.00-14.24-0.1938.78
BBDC3.SA166.0109.066.057.034.039.750.400.0039.73
BRKM5.SA169.0116.069.053.031.02787.0027.870.1640.24
SUZB3.SA82.057.070.025.030.0-1084.00-10.84-0.1343.75
GGBR4.SA166.0109.066.057.034.03557.0035.570.2147.41
QUAL3.SA163.0111.068.052.032.01773.0017.730.1147.47
CMIG4.SA177.0118.067.059.033.05508.2055.080.3147.85
BEEF3.SA168.0111.066.057.034.0-261.94-2.62-0.0250.04
BTOW3.SA165.0103.062.062.038.05706.0957.060.3550.66
AZUL4.SA102.070.069.032.031.01869.0018.690.1852.24
YDUQ3.SA167.0114.068.053.032.05165.0151.650.3155.73
CVCB3.SA169.0116.069.053.031.0-278.32-2.78-0.0257.04
COGN3.SA170.0104.061.066.039.0-3530.00-35.30-0.2161.17
VALE3.SA162.0107.066.055.034.07176.0071.760.4462.17
VVAR3.SA154.0101.066.053.034.08767.0087.670.5768.31
BRAP4.SA160.0115.072.045.028.09444.0094.440.5970.12
BRFS3.SA169.0106.063.063.037.0-4649.99-46.50-0.2874.57
CIEL3.SA181.0117.065.064.035.0-1530.71-15.31-0.0875.26
IRBR3.SA93.067.072.026.028.0-4434.52-44.35-0.4879.71
CSNA3.SA161.0100.062.061.038.0-703.00-7.03-0.0480.61
HGTX3.SA93.058.062.035.038.0-2112.99-21.13-0.2383.84
USIM5.SA159.0106.067.053.033.0-794.00-7.94-0.0599.44
RAIL3.SA158.0108.068.050.032.0994.659.950.06103.31
MGLU3.SA146.097.066.049.034.04077.4040.770.28110.56
GOLL4.SA170.0112.066.058.034.0-5540.00-55.40-0.33135.63
GOAU4.SA167.0117.070.050.030.054.990.550.00141.33


OBS: Repare que nós temos drawdowns acima de 100%. Isso ocorre pois a estratégia sempre investe uma quantia fixa de capital (initial_capital). Sendo assim, ativos que tiverem prejuízos acima do capital initial vão apresentar drawdown superior a 100%.

Por fim, criaremos uma função para plotar os gráficos em barras horizontais, plot_barh, que receberá os seguintes argumentos:

  • os valores do eixo x (x);
  • os valores do eixo y (y);
  • a legenda do eixo x (x_label);
  • o título (title);
  • o tamanho do gráfico (figsize), que terá como padrão (12, 18);
  • se existe um índice que deseja-se destacar (highlights_index) que, por padrão, será None;
  • invert_yaxis caso seja necessário inverter a ordem do eixo y. Sendo assim, ele será falso (False), por padrão.
def plot_barh(x, y, x_label, title, figsize=(12, 18), highlights_index=None, invert_yaxis=False):
    fig = plt.figure(figsize=figsize)

    positives = x > 0
    graph = plt.barh(y, x, color=positives.map({True: "#49ce8b", False: "r"}))
    
    if highlights_index != None:
        graph[y.get_loc(highlights_index)].set_color("#033660")

    plt.margins(y=0.01)
    plt.xlabel(x_label, fontsize='x-large')
    plt.title(title, fontsize='xx-large')
    
    if invert_yaxis == True:
        plt.gca().invert_yaxis()
    
    return graph 

Vamos plotar primeiro todos os ativos de acordo com o lucro por operação para termos uma visualização geral do ranking de backtest. Além disso, vamos destacar a estratégia do backtest passado.

plot_barh(
    x=sorted_by_profit_per_operation["profit_per_operation"],
    y=sorted_by_profit_per_operation.index,
    x_label="Lucro por Operação (%)",
    title="Backtest da estratégia de IFR2 aplicada nos ativos do Ibovespa, nos últimos 6 anos", 
    highlights_index="IBOV Portfolio Strategy"
)
<BarContainer object of 82 artists>

Agora vamos isolar os 10 melhores e os 10 piores resultados em relação ao lucro por operação.

top_10_profit_per_operation = sorted_by_profit_per_operation[:10]["profit_per_operation"]

plot_barh(
    x=top_10_profit_per_operation,
    y=top_10_profit_per_operation.index,
    x_label="Lucro por Operação (%)",
    title="Top 10 ativos do Ibovespa na estratégia de IFR2, nos últimos 6 anos",
    figsize=(8,6), 
    highlights_index="IBOV Portfolio Strategy",
    invert_yaxis=True
)
<BarContainer object of 10 artists>
bottom_10_profit_per_operation = sorted_by_profit_per_operation[-10:]["profit_per_operation"]

plot_barh(
    x=bottom_10_profit_per_operation,
    y=bottom_10_profit_per_operation.index,
    x_label="Lucro por Operação (%)",
    title="Os 10 piores ativos do Ibovespa na estratégia de IFR2, nos últimos 6 anos",
    figsize=(8,6)
)
<BarContainer object of 10 artists>

Vamos plotar também o top 10 no que diz respeito ao drawdown:

top_10_drawdown = sorted_by_drawdown[:10]["drawdown"]

plot_barh(
    x=top_10_drawdown,
    y=top_10_drawdown.index,
    x_label="Drawdown(%)",
    title="Top 10 ativos (do Ibovespa) com o melhor drawdown na estratégia de IFR2, nos últimos 6 anos",
    figsize=(10,6),
    invert_yaxis=True
)
<BarContainer object of 10 artists>

Por fim, vamos isolar os ativos em comum entre top_10_profit_per_operation e top_10_drawdown:

common = top_10_profit_per_operation[
    top_10_profit_per_operation.index.isin(
        top_10_drawdown.index
    )]

print(common)

ENGI11.SA 0.96
B3SA3.SA 0.91
EQTL3.SA 0.91
LREN3.SA 0.84
Name: profit_per_operation, dtype: float64

Conclusão

A tabela a seguir exibe os melhores ativos do IBOV em termos de Lucro / Operação e Drawdown:

Lucro / Operação (%)Drawdown (%)
PCAR3TIMS3
NTCO3EGIE3
LCAM3ENGI11
ENGI11TAEE11
TOTS3EQTL3
EQTL3CRFB3
CPLE6ENBR3
B3SA3B3SA3
LREN3RADL3
IBOV Portfolio StrategyLREN3

Observe que a estratégia utilizando a lista dinâmica está presente no top 10. Sendo assim, como discutimos no último artigo, esta se torna uma excelente alternativa para quem busca se proteger caso a estratégia de IFR2 pare de funcionar em um determinado ativo.

Como esperado, papeis que não figuraram na lista dos mais fortes do Ibovespa, como EQTL3 e LREN3, apresentaram um dos melhores resultados não só no lucro por operação, como também no drawdown. Já papeis que fizeram parte dessa lista, como GOLL4HGTX3SUZB3USIM5 e CSNA3, demonstraram baixos lucros por operação.

Uma explicação para esse resultado está atrelada à volatilidade desses papeis. Para se sair bem na estratégia de IFR2, um ativo não precisa fazer fortes movimentações de alta, desde que oscile de forma previsível num range de preços.

Isso se confirma ao observarmos que metade dos ativos que tiveram os melhores resultados, principalmente em relação ao drawdown, correspondem a empresas do setor elétrico (EGIE3TAEE11ENGI11EQTL3ENBR3). Essas são empresas de rendimentos mais previsíveis e que distribuem dividendos, o que as tornam menos voláteis e, portanto, favoráveis à estratégia de IFR2.

Concluindo, no post de hoje nós criamos um ranking dos melhores resultados de backtests da estratégia de IFR2 nos últimos 6 anos. Você pode utilizá-lo juntamente com nossa nova ferramenta e otimizar seus resultados para essa estratégia!