Skip to content
All library documents

Why Stock Beta May Not Predict Losses in a Market Sell-Off

Article Quant Q&A · Author: cephalopod

Summary

The document presents an empirical question about whether historical stock beta can identify shares likely to fall most during a broad market decline. The author estimates beta for ASX 200 constituents using daily returns over a pre-sell-off period, then compares those estimates with price moves during the February–March 2020 downturn. The plotted relationship appears weak, with a reported correlation of -0.285, and the author observes that some low-beta stocks fell more than high-beta stocks.

This is a request for interpretation rather than a completed explanation or trading method. The document includes R code and describes a binned classification analysis, but it supplies no stock-level findings or answers explaining the weak relationship. Its comparison may also be affected by the chosen return windows, price-change calculation, sample composition, and the distinction between beta estimated in ordinary conditions and realized losses during a stress event. The example illustrates why beta alone may not rank crash-period declines reliably, but it does not establish a general result.

Key ideas

  • The author estimates stock betas from historical daily returns against an ASX 200 tracking ETF.
  • The beta estimates are compared with stock price declines during the February–March 2020 sell-off.
  • The reported relationship is weak and negative, contrary to the author's expectation.
  • The document raises questions about beta's limits but does not provide an explanation or validated short-selection method.
  • Window choice and the sell-off price calculation may affect the observed association.

Tags

Full text
# Why does Beta of a stock not correlate well with market sell off


# Why does Beta of a stock not correlate well with market sell off












I posted a question a few days back: (Quantatively identifying stocks to short when overall market starts to roll-over)

@rubikscube09 suggested that stock `beta` could be of potential use to identify stocks to short when overall market rolls-over. To see the potential of this idea I analysed the ASX200 for the Feb20 sell-off. The analysis came-up with some unexpected results, i.e. high Beta did not necessarily result in a larger fall in the stock.

I would appreciate very much if you could provide some insight into:

- Why does high Beta not translate to higher decline in share price. To be fair there were stocks with high beta that did decline but I expected the basket to have a lot more declines

- Conversely, why does low beta result in larger declines (when the opposite should be true)

- Why is there such low overall correlation, in this case -0.285 (I expected correlation to be at least over 0.5)

- It would also help me very much if you could kindly point me to any resource that would help me understand further. This is the first time I have actually done this in practice.

My code in R is as follows:

```
library(tidyverse)
library(tidyquant)
library(lubridate)
require(xml2)
require(rvest)
require(janitor)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Select date range for Analysis ----
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#Pre-sell-off dates
to_date_p1   <- dmy("20/02/2020")
from_date_p1 <- to_date_p1 - 365 * 2

# Sell-off dates
from_date_p2 <- dmy("21/02/2020")
to_date_p2   <- dmy("23/03/2020")

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Select tickers to analyse
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

mkt_index <- "STW" # This is the ETF that tracks the ASX200 index

raw_webpage <- read_html("https://www.asx200list.com/")

tickers_mkt_index <- html_table(raw_webpage, fill = TRUE)[[1]] %>% pull(Code)

all_tickers <- c(mkt_index, tickers_mkt_index)
length(all_tickers)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Calculate daily returns and prepare dataset for analysis
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

ASX_data <- paste0(all_tickers, ".AX") %>%  tq_get(x = .)
unique(ASX_data$symbol) %>% length # check number of downloads

daily_returns <- ASX_data %>% 
       
  filter(between(date, from_date_p1, to_date_p1)) %>%
  
  group_by(symbol) %>%
  tq_transmute(select     = close, 
               mutate_fun = periodReturn, 
               period     = "daily", 
               col_rename = "daily_return") %>% 
  ungroup

mkt_index_returns <- daily_returns %>% filter(symbol == "STW.AX") %>% select(-symbol) %>% rename(mkt_daily_return = daily_return)

all_index_tickers <- ASX_data %>% 
  filter(between(date, from_date_p1, to_date_p1)) %>% 
  left_join(., daily_returns) %>% 
  left_join(., mkt_index_returns) %>% 
  filter(symbol != "STW.AX")
  

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Check price movement 
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

all_price_change <- ASX_data %>% 
  
  filter(between(date, to_date_p1, to_date_p2)) %>% 
  
  group_by(symbol) %>% 
  mutate(start_date = min(date),
         end_date   = max(date)) %>% 
  
  filter(date == start_date | date == end_date) %>% 
  arrange(symbol) %>% 
  
  mutate(price_at_start = lag(high)) %>% 
  mutate(total_price_move = low - price_at_start) %>% 
  mutate(total_price_move_pct =  total_price_move / price_at_start) %>% 
  
  select(symbol, contains("total_price")) %>%
  
  filter(!is.na(total_price_move_pct)) %>%
  
  ungroup()

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Calculate Beta with performanceanalytics
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

ticker_beta <- all_index_tickers %>% 
  group_by(symbol) %>%
  tq_performance(Ra = daily_return,
                 Rb = mkt_daily_return, 
                 scale = 252,
                 performance_fun = table.CAPM)

final_df <- left_join(all_price_change, ticker_beta) %>% 
  arrange(total_price_move_pct) %>% 
  na.omit()

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# View as dots
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

final_df %>% 
  ggplot(aes(x = Beta, y = total_price_move_pct)) +
  geom_point() +
  geom_smooth(method = "lm") +
  theme_bw() +
  labs(title = "Beta vs. Price decline")

# Overall correlation looks low
cor.test(final_df$Beta, final_df$total_price_move_pct)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Use OneR to further analyse output
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

no_of_cuts = 4
breaks <- rep("q", no_of_cuts) %>% paste0(., 1:no_of_cuts)

final_df %>% 
  select(symbol, total_price_move_pct, Beta) %>% 
  mutate(price_pct_factor = cut(x = total_price_move_pct, breaks = no_of_cuts, labels = breaks)) %>% 
  OneR::optbin(price_pct_factor ~ Beta, data = ., method = "infogain") %>%
  # xlopen
  OneR::OneR() %>% 
  summary()

# I expected much higher accuracy
```

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.