lib_retracement_patterns
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © robbatt

//@version=5

import robbatt/lib_plot_objects/32 as D
import robbatt/lib_pivot/45 as P
import robbatt/lib_retracement_label/2 as RL
import robbatt/lib_zig/16 as ZZ
import robbatt/lib_priceaction/6 as PA
import robbatt/lib_log/6 as LOG

//@description types and functions for XABCD pattern detection and plotting
library("lib_retracement_patterns", overlay = true)


//////////////////////////////////////////////////////////////////////////////////////
//#region                               HELPERS
//////////////////////////////////////////////////////////////////////////////////////

method replace_all(string source, string target, string replacement) =>
    str.replace_all(source, target, replacement)

//////////////////////////////////////////////////////////////////////////////////////
//#endregion                               HELPERS
//////////////////////////////////////////////////////////////////////////////////////


//////////////////////////////////////////////////////////////////////////////////////
//#region                               XABC SETUP
//////////////////////////////////////////////////////////////////////////////////////

type Setup
    string id
    P.Pivot X
    P.Pivot A
    P.Pivot B
    P.Pivot C

setup_id(P.Pivot X, P.Pivot A, P.Pivot B, P.Pivot C) =>
    str.format('X-{0}_A-{1}_B-{2}_C-{3}', X.point.index, A.point.index, B.point.index, C.point.index)

pattern_id(string name, Setup setup) =>
    str.format('{0}_{1}_bi-{2}', name, setup.id, bar_index)

//////////////////////////////////////////////////////////////////////////////////////
//#endregion                               XABC SETUP
//////////////////////////////////////////////////////////////////////////////////////


//////////////////////////////////////////////////////////////////////////////////////
//#region                               PATTERN
//////////////////////////////////////////////////////////////////////////////////////

//@type type to hold retracement ratios and tolerances for this pattern, as well as targets for trades
export type Pattern
    string name
    // setup
    float XABmin = na
    float XABmax = na
    float ABCmin = na
    float ABCmax = na
    float BCDmin = na
    float BCDmax = na

    // D target ranges
    float XBDmin = na
    float XBDmax = na
    float XADmin = na
    float XADmax = na
    float XCDmin = na
    float XCDmax = na

    // trade targets
    float tp1 = na
    float tp2 = na
    float sl = na

    // tolerances
    float tolerance_Bmin = 0
    float tolerance_Bmax = 0
    float tolerance_Cmin = 0
    float tolerance_Cmax = 0
    float tolerance_Dmin = 0
    float tolerance_Dmax = 0

    // another pattern with tolerances applied
    Pattern tolerances = na

//@function sets tolerances for B, C and D retracements. This creates another Pattern instance that is set as tolerances field on the original and will be used for detection instead of the original ratios.
export method set_tolerances(Pattern this, float tolerance_Bmin = 0, float tolerance_Bmax = 0, float tolerance_Cmin = 0, float tolerance_Cmax = 0, float tolerance_Dmin = 0, float tolerance_Dmax = 0) =>
    if not na(this)
        this.tolerance_Bmin := tolerance_Bmin
        this.tolerance_Bmax := tolerance_Bmax
        this.tolerance_Cmin := tolerance_Cmin
        this.tolerance_Cmax := tolerance_Cmax
        this.tolerance_Dmin := tolerance_Dmin
        this.tolerance_Dmax := tolerance_Dmax
        this.tolerances := Pattern.new(
         XABmin = this.XABmin - tolerance_Bmin,
         XABmax = this.XABmax + tolerance_Bmax,
         ABCmin = this.ABCmin - tolerance_Cmin,
         ABCmax = this.ABCmax + tolerance_Cmax,
         BCDmin = this.BCDmin - tolerance_Dmin,
         BCDmax = this.BCDmax + tolerance_Dmax,
         XBDmin = this.XBDmin - tolerance_Dmin,
         XBDmax = this.XBDmax + tolerance_Dmax,
         XADmin = this.XADmin - tolerance_Dmin,
         XADmax = this.XADmax + tolerance_Dmax,
         XCDmin = this.XCDmin - tolerance_Dmin,
         XCDmax = this.XCDmax + tolerance_Dmax
         )
    this
    
check_pattern_point(pivots, start_offset, offset, max_age_idx, float[] checked_starts_max_price_result, end_pivot) =>
    P.Pivot pivot = na
    string interrupt = na
    float starts_max_price = checked_starts_max_price_result.get(0)

    if offset >= pivots.size() // out of pivots
        interrupt := 'out of pivots'
    else 
        start_pivot = pivots.get(offset)
        if start_pivot.point.index < max_age_idx // out of bar range
            interrupt := 'out of bar range'
        else if start_offset % 2 == offset % 2 // point in direction
            if not na(starts_max_price) and start_pivot.exceeded_by(starts_max_price) // pattern broken by intermediate pivot
                interrupt := na
                // just skip
            else
                pivot := start_pivot
                checked_starts_max_price_result.set(0, 
                 na(starts_max_price) ? pivot.point.price
                 : pivot.direction() == 1 ? math.max(starts_max_price, pivot.point.price)
                 : math.min(starts_max_price, pivot.point.price)
                 )
        else if not start_pivot.exceeded_by(end_pivot) // checking intermediate pivot, same direction as end (not start), if pattern broken by intermediate pivot
            interrupt := 'pattern broken by intermediate pivot exceeded by endpoint'

    [pivot, interrupt]

method check_target_ranges_overlap(Pattern this, float priceX, float priceA, float priceB, float priceC, int dir) =>
    mins = array.new<float>()
    maxs = array.new<float>()
    
    if not na(this.BCDmin) and not na(this.BCDmax)
        mins.push(PA.target_ratio_price(priceB, priceC, this.BCDmin))
        maxs.push(PA.target_ratio_price(priceB, priceC, this.BCDmax))
    if not na(this.XADmin) and not na(this.XADmax)
        mins.push(PA.target_ratio_price(priceX, priceA, this.XADmin))
        maxs.push(PA.target_ratio_price(priceX, priceA, this.XADmax))   
    if not na(this.XBDmin) and not na(this.XBDmax)
        mins.push(PA.target_ratio_price(priceX, priceB, this.XBDmin))
        maxs.push(PA.target_ratio_price(priceX, priceB, this.XBDmax))   
    if not na(this.XCDmin) and not na(this.XCDmax)
        mins.push(PA.target_ratio_price(priceX, priceC, this.XCDmin))
        maxs.push(PA.target_ratio_price(priceX, priceC, this.XCDmax))   

    // target area validation
    min = dir == 1 ? mins.min() : mins.max()
    max = dir == 1 ? maxs.max() : maxs.min()
    targets_overlap = dir == 1 ? min >= max : min <= max
    targets_overlap := targets_overlap and min > 0 // ensure we don't target values under 0

    [targets_overlap, min, max, mins, maxs]



//////////////////////////////////////////////////////////////////////////////////////
//#endregion                               PATTERN
//////////////////////////////////////////////////////////////////////////////////////
    

//////////////////////////////////////////////////////////////////////////////////////
//#region                               PATTERN CONFIG
//////////////////////////////////////////////////////////////////////////////////////

//@type allows control of pattern plotting shape and colors, as well as settings for hiding overlapped patterns etc.
export type Config
    D.LineArgs line_bull
    D.LineArgs line_bear
    D.LineArgs line_bull_live
    D.LineArgs line_bear_live
    color fill_bull
    color fill_bear
    color fill_muted
    color color_muted
    D.LabelArgs label_args_name_up
    D.LabelArgs label_args_name_down
    D.LabelArgs label_args_xabcd_down
    D.LabelArgs label_args_xabcd_up
    D.LineArgs label_args_retrace_line
    D.LabelArgs retrace_down
    D.LabelArgs retrace_up
    D.LineArgs line_Dtarget
    D.LineArgs line_args_completion
    D.LineArgs line_args_tp1
    D.LineArgs line_args_tp2
    D.LineArgs line_args_sl
    D.LabelArgs label_args_completion
    D.LabelArgs label_args_tp1
    D.LabelArgs label_args_tp2
    D.LabelArgs label_args_sl
    D.LabelArgs label_args_terminal_bar_up
    D.LabelArgs label_args_terminal_bar_down
    string terminal_bar_up_char = '▲'
    string terminal_bar_down_char = '▼'
    bool draw_point_labels = true
    bool draw_retracements = true
    bool draw_target_range = true
    bool draw_levels = true
    bool hide_shorter_if_shared_legs_greater_than_max = true
    bool hide_engulfed_pattern = false
    bool hide_engulfed_pattern_of_same_type = false
    bool hide_longer_pattern_with_same_X = false
    bool mute_previous_pattern_when_next_overlaps = false
    bool keep_failed_patterns = false


//@function generates a complete Config for Pattern plotting from minimal information and defaults.
//@param pattern_line_args (default: na)
//@param pattern_point_args (default: na)
//@param name_label_args (default: na)
//@param retracement_line_args (default: na)
//@param retracement_label_args (default: na)
//@param line_args_Dtarget (default: na)
//@param line_args_completion (default: na)
//@param line_args_tp1 (default: na)
//@param line_args_tp2 (default: na)
//@param line_args_sl (default: na)
//@param label_args_completion (default: na)
//@param label_args_tp1 (default: na)
//@param label_args_tp2 (default: na)
//@param label_args_sl (default: na)
//@param label_terminal (default: na)
//@param label_terminal_up_char (default: '▲',)
//@param label_terminal_down_char (default: '▼',)
//@param color_bull (default: color.green)
//@param color_bear (default: color.red)
//@param color_muted (default: color.gray)
//@param fill_opacity (default: 15)
//@param draw_point_labels (default: true)
//@param draw_retracements (default: true)
//@param draw_target_range (default: true)
//@param draw_levels (default: true)
//@param hide_shorter_if_shared_legs_greater_than_max (default: true)
//@param hide_engulfed_pattern (default: false)
//@param hide_engulfed_pattern_of_same_type (default: true)
//@param hide_longer_pattern_with_same_X (default: false)
//@param mute_previous_pattern_when_next_overlaps (default: false)
export create_config(D.LineArgs pattern_line_args = na, D.LabelArgs pattern_point_args = na, D.LabelArgs name_label_args = na, D.LineArgs retracement_line_args = na, D.LabelArgs retracement_label_args = na, D.LineArgs line_args_Dtarget = na, D.LineArgs line_args_completion = na, D.LineArgs line_args_tp1 = na, D.LineArgs line_args_tp2 = na, D.LineArgs line_args_sl = na, D.LabelArgs label_args_completion = na, D.LabelArgs label_args_tp1 = na, D.LabelArgs label_args_tp2 = na, D.LabelArgs label_args_sl = na, D.LabelArgs label_terminal = na, string label_terminal_up_char = '▲', string label_terminal_down_char = '▼', color color_bull = color.green, color color_bear = color.red, color color_muted = color.gray, int fill_opacity = 15, bool draw_point_labels = true, bool draw_retracements = true, bool draw_target_range = true, bool draw_levels = true, bool hide_shorter_if_shared_legs_greater_than_max = true, bool hide_engulfed_pattern = false, bool hide_engulfed_pattern_of_same_type = true, bool hide_longer_pattern_with_same_X = false, bool mute_previous_pattern_when_next_overlaps = false, bool keep_failed_patterns = false) =>
    Config.new(
     pattern_line_args.copy().set_line_color(color_bull),
     pattern_line_args.copy().set_line_color(color_bear),
     pattern_line_args.copy().set_line_color(color_bull).set_style(line.style_dashed),
     pattern_line_args.copy().set_line_color(color_bear).set_style(line.style_dashed),
     color.new(color_bull, 100 - fill_opacity),
     color.new(color_bear, 100 - fill_opacity),
     color.new(color_muted, 100 - 5),  // muted fills
     color.new(color_muted, 100 - 30), // muted lines
     name_label_args.copy().set_style(label.style_label_up).set_text_color(color_bull),
     name_label_args.copy().set_style(label.style_label_down).set_text_color(color_bear),
     pattern_point_args.copy().set_style(label.style_label_down),
     pattern_point_args.copy().set_style(label.style_label_up),
     retracement_line_args,
     retracement_label_args.copy().set_style(label.style_label_down),
     retracement_label_args.copy().set_style(label.style_label_up),
     line_args_Dtarget,
     line_args_completion,
     line_args_tp1,
     line_args_tp2,
     line_args_sl,
     label_args_completion,
     label_args_tp1,
     label_args_tp2,
     label_args_sl,
     label_terminal.copy().set_text_color(color_bull),
     label_terminal.copy().set_text_color(color_bear),
     label_terminal_up_char,
     label_terminal_down_char,
     draw_point_labels,
     draw_retracements, 
     draw_target_range,
     draw_levels,
     hide_shorter_if_shared_legs_greater_than_max,
     hide_engulfed_pattern,
     hide_engulfed_pattern_of_same_type,
     hide_longer_pattern_with_same_X,
     mute_previous_pattern_when_next_overlaps,
     keep_failed_patterns
     )


//////////////////////////////////////////////////////////////////////////////////////
//#endregion                               PATTERN CONFIG
//////////////////////////////////////////////////////////////////////////////////////


//////////////////////////////////////////////////////////////////////////////////////
//#region                               PATTERN MATCH
//////////////////////////////////////////////////////////////////////////////////////

//@type holds all information on a Pattern and a successful match in the chart. Includes XABCD pivot points as well as all Line and Label objects to draw it
export type Match
    Pattern pattern
    string id
    string name
    P.Pivot X
    P.Pivot A
    P.Pivot B
    P.Pivot C
    float Dmin_price = na
    float Dmax_price = na
    P.Pivot D = na
    P.Pivot Dmin = na
    P.Pivot Dmax = na
    D.Line[] static_lines = na
    D.Line[] dynamic_lines = na // always update second point (idx:1)
    RL.RetracementLabel[] static_labels = na
    RL.RetracementLabel[] dynamic_labels = na // always update second point (idx:1)
    D.Label[] dynamic_point_labels = na // always update second point (idx:1)
    D.Label[] static_point_labels = na // 0:X , 1:A , 2:B , 3:C , 4:D , 5:Dmin , 6:Dmax // alwasy update last 3 as long as not is_complete / not is_expired / not is_invalidated
    D.LineFill[] fills = na
    D.Label name_label = na

    float hhll = na
    float price_offset_B = na
    float tp1 = na
    float tp2 = na
    float sl = na // a.k.a HOP / LRP
    int terminal_bar_idx = na
    int terminal_bar_time = na

    int length = 0
    float height = 0
    float XB_BD_ratio = 0

    bool is_complete = false
    bool is_expired = false
    bool is_invalidated = false
    bool is_hidden = false
    bool is_muted = false
    bool is_engulfed_by_completed = false // helps cut down plotting iterations
    bool is_overlapped_by_shared_legs = false // hide due to overlap by longer/higher pattern with more than max allowed shared legs
    bool is_overlapped_by_next = false // mute due to overlap by next pattern

//@function return the direction of this Match, determined by the C Point of this pattern
export method direction(Match this) =>
    na(this) ? 0 : na(this.C) ? 0 : this.C.direction()

//@function return the length of this pattern, determined by the distance between X and D point
export method length(Match this) =>
    na(this) ? 0 : na(this.X) or na (this.D) ? 0 : this.D.point.index - this.X.point.index

//@function return the height of this pattern, determined by the distance between the biggest distance between A/C and X/D
export method height(Match this) =>
    na(this) ? 0 : na(this.X) or na (this.A) or na (this.C) or na (this.D) ? 0 : 
     this.direction() == 1 
     ? math.max(this.A.point.price, this.C.point.price) - math.min(this.X.point.price, this.D.point.price) 
     : math.max(this.X.point.price, this.D.point.price) - math.min(this.A.point.price, this.C.point.price)

//@function returns true if not complete, not expired and not invalidated
export method is_forming(Match this) =>
    na(this) ? false : not this.is_complete and not this.is_expired and not this.is_invalidated

//@function return a string representation of this Match
export method tostring(Match this) =>
    na(this) ? 'null' : '{' + str.format('"id": "{0}", "Dmin_price": {1}, "Dmax_price": {2}, "is_complete": {3}, "is_expired": {4}, "is_invalidated": {5}', this.id, this.Dmin_price, this.Dmax_price, this.is_complete, this.is_expired, this.is_invalidated) + '}'
    // na(this) ? 'null' : this.id // TODO change back

//@function return a string representation of all Matches in this map
export method tostring(map<string, Match> this) =>
    if na(this)
        'null'
    else
        result = array.new<string>()
        for i in this.values()
            result.push(i.tostring())
        '[' + result.join(', ') + ']'


calc_XB_BD_ratio(idx_X, idx_B, idx_D) =>
    (idx_D - idx_B) / (idx_B - idx_X)

method XB_BD_ratio(Match this) =>
    na(this) ? 0 : calc_XB_BD_ratio (this.X.point.index, this.B.point.index, this.D.point.index)


//@function remove complete and expired Matches from this map
export remove_complete_and_expired(map<string, Match> this) =>
    if na(this) ? false : this.size() > 0
        for match_id in this.keys()
            match = this.get(match_id)
            if match.is_complete or match.is_expired
                this.remove(match_id)
    this

//@function add a match to this map, with it's id as key, silent skip if this or item is na
export method add(map<string, Match> this, Match item) =>
    if not na(this) and not na(item)
        this.put(item.id, item)
    this

//@function remove Matches from this map that fit the parameter criteria
//@param when_pattern_len_exceeds (default: na) remove a pattern if it's longer than this value
//@param when_older_than_idx (default: na) remove a pattern if it's X bar_index is less than this value
//@param when_BD_becomes_unproportional_ratio (default: na) remove a pattern if it's BCD part becomes unproportionally bigger than its XAB part
//@param when_completed (default: false) remove a pattern if it is completed
//@param when_invalidated (default: false) remove a pattern if it is invalidated (by D exceeding C in direction of C)
//@param when_engulfed_by_completed (default: false) remove a pattern if it is engulfed by another completed pattern
//@param when_overlapped_by_shared_legs (default: false) remove a pattern if it is overlapped by another, that shares a certain amount of legs with it
//@param handled_matches_buffer (default: na) all patterns that were removed will be added to this buffer (if not passed from caller, nothing happens)
//@param handled_completed_buffer (default: na) completed patterns will also be added to this buffer (if not passed from caller, nothing happens)
//@param invalid_buffer (default: na) invalidated patterns will also be added to this buffer (if not passed from caller, nothing happens)
method cleanup(map<string, Match> matches, int when_pattern_len_exceeds = na, int when_older_than_idx = na, simple float when_BD_becomes_unproportional_ratio = na, simple bool when_completed = false, simple bool when_invalidated = false, simple bool when_engulfed_by_completed = false, simple bool when_overlapped_by_shared_legs = false, map<string, Match> handled_matches_buffer = na, map<string, Match> handled_completed_buffer = na, map<string, Match> invalid_buffer = na) =>  
    if not na(matches)
        for match_id in matches.keys()
            match = matches.get(match_id)
            completed = when_completed and match.is_complete
            expired = na(when_older_than_idx) ? false : match.X.point.index < when_older_than_idx
            exceeded = na(when_pattern_len_exceeds) ? false : match.D.point.index - match.X.point.index > when_pattern_len_exceeds
            unproportional_BD_ratio = na(when_BD_becomes_unproportional_ratio) ? false : (match.D.point.index - match.B.point.index) / (match.B.point.index - match.X.point.index) > when_BD_becomes_unproportional_ratio
            invalidated = not when_invalidated ? false : match.direction() == 1 ? match.C.exceeded_by(high) or open < match.Dmax_price : match.C.exceeded_by(low) or open > match.Dmax_price
            engulfed = when_engulfed_by_completed and match.is_engulfed_by_completed
            overlapped_by_shared_legs = when_overlapped_by_shared_legs and match.is_overlapped_by_shared_legs

            if completed
                // if not na(handled_buffer)
                    // label.new(bar_index, low, str.format('c complete\n{0}', match.pattern.name), style = label.style_label_up, textcolor = color.white)
                complete_match = matches.remove(match_id)
                handled_completed_buffer.add(complete_match)
                handled_matches_buffer.add(complete_match)
            else if engulfed or overlapped_by_shared_legs
                handled_matches_buffer.add(matches.remove(match_id))
            else if expired or exceeded
                exp = matches.remove(match_id)
                exp.is_expired := true
                // if not na(handled_buffer)
                    // label.new(bar_index, low, str.format('c expired\n{0}', match.pattern.name), style = label.style_label_up, textcolor = color.white)
                handled_matches_buffer.add(exp)
                invalid_buffer.add(exp)
            else if invalidated or unproportional_BD_ratio
                inv = matches.remove(match_id)
                // label.new(bar_index, inv.B.point.price, str.format('invalidated\n{0}\ninvalid: {1}\nunproportional: {2}', inv.id, invalidated, unproportional_BD_ratio), style = label.style_label_up, textcolor = color.white, color = color.new(color.blue, 50))
                inv.is_invalidated := true
                handled_matches_buffer.add(inv)
                invalid_buffer.add(inv)
    matches


//@function checks if this Match is engulfed by the other
export method is_engulfed_by (Match this, Match other) =>
    if na(this) or na(other)
        false
    else 
        within_other_pattern = other.X.point.index <= this.X.point.index and this.D.point.index <= other.D.point.index
        same_start_and_end = other.X.point.index == this.X.point.index and this.D.point.index == other.D.point.index
        BC_shorter = (this.C.point.index - this.B.point.index) < (other.C.point.index - other.B.point.index)
        
        within_other_pattern and 
         (
         not same_start_and_end
         or this.height < other.height
         or (this.height == other.height) and BC_shorter
         )
 
method is_overlapped_by_shared_legs(Match this, Match other, int max_shared_legs = 1, string[] debug_log) =>

    is_overlapped = false
        
    if not na(this) and not na(other)
        if this.id != other.id
            shared_legs = 0
            shared_legs += this.X.point.index == other.X.point.index and this.A.point.index == other.A.point.index ? 1 : 0
            shared_legs += this.A.point.index == other.A.point.index and this.B.point.index == other.B.point.index ? 1 : 0
            shared_legs += this.B.point.index == other.B.point.index and this.C.point.index == other.C.point.index ? 1 : 0
            shared_legs += this.C.point.index == other.C.point.index and this.D.point.index == other.D.point.index ? 1 : 0
            if shared_legs > max_shared_legs
                is_overlapped := this.length < other.length
                 or this.length == other.length and 
                 (
                 this.height < other.height
                 or this.height == other.height and this.price_offset_B > other.price_offset_B
                 )      
                if not na(debug_log)
                    debug_log.push(str.format('{0} has {1} shared legs with {2} | len: {3}, other: {4} | overlapped: {5}', this.id, shared_legs, other.id, this.length, other.length, is_overlapped))
    
    is_overlapped


method is_overlapped_by_next(Match this, Match next) =>
    na(this) or na(next) ? false : this.X.point.index < next.X.point.index and this.D.point.index < next.D.point.index

method update_overlapped(map<string, Match> this, Match just_completed, int max_shared_legs = 1, string[] debug_log) =>
    if not na(this) and not na(just_completed)
        for [other_id, other] in this
            if just_completed.id != other_id
                if other.is_engulfed_by(just_completed)
                    other.is_engulfed_by_completed := true
                    if not na(debug_log)
                        debug_log.push(str.format('{0} completed, engulfs {1}', just_completed.id, other.id))
                if just_completed.is_engulfed_by(other)
                    just_completed.is_engulfed_by_completed := true
                    if not na(debug_log)
                        debug_log.push(str.format('{0} completed, engulfed by {1}', just_completed.id, other.id))

                if other.is_overlapped_by_shared_legs(just_completed, max_shared_legs, debug_log)
                    other.is_overlapped_by_shared_legs := true
                    if not na(debug_log)
                        debug_log.push(str.format('{0} completed, overlaps (shared legs) {1}', just_completed.id, other.id))
                if just_completed.is_overlapped_by_shared_legs(other, max_shared_legs, debug_log)
                    just_completed.is_overlapped_by_shared_legs := true
                    if not na(debug_log)
                        debug_log.push(str.format('{0} completed, overlapped by (shared legs) {1}', just_completed.id, other.id))

                if other.is_overlapped_by_next(just_completed)
                    other.is_overlapped_by_next := true
                    if not na(debug_log)
                        debug_log.push(str.format('{0} completed, overlaps (previous) {1}', just_completed.id, other.id))
                if just_completed.is_overlapped_by_next(other)
                    just_completed.is_overlapped_by_next := true
                    if not na(debug_log)
                        debug_log.push(str.format('{0} completed, overlapped by (next) {1}', just_completed.id, other.id))


method match(Pattern pattern, Setup setup, string[] debug_log) =>

    Match match = na

    P.Pivot X = setup.X
    P.Pivot A = setup.A
    P.Pivot B = setup.B
    P.Pivot C = setup.C

    int dir = C.direction()
    string prefix = dir == 1 ? "Bullish" : "Bearish"      
    string name = prefix + ' ' + pattern.name
    Pattern tolerances = na(pattern.tolerances) ? pattern : pattern.tolerances

    float priceX = X.point.price    
    float priceA = A.point.price
    float priceB = na
    float priceC = na
    bool XAB_price_inRange = false
    bool ABC_price_inRange = false

    float priceB_min = PA.target_ratio_price(priceX, priceA, tolerances.XABmin)
    float priceB_max = PA.target_ratio_price(priceX, priceA, tolerances.XABmax)
    if pattern.XABmin != pattern.XABmax // explicit retracement -> therefore price range for B
        priceB := B.point.price
        XAB_price_inRange := PA.in_range(priceB, priceB_min, priceB_max)
    else // explicit retracement for B -> whole candle allowed +- tolerance above/below (OUR USE CASE)
        priceB := PA.target_ratio_price(priceX, priceA, pattern.XABmin)
        // XAB_price_inRange := PA.range_overlap(priceB_min, priceB_max, B.candle.l, B.candle.h)
        XAB_price_inRange := PA.in_range(priceB, priceB_min, priceB_max) and PA.in_range(B.point.price, priceB_min, priceB_max) 

    float priceC_min = PA.target_ratio_price(priceA, B.point.price, tolerances.ABCmin)
    float priceC_max = PA.target_ratio_price(priceA, B.point.price, tolerances.ABCmax)
    float priceC_ideal_min = PA.target_ratio_price(priceA, priceB, tolerances.ABCmin)
    float priceC_ideal_max = PA.target_ratio_price(priceA, priceB, tolerances.ABCmax)
    if pattern.ABCmin != pattern.ABCmax // explicit retracement -> therefore price range for C (OUR USE CASE)
        priceC := C.point.price
        ABC_price_inRange := PA.in_range(priceC, priceC_min, priceC_max) and PA.in_range(priceC, priceC_ideal_min, priceC_ideal_max)
    else // explicit retracement for C -> whole candle allowed +- tolerance above/below
        priceC := PA.target_ratio_price(priceA, B.point.price, pattern.ABCmin)
        // ABC_price_inRange := PA.range_overlap(priceC_min, priceC_max, C.candle.l, C.candle.h)
        ABC_price_inRange := PA.in_range(priceC, priceC_min, priceC_max) and PA.in_range(priceC, priceC_ideal_min, priceC_ideal_max)

    // retracements must be calculated here, as they might be off for different pattern types
    float XAB = PA.retracement_ratio(X.point.price, A.point.price, priceB)
    float ABC = PA.retracement_ratio(A.point.price, priceB, priceC)
    bool XAB_inRange = PA.in_range(XAB, tolerances.XABmin, tolerances.XABmax)
    bool ABC_inRange = PA.in_range(ABC, tolerances.ABCmin, tolerances.ABCmax)

    // retracements must be calculated here, as they might be off for different pattern types
    bool xabc_match = XAB_inRange and ABC_inRange and XAB_price_inRange and ABC_price_inRange// Design decision: using the ideal price level (priceB) as origin for allowed retracement range of C, not the actual price (B.point.price)

    if xabc_match
        // check possible D within all target ranges
        [targets_overlap, Dmin_price, Dmax_price, mins, maxs] = tolerances.check_target_ranges_overlap(priceX, priceA, priceB, C.point.price, dir)

        // PATTERN FOUND, CHECKS PASSED
        if targets_overlap
            match := Match.new(pattern, pattern_id(prefix+'_'+pattern.name,setup), name,
             X.copy(),
             A.copy(),
             pattern.XABmin != pattern.XABmax ? B.copy() : P.Pivot.new(chart.point.new(B.point.time, B.point.index, priceB), candle = B.candle),
             pattern.ABCmin != pattern.ABCmax ? C.copy() : P.Pivot.new(chart.point.new(C.point.time, C.point.index, priceC), candle = C.candle), 
             Dmin_price, Dmax_price)
            match.A.update_relation(match.X)
            match.B.update_relation(match.A)
            match.C.update_relation(match.B)
            match.D := C.create_next(time, bar_index, close)
            match.Dmin := C.create_next(time, bar_index, Dmin_price)
            match.Dmax := C.create_next(time, bar_index, Dmax_price)
            match.hhll := (match.A.exceeded_by(match.C) ? match.C : match.A).point.price
            match.price_offset_B := math.abs(B.point.price - priceB)
            match.static_lines := array.new<D.Line>()
            match.dynamic_lines := array.new<D.Line>()
            match.static_labels := array.new<RL.RetracementLabel>()
            match.dynamic_labels := array.new<RL.RetracementLabel>()
            match.static_point_labels := array.new<D.Label>()
            match.dynamic_point_labels := array.new<D.Label>()
            match.fills := array.new<D.LineFill>()
            
            if not na(debug_log)
                label.new(bar_index, close, str.format('XABC\n{0}', match.id.replace_all('_','\n')), textcolor = color.white)
                debug_log.push(str.format('XABC {0} | mins: {1} | maxs: {2} | Dmin_price: {3} | Dmax_price: {4}', match.id, mins, maxs, Dmin_price, Dmax_price))
        else if not targets_overlap and not na(debug_log)
            debug_log.push(str.format('{0} - target range check failed | mins: {1} | maxs: {2} | Dmin_price: {3} | Dmax_price: {4}', setup.id +' '+name, mins, maxs, Dmin_price, Dmax_price))        
    else if not na(debug_log)
        msg = str.format('{0} - pattern match failed', setup.id +' '+name)

        if not XAB_price_inRange
            msg += str.format('| B price ({0}) not in range ({1}-{2}) for tolerance range({3}-{4})', B.point.price, priceB_min, priceB_max, tolerances.XABmin, tolerances.XABmax)
        if not ABC_price_inRange
            msg += str.format('| C price ({0}) not in range ({1}-{2})', C.point.price, priceC_min, priceC_max)
        if not XAB_inRange
            msg += str.format('| XAB ({0}) not in range ({1}-{2})', XAB, tolerances.XABmin, tolerances.XABmax)
        if not ABC_inRange
            msg += str.format('| ABC ({0}) not in range ({1}-{2})', ABC, tolerances.ABCmin, tolerances.ABCmax)
        debug_log.push(msg)

    // else if debug_enabled
    //     debug_log.push(str.format('{0} no XABC match for X:{1}, A:{2}, B:{3}, C:{4}', name, X.point.index, A.point.index, B.point.index, C.point.index))    
    match


find_xabc_patterns(P.Pivot[] pivots, Pattern[] patterns, int max_age_idx, simple int detect_dir = 0, simple int pattern_minlen = 30, simple int pattern_maxlen = 300, simple float max_XD_BD_ratio = 5, simple int max_sub_waves = 2, string[] debug_log) =>
    map<string, Match> matches = map.new<string, Match>()
    var max_sub_offset = 2 * max_sub_waves

    debug = array.new<string>()
    int count = pivots.size()

    if count >= 5
        bool interrupt = false

        int offset_C = 1
        P.Pivot C = pivots.get(offset_C)
        int dir = C.direction()
        
        if detect_dir == 0 or dir == detect_dir           
            var checked_Bs_max_price_result = array.new<float>(1,na)
            var checked_As_max_price_result = array.new<float>(1,na)
            var checked_Xs_max_price_result = array.new<float>(1,na)
            checked_Bs_max_price_result.set(0,na)
            checked_As_max_price_result.set(0,na)
            checked_Xs_max_price_result.set(0,na)
            possible_XABC_setups = array.new<Setup>()

            // find all valid XABC Pattern setups that don't violate pattern rules
            for offset_B = offset_C + 1 to offset_C + 1 + max_sub_offset // go through all Bs
                [B, interrupt_B] = check_pattern_point(pivots, offset_C + 1, offset_B, max_age_idx, checked_Bs_max_price_result, C)
                if not na(interrupt_B)
                    if not na(debug_log)
                        debug_log.push(str.format('found C at offset {0}({1})\ninterrupt B at offset {2}: {3}', offset_C, C.point.index, offset_B, interrupt_B))
                    break
                if not na(B)
                    if not na(debug_log)
                        debug_log.push(str.format('found C at offset {0}({1})\nfound B at offset {2}({3})', offset_C, C.point.index, offset_B, B.point.index))
                    // checked_As.clear()           
                    for offset_A = offset_B + 1 to offset_B + 1 + max_sub_offset // go through all As
                        [A, interrupt_A] = check_pattern_point(pivots, offset_B + 1, offset_A, max_age_idx, checked_As_max_price_result, B)
                        if not na(interrupt_A)
                            if not na(debug_log)
                                debug_log.push(str.format('interrupt A at offset {0}: {1}', offset_A, interrupt_A))
                            break
                        if not na(A)
                            if not na(debug_log)
                                debug_log.push(str.format('found A at offset {0}({1}) -> ABC:{2}', offset_A, A.point.index, PA.retracement_ratio(A.point.price, B.point.price, C.point.price)))
                            // checked_Xs.clear()
                            for offset_X = offset_A + 1 to offset_A + 1 + max_sub_offset  // go through all Xs
                                [X, interrupt_X] = check_pattern_point(pivots, offset_A + 1, offset_X, max_age_idx, checked_Xs_max_price_result, A)
                                if not na(interrupt_X)
                                    if not na(debug_log)
                                        debug_log.push(str.format('interrupt X at offset {0}: {1}', offset_X, interrupt_X))
                                    break
                                if not na(X)
                                    if not na(debug_log)
                                        debug_log.push(str.format('found X at offset {0}({1}) -> XAB:{2}', offset_X, X.point.index, PA.retracement_ratio(X.point.price, A.point.price, B.point.price)))

                                    XD_Bbi_ratio = calc_XB_BD_ratio(X.point.index, B.point.index, bar_index)
                                    if XD_Bbi_ratio <= max_XD_BD_ratio
                                        possible_XABC_setups.push(Setup.new(setup_id(X,A,B,C),X,A,B,C))
                                    else if not na(debug_log) // already too big
                                        debug_log.push(str.format('XB/BD ratio {0} already too big (>{1})', XD_Bbi_ratio, max_XD_BD_ratio))

                                
                                    
            // check each of these setups for match with any of our patterns
            for possible_XABC_setup in possible_XABC_setups
                for pattern in patterns
                    match = pattern.match(possible_XABC_setup, debug_log)
                    if not na(match)
                        matches.put(match.id, match)


    matches


//@function checks this map of tracking Matches if any of them was completed or invalidated in this iteration. This map is also updated with new XABC Setups as they appear.
//@param tracking_matches this map of tracking Matches
//@param zigzag the base ZigZag on which Pivot Points used for XABCD are based
//@param patterns the list of patterns to search for
//@param max_age_idx the oldest bar_index to consider for new Setups and to remove currently tracking matches respectively
//@param detect_dir setting to filter detection direction (-1: bearish only, 0: both, 1: bullish only)
//@param pattern_minlen minimum amount of bars per pattern
//@param pattern_maxlen maximum amount of bars per pattern
//@param max_sub_waves (default: 2) maximum up-down movements between two points of XABCD
//@param max_shared_legs (default: 1) maximum shared legs between two patterns before the shorter one is removed
//@param max_XB_BD_ratio (default: 5) maximum ratio of BCD compared to XAB part (XAB part should not drag on too long)
//@param debug_log (default : na) if not passed, no debug logs will be plotted
export method update(map<string, Match> tracking_matches, ZZ.Zigzag zigzag, Pattern[] patterns, int max_age_idx, simple int detect_dir, simple int pattern_minlen, simple int pattern_maxlen, simple int max_sub_waves = 2, simple int max_shared_legs = 1, simple float max_XB_BD_ratio = 5, string[] debug_log = na) =>
    var map<string, Match> handled_matches = map.new<string, Match>() // holds all handled patterns up to max_age_idx that should be ignored when found again by find_xabc_patterns (skip that, now handled by XABC_Setups)
    var map<string, Match> buffered_completed_matches = map.new<string, Match>() // holds matches completed, that need to be considered when hiding patterns due to being engulfed by patterns starting earlier
    var map<string, Match> just_completed_matches = map.new<string, Match>() // holds matches completed this iteration, for return
    var map<string, Match> invalidated_matches = map.new<string, Match>() // holds matches invalidated this iteration, for return
    var min_XB_BD_ratio = 1/max_XB_BD_ratio

    just_completed_matches.clear()
    invalidated_matches.clear()

    // clear the buffer of expired tracking_matches (they won't be added anymore and it saves us from iterating over them)
    buffered_completed_matches.cleanup(when_older_than_idx = max_age_idx, when_engulfed_by_completed = true, handled_matches_buffer = handled_matches)

    // remove completed / expired / invalidated (by pivot exceeding C in same direction)
    if not na(tracking_matches)
        tracking_matches.cleanup(when_pattern_len_exceeds = pattern_maxlen, when_older_than_idx = max_age_idx, when_BD_becomes_unproportional_ratio = max_XB_BD_ratio, when_completed = true, when_invalidated = true, handled_matches_buffer = handled_matches, handled_completed_buffer = buffered_completed_matches, invalid_buffer = invalidated_matches)
            
        // find new XABC patterns
        if zigzag.has_new_pivot()
            // TODO tracking live pivot was confirmed, ignore as long as there's one that exceeds C??? or there are more than x subwaves in between, or X idx is > max_age_bars
            new_matches = find_xabc_patterns(zigzag.pivots, patterns, max_age_idx, detect_dir, pattern_minlen, pattern_maxlen, max_XB_BD_ratio, max_sub_waves, debug_log)
            for [new_match_id, new_match] in new_matches
                if not tracking_matches.contains(new_match_id) and not handled_matches.contains(new_match_id)
                    tracking_matches.put(new_match_id, new_match)

    // every iteration so target lines are drawn and updated as long as they are there
    candle = D.create_candle(time, time_close, bar_index, open, high, low, close, volume)
    for [match_id, match] in tracking_matches

        if match.is_forming()

            // pattern length validation
            candle_in_target_range = PA.range_overlap(match.Dmin_price, match.Dmax_price, low, high)
            close_in_target_range = PA.in_range(close, match.Dmin_price, match.Dmax_price) 
            just_completed = (candle_in_target_range or close_in_target_range)// and match.is_forming()

            // update dynamic points
            match.Dmin.point.index := bar_index
            match.Dmax.point.index := bar_index
            match.D.point.index := bar_index

            match.Dmin.point.time := time
            match.Dmax.point.time := time
            match.D.point.time := time
            
            match.D.point.price := not just_completed or close_in_target_range ? close : match.Dmin_price
            match.D.candle := candle
            match.length := match.length()
            match.height := match.height()
            match.XB_BD_ratio := match.XB_BD_ratio()
            

            if just_completed
                pattern_len_inRange = PA.in_range(match.length, pattern_minlen, pattern_maxlen)
                XB_BD_ratio_inRange = PA.in_range(match.XB_BD_ratio, min_XB_BD_ratio, max_XB_BD_ratio)

                // label.new(bar_index, match.B.point.price, str.format('completing\n{0}\xbbd_ratio: {1}\npatternlen: {2}', match.id, match.XB_BD_ratio, pattern_len), style = label.style_label_down, textcolor = color.white, color = color.new(color.green,50))

                if pattern_len_inRange and XB_BD_ratio_inRange
                    match.is_complete := true

                    match.tp1 := PA.target_ratio_price(match.hhll, match.D.point.price, match.pattern.tp1)
                    match.tp2 := PA.target_ratio_price(match.hhll, match.D.point.price, match.pattern.tp2)
                    match.sl := PA.target_ratio_price(match.D.point.price, match.hhll, match.pattern.sl)
                    match.terminal_bar_idx := match.D.point.index
                    match.terminal_bar_time := match.D.point.time


                    // optimize drawing by marking this for removal from the buffered_completed_matches buffer (next round, needs to be hidden this iteration)
                    if not na(debug_log)
                        debug_log.push(str.format('update overlapped (buffered complete) {0}', buffered_completed_matches.tostring()))
                    buffered_completed_matches.update_overlapped(match, max_shared_legs, debug_log)

                    // in case other patterns finished simulteaneously, check them too, also allow them to overlap this match (wasn't in buffered_completed_matches before)
                    if not na(debug_log)
                        debug_log.push(str.format('update overlapped (just complete) {0}', just_completed_matches.tostring()))
                    just_completed_matches.update_overlapped(match, max_shared_legs, debug_log)

                    just_completed_matches.add(match)
                    // label.new(bar_index, low, str.format('complete\n{0}', match.pattern.name), style = label.style_label_up, textcolor = color.white)
                else
                    match.is_invalidated := true
                    invalidated_matches.add(match)

                    if not na(debug_log)
                        msg = str.format('{0} invalidated on completion', match.id)
                        if not pattern_len_inRange
                            msg += str.format('| pattern_len {0} not in range ({1}-{2})', match.length, pattern_minlen, pattern_maxlen)
                        if not XB_BD_ratio_inRange
                            msg += str.format('| XB_BD_ratio {0} not in range ({1}-{2})', match.XB_BD_ratio, min_XB_BD_ratio, max_XB_BD_ratio)
                        debug_log.push(msg)

    // cleanup expired matches to avoid memory overflow
    handled_matches.cleanup(when_older_than_idx = max_age_idx)                    
        
    [just_completed_matches, invalidated_matches, buffered_completed_matches, handled_matches]


//////////////////////////////////////////////////////////////////////////////////////
//#endregion                               PATTERN MATCH
//////////////////////////////////////////////////////////////////////////////////////




//////////////////////////////////////////////////////////////////////////////////////
//#region                              DEMO
//////////////////////////////////////////////////////////////////////////////////////


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//#region                                           DRAW
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//@function mute this pattern by making it all one color (lines and labels, for pattern fill there's another)
export method mute(Match this, color mute_color, color mute_fill_color) =>
    if not na(this)
        for i in this.static_lines
            i.plot.set_color(mute_color)
        for i in this.dynamic_lines
            i.plot.set_color(mute_color)
        for i in this.static_labels
            i.center_label.center_label.plot.set_textcolor(mute_color)
        for i in this.dynamic_labels
            i.center_label.center_label.plot.set_textcolor(mute_color)
        for i in this.fills            
            i.plot.set_color(mute_fill_color)
        for i in this.dynamic_point_labels
            i.plot.set_textcolor(mute_color)
        for i in this.static_point_labels
            i.plot.set_textcolor(mute_color)

        this.name_label.plot.set_textcolor(mute_color)
        this.name_label.plot.set_color(#00000000)


//@function hide this pattern by muting it with a transparent color
export method hide(Match this) =>
    this.mute(#00000000, #00000000)
    
//@function reset the style of a muted or hidden match back to the preset configuration
export method reset_styles(Match this) =>
    if not na(this)
        for i in this.static_lines
            i.plot.set_color(i.args.line_color)
        for i in this.dynamic_lines
            i.plot.set_color(i.args.line_color)
        for i in this.static_labels
            i.center_label.center_label.plot.set_textcolor(i.center_label.center_label.args.text_color)
        for i in this.dynamic_labels
            i.center_label.center_label.plot.set_textcolor(i.center_label.center_label.args.text_color)
        for i in this.fills            
            i.plot.set_color(i.fill_color)
        for i in this.dynamic_point_labels
            i.plot.set_textcolor(i.args.text_color)
        for i in this.static_point_labels
            i.plot.set_textcolor(i.args.text_color)

        this.name_label.plot.set_textcolor(this.name_label.args.text_color)

//@function remove the plot of this Match from the chart
export method delete(Match this) =>
    if not na(this)        
        this.static_lines.delete()
        this.dynamic_lines.delete()
        this.static_labels.delete()
        this.dynamic_labels.delete()
        this.static_point_labels.delete()
        this.fills.delete()
        this.dynamic_point_labels.delete()
        this.static_point_labels.delete()
        this.name_label.delete()


//@function mute all patterns in this map by making it all one color (lines and labels, for pattern fill there's another)
export method mute(map<string, Match> this, color mute_color, color mute_fill_color) =>
    if not na(this)
        for i in this.values()
            i.mute(mute_color, mute_fill_color)
    this

//@function remove all the plots of the Matches in this map from the chart
export method delete(map<string, Match> this) =>
    if not na(this)
        for i in this.values()
            i.delete()
    this

//@function draw this Match on the chart
//@param config configuration for plotting this Match
//@param hide set this Match's colors to transparent
//@param mute set this Match's colors to mute colors
//@param terminal_label_yoffset distance of terminal bar label from the terminal bar high/low
export method draw(Match this, Config config, bool hide = false, bool mute = false, float terminal_label_yoffset) =>
    tf_ms = timeframe.in_seconds() * 1000
    dir = this.direction()
    line_args = dir == 1 ? config.line_bull : config.line_bear
    line_args_live = dir == 1 ? config.line_bull_live : config.line_bear_live
    fill_color = dir == 1 ? config.fill_bull : config.fill_bear    
    point_in_dir = dir == 1 ? config.label_args_xabcd_up : config.label_args_xabcd_down
    point_opp_dir = dir == 1 ? config.label_args_xabcd_down : config.label_args_xabcd_up
    retrace_in_dir = dir == 1 ? config.retrace_up : config.retrace_down
    retrace_opp_dir = dir == 1 ? config.retrace_down : config.retrace_up
    name_in_dir = dir == 1 ? config.label_args_name_up : config.label_args_name_down
    terminal_args = dir == 1 ? config.label_args_terminal_bar_up : config.label_args_terminal_bar_down
    terminal_char = dir == 1 ? config.terminal_bar_up_char : config.terminal_bar_down_char
    // name_opp_dir = dir == 1 ? config.label_args_xabcd_down : config.label_args_xabcd_up

    initial = this.static_lines.size() == 0
    just_complete = this.is_complete and bar_index == this.terminal_bar_idx
    prefix = dir == 1 ? "Bullish\n" : "Bearish\n"

    if initial
        // name label
        center_points = array.from(this.A.point, this.B.point, this.C.point)
        this.name_label := D.Label.new(this.B.point, this.pattern.name, name_in_dir)

        // static
        XA = this.X.point.create_line(this.A.point, line_args)
        AB = this.A.point.create_line(this.B.point, line_args)
        BC = this.B.point.create_line(this.C.point, line_args)
        this.static_lines.enqueue(XA).enqueue(AB).enqueue(BC)

        // dynamic
        CD = this.C.point.create_line(this.D.point, this.is_complete ? line_args : line_args_live)
        this.dynamic_lines.enqueue(CD)

        // fills
        this.fills
         .enqueue(XA.create_fill(AB, fill_color))
         .enqueue(BC.create_fill(CD, fill_color))

        // prepare point labels
        if config.draw_point_labels
            // static
            this.static_point_labels
             .enqueue(this.X.point.create_label('X', point_in_dir))
             .enqueue(this.A.point.create_label('A', point_opp_dir))
             .enqueue(this.B.point.create_label('B', point_in_dir))
             .enqueue(this.C.point.create_label('C', point_opp_dir))

            // dynamic
            this.dynamic_point_labels.enqueue(this.D.point.create_label('D', point_in_dir))

        // prepare retracement lines and labels
        if config.draw_retracements
            XB = this.X.point.create_line(this.B.point, config.label_args_retrace_line)
            AC = this.A.point.create_line(this.C.point, config.label_args_retrace_line)
            this.static_lines.enqueue(XB).enqueue(AC)
            
            this.static_labels
             .enqueue(XB.create_retracement_label(this.A, retrace_in_dir)) //, 'XAB', this.pattern.XABmin, this.pattern.XABmax, this.pattern.tolerances.XABmin, this.pattern.tolerances.XABmax))
             .enqueue(AC.create_retracement_label(this.B, retrace_opp_dir)) //, 'ABC', this.pattern.ABCmin, this.pattern.ABCmax, this.pattern.tolerances.ABCmin, this.pattern.tolerances.ABCmax))

            if not na(this.pattern.BCDmin) and not na(this.pattern.BCDmax)
                BD = this.B.point.create_line(this.D.point, config.label_args_retrace_line)
                this.dynamic_lines.enqueue(BD)
                this.dynamic_labels.enqueue(BD.create_retracement_label(this.C, retrace_in_dir)) //, 'BCD', this.pattern.BCDmin, this.pattern.BCDmax, this.pattern.tolerances.BCDmin, this.pattern.tolerances.BCDmax))

            if not na(this.pattern.XADmin) or not na(this.pattern.XBDmin) or not na(this.pattern.XCDmin) or not na(this.pattern.XADmax) or not na(this.pattern.XBDmax) or not na(this.pattern.XCDmax)
                XD = this.X.point.create_line(this.D.point, config.label_args_retrace_line)
                this.dynamic_lines.enqueue(XD)
                if not na(this.pattern.XADmin) and not na(this.pattern.XADmax)
                    this.dynamic_labels.enqueue(XD.create_retracement_label(this.A, retrace_in_dir)) //, 'XAD', this.pattern.XADmin, this.pattern.XADmax, this.pattern.tolerances.XADmin, this.pattern.tolerances.XADmax))
                if not na(this.pattern.XBDmin) and not na(this.pattern.XBDmax)
                    this.dynamic_labels.enqueue(XD.create_retracement_label(this.B, retrace_in_dir)) //, 'XBD', this.pattern.XBDmin, this.pattern.XBDmax, this.pattern.tolerances.XBDmin, this.pattern.tolerances.XBDmax))
                if not na(this.pattern.XCDmin) and not na(this.pattern.XCDmax)
                    this.dynamic_labels.enqueue(XD.create_retracement_label(this.C, retrace_in_dir)) //, 'XCD', this.pattern.XCDmin, this.pattern.XCDmax, this.pattern.tolerances.XCDmin, this.pattern.tolerances.XCDmax))
            
        // prepare target range plot
        if config.draw_target_range
            this.dynamic_lines
             .enqueue(this.C.point.create_line(this.Dmin.point, config.line_Dtarget))
             .enqueue(this.C.point.create_line(this.Dmax.point, config.line_Dtarget))

            if config.draw_point_labels
                if this.Dmin_price == this.Dmax_price
                    this.dynamic_point_labels
                     .enqueue(this.Dmax.point.create_label(this.pattern.name + ' - Dtarget', point_in_dir)) //, str.format('{0}', this.Dmax_price)))
                else
                    this.dynamic_point_labels
                     .enqueue(this.Dmin.point.create_label('Dmin', point_opp_dir)) //, str.format('{0}', this.Dmin_price)))
                     .enqueue(this.Dmax.point.create_label('Dmax', point_in_dir)) //, str.format('{0}', this.Dmax_price)))

            // optional retracements
            if not na(this.pattern.XADmin) or not na(this.pattern.XBDmin) or not na(this.pattern.XCDmin)
                XDmin = this.X.point.create_line(this.Dmin.point, config.line_Dtarget)
                this.dynamic_lines.enqueue(XDmin)

            if not na(this.pattern.XADmax) or not na(this.pattern.XBDmax) or not na(this.pattern.XCDmax)
                XDmax = this.X.point.create_line(this.Dmax.point, config.line_Dtarget)
                this.dynamic_lines.enqueue(XDmax)
                // no center label, this is only drawn on the retracement line            

        this.name_label.draw()
        this.static_lines.draw(extend_only = false)
        this.static_labels.draw()
        this.static_point_labels.draw()

    if just_complete
        D_line = this.dynamic_lines.get(this.dynamic_lines.size() - 1).apply_style(line_args)
        if config.draw_point_labels
            D_label = this.dynamic_point_labels.remove(this.dynamic_point_labels.size() - 1)
            this.dynamic_point_labels.delete().clear()
            this.dynamic_point_labels.enqueue(D_label)

        if config.draw_levels
            from_idx = this.terminal_bar_idx
            from_time = this.terminal_bar_time
            until_idx = from_idx + 40
            until_time = from_time + 40 * tf_ms
            line_completion = D.Line.new(chart.point.new(from_time, from_idx, this.D.point.price), chart.point.new(until_time, until_idx, this.D.point.price), config.line_args_completion)
            line_tp1 = D.Line.new(chart.point.new(from_time, from_idx, this.tp1), chart.point.new(until_time, until_idx, this.tp1), config.line_args_tp1)
            line_tp2 = D.Line.new(chart.point.new(from_time, from_idx, this.tp2), chart.point.new(until_time, until_idx, this.tp2), config.line_args_tp2)
            line_sl = D.Line.new(chart.point.new(from_time, from_idx, this.sl), chart.point.new(until_time, until_idx, this.sl), config.line_args_sl)
            
            this.static_lines
             .enqueue(line_completion)
             .enqueue(line_tp1)
             .enqueue(line_tp2)
             .enqueue(line_sl)

            this.static_point_labels
             .enqueue(D.Label.new(line_completion.end, str.format('{0} complete ({1})', this.pattern.name, str.tostring(math.round_to_mintick(this.D.point.price))), config.label_args_completion))
             .enqueue(D.Label.new(line_tp1.end, str.format('TP 1 ({0})', str.tostring(math.round_to_mintick(this.tp1))), config.label_args_tp1))
             .enqueue(D.Label.new(line_tp2.end, str.format('TP 2 ({0})', str.tostring(math.round_to_mintick(this.tp2))), config.label_args_tp2))
             .enqueue(D.Label.new(line_sl.end, str.format('LRP ({0})', str.tostring(math.round_to_mintick(this.sl))), config.label_args_sl))

        if barstate.isconfirmed
            terminal_point = this.D.point.copy()            
            terminal_point.price := dir == 1 ? this.D.candle.l - terminal_label_yoffset : this.D.candle.h + terminal_label_yoffset
            this.static_point_labels.enqueue(D.Label.new(terminal_point, terminal_char, terminal_args))

        this.static_point_labels.draw()
        this.static_lines.draw(extend_only = false)

        // in case this is currently engulfed, we have drawn new lines and labels, but need to hide them (until the engulfing pattern fails)
        if this.is_hidden
            this.hide()
        else if this.is_muted
            this.mute(config.color_muted, config.fill_muted)


    // dynamic update (values of points are updated via update() method)
    if initial or just_complete or this.is_forming()
        this.dynamic_lines.draw(extend_only = false) // retracement and pattern lines
        this.dynamic_labels.draw() // retracement labels
        this.dynamic_point_labels.draw()
        this.fills.draw()


    // update hidden, muted matches
    if this.is_engulfed_by_completed and config.hide_engulfed_pattern
        this.delete()
        this.is_hidden := true
    else if hide and not this.is_hidden
        this.hide()
        this.is_hidden := true
    else if mute and not this.is_muted
        this.mute(config.color_muted, config.fill_muted)
        this.is_muted := true
    else if not hide and not mute and (this.is_hidden or this.is_muted)
        this.reset_styles()
        this.is_hidden := false
        this.is_muted := false

    this


//@function checks if this pattern needs to be hidden or muted based on other plotted patterns and given configuration
export method check_hide_or_mute(Match this, map<string, Match> all, Config config, string[] debug_log = na) =>
    hide = false
    mute = false

    if na(this) ? false : this.is_complete or this.is_forming()
        has_shorter_pattern_with_same_X = false
        is_engulfed = false
        is_engulfed_by_same_type = false
        is_sharing_too_many_points_with_longer_pattern = false

        for other in all.values()
           
            if na(other) ? false : other.is_complete or other.is_forming()

                if this.id != other.id
                    
                    if config.hide_engulfed_pattern
                        is_engulfed := is_engulfed or this.is_engulfed_by_completed// ? true : this.is_engulfed_by(other)

                    // kind of a inverse 'hide engulfed', hiding follow up patterns that start from the same X
                    if config.hide_longer_pattern_with_same_X and not is_engulfed
                        if has_shorter_pattern_with_same_X or other.X.point.index == this.X.point.index and other.D.point.index < this.D.point.index // and not other.is_hidden
                            has_shorter_pattern_with_same_X := has_shorter_pattern_with_same_X ? true : not (config.hide_engulfed_pattern and other.is_engulfed_by_completed) // TODO put this result into the match object, and maybe only check if ... think of something

        is_overlapped_by_shared_legs = config.hide_shorter_if_shared_legs_greater_than_max and this.is_overlapped_by_shared_legs
        is_overlapped_by_next = config.mute_previous_pattern_when_next_overlaps and this.is_overlapped_by_next

        hide := is_engulfed
         or has_shorter_pattern_with_same_X
         or is_overlapped_by_shared_legs

        mute := not hide and is_overlapped_by_next

        if not na(debug_log) and (hide or mute)
            debug_log.push(str.format('{0} changing visibility - hide:{1}, mute: {2}, is_engulfed:{3} - by_completed:{4}, has_shorter_pattern_with_same_X:{5}, overlapped_by_shared_legs: {6}, overlapped_by_next: {7}', this.id, hide, mute, is_engulfed, this.is_engulfed_by_completed, has_shorter_pattern_with_same_X, is_overlapped_by_shared_legs, is_overlapped_by_next))

    [hide, mute]
 

//@function draw all Matches in this map, considering all other patterns for engulfing and overlapping
export method draw(map<string, Match> this, Config config, map<string, Match> all_patterns, float terminal_label_yoffset = 0, string[] debug_log = na) =>
    if not na(this) and not na(config)
        for i in this.values()
            if not na(i)
                [hide, mute] = i.check_hide_or_mute(all_patterns, config, debug_log)
                i.draw(config, hide, mute, terminal_label_yoffset)
    this



//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//#endregion                                           DRAW
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


//////////////////////////////////////////////////////////////////////////////////////
//                                  DEMO
//////////////////////////////////////////////////////////////////////////////////////


// Inputs
//@function convenience function to add a search pattern to a list, only if given condition (input.bool) is true
export method add_if(Pattern[] id, Pattern item, bool condition) =>
    if not na(id) and condition
        id.push(item)
    id


to_style(style) =>
    switch style
        'dotted' => line.style_dotted
        'dashed' => line.style_dashed
        => line.style_solid

var g_selection = 'Pattern Selection'
var string i_direction = input.string('Bullish & Bearish', 'Direction', ['Bullish & Bearish', 'Bullish', 'Bearish'], group = g_selection, inline = 'ld')
var detect_bullish = str.contains(i_direction,'Bullish')
var detect_bearish = str.contains(i_direction,'Bearish')
var detect_both = detect_bullish and detect_bearish
var detect_dir = detect_both ? 0 : detect_bearish ? -1 : 1

var Pattern[] patterns = array.new<Pattern>()
 .add_if(Pattern.new('Bat',                XABmin = 0.382, XABmax = 0.500, ABCmin = 0.382, ABCmax = 0.886, BCDmin = 1.618, BCDmax = 2.618, XADmin = 0.886, XADmax = 0.886, tp1 = 0.5, tp2 = 1.5, sl = 1.5).set_tolerances(0.03, 0.03, 0.03, 0     ), input.bool(true, 'Bat'         , group = g_selection, inline = '1'))
 .add_if(Pattern.new('Alt Bat',            XABmin = 0.382, XABmax = 0.382, ABCmin = 0.382, ABCmax = 0.886, BCDmin = 2.000, BCDmax = 3.618, XADmin = 1.130, XADmax = 1.130, tp1 = 0.5, tp2 = 1.5, sl = 1.5).set_tolerances(0.03, 0.03, 0.03, 0     ), input.bool(true, 'Alt Bat'     , group = g_selection, inline = '1'))
 .add_if(Pattern.new('Crab',               XABmin = 0.382, XABmax = 0.618, ABCmin = 0.382, ABCmax = 0.886, BCDmin = 2.618, BCDmax = 3.618, XADmin = 1.618, XADmax = 1.618, tp1 = 0.5, tp2 = 1.5, sl = 1.5).set_tolerances(0.03, 0.03, 0.03, 0     ), input.bool(true, 'Crab'        , group = g_selection, inline = '2'))
 .add_if(Pattern.new('Deep Crab',          XABmin = 0.886, XABmax = 0.886, ABCmin = 0.382, ABCmax = 0.886, BCDmin = 2.000, BCDmax = 3.618, XADmin = 1.618, XADmax = 1.618, tp1 = 0.5, tp2 = 1.5, sl = 1.5).set_tolerances(0.03, 0.03, 0.03, 0     ), input.bool(true, 'Deep Crab'   , group = g_selection, inline = '2'))
 .add_if(Pattern.new('Butterfly',          XABmin = 0.786, XABmax = 0.786, ABCmin = 0.382, ABCmax = 0.886, BCDmin = 1.618, BCDmax = 2.618, XADmin = 1.272, XADmax = 1.618, tp1 = 0.5, tp2 = 1.5, sl = 1.5).set_tolerances(0.03, 0.03, 0.03, 0     ), input.bool(true, 'Butterfly'   , group = g_selection, inline = '3'))
 .add_if(Pattern.new('Gartley',            XABmin = 0.618, XABmax = 0.618, ABCmin = 0.382, ABCmax = 0.886, BCDmin = 1.272, BCDmax = 1.618, XADmin = 0.786, XADmax = 0.786, tp1 = 0.5, tp2 = 1.5, sl = 1.5).set_tolerances(0.03, 0.03, 0.03, 0     ), input.bool(true, 'Gartley'     , group = g_selection, inline = '3'))
 .add_if(Pattern.new('Shark',              XABmin = 0.382, XABmax = 0.618, ABCmin = 1.130, ABCmax = 1.618, BCDmin = 1.618, BCDmax = 2.240, XCDmin = 0.886, XCDmax = 1.130, tp1 = 0.5, tp2 = 1.5, sl = 1.5).set_tolerances(0.03, 0.03, 0    , 0.03 ), input.bool(true, 'Shark'       , group = g_selection, inline = '3'))

var g_debug = 'Debug'
var bool debug_detection = input.bool(false, 'detection', group = g_debug, inline = 'debug')
var bool debug_engulfing = input.bool(false, 'plotting', group = g_debug, inline = 'debug')
var bool debug_enabled = debug_detection or debug_engulfing
var bool log_completed_patterns = input.bool(false, 'log completed')

var g_patterns = 'Pattern Detection'
var bool live = false // input.bool(false, 'live (repainting)', group = g_patterns, inline = 'ld')
var bool keep_failed = false // input.bool(false, 'keep failed patterns', group = g_patterns, inline = 'ld')
var int length = 10 // input.int(10, 'Pivot confirmation bars', group = g_patterns)
var int pattern_minlen = 20 // input.int(30, 'Min length', group = g_patterns)
var int pattern_maxlen = 150 // input.int(300, 'Max length', group = g_patterns)
var int max_lookback = 244 // input.int(400, 'Lookback bars', group = g_patterns)
var float max_XB_BD_ratio = 5
var int tolerance_xabc_sub_waves = 2 // input.int(2, 'Max sub waves', 0, 20, group = g_patterns)


var g_filter = 'Pattern Filter'
var bool hide_engulfed_pattern = true // input.bool(true, 'hide engulfed patterns', group = g_filter, inline = 'engulf')
var bool hide_engulfed_pattern_of_same_type = false // input.bool(true, 'of same type', group = g_filter, inline = 'engulf') // TODO default: hide_engulfed_pattern = false
var bool hide_longer_pattern_with_same_X = false // input.bool(false, 'hide followup patterns with same origin', group = g_filter)
var bool mute_previous_pattern_when_next_overlaps = false // input.bool(false, 'mute previous patterns when next overlaps', group = g_filter)
var bool hide_shorter_if_shared_legs = false // input.bool(false, 'Hide shorter pattern where shared legs greater than', group = g_filter, tooltip = '(0-4) when overlap detected, longer pattern remains', inline = 'shared legs')
var int i_hide_shorter_if_shared_legs_greater_than = 1 // input.int(1, '', 0, 4, group = g_filter, inline = 'shared legs')
var int hide_shorter_if_shared_legs_greater_than = hide_shorter_if_shared_legs ? i_hide_shorter_if_shared_legs_greater_than : 5


var g_plot = 'Pattern Plot'
var color color_bull = color.green // input.color(color.green, 'Pattern:  Bull', group = g_plot, inline = 'col')
var color color_bear = color.red // input.color(color.red, ' Bear', group = g_plot, inline = 'col')
var color color_muted = color.gray // input.color(color.gray, ' Fail/Mute', group = g_plot, inline = 'col')
var int fill_opacity = 10 // input.int(10, ' Fill %', group = g_plot, inline = 'col')

var bool draw_retracements = false // input.bool(true, 'Show retracements: ', group = g_plot, inline = 'color_retr')
var color color_retracment_label = color.gray // input.color(color.gray, 'Label', group = g_plot, inline = 'color_retr')
var color color_retracment_line = color.gray // input.color(color.gray, ' Line', group = g_plot, inline = 'color_retr')
var string style_retracment_line = line.style_dotted // to_style(input.string('dotted', ' Style', ['dotted', 'dashed', 'solid'], group = g_plot, inline = 'color_retr'))

var bool draw_point_labels = false // input.bool(false, 'Show XABCD:   ', group = g_plot, inline = 'color_points')
var color color_point_labels  = color.gray // input.color(color.gray, '  Label', group = g_plot, inline = 'color_points')

var bool draw_target_range = false // input.bool(false, 'Show D target (live):        ', group = g_plot, inline = 'color_target')
var color color_target_range = color.gray // input.color(color.gray, 'Line ', group = g_plot, inline = 'color_target')
var string style_target_range = line.style_dotted // to_style(input.string('dotted', ' Style', ['dotted', 'dashed', 'solid'], group = g_plot, inline = 'color_target'))

var bool draw_levels = false // input.bool(true, 'Show trade zones: ', group = g_plot, inline = 'col_trade')
var color color_completion = color.gray // input.color(color.gray, ' Complete', group = g_plot, inline = 'col_trade')
var color color_tp1 = color.lime // input.color(color.lime, ' TP1', group = g_plot, inline = 'col_trade')
var color color_tp2 = color.green // input.color(color.green, ' TP2', group = g_plot, inline = 'col_trade')
var color color_sl = color.red // input.color(color.red, ' LRP', group = g_plot, inline = 'col_trade')

// Set up configuration
var D.LineArgs  pattern_line_args   = D.LineArgs.new(#00000000, width = 2)
var D.LabelArgs pattern_point_args = D.LabelArgs.new(color_point_labels)
var D.LabelArgs name_label_args = D.LabelArgs.new(#00000000, #00000000, size = size.large)
var D.LabelArgs retracement_label_args  = D.LabelArgs.new(color_retracment_label)
var D.LineArgs  retracement_line_args  = D.LineArgs.new(color_retracment_line, style = style_retracment_line)
var D.LineArgs  line_args_Dtarget  = D.LineArgs.new(color_target_range, style = style_target_range)
var D.LineArgs  line_args_completion = D.LineArgs.new(color_completion, style = line.style_dashed)
var D.LineArgs  line_args_tp1 = D.LineArgs.new(color_tp1, style = line.style_dashed)
var D.LineArgs  line_args_tp2 = D.LineArgs.new(color_tp2, style = line.style_dashed)
var D.LineArgs  line_args_sl = D.LineArgs.new(color_sl, style = line.style_dashed)
var D.LabelArgs label_args_completion = D.LabelArgs.new(color_completion, #00000000, style = label.style_label_lower_right, size = size.small)
var D.LabelArgs label_args_tp1 = D.LabelArgs.new(color_tp1, #00000000, style = label.style_label_lower_right, size = size.small)
var D.LabelArgs label_args_tp2 = D.LabelArgs.new(color_tp2, #00000000, style = label.style_label_lower_right, size = size.small)
var D.LabelArgs label_args_sl = D.LabelArgs.new(color_sl, #00000000, style = label.style_label_lower_right, size = size.small)
var D.LabelArgs label_terminal = D.LabelArgs.new(#00000000, #00000000, style = label.style_label_center, size = size.huge)

var LOG.Logger  logger = log_completed_patterns ? LOG.Logger.new(color_logs = false, display = LOG.LogDisplay.new(15, position.bottom_left)) : na
var LOG.Logger  debug_logger = debug_enabled ? LOG.Logger.new(color_logs = false, display = LOG.LogDisplay.new(15, position.bottom_right)) : na
string[] debug_log = debug_enabled ? array.new<string>() : na


var Config config = create_config(pattern_line_args, pattern_point_args, name_label_args, retracement_line_args, retracement_label_args, line_args_Dtarget, line_args_completion, line_args_tp1, line_args_tp2, line_args_sl, label_args_completion, label_args_tp1, label_args_tp2, label_args_sl, label_terminal, '▲', '▼', color_bull, color_bear, color_muted, fill_opacity, draw_point_labels, draw_retracements, draw_target_range, draw_levels, hide_shorter_if_shared_legs, hide_engulfed_pattern, hide_engulfed_pattern_of_same_type, hide_longer_pattern_with_same_X, mute_previous_pattern_when_next_overlaps)
var map<string, Match> tracking_matches = map.new<string, Match>()

var ZZ.Zigzag zigzag = ZZ.Zigzag.new(max_lookback, P.HLData.new(length, xloc.bar_time))
zigzag.update()

max_age_idx = bar_index - max_lookback
[just_completed_matches, just_invalidated_setups, buffered_completed_matches, handled_matches] = tracking_matches.update(zigzag, patterns, max_age_idx, detect_dir, pattern_minlen, pattern_maxlen, tolerance_xabc_sub_waves, hide_shorter_if_shared_legs_greater_than) //,debug_detection ? debug_log : na)

all_patterns = buffered_completed_matches.copy()
all_patterns.put_all(just_completed_matches)
if live
    all_patterns.put_all(tracking_matches)
    
terminal_label_yoffset = ta.atr(100)
all_patterns.draw(config, all_patterns, terminal_label_yoffset, debug_engulfing ? debug_log : na)

if keep_failed
    just_invalidated_setups.mute(config.color_muted, config.fill_muted)
else 
    just_invalidated_setups.delete()

if log_completed_patterns
    for match in just_completed_matches.values()
        if match.is_complete and not match.is_hidden and not match.is_muted
            logger.info(str.format('Completed {0}', debug_enabled ? match.id : match.name))
        
// DEBUG OUTPUT
if na(debug_log) ? false : debug_log.size() > 0   
    debug_logger.debug(debug_log.join('\n'))

// // for setup in xabc_setups
// //     // enter position

// // for fail in invalidated_setups
// //     // exit position

// // for match in just_completed_matches
//     // re-entry or tp
//     // alert(str.format('{0} completed', match.name), alert.freq_once_per_bar)

    
plot(bar_index, 'bar_index', display = display.data_window)
plot(tracking_matches.size(), 'Patterns active', display = display.data_window)

plotshape(ta.crossover(tracking_matches.size(), 0),  'Pattern new',      shape.cross,    location.bottom, color.gray, editable = false, size = size.tiny, display = display.data_window)
plotchar(tracking_matches.size() > 0,                'Pattern tracking', '_',            location.bottom, color.gray, editable = false, size = size.tiny, display = display.data_window)
plotshape(just_completed_matches.size() > 0,         'Pattern complete', shape.square,   location.bottom, color.gray, editable = false, size = size.tiny, display = display.data_window)
plotshape(just_invalidated_setups.size() > 0,        'Pattern invalid',  shape.xcross,   location.bottom, color.gray, editable = false, size = size.tiny, display = display.data_window)
plot(all_patterns.size(), 'Overlaps checked (complete, buffered)', display = display.data_window)
plot(buffered_completed_matches.size(), 'Patterns complete (buffered)', display = display.data_window)


//////////////////////////////////////////////////////////////////////////////////////
//#regionend                           DEMO 
//////////////////////////////////////////////////////////////////////////////////////