Why Stock Beta May Not Predict Losses in a Market Sell-Off
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.