Fusion: Pivot Angles and Thresholds
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Koalems

// @version=5

maxBarsBack   = 4999
maxLabels     = 500

indicator(
  title='Fusion: Pivot Angles and Thresholds', 
  shorttitle='Fusion: Angles', 
  max_bars_back     = maxBarsBack,
  max_labels_count  = 500,
  overlay           = true)

deg2RadConst  = math.pi / 180
rad2DegConst  = 180 / math.pi

// ***************************************************************************************************
//
//
// INPUTS
//
//
// ***************************************************************************************************
inputSeparator = '---------------------------------------------------------------------------------------------'
inputDummy = false

// ***************************************************************************************************
// 5100 Strategy: Exit on reverse trend
// ***************************************************************************************************
Group5100 = '5100 Strategy: Exit on reverse trend - SHOW'
stgyReverseTrendShowAngles            = input(true,                                 group=Group5100,                                                                  title='Angle within threshold')
stgyReverseTrendShowSource            = input(true,                                 group=Group5100,                                                                  title='Source')
stgyReverseTrendShowDebug             = input(false,                                group=Group5100,                                                                  title='Debug info')
stgyReverseTrendShowDebugLimit        = input(true,                                 group=Group5100,                                                                  title='  Limit')

Group5110 = '5110 Strategy: Exit on reverse trend - RULES'
tooltipThma  = 'TEHMA, the "T" = the "tripple" in the Tripple Expotential Moving Average. I.e., TEHMA is even more weighted to the most recent price moves than the EHMA.'
tooltipAngle = 'The smaller the angle the sharper the pivot so we are looking for angles smaller than our threshold.'

stgyReverseTrendAngleThresholdHigh    = input.float(70,                             group=Group5110, minval=0, maxval=180, step=0.1,                                  title='Angle °', tooltip=tooltipAngle) * deg2RadConst
stgyReverseTrendAngleThresholdLow     = input.float(70,                             group=Group5110, minval=0, maxval=180, step=0.1,                                  title='Angle °', tooltip=tooltipAngle) * deg2RadConst

Group5120 = '5120 Strategy: Exit on reverse trend - SETTINGS'
stgyReverseTrendSource                = input.source(hl2,                           group=Group5120,                                                                  title='Source')
stgyReverseTrendModeSwitch            = input.string('HMA',                         group=Group5120,           inline='sRTAPM',                                       title='MA variation', options=['EMA', 'EHMA', 'HMA', 'LINREG', 'RMA', 'SMA', 'SWMA', 'TEHMA', 'VWMA', 'WMA'], tooltip=tooltipThma)
stgyReverseTrendLength                = input.int(2,                                group=Group5120, minval=1, inline='sRTAPM',                                       title='Length')
stgyReverseTrendAnglePHighLeft        = input.int(1,                                group=Group5120, minval=1, inline='sRTAPH1',                                      title='Pivot high: Left')
stgyReverseTrendAnglePHighRight       = input.int(1,                                group=Group5120, minval=1, inline='sRTAPH1',                                      title='Right')
stgyReverseTrendAnglePLowLeft         = input.int(1,                                group=Group5120, minval=1, inline='sRTAPH2',                                      title='Pivot Low: Left')
stgyReverseTrendAnglePLowRight        = input.int(1,                                group=Group5120, minval=1, inline='sRTAPH2',                                      title='Right')

// ***************************************************************************************************
// FUNCTIONS
// ***************************************************************************************************

// https://alanhull.com/hull-moving-average
//     Hull Moving Average (HMA) formula
//     Integer(SquareRoot(Period)) WMA [2 x Integer(Period/2) WMA(Price) - Period WMA(Price)]
// THMA is even more weighted to shorter timeframes than the EMA. The 'T' is for Tripple and is like a Tripple EMA
// so TEHMA would be more correct.
HMA (_src, _length) => ta.wma(2 * ta.wma(_src, _length / 2) - ta.wma(_src, _length    ), math.round(math.sqrt(_length)))
EHMA(_src, _length) => ta.ema(2 * ta.ema(_src, _length / 2) - ta.ema(_src, _length    ), math.round(math.sqrt(_length)))
THMA(_src, _length) => ta.wma(3 * ta.wma(_src, _length / 3) - ta.wma(_src, _length / 2) - ta.wma(_src, _length), _length)

HullMode(mode, src, len) =>
    float ma = switch mode
        'HMA'   => HMA (src, len)
        'EHMA'  => EHMA(src, len)
        'TEHMA' => THMA(src, len / 2)
        =>  
            runtime.error("No matching Hull MA type found.")
            float(na)

MaTypes(mode, src, len) =>
    float ma = switch mode
        'EMA'     => ta.ema(src, len)
        'EHMA'    => HullMode(mode, src, len)
        'HMA'     => HullMode(mode, src, len)
        'LINREG'  => ta.linreg(src, len, 0)
        'RMA'     => ta.rma(src, len)
        'SMA'     => ta.sma(src, len)
        "SWMA"    => ta.swma(src)
        'TEHMA'   => HullMode(mode, src, len)
        "VWMA"    => ta.vwma(src, len)
        'WMA'     => ta.wma(src, len)
        =>  
            runtime.error("No matching MA type found.")
            float(na)
    ma

// ***************************************************************************************************
// MAIN
// ***************************************************************************************************
AngleFromXY(x1, y1, x2, y2, x3, y3) =>
    numerator =
      y2 * (x1 - x3) +
      y1 * (x3 - x1) +
      y3 * (x2 - x1)

    demoninator =
      (x2 - x1) * (x1 - x3) + 
      (y2 - y1) * (y1 - y3)

    angle = math.atan(math.abs(numerator / demoninator))

    debugStr = str.format('\nRadians: {0, number, #,##0.0000}', angle)
      +        str.format('\nDegrees: {0, number, #,##0.0000}', angle * rad2DegConst)

    [angle, debugStr]

AngleFromPivot(src, leftLen, rightLen, pivot) =>
    x1 = rightLen
    x2 = rightLen + leftLen
    x3 = 0

    y1 = src[x1]
    y2 = src[x2]
    y3 = src[x3]

    float angle = na
    debugStr    = ''
    if not na(pivot)
        [angle2, debugStr2] = AngleFromXY(x1, y1, x2, y2, x3, y3)
        angle    := angle2
        debugStr := debugStr2

    [angle, debugStr]

sRevTrend = stgyReverseTrendLength == 1
  ? stgyReverseTrendSource
  : MaTypes(stgyReverseTrendModeSwitch, stgyReverseTrendSource, int(stgyReverseTrendLength))

sRTPivotHigh = ta.pivothigh(sRevTrend, stgyReverseTrendAnglePHighLeft, stgyReverseTrendAnglePHighRight)
sRTPivotLow  = ta.pivotlow( sRevTrend, stgyReverseTrendAnglePLowLeft,  stgyReverseTrendAnglePLowRight)

[sRTPivotHighAngle, debugStrHigh] = AngleFromPivot(sRevTrend, stgyReverseTrendAnglePHighLeft, stgyReverseTrendAnglePHighRight, sRTPivotHigh)
[sRTPivotLowAngle,  debugStrLow]  = AngleFromPivot(sRevTrend, stgyReverseTrendAnglePLowLeft,  stgyReverseTrendAnglePLowRight,  sRTPivotLow)

// Remember: The smaller the angle the sharper the pivot so we
//           are looking for angles smaller than our threshold.
sRTPivotHighAngleTriggered = sRTPivotHighAngle <= stgyReverseTrendAngleThresholdHigh
sRTPivotLowAngleTriggered  = sRTPivotLowAngle  <= stgyReverseTrendAngleThresholdLow

// ***************************************************************************************************
// DEBUG
// ***************************************************************************************************
if stgyReverseTrendShowDebug 
  and not na(sRTPivotHigh or sRTPivotLow) 
  and (str.length(debugStrHigh) > 0 or str.length(debugStrLow) > 0)
  and (sRTPivotHighAngleTriggered or sRTPivotLowAngleTriggered or not stgyReverseTrendShowDebugLimit)
  
    t = str.format('Low threshold:  {0, number, #,##0.00}', stgyReverseTrendAngleThresholdLow  * rad2DegConst) + debugStrLow  + '\n\n'  
      + str.format('High threshold: {0, number, #,##0.00}', stgyReverseTrendAngleThresholdHigh * rad2DegConst) + debugStrHigh

    label.new(
      x         = bar_index - (sRTPivotHigh ? stgyReverseTrendAnglePHighRight : stgyReverseTrendAnglePLowRight),
      y         = sRevTrend,
      text      = t,
      color     = color.new(color.black, 50),
      style     = label.style_label_down,
      textcolor = color.white,
      size      = size.normal,
      textalign = text.align_left)

// ***************************************************************************************************
// ALERTS
// ***************************************************************************************************
if sRTPivotHighAngleTriggered
    msg = "Fusion: Angles"
      + "\nHigh pivot sharpe angle"
      + str.format('\nAngle: {0, number, #,##0.00}',                           sRTPivotHighAngle * rad2DegConst)
      + str.format('\nHigh threshold: {0, number, #,##0.00}', stgyReverseTrendAngleThresholdHigh * rad2DegConst)
    alert(msg, alert.freq_once_per_bar)

if sRTPivotLowAngleTriggered
    msg = "Fusion: Angles"
      + "\nLow pivot sharpe angle"
      + str.format('\nAngle: {0, number, #,##0.00}',                          sRTPivotLowAngle * rad2DegConst)
      + str.format('\nLow threshold: {0, number, #,##0.00}', stgyReverseTrendAngleThresholdLow * rad2DegConst)
    alert(msg, alert.freq_once_per_bar)

// ***************************************************************************************************
// SHOW
// ***************************************************************************************************
stgyRevTrendHighColor = color.new(color.red,   20)
stgyRevTrendLowColor  = color.new(color.green, 20)

plotshape(
  stgyReverseTrendShowAngles ? sRTPivotHighAngleTriggered : na,
  style     = shape.arrowdown,
  location  = location.abovebar,
  color     = stgyRevTrendHighColor,
  offset    = -stgyReverseTrendAnglePHighRight,
  size      = size.small,
  title     = 'Angles: High pivot')

plotshape(
  stgyReverseTrendShowAngles ? sRTPivotLowAngleTriggered : na,
  style     = shape.arrowup,
  location  = location.belowbar,
  color     = stgyRevTrendLowColor,
  offset    = -stgyReverseTrendAnglePLowRight,
  size      = size.small,
  title     = 'Angles: Low pivot')

sRevTrendColor = 
  sRevTrend[1] > sRevTrend ? stgyRevTrendHighColor : 
  sRevTrend[1] < sRevTrend ? stgyRevTrendLowColor  : color.new(color.gray, 20)

plot(stgyReverseTrendShowSource or stgyReverseTrendShowDebug ? sRevTrend : na, color=sRevTrendColor , linewidth=2, title='Angles: Source')
