Goal
This is a simple exploration of stock market peaks and troughs with a comparison of the current market to historic market troughs.
Background
Theoretically, as long as there is a functional economy in the world the general stock market (represented by the S&P 500 in this analysis) will climb higher over the long term. There are two reasons to expect this:
The companies in the index are the target of spending by consumers, businesses and government so naturally their revenues will grow at a rate that represents the economy as a whole.
Those companies who perform the best by turning revenues into profit will stay in the index, those who do not perform will drop out of the index. Given this continual vetting, we can expect that the index will not just match but outperform the economy as a whole.
However the stock market does not grow at a perfectly steady linear rate. Instead, it has a history of reaching new all time highs and then drops before gradually climbing to another new all time high.
Let’s explore the market dips where the price dropped from an all-time high peak by over 15% since 1985. We’ll look at how long it took from peak to trough and then recovery.
# This is my standard load package to load up a dataframe with:
# S&P 500, treasury rates and common sector ETF's for analytics of various strategies
if (str_detect(.Platform$OS.type, "win")) {
ArchiveFile = "C:\\Users\\cwool\\RepoArchive\\SavvyDemoProjects\\R\\SP500Strategy_PT1010.RData"
} else {
ArchiveFile = "/home/woolylinux01/Documents/SavvyDemoProjects/R/SP500Strategy_PT1010.RData"
}
Offline = FALSE
if (Offline) {
load(ArchiveFile)
} else {
# Load data using the QuantMod package to retrive historical data
# Treasury Bonds and rates are provided by the St. Louis Federal Reserve "FRED" system
StockNames = c("SP500", "Communication Services" ,"Consumer Discretionary" ,"Consumer Staples" ,
"Energy" ,"Financials" ,"Health Care" ,
"Industrials" ,"Materials" ,"Real Estate" ,
"Technology" ,"Utilities")
StockSymbols = c("^GSPC", "XLC" ,"XLY" ,"XLP" ,
"XLE" ,"XLF" ,"XLV" ,
"XLI" ,"XLB" ,"XLRE" ,
"XLK" ,"XLU")
BondNames = c("Frd Funds", "1Yr Treasury", "2Yr Treasury",
"5Yr Treasury", "10Yr Treasury", "20Yr Treasury",
"30Yr Treasury")
BondSymbols = c("DFF", "DGS1","DGS2",
"DGS5", "DGS10", "DGS20",
"DGS30")
# Get the S&P from Yahoo
getSymbols(StockSymbols, src='yahoo', from = '1900-01-01',
to = Sys.Date(), warnings = TRUE)
# Call use QuantMod's API into FRED
getSymbols(BondSymbols, src = "FRED")
# Function to convert yahoo stock quotes to a data frame
StockToDF = function(XTSObject, StockName) {
data.frame(XTSObject) %>%
rename(CloseValue = 4) %>%
mutate(SymbolName = StockName,
QuoteDate = as.Date(row.names(.), "%Y-%m-%d"),
WeekBeg = floor_date(QuoteDate, unit = "week")) %>%
group_by(WeekBeg) %>%
mutate(MaxDate = max(QuoteDate)) %>%
ungroup() %>%
filter(QuoteDate == MaxDate) %>%
dplyr::select(SymbolName, WeekBeg, CloseValue) %>%
filter(!is.na(CloseValue))}
# Function to convert FRED bond quotes to a data frame
BondToDF = function(XTSObject, BondName) {
data.frame(XTSObject) %>%
rename(CloseValue = 1) %>%
mutate(SymbolName = BondName,
QuoteDate = as.Date(row.names(.), "%Y-%m-%d"),
CloseValue = CloseValue / 100,
WeekBeg = floor_date(QuoteDate, unit = "week")) %>%
group_by(SymbolName, WeekBeg) %>%
summarise(CloseValue = mean(CloseValue, na.rm = TRUE),
.groups = "drop")}
AllSymbolsDF = bind_rows(
StockToDF(GSPC, "SP500"),
StockToDF(XLC,"SecTelecom"),
StockToDF(XLY,"SecDiscret"),
StockToDF(XLP,"SecStaples"),
StockToDF(XLE,"SecEnergy"),
StockToDF(XLF,"SecFins"),
StockToDF(XLV,"SecHealth"),
StockToDF(XLI,"SecIndust"),
StockToDF(XLB,"SecMaterl"),
StockToDF(XLRE,"SecRealEst"),
StockToDF(XLK,"SecTech"),
StockToDF(XLU,"SecUtil"),
BondToDF(DFF, "FedFunds"),
BondToDF(DGS1, "Tres01Yr"),
BondToDF(DGS2, "Tres02Yr"),
BondToDF(DGS5, "Tres05Yr"),
BondToDF(DGS10, "Tres10Yr"),
BondToDF(DGS20, "Tres20Yr"),
BondToDF(DGS30, "Tres30Yr"),
)
AllSymbolDF_Pivot = AllSymbolsDF %>%
pivot_wider(id_cols = WeekBeg, names_from = SymbolName, values_from = CloseValue) #%>%
# filter(complete.cases(.))
save(list=c("AllSymbolsDF", "AllSymbolDF_Pivot"), file = ArchiveFile)
}
# This custom function will plot out peaks, troughs and variances from the SP500 dataframe
# given a date range
#EndingDate = Sys.Date()
#PlotTitle = "Current"
PeakPlot = function(EndingDate, PlotTitle, StartDate = NULL, ShowAnnotations = NULL) {
DisplayDF = AllModelsDF %>%
filter(WeekBeg <= EndingDate & ModelName == "BuyAndHold")
HighDate = max(DisplayDF$HighDate)
if (is.null(StartDate)) {
StartDate = HighDate - months(6)
}
HighValue = max(DisplayDF$HighValue)
LowValue = DisplayDF$LowValue[length(DisplayDF$LowValue)]
HighDate = DisplayDF$HighDate[length(DisplayDF$HighDate)]
LowDate = DisplayDF$LowDate[length(DisplayDF$LowDate)]
CurrentValue = DisplayDF$SP500[length(DisplayDF$SP500)]
PeakLabel = paste0(percent((CurrentValue / HighValue) -1, accuracy = 2), " vs Peak")
TroughLabel = if_else(CurrentValue / LowValue > 1,
paste0("+",percent((CurrentValue / LowValue) -1, accuracy = 2), " vs Trough"),
"in Trough")
PeakvsTroughLabel = paste0(percent((LowValue / HighValue) -1, accuracy = 2), " Peak vs Trough")
if (!is.null(ShowAnnotations)) {
CycleLabel = paste0(as.character(difftime(EndingDate, HighDate, unit = "days")),
" day cycle : ",
as.character(difftime(LowDate, HighDate, unit = "days")),
" days from prior peak on ", HighDate,
" to trough on ", LowDate, " then ",
as.character(difftime(EndingDate, LowDate, unit = "days")),
" days from trough to ", EndingDate)
} else {
CycleLabel = paste0("Current market is ", if_else(CurrentValue - HighValue > 0, "+", ""),
percent((CurrentValue - HighValue) / HighValue, accuracy = 0.1),
" versus recent peak on ", HighDate)
}
DisplayDF = DisplayDF %>%
filter(WeekBeg >= StartDate)
DisplayDF = DisplayDF %>%
left_join(AllSymbolsDF %>%
filter(SymbolName == "FedFunds") %>%
rename(FedFunds = CloseValue) %>%
select(WeekBeg, FedFunds),
by = c("WeekBeg"))
DisplayDF = DisplayDF %>%
mutate(FedFundsAlt = rescale(FedFunds, c(min(SP500), max(SP500))))
# About 30 positions are displayable
BreakWeeks = paste0(max(1, round(nrow(DisplayDF) / 30, 1)), " weeks")
# Refactoring code 12/29/2023 as now both linux and Windows format uses "linewidth"
Plot1 = DisplayDF %>%
ggplot(aes(WeekBeg, SP500)) +
geom_line(aes(color = "Weekly Closing Value"), linewidth = 2, na.rm = T) +
geom_line(aes(WeekBeg, FedFundsAlt, color = "Fed Funds Rate (Not to scale)"), linewidth = 1, na.rm = T) +
geom_point(aes(WeekBeg, HighValue, color = "Peak to Date"), size = 2) +
geom_point(aes(WeekBeg, LowValue, color = "Trough Since Last Peak"), size = 2) +
scale_color_manual(name = "", values = c("Weekly Closing Value" = "black",
"Fed Funds Rate (Not to scale)" = "dark blue",
"Peak to Date" = "green",
"Trough Since Last Peak" = "red")) +
scale_x_date(breaks = BreakWeeks) +
scale_y_continuous(labels = comma_format(accuracy = 1)) +
theme_minimal() +
theme(legend.position = "bottom",
axis.text.x = element_text(angle = 90, vjust = 0.5, hjust=1)) +
labs(title = paste0("SP500 - ", PlotTitle),
subtitle = CycleLabel,
caption = "Peaks = All Time Highs, Troughs are Lowest Points after a Peak",
x = "", y = "S&P 500 Index Value")
if (!is.null(ShowAnnotations)) {
Plot1 +
# Peak Line and Label
annotate("segment", linewidth = 1,
x = HighDate, xend = EndingDate,
y = HighValue, yend = HighValue,
arrow = arrow(ends = "both", angle = 90, length = unit(.2,"cm"))) +
annotate("label",
label = PeakLabel,
x = HighDate + difftime(EndingDate, HighDate) / 2,
y = HighValue) +
# Peak to Trough Line and Label
annotate("segment", linewidth = 1,
x = HighDate, xend = LowDate,
y = mean(c(LowValue, HighValue)), yend = mean(c(LowValue, HighValue)),
arrow = arrow(ends = "both", angle = 90, length = unit(.2,"cm"))) +
annotate("label",
label = PeakvsTroughLabel,
x = HighDate + difftime(LowDate, HighDate) / 2,
y = mean(c(LowValue, HighValue))) +
# Trough Line and Label
annotate("segment", linewidth = 1,
x = LowDate, xend = EndingDate,
y = LowValue, yend = LowValue,
arrow = arrow(ends = "both", angle = 90, length = unit(.2,"cm"))) +
annotate("label",
label = TroughLabel,
x = LowDate + difftime(EndingDate, LowDate) / 2,
y = LowValue)
} else {
Plot1
}
}
2022 Inflation Shock: “Sudden onset inflation” might have been the biggest economic symptom of the Covid Pandemic. Some of the contributing factors were:
The high inflation rate eroded the purchasing power of consumers, threatened the profit margins of businesses, and raised the expectations of further price increases. It also prompted the Federal Reserve to tighten its monetary policy by raising the federal funds rate over ten times. Both the threat and the action led to a decline in the stock market, as investors feared that higher interest rates would slow down economic growth and reduce earnings.
PeakPlot(as.Date("2023-12-17"), "2022 Inflation Shock", NULL, TRUE)
Covid Panic: The 2020 Covid 19 Pandemic caused mandatory shut-down’s and economic concerns which created one of the sharpest drops from a peak in history. However, once the government committed to stimulus efforts and the markets realized that less than 20% of all employees would be impacted the market quickly recovered. Increased spending on the “stay at home economy” drove the market beyond recovery to new high’s even during the pandemic.
PeakPlot(as.Date("2020-08-09"), "2020 Covid Panic", as.Date("2019-08-05"), TRUE)
2018 Trade War Panic: The escalating trade war between the US and China, rising interest rates and the uncertainty in government policy drove an unexpected decline in the later part of 2018 which was mirrored in the European markets. China’s market lost about a quarter of its’ value. By early 2019 the market had recovered as the markets adjusted to the uncertainty.
PeakPlot(as.Date("2019-04-15"), "2019 Trade War Panic", as.Date("2018-03-15"), TRUE)
Financial Sector Crisis: The financial crisis of 2007–2009 was triggered by the collapse of the U.S. housing market, which exposed the fragility of many institutions who invested and speculated in it. The crisis caused a severe contraction of liquidity and credit in global financial markets, leading to a sharp decline in stock prices and a deep recession in the real economy. The U.S. government responded with unprecedented interventions, such as the $700 billion Troubled Asset Relief Program (TARP) and the $182 billion rescue of AIG. The crisis and its aftermath had significant and lasting effects on the financial system, the economy, and the society. There were many failures and government facilitated takeovers:
PeakPlot(as.Date("2013-03-11"), "Financial Sector Crisis 2007 - 2009", as.Date("2007-04-01"), TRUE)
The Dot Com Bubble : The dot-com bubble was a period of excessive speculation and investment in internet-based companies in the late 1990s. Many of these companies had no clear business model or profitability, but attracted huge amounts of capital from investors who hoped to cash in on the rapid growth of the online sector. The bubble burst in early 2000, when the market realized that many of these companies were overvalued and unsustainable. The NASDAQ composite index, which had risen by 800% between 1995 and 2000, had dropped 78% from it’s highs by October 20021. Many dot-com companies went bankrupt like Pets.com, Webvan, and Boo.com. Even surviving companies like Amazon and Cisco, suffered huge drop. Amazon’s stock price dropped from $107 in 1999 to $7 in 200 and Cisco’s declined by 80%. The dot-com bubble and its aftermath taught valuable lessons to investors, entrepreneurs, and regulators about the risks and opportunities of the internet economy.
This was the second longest peak to recovery time span in history taking just over 7 years to recover. The longest was the great depression which took 25 years from 1929 to 1954
PeakPlot(as.Date("2007-05-14"), "Dot-Com Bubble", as.Date("1999-09-19"), TRUE)
Global Uncertainty and Clinton Impeachment Hearings : This drop was initiated by global economic concerns with Russian debt default and a weakening US dollar alarming international markets along with the beginnings of troubles in the technology sector. However, the market rebounded in the following months, partly due to the resolution of the Clinton impeachment crisis. The impeachment hearings, which began in October and ended in February, distracted the public and the media from the financial turmoil with minimal risk as democratic control in the senate guaranteed rejection of the articles of impeachment sent to it from the US House of Representatives. Interest rate reductions by the federal reserve probably helped as well.
PeakPlot(as.Date("1998-11-16"), "Clinton Impeachment Hearings", as.Date("1998-04-01"), TRUE)
Desert Shield and Desert Storm : With Iraq’s invasion of Kuwait in August of 1990, the US began a two phase operation to protect Saudi Arabia and other middle eastern interests by a massive build-up of forces and then pushing the Iraqi forces out of Kuwait and back across the Iraq border but stopping short Baghdad, leaving the government in place. The stock market reacted with an immediate decline and didn’t fully recover until the end of the conflict whereupon the recovery was rapid.
PeakPlot(as.Date("1991-02-04"), "Desert Shield & Storm", as.Date("1990-01-01"), TRUE)
Black Monday : On October 19th 1987, the market had a sudden one day drop of 20%+ in one day which as of 2023 was still the largest one day drop in history. Many causes have been speculated but it was mostly a technical crash as there were no obvious underlying fundamental drivers behind it. Some say it was the first computer driven crash and some of today’s market “safety switches” where trading is suspended after sudden drops were implemented. It’s noteworthy that the market was already on a slight downward trend for two months before that day. The market recovered, even in the headwinds of rising interest rates.
PeakPlot(as.Date("1989-07-17"), "Black Monday (10/19/1987)", NULL, TRUE)
Finally, let’s look at where the current market is (data automatically refreshed and is current as of: 2025-12-18):
PeakPlot(Sys.Date(), "Current Market - 5 Year Look-Back", Sys.Date() - years(5))
Is there a reliable investment strategy you could base around this data? I’ll leave that up to you.
Whether you are in a peak or a trough.
Stay Savvy.