library(tidyverse)
library(lubridate)
library(scales)
library(ggrepel)
library(tidyquant)
library(jsonlite)
theme_set(theme_minimal())
invisible(Sys.setlocale("LC_TIME", "en_US.UTF-8"))

library(knitr)

# files <- list.files("data")
# files[which(str_detect(files, "2024-11-01"))]
# [1] "candidateprobs-2024-11-01.csv"     "correlations_daily-2024-11-01.RDS" "latest_daily-2024-11-01.RDS"       "latest_weekly-2024-11-01.RDS"     
# [5] "returns-2024-11-01.csv"            "rollingcor_daily-2024-11-01.RDS" 

candidateprobs <- read_csv("data/candidateprobs-2024-11-01.csv")
# correlations_daily <- readRDS("data/correlations_daily-2024-11-01.RDS")
latest_daily <- readRDS("data/latest_daily-2024-11-01.RDS")
latest_weekly <- readRDS("data/latest_weekly-2024-11-01.RDS")
# returns <- read_csv("data/returns-2024-11-01.csv")
rollingcor_daily <- readRDS("data/rollingcor_daily-2024-11-01.RDS")

name_sector_industry <- read_csv("data/profiles_usd.csv") |> 
  select(Symbol, companyName, industry, sector, MktCap)

Prediction markets

Prediction markets are based on the prices of contracts that pay out based on the outcome of the election. The price of the contract reflects the probability that the market assigns to the candidate winning the election. Here

candidateprobs |> 
  filter(name %in% c("Joe Biden", "Donald Trump", "Kamala Harris")) |> 
  ggplot(aes(x = date, y = prob, color = name)) +
  geom_line() +
  # geom_point() +
  geom_hline(yintercept = 0.5, linetype = "dashed") +
  scale_y_continuous(labels = scales::percent, breaks = 0:10/10) +
  scale_x_date(date_breaks = "1 month", date_labels = "%b %d", expand = c(0,0)) +
  scale_color_manual(values = c("red", "gray", "blue")) +
  labs(title = "Prediction markets: Candidate probabilities",
       x = NULL, color = NULL,
       y = "Probability") +
  theme(legend.position = "bottom",
        # date has to be 45 angle
        axis.text.x = element_text(angle = 45, hjust = 1))

This data is from https://www.realclearpolitics.com/.

Trump’s probability of winning the election spiked after Biden started behaving erratically and messing up the debate. As Kamala Harris entered the race, her probability of winning the election increased, surpassing Trump’s probability for some time. Since October, Trump’s probability has been increasing again and sits at over 60% at the moment.

Financial markets

Obviously, some parts of the economy might benefit more from one candidate winning the election than the other. Think of defence (traditionally a republican domain), infrastucture (democrats), or healthcare (democrats). So gauge the sensitivity of sectors, industries, or even individual stocks to the election outcome, we can look at the correlations between the returns of these assets and the prediction market probabilities.

Below is the Trump’s Trump Media & Technology Group Corp (ticker DJT) versus the probability of Trump winning the election. Obviously, the two move in tandem - at least to some extent.

djt <- tq_get("DJT")
saveRDS(djt, "data/djt.RDS")

tsla <- tq_get("TSLA")
saveRDS(tsla, "data/tsla.RDS")
djt <- readRDS("data/djt.RDS")

candidateprobs |>
  filter(name %in% c("Donald Trump")) |>
  filter(date >= as.Date("2024-01-01")) |> 
  ggplot(aes(x = date)) +
  geom_line(aes(y = prob, color = "Trump betting markets election probability")) +
  geom_line(data = djt |> filter(date >= as.Date("2024-01-01")),
            aes(y = close/100+0.3, color = "DJT close price")) +
  scale_y_continuous(sec.axis = sec_axis(~(.- 0.3)*100, name = "DJT close price ($)", breaks = 0:1000*4),
                     breaks = 0:100/10) +
  scale_x_date(date_breaks = "months", date_labels = ("%b'%y")) +
  scale_color_manual(values = c("Trump betting markets election probability" = "red", "DJT close price" = "black")) +
  labs(x = NULL, y = "Trump election probability", 
       color = NULL, fill = NULL) +
  theme(legend.position = "top")

Another interesting stock is Tesla (TSLA). As Elon Musk increasingly got involved in the election campaign, the stock price of Tesla started to correlate with the probability of Trump winning the election.

tsla <- readRDS("data/tsla.RDS")

candidateprobs |>
  filter(name %in% c("Donald Trump")) |>
  filter(date >= as.Date("2024-01-01")) |> 
  ggplot(aes(x = date)) +
  geom_line(aes(y = prob, color = "Trump betting markets election probability")) +
  geom_line(data = tsla |> filter(date >= as.Date("2024-01-01")),
            aes(y = close/400, color = "TSLA close price")) +
  scale_y_continuous(sec.axis = sec_axis(~(.)*400, name = "TSLA close price ($)", breaks = 0:1000*4),
                     breaks = 0:100/10) +
  scale_x_date(date_breaks = "months", date_labels = ("%b'%y")) +
  scale_color_manual(values = c("Trump betting markets election probability" = "red", "TSLA close price" = "black")) +
  labs(x = NULL, y = "Trump election probability", 
       color = NULL, fill = NULL) +
  theme(legend.position = "top")

Of course there are other drivers of stocks, actually many that are more important than the election outcome. But it is interesting to see how the probability of Trump winning the election is reflected in the stock price of Tesla.

Research

In a 2020 paper titled Political event portfolios, Hanke et al. show that the returns are sensitive to the election probabilities. They use a factor model to estimate the sensitivity of the returns on election probabilities.

What we do is estimate linear models of the form

\[r_i = \beta_0 + \beta_1 r_{\text{SPX}} + \beta_2 \text{chg}_{\text{Trump}} + \epsilon_i\]

where \(r_i\) is the return of asset \(i\), \(r_{\text{SPX}}\) is the return of the S&P 500, and \(\text{chg}_{\text{Trump}}\) is the change in the probability of Trump winning the election. This could be estimated daily, weekly, or over a rolling window. The sensitivity \(\beta_2\) is the coefficient of interest. A high (low) and significant value of \(\beta_2\) would indicate that the asset moves together with (aginst) the probability of Trump winning the election.

If we’re able to estimate enough stocks, we could look for patterns such as sectors that are more sensitive to the election outcome, or stocks that are more sensitive to the election outcome than others. This will hint at the sectors or stocks that will rise on the days after the election given one candidate wins over the other.

Correlations

The below table lists the stocks with a significant sensitivity to the change in the probability of Trump winning the election on a weekly basis. We filter by size with a minimum marketcap of 1B. It starts with the lowest estimtes (estimate_chg_trump) meaning these are the ones that are more sensitive to Harris winning.

latest_weekly |> 
  left_join(name_sector_industry, by = c("symbol" = "Symbol")) |> 
  # filter MktCap over 1 billion
  filter(MktCap > 1E9) |> 
  # filter significant (10% level)
  filter(p.value_chg_trump < 0.1) |>
  arrange(estimate_chg_trump) |> 
  select(symbol, companyName, industry, sector, estimate_chg_trump, p.value_chg_trump) |> 
  DT::datatable( 
         extensions = c('FixedColumns',"FixedHeader"),
          options = list(scrollX = F, 
                         paging=T,
                         fixedHeader=TRUE))

Below is a plot showing the sensitivity to Trump winning the election versus the p-value of the estimate.

latest_weekly |> 
  filter(p.value_chg_trump < 0.2) |>
  left_join(name_sector_industry, by = c("symbol" = "Symbol")) |>
  ggplot(aes(x = estimate_chg_trump, y = p.value_chg_trump, size = MktCap, color = sector)) +
  geom_point(alpha = 0.3) +
  geom_text_repel(aes(label = symbol), size = 2) +
  geom_hline(yintercept = 0.1, linetype = "dashed") +
  geom_vline(xintercept = 0, linetype = "dashed") +
  labs(x = "Estimate of sensitivity to Trump winning", y = "p-value") +
  # no legend for size (only for color!)
  scale_size_continuous(guide = FALSE) +
  theme_minimal()

Rolling correlations

rollingcor_daily |> 
  filter(symbol == "TSLA") |> 
  ggplot(aes(x = date, y = cor63d, color = cor_pval63d)) +
  geom_line() +
  geom_hline(yintercept = 0, linetype = "dashed") +
  scale_color_gradient(low = "blue", high = "red") +
  labs(title = "Rolling 63-day correlation between TSLA and Trump winning the election",
       x = NULL, y = NULL) +
  theme_minimal()

While Tesla started with a significant (significance is blue in chart) negative correlation to a Trump victory (green mobility seems more a democratic thing), the correlation has been increasing since July. More recently, the correlation dipped again, but it is still positive.

Next comes an obvious one: Trump’s Trump Media & Technology Group Corp (ticker DJT). Strong correlation as one would expect.

rollingcor_daily |> 
  filter(symbol == "DJT") |> 
  ggplot(aes(x = date, y = cor63d, color = cor_pval63d)) +
  geom_line() +
  geom_hline(yintercept = 0, linetype = "dashed") +
  scale_color_gradient(low = "blue", high = "red") +
  labs(title = "Rolling 63-day correlation between DJT and Trump winning the election",
       x = NULL, y = NULL) +
  theme_minimal()

Sectors and industries

rollingcor_daily |> 
  slice_max(date) |> 
  select(symbol, cor63d, cor_pval63d) |> 
  left_join(name_sector_industry, by = c("symbol" = "Symbol")) |> 
  group_by(sector) |> 
  summarise(cor = mean(cor63d)) |> 
  ggplot(aes(x = cor, y = 1)) +
  geom_vline(xintercept = 0, linetype = "dashed") +
  geom_point(alpha = 0.5, aes(color = sector), size = 4) +
  geom_text_repel(aes(label = sector), size = 3) +
  labs(x = "Harris sectors <--> Trump sectors",
       y = NULL) +
  scale_size_continuous(guide = FALSE) +
  scale_x_continuous(limits = c(-0.2, 0.2)) +
  theme_minimal() +
  theme(legend.position = "none",
        # no labels on y axis
        axis.text.y = element_blank())

rollingcor_daily |> 
  slice_max(date) |> 
  select(symbol, cor63d, cor_pval63d) |> 
  left_join(name_sector_industry, by = c("symbol" = "Symbol")) |> 
  group_by(industry) |> 
  summarise(cor = mean(cor63d)) |> 
  ggplot(aes(x = cor, y = 1)) +
  geom_vline(xintercept = 0, linetype = "dashed") +
  geom_point(alpha = 0.5, aes(color = industry), size = 4) +
  geom_text_repel(aes(label = industry), size = 3) +
  labs(x = "Harris industries <--> Trump industries",
       y = NULL) +
  scale_size_continuous(guide = FALSE) +
  scale_x_continuous(limits = c(-0.2, 0.2)) +
  theme_minimal() +
  theme(legend.position = "none",
        # no labels on y axis
        axis.text.y = element_blank())

Implications

tbc