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).
[*********************100%***********************] 81 of 81 completed
Open
...
Close
ABEV3.SA
AZUL4.SA
B3SA3.SA
BBAS3.SA
BBDC3.SA
BBDC4.SA
BBSE3.SA
BEEF3.SA
BPAC11.SA
BRAP4.SA
...
TAEE11.SA
TIMS3.SA
TOTS3.SA
UGPA3.SA
USIM5.SA
VALE3.SA
VIVT3.SA
VVAR3.SA
WEGE3.SA
YDUQ3.SA
Date
2015-01-02
16.139999
NaN
9.81
23.430000
15.969308
16.482443
31.850000
9.744451
NaN
14.06
...
18.850000
11.74
11.910702
25.330000
4.79
21.280001
37.820000
6.80
11.846153
22.049999
2015-01-05
15.910000
NaN
9.43
22.580000
15.794703
15.989155
30.340000
9.301968
NaN
13.36
...
18.780001
11.46
11.544731
24.655001
4.50
20.959999
37.070000
6.80
11.926923
20.730000
2015-01-06
15.730000
NaN
9.20
22.230000
16.021217
16.321175
29.450001
8.938149
NaN
13.51
...
18.809999
11.44
10.822770
24.690001
4.72
21.799999
36.150002
6.80
11.750000
19.520000
2015-01-07
16.340000
NaN
9.40
22.940001
16.460091
17.046877
30.830000
8.800488
NaN
14.11
...
18.840000
11.48
10.746248
25.360001
5.00
22.600000
37.389999
7.36
11.615384
18.910000
2015-01-08
16.430000
NaN
9.89
23.770000
16.993345
17.549650
30.590000
9.124975
NaN
14.67
...
19.020000
12.00
10.995774
25.100000
4.75
22.840000
38.910000
7.36
11.811538
19.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.SA
Open AZUL4.SA
Open B3SA3.SA
Open BBAS3.SA
Open BBDC3.SA
Open BBDC4.SA
Open BBSE3.SA
Open BEEF3.SA
Open BPAC11.SA
Open BRAP4.SA
...
Close TAEE11.SA
Close TIMS3.SA
Close TOTS3.SA
Close UGPA3.SA
Close USIM5.SA
Close VALE3.SA
Close VIVT3.SA
Close VVAR3.SA
Close WEGE3.SA
Close YDUQ3.SA
Date
2015-01-02
16.139999
NaN
9.81
23.430000
15.969308
16.482443
31.850000
9.744451
NaN
14.06
...
18.850000
11.74
11.910702
25.330000
4.79
21.280001
37.820000
6.80
11.846153
22.049999
2015-01-05
15.910000
NaN
9.43
22.580000
15.794703
15.989155
30.340000
9.301968
NaN
13.36
...
18.780001
11.46
11.544731
24.655001
4.50
20.959999
37.070000
6.80
11.926923
20.730000
2015-01-06
15.730000
NaN
9.20
22.230000
16.021217
16.321175
29.450001
8.938149
NaN
13.51
...
18.809999
11.44
10.822770
24.690001
4.72
21.799999
36.150002
6.80
11.750000
19.520000
2015-01-07
16.340000
NaN
9.40
22.940001
16.460091
17.046877
30.830000
8.800488
NaN
14.11
...
18.840000
11.48
10.746248
25.360001
5.00
22.600000
37.389999
7.36
11.615384
18.910000
2015-01-08
16.430000
NaN
9.89
23.770000
16.993345
17.549650
30.590000
9.124975
NaN
14.67
...
19.020000
12.00
10.995774
25.100000
4.75
22.840000
38.910000
7.36
11.811538
19.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
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
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.
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_operations
gains
pct_gains
losses
pct_losses
total_profit
pct_profit
profit_per_operation
drawdown
VIVT3.SA
184.0
139.0
76.0
45.0
24.0
14010.00
140.10
0.76
16.21
VVAR3.SA
154.0
101.0
66.0
53.0
34.0
8767.00
87.67
0.57
68.31
WEGE3.SA
164.0
121.0
74.0
43.0
26.0
11089.16
110.89
0.68
15.44
YDUQ3.SA
167.0
114.0
68.0
53.0
32.0
5165.01
51.65
0.31
55.73
IBOV Portfolio Strategy
72.0
52.0
72.0
20.0
28.0
5842.15
58.42
0.81
26.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.
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;
e invert_yaxis caso seja necessário inverter a ordem do eixo y. Sendo assim, ele será falso (False), por padrão.
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)
A tabela a seguir exibe os melhores ativos do IBOV em termos de Lucro / Operação e Drawdown:
Lucro / Operação (%)
Drawdown (%)
PCAR3
TIMS3
NTCO3
EGIE3
LCAM3
ENGI11
ENGI11
TAEE11
TOTS3
EQTL3
EQTL3
CRFB3
CPLE6
ENBR3
B3SA3
B3SA3
LREN3
RADL3
IBOV Portfolio Strategy
LREN3
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 GOLL4, HGTX3, SUZB3, USIM5 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 (EGIE3, TAEE11, ENGI11, EQTL3, ENBR3). 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!