Quickfingers Luc base scanner - version 2
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © rex_wolfe

// Quickfingers Luc (QFL) base scanner - Marvin
// Version 2.2 (Marvin)

//@version=5
indicator(title = "Quickfingers Luc base scanner - Marvin", shorttitle = "QFL Marvin", overlay = true, max_lines_count = 500)

// Get user input
factor = input.float(title = "Volatility factor", tooltip = "This value is multiplied by the volatility measured as a percent. The price needs to move up or down by more than the resulting percentage to confirm a base or top respectively. Note: This value is ONLY used if the following tickbox is unchecked.", defval = 5.2, step = 0.1, minval = 0.1)
useManual = input.bool(title = "Use manual-based measures", tooltip = "If unticked, the above volatility factor will be used to confirm bases and tops and the following manual percentage value will be ignored. If ticked, the reverse is true.", defval = false)
move = input.float(title = "Move to confirm previous base or top (%)", tooltip = "Price needs to move up or down by more than this percentage to confirm the previous base or top respectively. Note: This value is ONLY used if the above tickbox is checked.", defval = 5, step = 0.1, minval = 0) / 100

crack = input.float(title = "Crack below base (%)", tooltip = "Price needs to fall more than this percentage below the base to flag a buy signal.", defval = 3, step = 0.1, minval = 0.1) / 100

// Select information to show on the chart
showBuys = input.string(title = "Show buy signals", options = ["First", "All", "None"], tooltip = "Show all buy signals, first buy signal only, or no buy signals.", defval = "First")
showBases = input.bool(title = "Show bases", tooltip = "Shows bases.", defval = true)
showTops = input.bool(title = "Show tops", tooltip = "Shows hidden calculations that are used to find new bases.", defval = false)
showLowest = input.bool(title = "Show simple lowest lows", tooltip = "Shows hidden calculations that are used to find new bases.", defval = false)
showHighest = input.bool(title = "Show simple highest highs", tooltip = "Shows hidden calculations that are used to find the pump from the base.", defval = false)

// Declare variables
var atrp = float(na)
var highestHigh = float(high)
var top = float(high)
var lowestLow = float(low)
var base = float(low)
var buyLimit = float(na)
var buyFlag = bool(false)
var flag = string("first")
var count = int(0)
var saveCount = int(0)
var fill = bool(false)
var labelText = string(na)
var box buyRange = na

// Index the count by one
count := count + 1

// Reset buy limit
buyLimit := na

// Calculate volatility using a hybrid average true range as a percentage and then smoothed
if useManual == false
    atrp := ta.tr / hl2
    move := ta.sma(atrp, 200) * factor
    
// Need to do something different at the start of the time series to seed the variables
if flag == "first"
    if (highestHigh - low ) / highestHigh > move
        flag := "top"
        top := highestHigh
        highestHigh := high
        count := 0
    else if (high - lowestLow) / lowestLow > move
        flag := "base"
        base := lowestLow
        lowestLow := low
        count := 0
        
// Find next TOP
else if flag == "base"
    if low < lowestLow
        lowestLow := low
    if high > highestHigh
        highestHigh := high
        lowestLow := low
        count := 0
    if (highestHigh[1] - low) / highestHigh[1] > move
        // Top is confirmed therefore save details
        flag := "top"
        fill := true
        top := highestHigh[1]
        saveCount := count[1] + 1
        // Reset variables
        highestHigh := high
        lowestLow := low
        count := 0

// Find next BASE
else if flag == "top"
    if high > highestHigh
        highestHigh := high
    if low < lowestLow
        highestHigh := high
        lowestLow := low
        count := 0
    if (high - lowestLow[1]) / lowestLow[1] > move
        // Base is confirmed therefore save details
        flag := "base"
        fill := true
        base := lowestLow[1]
        saveCount := count[1] + 1
        // Reset variables
        highestHigh := high
        lowestLow := low
        count := 0
        buyFlag := false

// Set buy condition
if showBuys == "All" or (showBuys == "First" and buyFlag == false)
    if (base - low) / base > crack 
        buyLimit := base * (1 - crack)
        buyFlag := true

// Plot the optional extra information
plot(showLowest ? lowestLow : na, title = "Simple lowest low", color = color.purple)
plot(showHighest ? highestHigh : na, title = "Simple highest high", color = color.purple)
plot(showTops ? top : na, "Top", color = top == top[1] ? color.new(color.purple, 0) : color.new(color.white, 100))

// Fill the gap between the top and when it was identified with a dotted line
if fill and showTops and flag == "top"
    line.new(bar_index - saveCount, top, bar_index, top, color = color.purple, style=line.style_dotted) // This line causes repainting
    fill := false

// Plot bases
plot(showBases ? base : na, "Base", color = base == base[1] ? color.new(color.blue, 0) : color.new(color.white, 100))

// Fill the gap between the base and when it was identified with a dotted line
if fill and showBases and flag == "base"
    line.new(bar_index - saveCount, base, bar_index, base, color = color.blue, style=line.style_dotted) // This line causes repainting
    fill := false

// Plot buy signals
plotshape(showBuys == "All" ? buyLimit : na, title="Buy flag", text="B", location=location.belowbar, style=shape.labelup, size=size.tiny, color=color.blue, textcolor=color.white)
if showBuys == "First" 
    if buyLimit
        label.new(x = bar_index, y = buyLimit, text = "Buy < " + str.tostring(buyLimit), color = color.blue, style = label.style_label_upper_right, textcolor = color.white)
        buyRange := box.new(left = bar_index, top = buyLimit, right = bar_index + 1, bottom = lowestLow, border_color = color.new(color.blue, 90), bgcolor = color.new(color.blue, 90))
    else if buyFlag == true
        box.set_rightbottom(buyRange, bar_index + 1, lowestLow) // This line causes repainting

// Variable sent to the data window that shows the size of move currently being used to confirm bases and tops
plotchar(move * 100, "Move (%)", "", location.bottom, color = color.new(color.white, 100))

// Send an alert
if buyLimit
    alert("QFL buy " + syminfo.tickerid + " below $" + str.tostring(buyLimit) + ".", freq = alert.freq_once_per_bar)

// Roger, Candy, Elwood, Blacky, Bill. Not yet, Captain Chaos, not yet.