Macy's stock seasonality ======================== Background ---------- I noticed that in the last few years, Macy's stock has been growing around the Thanksgiving season, which aligns with makes sense, since that's where Americans buy their presents. I wanted to do a little more math than just eye-balling the chart. Below is an export from Jupyter Notebook that I used to analyze daily stock prices for Macy's in the range from October to December from 2015 to 2023. Imports & Constants ------------------- .. code:: ipython3 import pandas import matplotlib.pyplot as plt .. code:: ipython3 # Inclusive range for dicing YEARS = (2015, 2023) MONTHS = (10, 12) Download S&P 500 stock prices history as a CSV and parse the date ----------------------------------------------------------------- .. code:: ipython3 # https://www.nasdaq.com/market-activity/index/spx/historical?page=102&rows_per_page=25&timeline=y10 spx = pandas.read_csv('60-SPX.csv') spx = spx.reindex(index=spx.index[::-1]) spx = spx.reset_index(drop=True) del spx['Open'] del spx['Close/Last'] spx['Year'] = spx['Date'].str.split('/', expand=True)[2].astype(int) spx['Month'] = spx['Date'].str.split('/', expand=True)[0].astype(int) spx['Day'] = spx['Date'].str.split('/', expand=True)[1].astype(int) spx .. raw:: html
Date High Low Year Month Day
0 08/26/2014 2005.04 1998.59 2014 8 26
1 08/27/2014 2002.14 1996.20 2014 8 27
2 08/28/2014 1998.55 1990.52 2014 8 28
3 08/29/2014 2003.38 1994.65 2014 8 29
4 09/01/2014 0.00 0.00 2014 9 1
... ... ... ... ... ... ...
2523 08/19/2024 5608.30 5550.74 2024 8 19
2524 08/20/2024 5620.51 5585.50 2024 8 20
2525 08/21/2024 5632.68 5591.57 2024 8 21
2526 08/22/2024 5643.22 5560.95 2024 8 22
2527 08/23/2024 5641.82 5585.16 2024 8 23

2528 rows × 6 columns

Download the target stock (M) prices history and parse the date --------------------------------------------------------------- .. code:: ipython3 # https://finance.yahoo.com/quote/M/history/ prices = pandas.read_csv('60-M.csv') del prices['Open'] del prices['Close'] del prices['Adj Close'] del prices['Volume'] prices['Year'] = prices['Date'].str.split('-', expand=True)[0].astype(int) prices['Month'] = prices['Date'].str.split('-', expand=True)[1].astype(int) prices['Day'] = prices['Date'].str.split('-', expand=True)[2].astype(int) Merge the two datasets using date --------------------------------- Discard any rows with incomplete data .. code:: ipython3 df = pandas.merge(spx, prices, on=['Year', 'Month', 'Day'], how='inner', suffixes=('Spx', 'Stock')) df = df[(YEARS[0] <= df.Year) & (df.Year <= YEARS[1]) & (MONTHS[0] <= df.Month) & (df.Month <= MONTHS[1])] df['didx'] = [ d for year in range(YEARS[0], YEARS[1] + 1) for d in range((df.Year == year).sum()) ] df.describe() .. raw:: html
HighSpx LowSpx Year Month Day HighStock LowStock didx
count 571.000000 571.000000 571.000000 571.000000 571.000000 571.000000 571.000000 571.000000
mean 3249.744991 3215.057180 2018.998249 10.984238 15.562172 25.290193 24.382172 31.224168
std 899.323476 888.901796 2.582328 0.821699 8.823070 11.689632 11.410891 18.331992
min 1927.210000 1893.700000 2015.000000 10.000000 1.000000 5.990000 5.570000 0.000000
25% 2556.490000 2543.895000 2017.000000 10.000000 8.000000 15.915000 15.395000 15.000000
50% 3098.200000 3083.260000 2019.000000 11.000000 15.000000 23.059999 22.040001 31.000000
75% 3981.490000 3935.905000 2021.000000 12.000000 23.000000 35.070002 33.399999 47.000000
max 4808.930000 4780.980000 2023.000000 12.000000 31.000000 52.480000 51.209999 63.000000
Square up the data ------------------ We want to plot several years on the same chart, so let’s trim the excess. For all the covered years find the smallest last day index. .. code:: ipython3 years = list(range(min(df.Year), max(df.Year) + 1)) max_didx = min(max(df[df.Year==year].didx) for year in years) min_didx = max(min(df[df.Year==year].didx) for year in years) didxs = list(range(min_didx, max_didx + 1)) Plot ---- 1. The stock price deviations for the selected months range over the selected years. 2. The S&P index price for the same dates. 3. Stock price corrected to S&P index. Note that we plot percentage changes of the stock from the first day of the season. On day one all prices are at 0%, and then for each year they deviate. .. code:: ipython3 def convert_to_season(df, y, years, didxs): base = dict(zip(df.Year[df.didx==0], y[df.didx==0])) return pandas.DataFrame( [ [ (y[(df.Year==year) & (df.didx==didx)].iloc[0] - base[year]) * 100 / base[year] for year in years ] for didx in didxs ], columns=map(str, years) ) stock_seasons = convert_to_season(df, df.LowStock, years, didxs) spx_seasons = convert_to_season(df, df.LowSpx, years, didxs) stock_seasons.plot() spx_seasons.plot() (stock_seasons - spx_seasons).plot(); .. image:: /12_articles/images/12.60.0.png .. image:: /12_articles/images/12.60.1.png .. image:: /12_articles/images/12.60.2.png Download the M prices history with 1-minute precision ----------------------------------------------------- .. code:: ipython3 # https://lime.co/market-data-source/#i5z6wk prices = pandas.read_csv('60-M-1m.csv', delimiter=';') for column in ('ticker', 'open', 'high', 'low', 'volume'): del prices[column] prices['Year'] = prices['date'].str.split('/', expand=True)[2].astype(int) prices['Month'] = prices['date'].str.split('/', expand=True)[0].astype(int) prices['Day'] = prices['date'].str.split('/', expand=True)[1].astype(int) prices['Hour'] = prices['time'].str.split(':', expand=True)[0].astype(int) prices['Minute'] = prices['time'].str.split(':', expand=True)[1].astype(int) prices .. raw:: html
date time close Year Month Day Hour Minute
0 7/1/2024 6:30:00 AM 19.100 2024 7 1 6 30
1 7/1/2024 6:31:00 AM 19.030 2024 7 1 6 31
2 7/1/2024 6:32:00 AM 19.050 2024 7 1 6 32
3 7/1/2024 6:33:00 AM 19.065 2024 7 1 6 33
4 7/1/2024 6:34:00 AM 19.020 2024 7 1 6 34
... ... ... ... ... ... ... ... ...
31726 10/24/2024 12:55:00 PM 15.430 2024 10 24 12 55
31727 10/24/2024 12:56:00 PM 15.450 2024 10 24 12 56
31728 10/24/2024 12:57:00 PM 15.445 2024 10 24 12 57
31729 10/24/2024 12:58:00 PM 15.445 2024 10 24 12 58
31730 10/24/2024 12:59:00 PM 15.410 2024 10 24 12 59

31731 rows × 8 columns

Hypothesis 1 ------------ When Macy’s stock spikes in the morning by at least 20c, it slides down in the evening by at least 10c. .. code:: ipython3 def analyze_day(date: str, df: list[float]) -> tuple: open_price = df[(df.Hour==6) & (df.Minute==30)].close.iloc[0] morning_spike = max(df[df.Hour <= 6].close) day_decline = min(df[(df.Hour >= 8) & (df.Hour <= 11)].close) return open_price, morning_spike, day_decline, (morning_spike - open_price), (morning_spike - day_decline) data = [] for date in set(prices['date']): data.append([date, *analyze_day(date, prices[prices.date == date])]) # Spike-decline sd = pandas.DataFrame(data, columns=('date', 'open_price', 'morning_spike', 'day_decline', 'spike', 'decline')) .. code:: ipython3 len(sd[(sd.spike > 0.2)]), len(sd[(sd.spike > 0.2) & (sd.decline > 0.1)]) .. parsed-literal:: (14, 9) .. code:: ipython3 sd[(sd.spike > 0.2) & (sd.decline > 0.1)][:5] .. raw:: html
date open_price morning_spike day_decline spike decline
4 9/13/2024 14.760 15.160 14.8347 0.400 0.3253
7 8/6/2024 15.224 15.485 15.3800 0.261 0.1050
8 7/11/2024 19.130 19.470 19.2900 0.340 0.1800
17 7/5/2024 19.420 19.900 19.4800 0.480 0.4200
34 7/3/2024 17.940 18.180 17.9250 0.240 0.2550
.. code:: ipython3 import matplotlib.pyplot as plt positive_days = sd[(sd.spike > 0.2) & (sd.decline > 0.1)] negative_days = sd[(sd.spike > 0.2) & (sd.decline <= 0.1)] .. code:: ipython3 def plot_samples(days, prices, count=5): fig, axes = plt.subplots(nrows=count, ncols=1, figsize=(10, 10)) for i in range(count): day = days.iloc[i] date = day.date prices[prices.date == date].close.plot(ax=axes[i]) axes[i].axhline(y=day.morning_spike, color='red', linestyle='--') axes[i].axhline(y=day.day_decline, color='green', linestyle='--') plot_samples(positive_days, prices) .. image:: /12_articles/images/12.60.3.png .. code:: ipython3 plot_samples(negative_days, prices) .. image:: /12_articles/images/12.60.4.png Hypothesis 2 ------------ If you sell at high point before 11 AM, you can buy back by the end of day at least 10c lower price. .. code:: ipython3 def analyze_day(date: str, df: list[float]) -> tuple: prices = list(df.close) open_price = prices[0] high_price = max(df[df.Hour <= 8].close) close_price = prices[-1] open_delta = high_price - open_price close_delta = high_price - close_price day_delta = close_price - open_price return [high_price - close_price, close_price - open_price] data = [] for date in set(prices['date']): data.append([date, *analyze_day(date, prices[prices.date == date])]) chart = pandas.DataFrame(data, columns=('date', 'play', 'nothing')) chart.plot() len(chart[chart.play - 0.2 > chart.nothing]) / len(chart) .. parsed-literal:: 0.524390243902439 .. image:: /12_articles/images/12.60.5.png