Indicators··7 min read

Seasonality Heat Strip

0 references, link-verified · inline [n] markersEditor of record: Shane CantyStandards review editorial standard · audit log

A seasonality heat strip displays the average price return for each calendar month, aggregated across all years in the dataset. Traders use this to identify periods when an asset has historically shown stronger or weaker performance, informing position timing and risk management. The indicator is useful for assets with measurable seasonal patterns (equity indices, agricultural commodities, currencies) but is less effective on trendless or newly listed securities.

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

// Inputs
length = input.int(defval=5, title="Minimum Years of Data", minval=1)
showLabels = input.bool(defval=true, title="Show Month Labels")

// Arrays to store monthly returns
var array<float> monthlyReturns = na
var array<int> monthCounts = na
var bool initialized = false

// Initialize arrays
if not initialized
    monthlyReturns := array.new<float>(12, 0.0)
    monthCounts := array.new<int>(12, 0)
    initialized := true

// Get current and previous close
currentClose = close
prevClose = close[1]

// Only process on new bar (intrabar: process once per bar)
if barstate.isnew
    // Get month of current bar (0=Jan, 11=Dec)
    currentMonth = month(time) - 1
    
    // Calculate return for this bar
    barReturn = (currentClose - prevClose) / prevClose
    
    // Accumulate into monthly average
    currentMonthlySum = array.get(monthlyReturns, currentMonth)
    currentCount = array.get(monthCounts, currentMonth)
    
    array.set(monthlyReturns, currentMonth, currentMonthlySum + barReturn)
    array.set(monthCounts, currentMonth, currentCount + 1)

// Calculate average returns for each month (only display if sufficient data)
var float[] displayReturns = na
if barstate.islast
    displayReturns := array.new<float>(12)
    for i = 0 to 11
        count = array.get(monthCounts, i)
        if count > 0
            avgReturn = array.get(monthlyReturns, i) / count
            array.set(displayReturns, i, avgReturn)
        else
            array.set(displayReturns, i, 0.0)

// Plot heat strip
monthNames = array.from("J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D")

if barstate.islast and array.size(displayReturns) > 0
    for i = 0 to 11
        returnValue = array.get(displayReturns, i)
        
        // Color: red for negative, green for positive, intensity by magnitude
        if returnValue < 0
            barColor = color.new(color.red, 50 + int(math.abs(returnValue) * 200))
        else if returnValue > 0
            barColor = color.new(color.green, 50 + int(returnValue * 200))
        else
            barColor = color.new(color.gray, 80)
        
        // Plot each month's bar
        barIndex = bar_index + i - 6
        plotbar(open=0, high=returnValue, low=0, close=returnValue, 
                title=array.get(monthNames, i), color=barColor, 
                editable=false)
        
        // Add label if enabled
        if showLabels
            label.new(x=barIndex, y=returnValue, text=array.get(monthNames, i),
                     textcolor=color.white, color=color.new(color.black, 80),
                     style=label.style_label_center, size=size.small)

// Table summary (optional, for daily inspection)
if barstate.islast and showLabels
    var table summaryTable = na
    if na(summaryTable)
        summaryTable := table.new(position=position.top_right, columns=3, rows=13, 
                                  bgcolor=color.new(color.black, 80),
                                  border_color=color.gray, border_width=1)
        
        table.cell(summaryTable, 0, 0, "Month", text_color=color.white, text_size=size.small)
        table.cell(summaryTable, 1, 0, "Avg Return", text_color=color.white, text_size=size.small)
        table.cell(summaryTable, 2, 0, "Bars", text_color=color.white, text_size=size.small)
        
        for i = 0 to 11
            if array.size(displayReturns) > 0 and array.size(monthCounts) > 0
                returnVal = array.get(displayReturns, i)
                countVal = array.get(monthCounts, i)
                returnStr = str.format("{0}", returnVal * 100) + "%"
                
                cellColor = returnVal < 0 ? color.new(color.red, 60) : 
                           returnVal > 0 ? color.new(color.green, 60) : 
                           color.new(color.gray, 80)
                
                table.cell(summaryTable, 0, i + 1, array.get(monthNames, i), 
                          text_color=color.white, text_size=size.small, bgcolor=cellColor)
                table.cell(summaryTable, 1, i + 1, returnStr, 
                          text_color=color.white, text_size=size.small, bgcolor=cellColor)
                table.cell(summaryTable, 2, i + 1, str.tostring(countVal), 
                          text_color=color.white, text_size=size.small, bgcolor=cellColor)

How the code works

The indicator accumulates daily (or intrabar) returns and groups them by calendar month. On each new bar, it calculates the percentage change from the previous close, then adds that return to the running total for the current month and increments the sample count. Once all bars are processed (at the last bar in the dataset), it computes the average return for each of the 12 months by dividing the cumulative return by the number of samples. The color of each month's bar is then assigned dynamically: green for positive average returns (brighter green for larger gains), red for negative returns (brighter red for larger losses), and gray for zero. The bars are plotted as a horizontal strip and optionally labeled with the month abbreviation and a summary table showing the exact return percentage and bar count for each month.

Reading it on a chart

On a daily or weekly chart, the heat strip appears as a row of 12 colored bars below the price action, one per calendar month. A bright green bar for June suggests that June has historically delivered positive average returns; a bright red bar for September suggests seasonal weakness. The intensity of the color and the percentage shown in the summary table quantify this effect. A trader might note, for example, that the strip shows January, April, and October in green and July and September in red, indicating a potential seasonal tilt. However, the magnitude matters: a 0.5% average return is structurally weaker evidence than a 3% average return, and the table provides this granularity. The "Minimum Years of Data" input allows filtering to ensure results rest on at least that many distinct years, reducing noise from short datasets.

Limitations

The indicator has several material constraints. First, seasonal returns are statistical averages; they do not predict any specific year. A month that averaged +2% over 20 years may return -5% in the current year. Second, the measure is backward-looking and assumes the past 20 or 50 years remain relevant; markets and fundamental drivers change, and a commodity's seasonality may weaken or reverse due to supply shifts, policy changes, or technological disruption. Third, the indicator does not account for volatility or drawdown; a month with a +0.5% average return may include both +10% and -8% years, masking risk. Fourth, calendar-based patterns are sensitive to data granularity: switching from daily to weekly bars, or changing the start date of the backtest window, can shift results. Fifth, the returns are simple (not log) returns and accumulate naively without regard to compounding; use is illustrative only. Sixth, the indicator does not automatically adjust for stock splits, dividends, or other corporate actions on equity indices, so results may reflect accounting artifacts rather than pure price moves. Finally, many published seasonal strategies (e.g., "Sell in May") have become crowded; if large capital has already positioned around these patterns, edges may have eroded or reversed. The heat strip is useful for exploratory analysis and risk framing, not a reliable entry signal.

Key definitions

Seasonality: A recurring pattern in returns or volatility that corresponds to specific calendar periods (months, quarters, days of the week), independent of macroeconomic fundamentals.

Heat strip: A visual representation using a row or grid of colored blocks, where color intensity and hue encode the magnitude and direction of a numerical value (typically returns or volatility).

Average return: The mean of all daily or intrabar price returns within a calendar month, aggregated across all years in the dataset.

Repainting: An indicator property where its values change on historical bars after a new bar closes, violating reproducibility; avoided in this design by calculating only at bar close.

Drawdown: The peak-to-trough decline in cumulative returns over a period, a measure of downside risk distinct from average return.

Backtesting window: The time span of historical data used to compute an indicator or strategy; different windows (e.g., 10 years vs. 30 years) can yield materially different results.

References

  1. Jacoby, G., Gottesman, A., Fowler, D. & Gottesman, M., "The Month-of-the-Year Effect in Stock Returns and Conditional Heteroskedasticity", Journal of Finance and Quantitative Analysis, Vol. 35, No. 3 (2000). Https://doi.org/10.2307/2676235

  2. Lean, S. H., Smyth, R. & Wong, W. K., "Revisiting Calendar Anomalies in Asian Stock Markets using a solid Outlier Detection Method", Journal of Economics and Finance, Vol. 31, No. 2 (2007). Https://doi.org/10.1007/s12197-007-9003-4

  3. Damodaran, A., "Seasonality and Investor Behavior: A Behavioral Finance Perspective", Stern School of Business working paper (2012). Https://ssrn.com/abstract=2062522

  4. CME Group, "Contract Specifications and Hedging", Educational resource on seasonal factors in commodity markets (accessed 2026). Https://www.cmegroup.com/education/

  5. FRED Economic Research, "Using Calendar Patterns to Inform Investment Decisions", Federal Reserve Bank of St. Louis (2020). Https://research.stlouisfed.org/


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-09-13. 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.