◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Indicators / Seasonality Analysis
Session & Time indicator

Seasonality Analysis

A statistical study that averages historical returns by calendar slot — day of week, month of year, or hour of day — to describe recurring patterns in past data.

Illustrative diagram — not live market data.

What it is

Seasonality analysis groups historical price changes by a calendar dimension — day of the week, month of the year, hour of the day, or day of the month — and averages the returns within each group. The output is a profile such as 'the average Tuesday close-to-close change over the last 500 trading days' for each weekday. Unlike a plotted line indicator, seasonality is a summary statistic: it condenses many past observations into one number per calendar slot. Analysts use it to describe tendencies like month-end flows, session-open volatility, or the much-discussed monthly effects in equities. It is important to be precise about what this is: a backward-looking average of noisy data, not a forecast. An average is not a guarantee — a positive 'average Friday' can coexist with many sharply negative Fridays, and a pattern measured in one sample can vanish in the next. Seasonality output is educational, descriptive context only, not financial advice, and trading involves risk of loss regardless of what any historical average shows.

How it works

The mechanics are deterministic averaging. First choose a return definition — typically the percentage close-to-close change of each bar, which keeps observations comparable across price levels. Second, choose a calendar bucket: on a daily chart, the weekday of each bar; on a monthly view, the calendar month; on intraday charts, the hour or session. Each bar's return is added to its bucket's running sum, and a count is kept per bucket. The seasonal value for a bucket is its sum divided by its count — a plain arithmetic mean. Done correctly, this must respect the chart's timezone (a 'Monday' in exchange time may differ from UTC), handle holidays and missing sessions by simply skipping them rather than filling zeros, and use only confirmed bars so the current forming bar does not distort the average. The critical statistical caveat is the multiple-comparisons problem: if you scan seven weekdays, twelve months, thirty-one days-of-month, and twenty-four hours, you are running dozens of implicit tests, and some buckets will look impressive purely by chance. With roughly 100 observations per weekday bucket, the standard error of the mean is often as large as the mean itself — which is why honest seasonality work reports sample sizes and dispersion, not just the averages.

How traders read it

Common settings

There are no standard indicator settings, only study choices: the return definition (close-to-close percentage change is common), the calendar bucket (weekday on daily charts, month on monthly views, hour or session intraday), and the lookback window (for example 2-5 years of daily bars, giving very roughly 100-250 observations per weekday). Longer lookbacks give larger samples but mix in older market regimes; shorter ones are more current but statistically weaker. The chart timezone setting materially changes intraday and weekday bucketing and should always be stated with any result.

Strengths

Pitfalls to watch

Pine v6 example

//@version=6
indicator("Day-of-Week Average Return", overlay = false)

// Accumulates the average close-to-close % change per weekday.
// Descriptive statistics only - averages of past data, not forecasts.
var float[] sums   = array.new_float(7, 0.0)
var int[]   counts = array.new_int(7, 0)

pctChange = 100.0 * ta.change(close) / close[1]

if barstate.isconfirmed and not na(pctChange)
    idx = dayofweek - 1 // 1 = Sunday ... 7 = Saturday
    array.set(sums, idx, array.get(sums, idx) + pctChange)
    array.set(counts, idx, array.get(counts, idx) + 1)

todayIdx = dayofweek - 1
n = array.get(counts, todayIdx)
avgToday = n > 0 ? array.get(sums, todayIdx) / n : na

plot(avgToday, "Avg % change, this weekday", color = color.blue, style = plot.style_histogram)
hline(0, "Zero", color = color.gray)

Pro tip: Before trusting any seasonal pattern, count how many calendar slots you scanned to find it and divide your enthusiasm accordingly — testing dozens of buckets guarantees a few will look good by luck. Then split your history in half and check the pattern exists in both halves, and always read the average next to its sample size and spread. This is educational statistical context only, not advice: a historical average predicts nothing, and trading involves risk of loss.

Built an indicator from this? Run it through the Validator to catch look-ahead bias and repainting, or convert a strategy to Pine Script.

Educational only — not financial advice, not a recommendation to trade. No indicator is predictive; trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro