The “Midterm Miracle” Investing Phenomenon
Evaluate a market buy signal with Python and yfinance
Timing the stock market precisely may be elusive, but certain periods come remarkably close to being opportune. In a previous article, I delved into the “-3.5% Solution” timing strategy. Today, I want to explore another promising approach known as the “Midterm Miracle” (MtM).
The Midterm Miracle encompasses the period from October of a US Congressional midterm election year to the following June. Historically, these 9-month intervals have proven to be consistently profitable for investments in the US stock market. According to insights from billionaire investor Ken Fisher, stocks have shown gains 92% of the time during these periods and delivered higher-than-average returns.
Noteworthy publications such as the Financial Times and Forbes have reported on this phenomenon. The MtM’s success is attributed to the tendency for the president’s party to lose seats in midterm elections, resulting in increased political gridlock in Washington. More gridlock means less uncertainty around legislative actions, so investor sentiment improves.
In this Quick Success Data Science project, we’ll harness the power of Python, pandas, and the yfinance library to conduct an analysis of the MtM and gain insights into its potential as a profitable timing strategy.
The yfinance Library
The yfinance library is an open-source tool that uses Yahoo’s publicly available APIs. It’s intended for research and educational purposes, such as prototyping or downloading historical data, rather than trading real money. It offers a threaded and Pythonic way to download market data from Yahoo! Finance, but it’s not affiliated with Yahoo! Finance. You can install it using either:
conda install -c conda-forge yfinance
or
pip install yfinance
We’ll also use the pandas library for preparing the data and matplotlib for plotting. Here are the installation commands for the command line interface:
conda install matplotlib pandas
or
pip install matplotlib pandas
The Code
The following code was written and executed in JupyterLab and is described by cell.
Importing Libraries and Downloading Data
We’ll start by importing the libraries and then use yfinance to download the data. The yfinance download()
method directly builds a pandas DataFrame. Pass it the symbol for the S&P 500 (^GSPC) along with a date range. In this case, we'll look at the period January 1, 1926 to June 30, 2023.
import pandas as pd
import yfinance as yf
df = yf.download('^GSPC', '1926-01-01', '2023-6-30')
display(df)
A quick look at the DataFrame indicates that we have 6 columns of data plus a date index. The earliest available data starts in 1927, so we won’t be able to evaluate the 1926 midterm election year.
Removing Unwanted Columns
We need only the closing price (Close
) values, so we'll reassign the DataFrame with just this column. We’ll also reset the index so that the dates move to a dedicated Date
column in “date aware” datetime
format.
df = df['Close']
df = df.reset_index()
pd.to_datetime(df['Date'])
df.head(3)
Creating the Midterm Miracle DataFrame
To evaluate the MtM, we’ll need a DataFrame with just the relevant 9-month intervals. Unlike presidential election years, which can be evenly divided by four, midterm years are two years out of sync. So, all we need to do is find the years in the Date
column that yield a remainder of 2 when divided by 4. The output will be a pandas Series named midterm_years
.
# Filter for rows corresponding to midterm election years:
midterm_years = df[(df['Date'].dt.year % 4 == 2)]['Date'].dt.year
Next, we make a new DataFrame, df_mtm
, to hold our MtM data. Then we loop through the midterm_years
series and assign start and end dates for the period. This involves using pandas' built-in datetime
functionality.
With these dates, we can make a temporary DataFrame, named filtered_data
, from which we can extract the starting and ending values and calculate the simple return.
“Simple return” is calculated by subtracting the starting price of an investment from an ending price and then dividing by the starting price.
# Group data into 9-month periods from end Sept. of each midterm election year:
df_mtm = pd.DataFrame()
for year in midterm_years:
start_date = pd.to_datetime(f"{year}-9-30")
end_date = start_date + pd.DateOffset(months=9)
filtered_data = df[(df['Date'] >= start_date) & (df['Date'] < end_date)]
if not filtered_data.empty:
start_price = filtered_data.iloc[0]['Close']
end_price = filtered_data.iloc[-1]['Close']
simple_return = round(((end_price - start_price) / start_price) * 100, 1)
df_mtm.loc[year, 'Year'] = year
df_mtm.loc[year, 'Start Date'] = start_date
df_mtm.loc[year, 'End Date'] = end_date
df_mtm.loc[year, 'Simple Return %'] = simple_return
df_mtm = df_mtm.astype({'Year':'int'})
df_mtm.reset_index(drop=True, inplace=True)
print(df_mtm)
Plotting the Results
To see how often this strategy produces a winner, we’ll calculate the percent of positive returns and print it. Then we’ll use pandas’ built-in plotting functionality to plot a bar chart.
# Find and print the percentage of MtM years with positive returns:
pct_positive = len(df_mtm[df_mtm['Simple Return %'] > 0]) / len(df_mtm) * 100
start = midterm_years.values[0] # Find the first midterm year in dataset.
print(f"\nMidterm Miracle % positive outcomes since {start}: \
{pct_positive:.2f}%\n")
# Plot each year's Simple Return as a bar chart:
df_mtm.plot(kind='bar',
x='Year',
y='Simple Return %',
color=(df_mtm['Simple Return %'] > 0).map({True: 'blue',
False: 'red'}),
title='S&P 500 MidTerm Miracle (End September - End June)',
legend=False,
ylabel='Simple Return (%)');
Examining the Daily Behavior
The previous analysis revealed the outcome of the MtMs, but it didn’t provide a lot of detail. If you adopt this strategy, should you expect smooth sailing or a roller coaster ride? To determine this, we’ll need to inspect the daily closing prices for the MtM periods.
To start, we’ll make a new DataFrame (df_mtm_days
) that includes rows for every day of every MtM.
# Make new DataFrame with daily data for MtM periods:
df_mtm_days = pd.DataFrame()
for year in midterm_years:
start_date = pd.to_datetime(f"{year}-9-30")
end_date = start_date + pd.DateOffset(months=9)
filtered_data = df[(df['Date'] >= start_date) & (df['Date'] < end_date)]
df_mtm_days = pd.concat([filtered_data, df_mtm_days])
Next, we’ll plot the results. If we plot them all at once, however, the chart will be unreadable. So, we’ll write code that lets you enter a start year and see the associated MtM. To do this, we’ll plot a temporary DataFrame with just the results for the relevant period.
start_year = 1982
end_year = start_year + 1
df_temp = (df_mtm_days[(df_mtm_days['Date'].dt.year >= start_year) &
(df_mtm_days['Date'].dt.year <= end_year)])
df_temp.plot(x='Date', y='Close',
title=f"Daily Price Change for {start_year} Midterm Miracle",
ylabel='Daily Percent Change',
lw=1,
color='k',
grid=True);
For MtM periods with sensational results, like 1942 and 1982, it’s mostly a constant climb to success. Other years, however, may have tested your resolve. MtMs in 1934, 2018, and 2002, for example, had to climb out of holes along the way.
Outcome
The pandas, matplotlib, and yfinance libraries are great tools for acquiring and analyzing financial data. In this article, we conducted a quick yet insightful analysis of the Midterm Miracle strategy, unveiling a remarkable 87.5% positive return rate since the 1930 midterm election.
Over the last 92 years, the only time this strategy suffered significant losses was during the Great Depression. While “past performance is no indication of future success,” the strategy’s consistently impressive track record warrants serious consideration from savvy investors seeking an edge in their financial endeavors.
Thank You!
Thanks for reading and please follow me for more Quick Success Data Science projects in the future.
In Plain English
Thank you for being a part of our community! Before you go:
- Be sure to clap and follow the writer! 👏
- You can find even more content at PlainEnglish.io 🚀
- Sign up for our free weekly newsletter. 🗞️
- Follow us on Twitter, LinkedIn, YouTube, and Discord.