Recessions & crises shading (custom dates & stats)
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © haribotagada

//@version=5
indicator(title = 'Recessions & crises shading', shorttitle='Recessions', overlay=true)

// https://en.wikipedia.org/wiki/List_of_recessions_in_the_United_States
// https://www.nber.org/research/data/us-business-cycle-expansions-and-contractions
// https://fred.stlouisfed.org/series/USRECDP

recessionsInput = input.text_area('2020-02-01,2020-03-31,COVID-19\n' +
      '2007-12-01,2009-05-31,Subprime mortgages\n' +
      '2001-03-01,2001-10-30,Dot-com\n' +
      '1990-07-01,1991-03-01,Oil shock\n' +
      '1981-07-01,1982-11-01,US unemployment\n' +
      '1980-01-01,1980-07-01,Volker\n' +
      '1973-11-01,1975-03-01,OPEC',
      title='Enter each recession/crisis on a new line', tooltip = 'Type comma separated values without spaces:\n<start>,<end>,<name>\nDate format = YYYY-MM-DD', confirm=true)

correctionTypeInput = input.bool(true, 'Measure correction from peak to through. If unticked, uses prices for the provided recession(s) start and end dates')


GRP2= 'Display preferences'
showLabelsInput = input.bool(true, 'Show labels', group = GRP2)
showStatsInput = input.bool(true, 'Show stats', group = GRP2)
colorTheme = input.string('Dark', title='Color theme', options=['Dark', 'Light'], group = GRP2)
headerBkgrColor = switch colorTheme
    'Light' => color.new(#8d8d8d,80) 
    'Dark' => color.new(#1E232E,0) 
headerTxtColor = switch colorTheme
    'Light' => color.new(#000000, 0) 
    'Dark' => color.new(#5f6266, 0)
cellBkgrColor = switch colorTheme
    'Light' => color.new(#ffffff, 0) 
    'Dark' => color.new(color.gray,80)
recessionColor = switch colorTheme
    'Light' => color.new(#d0cccc, 0) 
    'Dark' => color.new(#8d8d8d,80)
textColor = switch colorTheme
    'Light' => color.new(#000000, 0) 
    'Dark' => color.new(#ffffff,60)
statsPositionInput = input.string('Top right', 'Stats position', options=['Bottom left', 'Top center', 'Bottom center', 'Bottom right', 'Top left', 'Top right'], group = GRP2)
statsPosition = switch statsPositionInput
    'Top center' => position.top_center
    'Bottom center' => position.bottom_center
    'Bottom left' => position.bottom_left
    'Bottom right' => position.bottom_right
    'Top left' => position.top_left
    'Top right' => position.top_right
textSizeInput = input.string('Normal', 'Text Size', options=['Auto', 'Tiny', 'Small', 'Normal', 'Large', 'Huge'], group = GRP2)
textSize = switch textSizeInput
    'Auto' => size.auto
    'Tiny' => size.tiny
    'Small' => size.small
    'Normal' => size.normal
    'Large' => size.large
    'Huge' => size.huge

// Helper functions
stringToUnixDate(_date) =>
    dataArray = str.split(_date, '-')
    yearInput = int(str.tonumber(array.get(dataArray, 0))) 
    monthInput = int(str.tonumber(array.get(dataArray, 1)))
    dayInput = int(str.tonumber(array.get(dataArray, 2)))
    timestamp(syminfo.timezone, year = yearInput, month = monthInput, day = dayInput, hour = 0, minute= 0, second=0) // We use int as date inputs and not the raw date string as the timerstamp() function expects a const string if using text input

// Variable declarations
type recession
    int start
    int end
    string name

var recessionsArray = array.new<recession>()
var correctionsArray = array.new_float(na)
var durationsArray = array.new_int(na)
var startBar = 0
var endBar = 0
var startPrice = 0.0
var endPrice = 0.0
var peak = float(na)
var through = float(na)
durationSuffix = timeframe.period

// We only create the recession data array once. No need to recalculate on each bar
if barstate.isfirst
    inputArray = str.split(recessionsInput, '\n')
    for i=0 to array.size(inputArray) - 1
        if array.get(inputArray,i) != ''
            info = str.split(array.get(inputArray,i), ',')
            startDate = stringToUnixDate(array.get(info,0))
            endDate = stringToUnixDate(array.get(info,1))
            recessionLabel = array.get(info,2)
            array.push(recessionsArray, recession.new(startDate, endDate, recessionLabel))

// On each bar, check if within the provided recession dates.
isRecession = false
var recessionIndex = 0
for i=0 to array.size(recessionsArray) - 1
    if time >= array.get(recessionsArray,i).start and time <= array.get(recessionsArray,i).end
        recessionIndex := i
        isRecession := true

// Code block which runs at the start bar of a new recession
if not isRecession[1] and isRecession 
    startBar := bar_index
    startPrice := close
    // initialise peak & thru values using price at start of recession
    peak := close
    through := close

// Code block that runs if the bar is within a recession
if isRecession
    if high > peak
        peak := high
    if low < through
        through := low

// Code block to gather info at end of recession and display label
if isRecession[1] and not isRecession
    endBar := bar_index
    endPrice := close
    duration = endBar - startBar
 
    middle = startBar + int((duration) / 2)
    peakToThrough = 100 * (through - peak) / peak
    startToEndChg = 100 * (endPrice - startPrice) / startPrice
    correction = correctionTypeInput ? peakToThrough : startToEndChg
   
    if showLabelsInput
        recessionLabel = array.get(recessionsArray, recessionIndex).name + '\n' +
              str.format('{0,number,#.#}%', correction) + '\n' +
              str.tostring(duration) + durationSuffix
        label.new(x=middle, y=through, text=recessionLabel, style = label.style_label_up, color = color.new(#ffffff,100),  textcolor= textColor, size=textSize)
    
    // Save correction and duration info to calculate stats
    if showStatsInput
        array.push(correctionsArray, correction)
        array.push(durationsArray, duration)

// Add shading if bar is within a recession
bgcolor(isRecession ? recessionColor : na)

// display stats
var table stats= table.new(statsPosition, 2, 2, border_width = 1)
if barstate.islast and showStatsInput
    if array.size(correctionsArray) > 0 and array.size(durationsArray) > 0
        table.cell(stats, 0, 0, 'Avg correction', text_size=textSize, bgcolor=headerBkgrColor, text_color=headerTxtColor)
        table.cell(stats, 0, 1, 'Avg duration', text_size=textSize, bgcolor=headerBkgrColor, text_color=headerTxtColor)
        table.cell(stats, 1, 0, str.format('{0,number,#.#}%',array.avg(correctionsArray)), text_size=textSize, bgcolor=cellBkgrColor, text_color=textColor)
        table.cell(stats, 1, 1, str.format('{0,number,#.#} ',array.avg(durationsArray)) + durationSuffix, text_size=textSize, bgcolor=cellBkgrColor, text_color=textColor)