tickerstance
Lookup⌘K
Free
Reads

Methodology·9 min read·Updated May 23, 2026

Seasonality Methodology

Seasonality answers a single question: where in the calendar are we, historically? This is the formula sheet behind the /seasonality page, with every stat defined, every aggregation rule named, and the survivorship bias on the stock-level layers disclosed in full.

On this page

  1. ★Key takeaways
  2. 01What this is
  3. 02Stat formulas
  4. 03How we aggregate
  5. 04Survivorship bias
  6. 05Data depth
  7. 06What seasonality is NOT
  8. ?Frequently asked questions
On this page · 6 sections
  1. ★Key takeaways
  2. 01What this is
  3. 02Stat formulas
  4. 03How we aggregate
  5. 04Survivorship bias
  6. 05Data depth
  7. 06What seasonality is NOT
  8. ?Frequently asked questions

Key takeaways · 6

  1. 01Seasonality is a four-layer rollup: index series, sector SPDR ETFs, Fama-French 49 equal-weighted industry baskets, and individual tickers. Each layer is computed from adjusted closes; each gets its own row in seasonality_stats.
  2. 02Five stats per period bucket: avg_return, median_return, win_rate, std_dev, and sample_size. Each is computed across the years that fall inside the window slice (10y / 20y / 30y / all).
  3. 03Indexes and sector ETFs are single-ticker series, so they carry no survivorship bias. The historical return is what an investor who held the index or ETF actually earned.
  4. 04FF49 industries and per-ticker layers are survivorship-biased: the historical universe contains only tickers Massive still indexes today. Companies that delisted before today are absent, so aggregates lean optimistic in windows with heavy delistings.
  5. 05Data depth varies by layer: indexes and ETFs reach 25–30 years from yfinance, FF49 and stocks reach 10 years from Massive (20 years after the planned data-vendor upgrade).
  6. 06Seasonality is contextual, not predictive. It reports what historically happened in this part of the calendar — it does not forecast and does not recommend.
§ 01

What this is

Seasonality answers a different question from regime. Regime asks what kind of market this is right now (Stance, the four subscores). Seasonality asks where in the calendar we are, historically — is this the part of the year that has tended to reward exposure or to punish it, across the available history?

The /seasonality page is built on a single computed table, seasonality_stats. Every row is a five-stat summary for one unit (index, sector ETF, FF49 industry, or ticker) within one period bucket (a month, a day-of-month, a day-of-week, or a trading-day-of-year) over one window slice (10 years, 20 years, 30 years, or all available history). The page lets you drill across those four dimensions; the math behind every cell is documented here.

Nothing on /seasonality is a forecast. The stats describe history, not the future. The same caveat that runs through the rest of TickerStance applies: we report conditions, we do not recommend trades.

§ 02

Stat formulas

Five stats are computed for every (unit, period_type, period_index, window) row. The formulas are deliberately simple — there is no smoothing, no shrinkage, no regime overlay. The point is to be reproducible from the raw return series, not to be clever.

avg_return = the arithmetic mean of the unit's period returns across every year that contributed to the bucket. For a "January for ^GSPC over the last 20 years" cell, this is the average of the 20 January full-month returns.

median_return = the 50th-percentile period return across the same observation set. Sits alongside avg_return so you can spot when one or two extreme years are dragging the average around (the dot-com 2000 January, the COVID-shock 2020 March, the 2008 October).

win_rate = the fraction of contributing years whose period return was strictly greater than zero. A January with 14 up years out of 20 prints win_rate 0.70. Years with a return of exactly zero do not count as wins (this is rare in practice for monthly buckets, common for single-day buckets where prices closed flat).

std_dev = the sample standard deviation of period returns across the contributing years. The dispersion gauge — when this is high, the average return is noisy even if it is positive. Std dev is one denominator you might want for a t-stat, but the page reports it raw rather than computing a t-stat for you.

sample_size = the number of years (or, for daily/weekly buckets, the number of period-observations) that contributed to the row. The dashboard fades cells with sample_size < 10 and hides them entirely below 5 — at very small n, every stat above is statistically meaningless.

§ 03

How we aggregate

The four unit layers each have a different return source. The first two are clean single-ticker series; the second two are bottom-up rollups that carry the survivorship-bias caveat covered in the next section.

Index layer (^GSPC, ^NDX, ^RUT, ^DJI). Daily returns are computed as adj_close[t] / adj_close[t-1] - 1 from the yfinance-seeded series. Indexes already account for constituent rebalancing internally — when a company is removed from the S&P 500 and replaced, the index level reflects the swap. No survivorship bias.

Sector layer (the 11 SPDR ETFs: XLE, XLU, XLK, XLF, XLV, XLY, XLP, XLI, XLB, XLRE, XLC). Daily returns from the ETF adjusted-close series. These are also single-ticker series — the ETF sponsor rebalances the basket inside the fund, so the historical NAV return is what a holder of the ETF actually earned. No survivorship bias.

FF49 industry layer (Fama-French 49 industries, classified by SIC code via the existing classifyFF49 helper). On each date, we take every ticker in our universe whose SIC code maps to that bucket and whose daily_bars row has a valid prior-day close, then compute the equal-weighted average of their daily returns. Buckets with fewer than 5 constituents on a given date are marked null for that day. Survivorship-biased — the historical universe contains only tickers Massive still indexes today.

Ticker layer (individual stocks). Daily returns from the per-ticker daily_bars rows. The same survivorship caveat applies: a ticker exists in the dataset only if it is still listed and still in Massive's coverage today.

Aggregation up to period buckets. Once daily returns are in place, each period bucket is computed by grouping dates and either summing the log-returns (for month / dom / wom / dow buckets) or taking the day's return directly (for the trading-day-of-year doy bucket). The yearly observations then feed avg/median/win-rate/std/n as described above.

Window slicing. For each (unit, period_type) we materialize four window slices: 10y, 20y, 30y, and all. A unit whose available history is shorter than the requested window simply produces a smaller sample_size for that row — the page does not extrapolate.

§ 04

Survivorship bias

This is the most important caveat on the page. Read it before drawing any conclusions from FF49 or per-ticker aggregates.

Indexes and sector ETFs are clean. ^GSPC, ^NDX, ^RUT, ^DJI and the SPDR sectors are all single-ticker series whose providers handle constituent rebalancing internally. The historical return is what an investor who held the index or ETF actually earned. No survivorship correction is needed because no correction is being faked.

FF49 industries and per-ticker layers are survivorship-biased. The historical universe at any past date contains only tickers that Massive still indexes today. Companies that listed and then delisted before today — bankruptcies, take-privates, acquisitions, exchange delistings — are absent from the dataset entirely. Their losing months are not in the average; their failures are not in the win rate.

Which windows are most affected. Three historical episodes wiped out unusually large numbers of US-listed companies, and in those years the FF49/ticker aggregates lean optimistically the most. The 2000–2002 dot-com bust delisted a large fraction of small-cap technology names. The 2008–2009 global financial crisis removed banks, mortgage lenders, and homebuilders that never came back. The 2020 March COVID shock delisted a tail of leveraged energy, retail, and hospitality names. A September average that looks tame for technology in the early 2000s is partly tame because the companies whose September returns would have been -90% are simply not in the dataset.

What this means in practice. Treat FF49 industry seasonality and per-ticker seasonality as descriptive of currently-listed winners, not as descriptive of the historical population. The bias is unavoidable as long as we are running off a single equities-data vendor whose history is itself current-universe-only. The right fix is a survivorship-bias-free vendor like Norgate or CRSP; that is a v2 candidate, not a v1 line item.

The page surfaces this. The /seasonality footnote, the ticker mini-panel footnote, and the per-unit tooltips all carry a "constituents reflect currently-listed only" disclosure on FF49 and ticker units. Index and sector cells do not show the warning because it does not apply to them.

§ 05

Data depth

The four layers reach back different distances, because the underlying vendors have different histories. The window toggle on /seasonality (10y / 20y / 30y / all) gates each row by the unit's available depth.

Indexes and sector ETFs reach the deepest. The yfinance-seeded one-shot import covers roughly 25 to 30 years for ^GSPC, ^NDX, ^RUT, ^DJI, and the SPDR sectors — long enough that a 20-year window is well-sampled and a 30-year window is meaningful for the indexes (less so for the newer XLRE / XLC ETFs, which were not listed until later).

FF49 industries and per-ticker layers are shallower. The Massive vendor history that backs daily_bars is currently 10 years (back to roughly 2016). The 10-year window slice is well-sampled; the 20-year, 30-year, and all-history slices fall back to roughly the same 10-year sample for these layers. A planned upgrade to Massive's 20-year data plan will extend FF49 / ticker history to roughly 2006 and make the 20-year window meaningful for stock-level seasonality. Until then, the 30-year slice is an indexes-and-ETFs feature.

Recompute cadence. The full seasonality_stats table rebuilds on the first of each month after market close via the tickerstance-seasonality-recompute systemd timer. The marginal day's data shifts averages by parts per million, so daily recompute would be wasted work.

§ 06

What seasonality is NOT

Seasonality on TickerStance is contextual, not predictive. The /seasonality page reports what historically happened during this part of the calendar across the available history. It does not say what will happen next month, next week, or tomorrow.

It is not a forecast. A January with avg_return +1.2% and win_rate 0.65 over 20 years is a statement about 20 historical Januaries, not a prediction that this January will be up. The standard deviation column is right there to remind you that the dispersion across those 20 years is wide.

It is not a trade signal. The site does not tell you to buy in November because the "Santa rally" averaged positive, and it does not tell you to sell in September because that month's average is the worst. Seasonality is one piece of context to weigh against the regime read (Stance), the leadership read (sector RS, stock RS), and the macro read (yield curve, credit spreads, VIX). When seasonality and regime agree, you have stacked evidence. When they disagree, the regime read wins — three out of four stocks follow the broad market, and a strong-seasonality September inside a confirmed-downtrend regime is still a downtrend.

It is not stationary. The 2020s are not the 1990s. Calendar effects that were robust in one cycle (the "January effect" in small-caps, the "sell in May" weakness) have weakened or disappeared in others as market structure and participant composition changed. The window toggles (10y / 20y / 30y) exist so you can see how stable a pattern is across cycles, not so you can pick the window that flatters the trade you already wanted to make.

It is not a substitute for risk management. Whatever the seasonality calendar says, your stop-loss, your position size, and your exit plan are still yours to write. TickerStance reports conditions; you decide what to do about them.

Frequently asked questions

What is market seasonality?

Market seasonality is the historical pattern of returns across calendar buckets — months, days of the month, weeks of the month, days of the week, or trading days of the year. It answers "where in the calendar are we, historically?" by averaging the period's return across the years that fall inside a chosen window. It is descriptive of the past, not predictive of the future.

How do you compute average return for a seasonality cell?

For each (unit, period_bucket, window) row, we take the unit's realized return for that period in each year that falls inside the window and average them arithmetically. A "January for ^GSPC over the last 20 years" cell averages the 20 individual January full-month returns. Daily returns are adjusted-close-based; period returns are compounded from daily returns inside the bucket.

What does win_rate mean in a seasonality table?

win_rate is the fraction of contributing years whose period return was strictly greater than zero. A win_rate of 0.70 over 20 years means 14 of the 20 years were positive in that period and 6 were not. Years that closed exactly flat do not count as wins.

Does TickerStance seasonality account for survivorship bias?

Indexes (^GSPC, ^NDX, ^RUT, ^DJI) and sector SPDR ETFs are single-ticker series and carry no survivorship bias. Fama-French 49 industries and individual tickers are survivorship-biased: the universe contains only tickers still listed and still in Massive's coverage today, so failed-and-delisted companies are absent and aggregates lean optimistic — especially in windows that included the 2000–2002 dot-com bust, 2008–2009 GFC, and 2020-03 COVID shock.

How far back does the data go?

Indexes and sector ETFs reach roughly 25 to 30 years from the yfinance one-shot seed. Fama-French 49 industries and individual tickers reach roughly 10 years from Massive, with a planned upgrade to 20 years. The 30-year window toggle is therefore meaningful for indexes and ETFs only until the Massive history upgrade lands.

Can I use seasonality as a trading signal?

No. TickerStance reports historical patterns, not forecasts. A positive average return in a calendar bucket says only that the period has tended to be positive across the available history — the dispersion across years is wide, market structure changes between cycles, and the survivorship caveat on stock-level layers tilts averages upward. Pair seasonality context with the regime read (Stance) and your own risk plan; do not use it as a standalone trigger.

How often is the seasonality data recomputed?

Monthly, on the first of each month after US market close, via a dedicated systemd timer on the VPS. Daily recompute would only shift averages by parts per million and is not worth the work. On-demand recompute is available via pnpm seasonality:recompute when a backfill changes the underlying daily_bars history.

Keep reading

  • MethodologyHow the four Stance subscores (Trend, Breadth, Leadership, Macro) compose the headline regime read — the same composition discipline applied to seasonality.
  • Market RegimeThe "what kind of market is this right now" read that seasonality is meant to contextualize, never override.
  • Sector RotationThe Leadership-side read on which sectors are leading; sector seasonality is the calendar overlay on top of it.
  • Relative StrengthHow leadership is quantified versus the index — seasonality is the rotation's long-horizon backdrop.

See it on the dashboard

Concepts in this read map to live numbers on the tickerstance dashboard. Open today's snapshot.

Open /seasonality
tickerstance
Live · Thu, May 21
TodayStanceProExploreLeadersProCapital MapProShortlistsProSectorsProIndustriesProSeasonalityDataProVolumeProEditorialReadsWeekly
Today's Stance · LiveLive
55/ 100
Neutral
Trend30%69
Breadth30%53
Leadership20%39
Macro20%55

What we're reading

Read · how it worksMethodology — full signal-by-signal breakdown.

Glossary

What is a follow-through day?

Relative strength · RS percentile

Advance/decline line

Sector rotation

The five lenses

Site

About

Pricing · Pro

What's new

All tickers

Computed 2026-05-21 · v1.12.1-2026-05-18-market-rvol-denominatorNot advice · regime read · past ≠ future