# indie:lang_version = 5
# ─────────────────────────────────────────────────────────────────────────────
#  Triangle & Wedge Breakout Pro  –  v5.6  (true apex triangle)
#  TakeProfit Indie port  |  ST / AT / DT / FW / RW
#
#   
#  ─────────────────────────────────────────────────────────────────────────────
#  Lines were drawn anchor → breakout bar. But the breakout is REQUIRED to
#  happen well before the apex (max_breakout_delay keeps bi at least
#  pat_len*delay bars ahead of apex_x), so at the breakout bar the lines are
#  still far apart. The right side stayed an open gap → a 4-corner shape, not
#  a triangle. With staggered anchors the long dashed back chord made it look
#  even more like an arbitrary quad.
#
#  v5.6 solution
#  ─────────────────────────────────────────────────────────────────────────────
#  Draw the ACTUAL triangle: both lines are extended forward to their true
#  intersection point (the apex), which always exists for a detected pattern
#  and is bounded (convergence filter ⇒ apex ≲ 3 pattern lengths ahead;
#  ST/FW/RW additionally ≤ 2). Three corners, three edges:
#      upper:  (h1t, h1p) → (t_apex, apex_p)     solid
#      lower:  (l1t, l1p) → (t_apex, apex_p)     solid
#      back:   (h1t, h1p) → (l1t, l1p)           dashed
#  FILL: one single Triangle(h1, apex, l1) — literally a triangle, edges
#  coincide exactly with the lines.
#  Apex time is projected as t0 + (apex_x - bi) * timeframe_ms (timeframe
#  taken from the last two bars). If the apex or timeframe is unavailable
#  (degenerate case), falls back to the v5.5 breakout-bar geometry.
#
#  Since v5.5: slope_tol default 0.002 (per-bar fraction of price).
#  Since v5.4: drawing happens only on confirmed breakout.
# ─────────────────────────────────────────────────────────────────────────────

from math import isnan
from indie import (
    indicator, param, color, Color, source, SeriesF,
    plot, MainContext, MutSeriesF, line_style
)
from indie.algorithms import Ema, Atr
from indie.drawings import LineSegment, LabelAbs, AbsolutePosition, Triangle


@indicator('Triangle & Wedge Breakout Pro', overlay_main_pane=True)

@plot.line('signal', title='Signal  +1/-1/0', color=color.TRANSPARENT,
           display_options=plot.LineDisplayOptions(pane=False, price_label=False, status_line=False))

@param.int('left_bars',              default=5,    min=2,    max=30,   title='Bars Left (Fractal)')
@param.int('right_bars',             default=2,    min=1,    max=15,   title='Bars Right (Fractal)')

@param.bool('use_sym_tri',           default=True,                     title='Symmetrical Triangle')
@param.bool('use_asc_tri',           default=True,                     title='Ascending Triangle')
@param.bool('use_desc_tri',          default=True,                     title='Descending Triangle')
@param.bool('use_fall_wdg',          default=True,                     title='Falling Wedge')
@param.bool('use_rise_wdg',          default=True,                     title='Rising Wedge')

@param.float('slope_tol',            default=0.002, min=0.001, max=0.5, title='Slope Tolerance  AT / DT  (%/bar of price)')
@param.float('triangularity',        default=0.5,  min=0.0,   max=1.0, title='Convergence  0=loose  1=strict')
@param.int('min_pattern_bars',       default=10,   min=3,     max=300, title='Min Pattern Length (bars)')
@param.float('st_symmetry_tol',      default=0.5,  min=0.05,  max=2.0, title='ST Slope Symmetry Tolerance')

@param.float('breakout_buffer',      default=0.1,  min=0.0,   max=2.0, title='Breakout Buffer (%)')
@param.bool('wait_for_close',        default=True,                     title='Wait for Bar Close  (anti-repaint)')
@param.int('confirm_bars',           default=1,    min=1,     max=3,   title='Consecutive Close Confirmations')

@param.float('max_breakout_delay',   default=0.35, min=0.1,   max=1.0, title='Max Breakout Delay  (apex fraction)')

@param.int('atr_period',             default=14,   min=5,     max=50,  title='ATR Period')
@param.bool('use_atr_compression',   default=False,                    title='ATR Compression Filter')
@param.float('atr_compression_ratio',default=0.8,  min=0.3,   max=1.0, title='ATR Compression Ratio')
@param.bool('use_breakout_impulse',  default=True,                     title='Breakout Impulse Filter')
@param.float('breakout_atr_mult',    default=0.25, min=0.05,  max=2.0, title='Breakout ATR Multiplier')

@param.bool('require_3rd_touch',     default=False,                    title='Require 3rd Touch')

@param.bool('fill_pattern',          default=True,                     title='Fill Pattern Body')
@param.float('fill_opacity',         default=0.15, min=0.05,  max=0.50, title='Fill Opacity  0.05-0.50')
@param.bool('show_target',           default=True,                     title='Show Target Projection')

@param.color('color_st',             default=color.BLUE,               title='Symmetrical Triangle Color')
@param.color('color_at',             default=color.GREEN,              title='Ascending Triangle Color')
@param.color('color_dt',             default=color.RED,                title='Descending Triangle Color')
@param.color('color_fw',             default=color.LIME,               title='Falling Wedge Color')
@param.color('color_rw',             default=color.ORANGE,             title='Rising Wedge Color')

@param.bool('enable_alerts',         default=True,                     title='Enable Alerts  (configure via Set Alert dialog)')

@param.bool('use_macd',              default=False,                    title='Enable MACD Filter')
@param.source('macd_price',          default=source.CLOSE,             title='MACD Applied Price')
@param.int('macd_fast',              default=12,   min=1,              title='MACD Fast EMA')
@param.int('macd_slow',              default=26,   min=1,              title='MACD Slow EMA')
@param.int('macd_sig_len',           default=9,    min=1,              title='MACD Signal Period')

@param.bool('check_prev_mov',        default=False,                    title='Require Prior Trend')
@param.int('prev_mov_bars',          default=20,   min=5,    max=300,  title='Prev-movement Lookback (bars)')
@param.bool('use_uniform',           default=False,                    title='Use Uniformity Filter')

@param.int('max_bars',               default=300,  min=50,   max=1500, title='Max Bars Lookback')


class Main(MainContext):

    def __init__(self,
                 left_bars: int, right_bars: int,
                 use_sym_tri: bool, use_asc_tri: bool, use_desc_tri: bool,
                 use_fall_wdg: bool, use_rise_wdg: bool,
                 slope_tol: float, triangularity: float, min_pattern_bars: int,
                 st_symmetry_tol: float,
                 breakout_buffer: float, wait_for_close: bool, confirm_bars: int,
                 max_breakout_delay: float,
                 atr_period: int,
                 use_atr_compression: bool, atr_compression_ratio: float,
                 use_breakout_impulse: bool, breakout_atr_mult: float,
                 require_3rd_touch: bool,
                 fill_pattern: bool, fill_opacity: float, show_target: bool,
                 color_st: Color, color_at: Color, color_dt: Color,
                 color_fw: Color, color_rw: Color,
                 enable_alerts: bool,
                 use_macd: bool, macd_price: SeriesF,
                 macd_fast: int, macd_slow: int, macd_sig_len: int,
                 check_prev_mov: bool, prev_mov_bars: int,
                 use_uniform: bool,
                 max_bars: int):

        self._ml = self.new_mut_series_f(0.0)

        self.hbi: list[int]   = []
        self.ht:  list[float] = []
        self.hp:  list[float] = []

        self.lbi: list[int]   = []
        self.lt:  list[float] = []
        self.lp:  list[float] = []

        self.drawn: list[str] = []
        self.touch_tol = 0.003

        self.left_bars             = left_bars
        self.right_bars            = right_bars
        self.use_sym_tri           = use_sym_tri
        self.use_asc_tri           = use_asc_tri
        self.use_desc_tri          = use_desc_tri
        self.use_fall_wdg          = use_fall_wdg
        self.use_rise_wdg          = use_rise_wdg
        self.slope_tol             = slope_tol
        self.triangularity         = triangularity
        self.min_pattern_bars      = min_pattern_bars
        self.st_symmetry_tol       = st_symmetry_tol
        self.breakout_buffer       = breakout_buffer
        self.wait_for_close        = wait_for_close
        self.confirm_bars          = confirm_bars
        self.max_breakout_delay    = max_breakout_delay
        self.atr_period            = atr_period
        self.use_atr_compression   = use_atr_compression
        self.atr_compression_ratio = atr_compression_ratio
        self.use_breakout_impulse  = use_breakout_impulse
        self.breakout_atr_mult     = breakout_atr_mult
        self.require_3rd_touch     = require_3rd_touch
        self.fill_pattern          = fill_pattern
        self.fill_opacity          = fill_opacity
        self.show_target           = show_target
        self.color_st              = color_st
        self.color_at              = color_at
        self.color_dt              = color_dt
        self.color_fw              = color_fw
        self.color_rw              = color_rw
        self.enable_alerts         = enable_alerts
        self.use_macd              = use_macd
        self.macd_price            = macd_price
        self.macd_fast             = macd_fast
        self.macd_slow             = macd_slow
        self.macd_sig_len          = macd_sig_len
        self.check_prev_mov        = check_prev_mov
        self.prev_mov_bars         = prev_mov_bars
        self.use_uniform           = use_uniform
        self.max_bars              = max_bars

    def calc(self) -> float:
        atr          = Atr.new(self.atr_period)
        fema         = Ema.new(self.macd_price, self.macd_fast)
        sema         = Ema.new(self.macd_price, self.macd_slow)
        self._ml[0]  = fema[0] - sema[0]
        slma         = Ema.new(self._ml, self.macd_sig_len)
        hist         = self._ml[0] - slma[0]
        return self._detect(hist, atr)

    def _detect(self, hist: float, atr: SeriesF) -> float:
        bi = self.bar_index
        bc = self.bar_count
        c0 = self.close[0]
        t0 = self.time[0]

        self._push_fractals(self.left_bars, self.right_bars, bi, bc)

        nh = len(self.hbi)
        nl = len(self.lbi)
        if nh < 2 or nl < 2:
            return 0.0

        h1bi = self.hbi[nh - 2]; h1t = self.ht[nh - 2]; h1p = self.hp[nh - 2]
        h2bi = self.hbi[nh - 1]; h2t = self.ht[nh - 1]; h2p = self.hp[nh - 1]
        l1bi = self.lbi[nl - 2]; l1t = self.lt[nl - 2]; l1p = self.lp[nl - 2]
        l2bi = self.lbi[nl - 1]; l2t = self.lt[nl - 1]; l2p = self.lp[nl - 1]

        oldest_bi = min(h1bi, l1bi)
        newest_bi = max(h2bi, l2bi)

        if (bi - oldest_bi) > self.max_bars:
            return 0.0
        if (newest_bi - oldest_bi) < self.min_pattern_bars:
            return 0.0

        h_dx  = max(1, h2bi - h1bi)
        l_dx  = max(1, l2bi - l1bi)
        h_slp = (h2p - h1p) / h_dx
        l_slp = (l2p - l1p) / l_dx

        avg_p = (h1p + h2p + l1p + l2p) * 0.25
        if avg_p <= 0.0:
            return 0.0

        h_pct = abs(h_slp) / avg_p
        l_pct = abs(l_slp) / avg_p
        tol   = self.slope_tol

        ref_early = max(h1bi, l1bi)
        ref_late  = max(h2bi, l2bi)
        we = (h1p + h_slp * (ref_early - h1bi)) - (l1p + l_slp * (ref_early - l1bi))
        wl = (h1p + h_slp * (ref_late  - h1bi)) - (l1p + l_slp * (ref_late  - l1bi))
        if we <= 0.0:
            return 0.0
        if (wl / we) > (1.0 - self.triangularity * 0.5):
            return 0.0

        conv_fac = 1.0 - (wl / we)
        if conv_fac > 1.0:
            conv_fac = 1.0
        if conv_fac < 0.0:
            conv_fac = 0.0

        slp_diff   = abs(h_slp - l_slp)
        apex_valid = False
        apex_x: float = 0.0
        if slp_diff > 1e-12:
            apex_x     = (l1p - h1p + h_slp * h1bi - l_slp * l1bi) / (h_slp - l_slp)
            apex_valid = True

        pat_len = newest_bi - oldest_bi

        if apex_valid:
            if bi > apex_x - pat_len * self.max_breakout_delay:
                return 0.0

        atr_comp_ok = False
        if self.use_atr_compression:
            offset_old     = bi - oldest_bi
            atr_old: float = atr[0]
            if offset_old < bc:
                atr_old = atr[offset_old]
            if atr_old > 0.0:
                if atr[0] > atr_old * self.atr_compression_ratio:
                    return 0.0
                atr_comp_ok = True

        if self.use_uniform:
            h_dt   = h2bi - h1bi
            l_dt   = l2bi - l1bi
            dt_max = max(h_dt, l_dt)
            dt_min = min(h_dt, l_dt)
            ratio: float = 0.0
            if dt_max > 0:
                ratio = dt_min / dt_max
            if ratio < 0.5:
                return 0.0

        pattern:        str  = ''
        is_bull_known:  bool = False
        is_bull:        bool = False
        need_apex_dist: bool = False

        if self.use_sym_tri and h_slp < 0.0 and l_slp > 0.0:
            sym_diff = abs(abs(h_slp) - abs(l_slp)) / avg_p
            avg_pct  = (h_pct + l_pct) * 0.5
            if sym_diff >= self.st_symmetry_tol * avg_pct + 0.001:
                return 0.0
            pattern        = 'ST'
            is_bull_known  = False
            need_apex_dist = True
        elif self.use_asc_tri and h_pct < tol and l_slp > 0.0:
            pattern        = 'AT'
            is_bull_known  = True
            is_bull        = True
            need_apex_dist = False
        elif self.use_desc_tri and h_slp < 0.0 and l_pct < tol:
            pattern        = 'DT'
            is_bull_known  = True
            is_bull        = False
            need_apex_dist = False
        elif self.use_fall_wdg and h_slp < 0.0 and l_slp < 0.0 and h_slp < l_slp:
            pattern        = 'FW'
            is_bull_known  = True
            is_bull        = True
            need_apex_dist = True
        elif self.use_rise_wdg and h_slp > 0.0 and l_slp > 0.0 and l_slp > h_slp:
            pattern        = 'RW'
            is_bull_known  = True
            is_bull        = False
            need_apex_dist = True

        if pattern == '':
            return 0.0

        if need_apex_dist and apex_valid:
            apex_dist = apex_x - newest_bi
            pat_len_f = float(pat_len)
            if apex_dist <= 0.0 or apex_dist > pat_len_f * 2.0:
                return 0.0

        if self.use_macd and is_bull_known:
            if is_bull:
                if hist < 0.0:
                    return 0.0
            else:
                if hist > 0.0:
                    return 0.0

        if self.check_prev_mov and is_bull_known:
            if not self._prev_ok(oldest_bi, is_bull, bi, bc):
                return 0.0

        q = self._quality(h_slp, l_slp, h1bi, h1p, l1bi, l1p, nh, nl)

        if self.require_3rd_touch and q == '':
            return 0.0

        if self.wait_for_close and not self.is_closed_bar:
            return 0.0

        cur_h = h1p + h_slp * (bi - h1bi)
        cur_l = l1p + l_slp * (bi - l1bi)
        buf   = self.breakout_buffer / 100.0

        bull_ok = c0 > cur_h * (1.0 + buf)
        bear_ok = c0 < cur_l * (1.0 - buf)

        if is_bull_known:
            if is_bull:
                if not bull_ok:
                    return 0.0
            else:
                if not bear_ok:
                    return 0.0
        else:
            is_bull = False
            if bull_ok:
                is_bull = True
            elif not bear_ok:
                return 0.0

        if self.use_macd:
            if is_bull:
                if hist < 0.0:
                    return 0.0
            else:
                if hist > 0.0:
                    return 0.0
        if self.check_prev_mov and pattern == 'ST':
            if not self._prev_ok(oldest_bi, is_bull, bi, bc):
                return 0.0

        n_conf = self.confirm_bars
        if n_conf > 1:
            i_conf = 1
            while i_conf < n_conf:
                past_bi  = bi - i_conf
                proj_h_i = h1p + h_slp * (past_bi - h1bi)
                proj_l_i = l1p + l_slp * (past_bi - l1bi)
                if is_bull:
                    if self.close[i_conf] <= proj_h_i * (1.0 + buf):
                        return 0.0
                else:
                    if self.close[i_conf] >= proj_l_i * (1.0 - buf):
                        return 0.0
                i_conf = i_conf + 1

        impulse_ok = False
        if self.use_breakout_impulse:
            needed = atr[0] * self.breakout_atr_mult
            if is_bull:
                if (c0 - cur_h) < needed:
                    return 0.0
                impulse_ok = True
            else:
                if (cur_l - c0) < needed:
                    return 0.0
                impulse_ok = True

        score = self._score(conv_fac, q, atr_comp_ok, impulse_ok, pat_len)

        dc = 'B'
        if not is_bull:
            dc = 'S'
        bk = 'B:' + pattern + ':' + str(h1bi) + ':' + str(l1bi) + ':' + dc
        if bk not in self.drawn:
            fill_op = self.fill_opacity * (0.4 + 0.6 * conv_fac)
            if fill_op > 0.90:
                fill_op = 0.90

            # apex point in chart coordinates (time projected forward
            # from the current bar using the timeframe of the last 2 bars)
            tf: float = 0.0
            if bc >= 2:
                t1 = self.time[1]
                if not isnan(t1):
                    tf = t0 - t1
            apex_ok = apex_valid and tf > 0.0
            t_apex: float = t0
            apex_p: float = (cur_h + cur_l) * 0.5
            if apex_ok:
                t_apex = t0 + (apex_x - bi) * tf
                apex_p = h1p + h_slp * (apex_x - h1bi)

            self._draw_shape(pattern,
                             h1t, h1p, h2t, h2p,
                             l1t, l1p, l2t, l2p,
                             t0, cur_h, cur_l,
                             apex_ok, t_apex, apex_p,
                             q, fill_op)
            self._draw_signal(pattern, is_bull, cur_h, cur_l, we, score, t0)
            self._append_key(bk)

        out: float = -1.0
        if is_bull:
            out = 1.0
        return out

    def _pat_lc(self, p: str) -> Color:
        result: Color = self.color_st
        if p == 'AT':
            result = self.color_at
        elif p == 'DT':
            result = self.color_dt
        elif p == 'FW':
            result = self.color_fw
        elif p == 'RW':
            result = self.color_rw
        return result

    def _draw_shape(self,
                    p: str,
                    h1t: float, h1p: float,
                    h2t: float, h2p: float,
                    l1t: float, l1p: float,
                    l2t: float, l2p: float,
                    t0: float, cur_h: float, cur_l: float,
                    apex_ok: bool, t_apex: float, apex_p: float,
                    q: str, fill_op: float) -> None:
        """
        TRUE TRIANGLE: both boundary lines extended forward to their actual
        intersection (apex). Three corners — h1, apex, l1 — three edges:
        upper h1→apex (solid), lower l1→apex (solid), back h1→l1 (dashed).
        FILL: one Triangle(h1, apex, l1); edges coincide with the lines.
        Fallback (no apex / no timeframe): v5.5 breakout-bar geometry.
        """
        lc = self._pat_lc(p)

        if apex_ok:
            self.chart.draw(LineSegment(
                AbsolutePosition(h1t, h1p),
                AbsolutePosition(t_apex, apex_p),
                color=lc, line_width=2))
            self.chart.draw(LineSegment(
                AbsolutePosition(l1t, l1p),
                AbsolutePosition(t_apex, apex_p),
                color=lc, line_width=2))
            self.chart.draw(LineSegment(
                AbsolutePosition(h1t, h1p),
                AbsolutePosition(l1t, l1p),
                color=lc, line_width=1, line_style=line_style.DASHED))
            if self.fill_pattern:
                self.chart.draw(Triangle(
                    AbsolutePosition(h1t, h1p),
                    AbsolutePosition(t_apex, apex_p),
                    AbsolutePosition(l1t, l1p),
                    line_color=color.TRANSPARENT, bg_color=lc(fill_op)))
        else:
            self.chart.draw(LineSegment(
                AbsolutePosition(h1t, h1p),
                AbsolutePosition(t0, cur_h),
                color=lc, line_width=2))
            self.chart.draw(LineSegment(
                AbsolutePosition(l1t, l1p),
                AbsolutePosition(t0, cur_l),
                color=lc, line_width=2))
            self.chart.draw(LineSegment(
                AbsolutePosition(h1t, h1p),
                AbsolutePosition(l1t, l1p),
                color=lc, line_width=1, line_style=line_style.DASHED))
            if self.fill_pattern:
                fc = lc(fill_op)
                self.chart.draw(Triangle(
                    AbsolutePosition(h1t, h1p),
                    AbsolutePosition(t0, cur_h),
                    AbsolutePosition(t0, cur_l),
                    line_color=color.TRANSPARENT, bg_color=fc))
                self.chart.draw(Triangle(
                    AbsolutePosition(h1t, h1p),
                    AbsolutePosition(t0, cur_l),
                    AbsolutePosition(l1t, l1p),
                    line_color=color.TRANSPARENT, bg_color=fc))

        lbl_t = h2t
        if l2t > h2t:
            lbl_t = l2t
        lbl_p = (h2p + l2p) * 0.5
        self.chart.draw(LabelAbs(
            p + q, AbsolutePosition(lbl_t, lbl_p),
            bg_color=lc, text_color=color.WHITE))

    def _draw_signal(self, p: str, is_bull: bool,
                     cur_h: float, cur_l: float,
                     we: float, score: int, t0: float) -> None:

        score_s = str(score)
        lbl = 'SELL ' + p + ' ' + score_s
        bg  = color.RED
        sp  = cur_h * 1.0002
        if is_bull:
            lbl = 'BUY ' + p + ' ' + score_s
            bg  = color.GREEN
            sp  = cur_l * 0.9998
        self.chart.draw(LabelAbs(
            lbl, AbsolutePosition(t0, sp),
            bg_color=bg, text_color=color.WHITE))

        if self.show_target:
            tc: Color = color.TEAL
            tp = cur_h + we
            if not is_bull:
                tc = color.PURPLE
                tp = cur_l - we
            self.chart.draw(LineSegment(
                AbsolutePosition(t0, sp),
                AbsolutePosition(t0, tp),
                color=tc, line_width=2))
            self.chart.draw(LabelAbs(
                'T', AbsolutePosition(t0, tp),
                bg_color=tc, text_color=color.WHITE))

    def _push_fractals(self, lb: int, rb: int, bi: int, bc: int) -> None:
        total = lb + rb + 1
        if bc < total:
            return
        ch = self.high[rb]
        cl = self.low[rb]
        if isnan(ch) or isnan(cl):
            return
        is_h = True
        is_l = True
        for j in range(total):
            if j == rb:
                continue
            if is_h:
                hj = self.high[j]
                if not isnan(hj) and hj >= ch:
                    is_h = False
            if is_l:
                lj = self.low[j]
                if not isnan(lj) and lj <= cl:
                    is_l = False
            if not is_h and not is_l:
                break
        cbi = bi - rb
        ct  = self.time[rb]
        nh  = len(self.hbi)
        if is_h and (nh == 0 or self.hbi[nh - 1] != cbi):
            self.hbi.append(cbi); self.ht.append(ct); self.hp.append(ch)
            if len(self.hbi) > 3:
                self.hbi.pop(0); self.ht.pop(0); self.hp.pop(0)
        nl = len(self.lbi)
        if is_l and (nl == 0 or self.lbi[nl - 1] != cbi):
            self.lbi.append(cbi); self.lt.append(ct); self.lp.append(cl)
            if len(self.lbi) > 3:
                self.lbi.pop(0); self.lt.pop(0); self.lp.pop(0)

    def _score(self, conv_fac: float, q: str,
               atr_comp_ok: bool, impulse_ok: bool,
               pattern_len: int) -> int:
        s = 0
        c_pts = conv_fac * 25.0
        if c_pts > 25.0:
            c_pts = 25.0
        s = s + int(c_pts)
        if self.use_uniform:
            s = s + 15
        if q == '+':
            s = s + 10
        elif q == '++':
            s = s + 20
        if atr_comp_ok:
            s = s + 15
        if impulse_ok:
            s = s + 15
        len_pts = pattern_len // 10
        if len_pts > 10:
            len_pts = 10
        s = s + len_pts
        return s

    def _quality(self, h_slp: float, l_slp: float,
                 h1bi: int, h1p: float,
                 l1bi: int, l1p: float,
                 nh: int, nl: int) -> str:
        q = 0
        if nh >= 3:
            h3bi = self.hbi[nh - 3]; h3p = self.hp[nh - 3]
            diff = abs(h1p + h_slp * (h3bi - h1bi) - h3p)
            if h3p != 0.0 and diff / abs(h3p) < self.touch_tol:
                q = q + 1
        if nl >= 3:
            l3bi = self.lbi[nl - 3]; l3p = self.lp[nl - 3]
            diff = abs(l1p + l_slp * (l3bi - l1bi) - l3p)
            if l3p != 0.0 and diff / abs(l3p) < self.touch_tol:
                q = q + 1
        result: str = ''
        if q == 1:
            result = '+'
        elif q >= 2:
            result = '++'
        return result

    def _prev_ok(self, oldest_bi: int, is_bull: bool,
                 bi: int, bc: int) -> bool:
        off_old = bi - oldest_bi
        off_pre = off_old + self.prev_mov_bars
        if off_pre >= bc:
            return True
        cb = self.close[off_pre]
        ca = self.close[off_old]
        if isnan(cb) or isnan(ca):
            return True
        result: bool = cb < ca
        if is_bull:
            result = cb > ca
        return result

    def _append_key(self, key: str) -> None:
        self.drawn.append(key)
        if len(self.drawn) > 400:
            self.drawn = self.drawn[200:]
