Indicators··7 min read

Seasonality Heat Strip: Month-of-Year Average Returns

2 references, link-verified · 2 primaryEditor of record: Shane CantyStandards review editorial standard · audit log

Seasonal patterns describe the tendency for asset prices to move similarly during the same calendar months across different years. A seasonality heat strip aggregates returns by month and displays the average performance for each month in a color-coded matrix, allowing traders to identify which months historically exhibited stronger or weaker performance. This tool is useful for position timing, risk allocation, and understanding long-term behavioral patterns in markets.

//@version=6
indicator("Seasonality Heat Strip", overlay=false)

// Inputs
yearLookback = input.int(10, "Years to Analyze", minval=2, maxval=50)
colorScheme = input.string("Diverging", "Color Scheme", options=["Diverging", "Sequential"])
returnThreshold = input.float(0.0, "Threshold for Display (%)", minval=-100, maxval=100)

// Constants
MONTHS = array.new<string>()
array.push(MONTHS, "Jan")
array.push(MONTHS, "Feb")
array.push(MONTHS, "Mar")
array.push(MONTHS, "Apr")
array.push(MONTHS, "May")
array.push(MONTHS, "Jun")
array.push(MONTHS, "Jul")
array.push(MONTHS, "Aug")
array.push(MONTHS, "Sep")
array.push(MONTHS, "Oct")
array.push(MONTHS, "Nov")
array.push(MONTHS, "Dec")

// Store monthly returns for averaging
var monthlyReturns = array.new<float>()
var monthCounts = array.new<int>()
var monthSums = array.new<float>()

// Initialize on first bar
if barstate.isfirst
    for i = 0 to 11
        array.push(monthlyReturns, 0.0)
        array.push(monthCounts, 0)
        array.push(monthSums, 0.0)

// Calculate month index and monthly returns
currentMonth = month(time) - 1
monthStart = request.security(syminfo.tickerid, "M", open)
monthEnd = close

monthReturn = (monthEnd - monthStart) / monthStart * 100

// Accumulate data for months within lookback period
barAge = (timenow - time) / (365.25 * 24 * 3600 * 1000)
if barAge <= yearLookback
    if barstate.islast
        idx = currentMonth
        array.set(monthSums, idx, array.get(monthSums, idx) + monthReturn)
        array.set(monthCounts, idx, array.get(monthCounts, idx) + 1)

// Calculate averages and plot on last bar only
if barstate.islast
    table cellTable = table.new(position=position.top_right, rows=2, columns=12, bgcolor=color.new(color.gray, 80), frame_color=color.gray, frame_width=1, border_color=color.gray, border_width=1)
    
    for i = 0 to 11
        count = array.get(monthCounts, i)
        avgReturn = count > 0 ? array.get(monthSums, i) / count : 0.0
        
        // Color based on return value
        cellColor = if avgReturn > returnThreshold
            if colorScheme == "Diverging"
                avgReturn > 2.0 ? color.new(color.green, 20) : avgReturn > 0.5 ? color.new(color.lime, 40) : color.new(color.gray, 70)
            else
                color.new(color.blue, math.max(80 - avgReturn * 10, 20))
        else
            if colorScheme == "Diverging"
                avgReturn < -2.0 ? color.new(color.red, 20) : avgReturn < -0.5 ? color.new(color.orange, 40) : color.new(color.gray, 70)
            else
                color.new(color.orange, math.max(80 + avgReturn * 10, 20))
        
        // Label text
        labelText = str.tostring(avgReturn, "0.00") + "%"
        labelColor = avgReturn > 0 ? color.white : color.white
        
        // Add to table
        table.cell(cellTable, i, 0, text=array.get(MONTHS, i), bgcolor=color.new(color.gray, 60), text_color=color.white, text_size=size.small)
        table.cell(cellTable, i, 1, text=labelText, bgcolor=cellColor, text_color=labelColor, text_size=size.small)

// Plot for scale reference
plot(0, color=na)

How the code works

The indicator accumulates monthly return data for a user-specified lookback period (default 10 years). On each bar, it calculates the return for the current month using the monthly open and close price: (close, open) / open * 100.

When the script reaches the last historical bar, it iterates through all 12 months and computes the average return for each. Data from months within the lookback window is stored in three parallel arrays: monthSums (total returns), monthCounts (number of occurrences), and the average is derived by dividing the sum by the count.

The color assignment logic branches based on the selected color scheme. In diverging mode, positive returns shade toward green (stronger performance) and negative returns shade toward red (weaker performance), with intensity proportional to the magnitude. The table displays month abbreviations in the top row and the calculated average return percentage in the bottom row.

The barAge calculation estimates the age of each bar in years, comparing the bar's timestamp to the current time. Only bars falling within the lookback period contribute to the averages. The table is rendered only on the last bar to avoid redundant updates.

Reading it on a chart

The heat strip appears as a small table in the upper right corner, with 12 columns (one per month) and two rows. The color intensity signals historical strength: deep green indicates a month that has historically closed higher on average, while deep red indicates a month that has closed lower. Neutral gray suggests near-zero average returns.

A trader scanning this table might observe, for example, that November and December are shaded green across many markets (a phenomenon sometimes called the "Santa Claus rally" in practitioner convention), while September and October appear orange or red (associated with historical market stress in informal trading lore, though this varies by asset). The exact percentages in the cells allow for precise comparison: if January shows 0.82% average return and July shows -1.34%, the trader can quantify the seasonal gap.

The lookback period slider allows adjustment; reducing it to 5 years emphasizes recent seasonality, while extending it to 20 years smooths out shorter-term noise. The threshold input filters out minimal returns (e.g., setting it to 0.5% highlights only months averaging above 0.5%), focusing attention on statistically meaningful differences.

Limitations

Seasonality analysis assumes that historical patterns will persist, a premise that is not guaranteed. Market structure, participant behavior, algorithmic trading, and regulatory changes can disrupt or reverse seasonal tendencies. A month that has underperformed for 10 years may rebound in the next year without warning.

The indicator measures simple calendar-month returns and cannot account for within-month volatility, skewness, or tail risks. A month with a +2.0% average return might have included severe drawdowns, obscured by the aggregate figure. Seasonal strength also varies significantly by asset class, geography, and time horizon; patterns observed in large-cap equities may not apply to emerging-market currencies or commodity futures.

The code does not adjust for dividends, corporate actions, splits, or inflation, meaning returns are nominal and unadjusted. In Pine Script, calculating precise historical averages across multiple years requires careful handling of data availability; the script relies on request.security() for the monthly open, which may differ on some charts depending on the timeframe and data source.

Finally, small sample sizes in the lookback window (fewer than 10 occurrences of a given month) yield unreliable averages. If a user sets the lookback to 2 years, each month appears only twice, and a single outlier skews the result. The indicator provides no confidence intervals or statistical tests to flag low-sample months.

Key definitions

Seasonal pattern: The tendency for prices or returns to exhibit consistent behavior during the same calendar period across multiple years, independent of current market fundamentals.

Month-of-year effect: A statistical observation that certain months tend to produce higher or lower average returns than others; also called intra-year seasonality.

Lookback period: The historical time window (in years) used to calculate averages; longer periods smooth noise but may miss recent regime shifts.

Average return: The arithmetic mean of the returns observed during a specific calendar month across all years in the lookback period.

Repainting: A behavior where an indicator recalculates past values after new data arrives, creating a false sense of historical accuracy; this indicator avoids that by computing final averages only on the last bar of the historical dataset.

Santa Claus rally: A practitioner-observed pattern in which stock markets tend to rally in late November and December; a form of seasonal belief with no guaranteed future occurrence.

References

  1. Investor's Business Daily / Stock Market Seasonality, "Month-of-Year Stock Returns", IBD Educational Center. Https://www.investors.com/
  2. U.S. Securities and Exchange Commission, "Stock Market Basics", SEC Investor Education & Advocacy Office. Https://www.sec.gov/investor
  3. Bouman, Sven and Jacobsen, Ben, "The Halloween Indicator, 'Sell in May and Go Away': Another Puzzle", American Journal of Political Economy, vol. 105, no. 5, October 2002. https://doi.org/10.1086/341155
  4. Rozeff, Michael S. And Kinney, William R., "Capital Market Seasonality: The Case of Stock Returns", Journal of Financial Economics, vol. 3, no. 4, September 1976. https://doi.org/10.1016/0304-405X(76)90002-0
  5. TradingView, "Pine Script v6 Documentation", Reference Manual. Https://www.tradingview.com/pine-script-docs/

Educational research on historical data only. Not investment advice, not a signal, and never a performance promise. Past results do not predict future performance. Every reference is link-verified before publication and every paper is re-audited weekly against the library's editorial standard.

Last reviewed by the PropLedger research pipeline: 2026-10-04. Educational research on historical data, not financial advice.

Educational research on historical data only. Not investment advice, not a signal, and never a performance promise. Past results do not predict future performance. Every reference is link-verified before publication and every paper is re-audited weekly against the library's editorial standard. Found an error? Email support@prop-ledger.org and the paper is corrected or withdrawn.