Betting against beta

Betting agains beta (BAB), ou apostando contra o beta é uma conhecida estratégia de investimento que surgiu com o trabalho de Frazzini&Pedersen(2014). De fato, a estratégia é tratada como um fator explicativo de retornos e deriva de uma conhecida “anomalia” de mercado, a low risk anomaly da qual a estratégia é um caso particular.

Em nosso artigo anterior, utilizamos esta estratégia para apresentar a plataforma Quantopian. Agora vamos aprofundar na análise e implementação desta estratégia, pormenorizando seus fundamentos, (tentativas de) explicações sobre o porquê esta estratégia funciona e como implementá-la no Quantopian.

Anomalia do baixo risco

Esta anomalia, em relação a hipótese de mercados eficientes e das teorias de risco-retorno em geral, relaciona-se ao fato que, empiricamente é observado que retornos são negativamente relacionados ao risco, seja este risco medido por volatilidade ou, em nosso caso de interesse, o beta. É chamada anomalia pelo simples fato que a teoria financeira, e basicamente todo o senso comum sobre o assunto, nos informa que quanto maior o risco tomado, maiores devem ser os retornos esperados, ou seja, uma relação positiva entre risco e retorno. O fato de empiricamente, dentro de uma mesma classe de ativos (e.g. ações) observarmos uma relação inversa, vai de encontro a teoria e portanto seria uma “anomalia”.

Esta anomalia em geral é tratada através de duas vertentes, da baixa volatilidade ou low volatility anomaly que busca explicar os resultados em que ações com baixa volatilidade apresentam retornos consideravelmente maiores que ações de alta volatilidade, e a anomalia do beta. A teoria de Sharpe-Lintner, o CAPM, prescreve que ações com maiores betas, assim entendido a sensibilidade do retorno de uma ação específica com relação ao retorno da carteira de mercado, devem apresentar maiores retornos que ações com menores betas. Este maior retorno seria explicado pela maior exposição ao risco de mercado, de forma que o beta seria esta medida de risco.

Porém, já na década de 70 esta relação positiva entre beta e retorno começou a ser atacada através de testes econométricos. Black, Jensen e Scholes (1972) encontraram de fato uma relação positiva, entretanto muito horizontal, com pouco retorno excedente para ativos de alto beta, o que não condizia com o CAPM. Fama&French (1992) também atacaram o beta em seu artigo seminal. Considerando os fatores valor e tamanho, o mercado tornava-se um fator de pouco poder de explicação.

Em seu livro “Asset Management: a systematic approach to factor investing”, Andrew Ang (2014) fornece um bom resumo da anomalia do beta. Um dos problemas centrais, e que pode causar as discrepâncias com relação ao CAPM observadas, é como estimar o beta de uma ação e relacioná-lo aos retornos futuros. A grande maioria dos artigos utiliza uma regressão linear simples entre retornos passados tanto do ativo quanto do mercado para a estimação do beta. Feita a estimação, o que se obtém é uma medida ex-ante que será então comparada ao retornos ex-post (i.e. futuros). Por detrás desta metodologia está a assunção que os betas estimados permanecerão os mesmos durante o período futuro, o que na prática não é verdade.

Valores de beta das ações pode variar bastante a medida que novas informações fluem dia após dia, em outras palavras, o beta não é constante no tempo. A categorização de ações de alto/baixo beta em um período pode não ser mantida em período posterior, o que poderia ao menos em parte explicar a falha dos testes em verificar a relação positiva entre retornos e beta.

De qualquer forma, a previsão que o CAPM faz é que os retornos e betas contemporâneos estão positivamente relacionados. Ou seja, o que realmente interessa para a teoria é a relação entre beta/retorno realizados na mesma janela temporal. Quando feitos testes neste sentido os retornos se apresentam crescentes quanto maior o beta realizado. Porém existe um problema de ordem prática com a teoria, não é possível fazer uma estimação de variáveis contemporâneas! Não podemos estimar o beta sem que ainda tenhamos observado o retorno da carteira de mercado. E este problema nos remete a situação anterior, onde os retornos ex-post estão negativamente relacionados aos betas ex-ante.

BAB e sua fundamentação

O fator BAB foi originalmente proposto por Frazzini&Pederesen (2014) que constroem o fator fazendo-o comprar ações de baixo beta e vender aquelas de alto risco sistemático. Porém construir um fator para negociar a anomalia do beta não pode ser feito apenas tomando as diferenças dos portfólios na figura 1 abaixo. As diferenças nos retornos médios entre os quintis são pequenos, a verdadeira diferença reside nas razões de Sharpe entre as carteiras. Os autores portanto, formam seu fator BAB escalando os portfólios de beta baixo e alto pelos betas ex-ante dos próprios portfólios.

As ações são classificadas em ordem crescente com base em seu beta estimado e são atribuídas a um dos dois portfólios: beta baixo e beta alto. Em cada carteira, os títulos são ponderados pelos betas (ou seja, as ações de beta inferior têm pesos maiores no portfólio de beta baixo e aquelas de beta mais altos têm pesos maiores no portfólio de beta alto). As carteiras são rebalanceadas a cada mês.

Porém, apenas ponderando o peso das ações pelos seus betas no momento de formação do portfólio, este não seria neutro ao mercado e, portanto, não poderia ser considerado um fator de risco que explica retornos em excesso ao retorno do mercado. Assim, é necessário reescalar as posições compradas e vendidas pelos seus respectivos betas e o resultado será um portólio market neutral (i.e. ex-ante ao menos) porém alavancado. As ações de baixo beta necessitarão de alavancagem para chegar a um beta igual a 1, enquanto as ações vendidas, de alto beta, serão apenas uma fração menor da carteira e não financiarão completamente a parte comprada. A diferença, é tomada emprestada a taxa de juros livre de risco.

Ao final, o retorno do fator BAB pode ser escrito como:

$$BAB_{t+1} = \frac{r_{L,t+1} − r_f}{\beta_{L,t}} – \frac{r_{H,t+1} − r_f}{\beta_{H,t}}$$

onde \(r_{L,t+1}\) é o retorno da carteira de beta baixo e \(r_{H,t+1}\) é o retorno da carteira de beta alto. Os betas dos sub-portfólios baixo (comprado) e alto (vendido) no início do período são dados por \(\beta_{L,t}\) e \(\beta_{H,t}\), respectivamente.

Por ser um fator construído de forma a ser neutro ao mercado, qualquer retorno positivo que esta carteira apresente será um retorno não explicado pelo mercado, um retorno extraordinário.

Quantopian research

Terminamos com a teoria e passamos a prática. Conforme prometido no artigo anterior, vamos demonstrar os códigos Python necessários para implementar a estratégia BAB no Quantopian.

Começamos no ambiente de pesquisa. Vamos importar as bibliotecas necessárias para realizar o pipeline de aquisição de informações e cálculo de fatores (i.e. beta), do universo de ativos de nosso interesse e do Alphalens para análise da estratégia.

Utilizaremos a função SimpleBeta para calcular o beta das ações e então ranquea-los. Esta é uma função de cálculo de fator fornecida pela API do Quantopian e é desenvolvida tendo em mente desempenho, ela é diversas vezes mais rápida que, por exemplo, extrair o beta via regressão linear de alguma biblioteca como a StatsModels.

# Importa Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.research import run_pipeline
from quantopian.pipeline import Pipeline
from quantopian.pipeline.factors import SimpleBeta
from quantopian.pipeline.filters import QTradableStocksUS
# Importa Alphalens
import alphalens as al
# Gerais
import datetime as dt

Para análise no Alphalens, deve-se estabelecer as datas inicial e final.

# Start and end dates for analysis
sdate = '2008-01-01'
edate = '2008-12-31'

Criando a pipeline

A função make_pipeline é ideal para coletar os dados dos ativos e fazer o screen destes. Ou seja, ela carrega dados de preços, volumes, fundamentos, etc. o que for necessário para o analista criar métricas, chamadas de “fatores” no Quantopian, que então possibilitam a filtragem destes ativos, restando apenas aqueles interessantes para compor a carteira.

Cálculos mais detalhados, como por exemplo os pesos que os ativos escolhidos irão ocupar na carteira não são adequados de serem feitos nesta função. Portanto, usualmente o que make_pipeline deve retornar são os valores dos filtros aplicados, se forem utilizados mais tarde no algoritmo, e valores lógicos indicando se o ativo faz parte da carteira como compra (long) ou venda (short). Ativos que não farão parte do portfólio neste momento podem ser excluídos da lista.

# Pipeline definition
def make_pipeline():
    """Pipeline for Betting Against Beta strategy.
    Author: Clube de Finanças Esag
    Collect assets and benchmark returns.
    Computes assets betas. Remember, beta is not affected by risk-free rate
    Rank the assets according to beta, sell top half and buy lowest half
    Rebalance every month"""
    universe = QTradableStocksUS() 
    beta = SimpleBeta(target=symbols('SPY'), regression_length = 252)
    z = - beta.zscore(mask = universe) # Lower betas, higher zscore

    # Filer for zscore >= 0
    zgeq0 = (z >= 0)

    return Pipeline(
        columns = {
            'beta': beta,
            'zscore': z,
            'zgeq0': zgeq0,
        },
        screen = (universe & z.notnan() & z.notnull())
    )

Agora basta rodar esta função que criamos, make_pipeline, através da API run_pipeline e atribuir o resultado a uma variável, que chamamos de output. Utilizamos o método head para verificar o formato retornado, que é um pandas DataFrame.

output = run_pipeline(make_pipeline(), sdate, edate)
output.head()
beta zgeq0 zscore
2008-01-02 00:00:00+00:00 Equity(2 [ARNC]) 1.344555 False -0.739436
Equity(24 [AAPL]) 1.183625 False -0.273716
Equity(31 [ABAX]) 1.207313 False -0.342267
Equity(41 [ARCB]) 1.346829 False -0.746016
Equity(52 [ABM]) 1.081440 True 0.022002

Agora que temos o resultado do pipeline, podemos fazer os cálculos necessários para atribuir os ativos às suas carteiras e seus pesos nesta. Como as carteiras serão apenas duas, metade long e outra metade dos ativos short, a coluna zgeq0 serve como indicador dos ativos comprados (true) e ativos vendidos. Já o cálculo dos pesos é realizado em duas etapas.

A primeira é atribuir os pesos proporcionais ao beta do ativo, dentro da sua carteira. Ao final da primeira etapa, teremos a soma dos pesos dos ativos comprados igual a 1 e a soma dos ativos vendidos igual a -1. Na segunda etapa deve-se reescalar estes pesos pelos betas das carteiras comprada e vendida, chegando aos pesos finais de cada ativo no portfólio completo.

# First round
# Compute weights for assets in output
k = 2.0 / sum(abs(output['zscore']))
output.loc[:, 'weight'] = k * output['zscore']
output.loc[:, 'beta_w'] = output['beta'] * output['weight']
print('Soma dos pesos comprados: ' + str(sum(output[output['zgeq0']]['weight'])))
print('Soma dos pesos vendidos: ' + str(sum(output[~output['zgeq0']]['weight'])))
Soma dos pesos comprados: 1.0
Soma dos pesos vendidos: -1.0
# Second round
betaL = sum(output[output['zgeq0']]['beta_w'])
betaH = sum(-output[~output['zgeq0']]['beta_w'])
output.loc[output['zgeq0'], 'weight'] = output.loc[output['zgeq0'], 'weight'] / betaL
output.loc[~output['zgeq0'], 'weight'] = output.loc[~output['zgeq0'], 'weight'] / betaH
print('BetaL: ' + str(betaL))
print('BetaH: ' + str(betaH))
print('Soma dos pesos comprados: ' + str(sum(output[output['zgeq0']]['weight'])))
print('Soma dos pesos vendidos: ' + str(sum(output[~output['zgeq0']]['weight'])))
BetaL: 0.667586254686
BetaH: 1.61494272142
Soma dos pesos comprados: 1.49793377707
Soma dos pesos vendidos: -0.619217007969

E para verificar como ficou nosso DataFrame, novamente o método head:

output.head()
beta zgeq0 zscore weight beta_w
2008-01-02 00:00:00+00:00 Equity(2 [ARNC]) 1.344555 False -0.739436 -2.311748e-06 -5.019681e-06
Equity(24 [AAPL]) 1.183625 False -0.273716 -8.557359e-07 -1.635728e-06
Equity(31 [ABAX]) 1.207313 False -0.342267 -1.070051e-06 -2.086322e-06
Equity(41 [ARCB]) 1.346829 False -0.746016 -2.332319e-06 -5.072912e-06
Equity(52 [ABM]) 1.081440 True 0.022002 1.663968e-07 1.201309e-07

Análise no Alphalens

Alphalens é uma biblioteca desenvolvida pelo próprio Quantopian que auxilia na análise preliminar de fatores explicativos de retorno, que os autores chamam de alfa. A função do Alphalens, portanto, é desenvedar quais fatores realmente possuem poder preditivo sobre os retornos dos ativos e desta forma orientar na formação de portfólios.

Esta biblioteca é capaz de gerar uma grande gama de estatísticas e gráficos dos fatores que incluem relatórios de retornos, turnover, grupamentos, coeficientes de informação, etc.

Uma vez que tem-se o DataFrame resultado da pipeline, devemos coletar os preços dos ativos utilizados pela estratégia no período de análise. O primeiro passo para tanto é saber quais ativos foram estes e guardá-los em uma lista. Em seguida utiliza-se a função da API get_pricing para baixar os preços destes ativos no período de interesse.

# Testing with Alphalens
# Get list of unique assets from the pipeline output
asset_list = output.index.levels[1].unique()

# Query pricing data for all assets present during
# evaluation period
asset_prices = get_pricing(
    symbols = asset_list,
    start_date = sdate,
    # end_date must be further down than edate from pipeline
    end_date = dt.datetime.strptime(edate, '%Y-%m-%d') + dt.timedelta(180),
    fields = 'open_price',
)

Uma vez com os preços capturados, o Alphalens fornece uma função para analisarmos o poder preditivo do nosso fator (i.e. os pesos calculados com base na estratégia BAB) em explicar retornos futuros. Basicamente esta função alinha o valor do fator em uma data t com os retornos obtidos em datas futuras de nossa escolha, por exemplo t+30, t+60, t+90. No caso de nossa estratégia, estaremos utilizando o fator weight para separar os ativos em duas metades, alto e baixo beta. Assim o argumento quantiles é utilizado para criar um determinado número de grupos de ativos dados pelo valor do fator. Se nosso objetivo fosse, por exemplo, comprar os 20% dos ativos de menores beta (maiores pesos) e vender os 20% com maiores betas, o argumento quantiles seria igual a 5 (cinco partes iguais cada uma com 20% dos ativos) e o Alphalens irá simular a compra do quantil mais alto e a venda do mais baixo.

# Get asset forward returns and quantile classification
# based on beta
factor_data = al.utils.get_clean_factor_and_forward_returns(
    factor = output['weight'],
    prices = asset_prices,
    quantiles = 2,
    periods = (30, 60, 90),
)
factor_data.head()
Dropped 0.6% entries from factor data: 0.6% in forward returns computation and 0.0% in binning phase (set max_loss=0 to see potentially suppressed Exceptions).
max_loss is 35.0%, not exceeded: OK!
30D 60D 90D factor factor_quantile
date asset
2008-01-02 00:00:00+00:00 Equity(2 [ARNC]) -0.007205 -0.003878 0.086637 -2.311748e-06 1
Equity(24 [AAPL]) -0.350630 -0.281026 -0.070558 -8.557359e-07 1
Equity(31 [ABAX]) -0.176192 -0.357959 -0.284639 -1.070051e-06 1
Equity(41 [ARCB]) 0.389765 0.440274 0.793690 -2.332319e-06 1
Equity(52 [ABM]) 0.006953 0.111918 0.023621 1.663968e-07 1

Visualizando resultados preliminares

Agora que possuímos os dados preparados pelo Alphalens, é possível visualizar alguns resultados. Apresentamos abaixo apenas um gráfico de retornos futuros separados por quantis, entretanto a biblioteca possui várias outras funções sendo uma delas bastante completa, que traz diversas tabelas e gráficos com inúmeras análises, al.tears.create_full_tear_sheet(factor_data) que não será mostrada aqui devido ao grande tamanho de sua resposta. Sugerimos também a leitura do tutorial do Alphalens.

# Calculate mean return by factor quantile
mean_return_by_q, std_err_by_q = al.performance.mean_return_by_quantile(factor_data)

# Plot mean returns by quantile and holding period
# over evaluation time range
al.plotting.plot_quantile_returns_bar(
    mean_return_by_q.apply(
        al.utils.rate_of_return,
        axis=0,
        args=('1D',)
    )
);

Os resultados obtidos, mesmo que preliminarmente, parecem promissores. O quantil 2, nossos ativos comprados, obtiveram retornos consideravelmente maiores que o quantil vendido, ou seja, nossa estratégia long-short em tese extrairá este spread de retornos e está pronta para ser levada ao ambiente de backtest, a IDE.

Portando para a IDE

A função make_pipeline foi criada de forma a ser copiada do ambiente de pesquisa e colada diretamente na IDE para implementação do algoritmo de backtest, sem a necessidade de maiores adaptações. Este é todo o propósito da bilioteca Pipeline. Entretanto, os cálculos que realizamos fora desta função, como por exemplo a definição dos pesos, devem sem implementadas no algoritmo de acordo com as bibliotecas e funções disponíveis neste.

Além das funções já programadas no ambiente de pesquisa, para o correto funcionamento do backtest deve-se programar obrigatoriamente a função initialize, que roda uma única vez ao início do algoritmo, e opcionalmente as funções handle_data e before_trading_start. Nosso algoritmo não faz uso de dados minuto-a-minuto, portanto não será implementada a função handle_data. Já a função before_trading_start é utilizada para cálculos antes de o mercado abrir, todos os dias. Também não haverá necessidade em implementar esta função, uma vez que nosso portfólio é rebalanceado apenas uma vez por mês.

Prosseguimos, portanto, com a implementação da função inicial. Relembrando, ela é executada apenas uma vez no início do backtest, logo, esta é a função onde faremos o agendamento (i.e. schedule) do rebalanceamento da carteira, que por sua vez irá chamar o pipeline criado.

import quantopian.algorithm as algo
# Pipeline
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.filters import QTradableStocksUS
# Importing Factors used
from quantopian.pipeline.factors import  SimpleBeta
# Import Optimize API module
import quantopian.optimize as opt

def initialize(context):
    """
    Called once at the start of the algorithm.
    """    
    # Rebalance every month
    algo.schedule_function(
        rebalance,
        algo.date_rules.month_start(),
        algo.time_rules.market_open(),
    )
    # Create our dynamic stock selector.
    algo.attach_pipeline(make_pipeline(), 'pipeline')

Vejam como a função de inicialização é simples para esta estratégia. Antes de mais nada, é necessário importar as bibliotecas que serão utilizadas pelo algoritmo. No caso, estamos usando apenas implemetações do próprio Quantopian, entre elas funções da pipeline, fatores e o módulo de otimização que na verdade será usado apenas para o envio das ordens, conforme sugere a própria plataforma.

Após, faz-se a definição da função initialize. Tudo o que temos de fazer é programar nossa função de agendamento, schedule_function e informar qual o pipeline que será utilizado. Na função de agendamento informamos qual função deve ser chamada periodicamente, rebalance que definiremos a seguir e qual a periodicidade e momento da chamada. Nosso algoritmo chama a função rebalance todo início de mês assim que o mercado abre. Já a nossa conhecida função make_pipeline é atribuída ao nome pipeline e seu resultado será chamado durante a execução do rebalanceamento da carteira.

def rebalance(context, data):
    """
    Execute orders according to our schedule_function() timing.
    It is called every month start.
    """
    # Actually runs the pipeline. A DataFrame is the output
    output = algo.pipeline_output('pipeline')       
    output.dropna(inplace=True)

    # Compute weights for assets in output
    k = 2.0 / sum(abs(output['zscore']))
    output.loc[:, 'weight'] = k * output['zscore']
    output.loc[:, 'beta_w'] = output['beta'] * output['weight']
    betaL = sum(output[output['zgeq0']]['beta_w'])
    betaH = sum(-output[~output['zgeq0']]['beta_w'])
    output.loc[output['zgeq0'], 'weight'] = output.loc[output['zgeq0'], 'weight'] / betaL
    output.loc[~output['zgeq0'], 'weight'] = output.loc[~output['zgeq0'], 'weight'] / betaH

    # Sets objective function
    obj = opt.TargetWeights(output['weight'])
    # Execute orders to rebalance portfolio
    algo.order_optimal_portfolio(
        objective=obj,
    )

E por fim, implementa-se a função de rebalanceamento. É nesta função que chamamos a aquisição dos dados através de pipeline_output e salvamos o resultado daquele dia em uma variável chamada output. Este é um DataFrame contendo os zscores exatamente como mostrado no ambiente de pesquisa. Os mesmos ajustes dos pesos são realizados para tornar (na medida do possível) a estratégia em market neutral e então o objeto de otimização é utilizado para enviar as ordens.

Como nosso algoritmo calcula diretamente os pesos de cada ativo na carteira, utiliza-se a função TargetWeights para esta informação e então order_optimal_portfolio se encarrega de enviar as ordens de compra e venda necessárias para obter a carteira desejada com os pesos corretos.

Na figura acima, podemos verificar o resultado do backtest para o ano de 2009, da grande crise financeira. A lógica central de estratégias long-short como esta é terem pouca ou nenhuma correlação com o benchmark, no caso o índice S&P500, e ainda assim apresentarem retornos consistentemente positivos, indicando que a estratégia possui alpha. Este teste não foi tão animador quanto os resultados de pesquisa. Enquanto, de fato, a estratégia obteve baixa correlação com o mercado, representada pelo seu beta com pequena magnitude, os retornos não foram atraentes, ficando muito próximos a zero no valor acumulado e um drawdown não desprezível de quase 20%1.

Conclusão

Este artigo apresentou uma implementação simplificada da estratétia Betting Against Beta – BAB utilizando a plataforma Quantopian. O objetivo maior foi demonstrar o uso da plataforma e não necessariamente desenvolver uma estratégia lucrativa de investimento.

Uma vez conhecida a estratégia, a teoria por trás desta e o algoritmo de operação, pode-se utilizar os ambientes de pesquisa e desenvolvimento do Quantopian para pesquisar, testar e implementar (em Python somente) a estratégia. A plataforma dispõe de um grande conjunto de dados, especialmente para o mercado Estado Unidense, mas também existem dados para o Brasil. Diversas técnicas de algorithmic trading podem ser investigadas, aprimoradas e compartilhadas.

Referências

ANG, Andrew. Asset management: A systematic approach to factor investing. 2014.

BLACK, Fischer et al. The capital asset pricing model: Some empirical tests. Studies in the theory of capital markets, v. 81, n. 3, p. 79-121, 1972.

FRAZZINI, Andrea; PEDERSEN, Lasse Heje. Betting against beta. Journal of Financial Economics, v. 111, n. 1, p. 1-25, 2014.


  1. Este resultado pode ser devido a nossa implementação da estratégia ou ao pouco tempo coberto pelo backtest. O resultado da estratégia BAB, para um longo período de tempo pode ser encontrado no site da AQR Capital em https://www.aqr.com/Insights/Datasets 

Posted by Rafael F. Bressan

Foi membro do Clube de Finanças Esag e gerente do núcleo de pesquisa em riscos e derivativos, no período 2018 a 2019. Bacharel em Ciências Econômicas pela UDESC/Esag. Aluno do mestrado acadêmico em Economia na FGV/EESP.

Leave a Reply

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax

This site uses Akismet to reduce spam. Learn how your comment data is processed.