Indicator · Pine Script v6 · On the chart

Kinetic Trend Envelope

by vedkanani123· code credited to GoemonYae, 2026-07

An adaptive trailing-stop indicator on the price chart from the GoemonYae trading system community edition.

View on GitHub

GitHub stars1on the repository
Pine versionv6current
Lines of code828
Settings25inputs
LicenceNone statedfile: MPL-2.0
Last update2 months ago23 Aug 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

828 lines of Pine Script v6 from indicator/olg_7_6_26/ALL_2/codex/indicator/6/EMA Crossover Buy/[GYTS-CE] Kinetic Trend Envelope (adaptive trailing stop)_31.txt, published under MPL-2.0 (file header). Keep the author's credit and licence lines when you reuse it.

[GYTS-CE] Kinetic Trend Envelope (adaptive trailing stop)_31.txt
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © GoemonYae, 2026-07
//
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
// ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░
// ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
// ░▒▓                                                                                                   ▓▒░
// ░▒▓        ██████╗ ██╗   ██╗████████╗███████╗                                                         ▓▒░
// ░▒▓       ██╔════╝ ╚██╗ ██╔╝╚══██╔══╝██╔════╝        GoemonYae Trading System                         ▓▒░
// ░▒▓       ██║  ███╗ ╚████╔╝    ██║   ███████╗                                                         ▓▒░
// ░▒▓       ██║   ██║  ╚██╔╝     ██║   ╚════██║        Kinetic Trend Envelope (Community Edition)       ▓▒░
// ░▒▓       ╚██████╔╝   ██║      ██║   ███████║        Adaptive Trailing Stop Indicator                 ▓▒░
// ░▒▓        ╚═════╝    ╚═╝      ╚═╝   ╚══════╝        Version 1.0.1                                    ▓▒░
// ░▒▓                                                                                                   ▓▒░
// ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
// ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░
// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
//
//@version=6
//@description     GYTS "Kinetic Trend Envelope" (Community Edition) — Adaptive Trailing Stop Indicator

indicator('[GYTS-CE] Kinetic Trend Envelope', '[GYTS-CE] KTE', overlay=true, precision=4, max_lines_count=500, max_boxes_count=500)

const string VERSION = '1.0.1'

// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
//                                           KINETIC TREND ENVELOPE (KTE)
//                                      An Energy-State Adaptive Trailing Stop
// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
//
// PHILOSOPHY
// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// Markets are not static structures but kinetic systems. Energy flows through them, accumulates, and dissipates. The Kinetic
// Trend Envelope captures this energy signature, expanding when the market expends force and contracting when motion settles
// into equilibrium.
//
// Unlike conventional trend-following stops that treat volatility as mere noise to be filtered, the KTE recognises volatility
// for what it truly is: the kinetic energy of the market. Every price movement represents energy transfer — from buyer to
// seller, from uncertainty to conviction, from accumulation to distribution.
//
// WHY "KINETIC"?
// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// The term "kinetic" is deliberately chosen from physics, where kinetic energy (½mv²) describes energy possessed by an object
// due to its motion. In our context:
//
//   • Volatility IS kinetic energy — ATR, Parkinson, Garman-Klass, Rogers-Satchell, Yang-Zhang are not arbitrary statistical measures.
//     They quantify how much the market is moving, how much energy is being expended in price discovery.
//   • Higher volatility = more kinetic energy = wider envelope bands
//   • Lower volatility = less kinetic energy = tighter envelope bands
//
// This indicator doesn't just follow price; it measures the market's energy state and adapts its protective envelope
// accordingly. The bands breathe with the market's rhythm, expanding during periods of high energy expenditure and
// contracting when the market conserves its force.
//
// MATHEMATICAL FOUNDATIONS
// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// The KTE employs sophisticated volatility estimators from quantitative finance:
//
//   1. Average True Range (ATR)      — Wilder's classic, handles gaps, industry standard
//   2. Parkinson Estimator           — Range-based, ~5× statistical efficiency
//   3. Garman-Klass Estimator        — OHLC-optimised, ~7× efficiency
//   4. Rogers-Satchell Estimator     — Drift-independent, ~8× efficiency
//   5. Yang-Zhang Estimator          — Gap-aware, drift-independent, most comprehensive
//
// Each estimator captures different aspects of market energy, allowing traders to select the measure most appropriate
// for their instrument and timeframe.
//
// ADAPTIVE INTELLIGENCE
// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// The KTE features asymmetric volatility profiling — it separately tracks volatility during bullish and bearish candles,
// recognising that markets often exhibit different energy characteristics in each direction. This creates naturally
// asymmetric bands that respect the market's directional energy bias.
//
// ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════


//==================================================================//
//#region                       IMPORTS                             //
//==================================================================//

import GoemonYae/FiltersToolkit/2 as filt
import GoemonYae/MathTransform/5 as mt
import GoemonYae/ColourUtilities/8 as cu
import GoemonYae/VolatilityToolkit/3 as vol
import GoemonYae/ValidationUtilities/1 as vu

//#endregion


//==================================================================//
//#region                 CONSTANTS AND ENUMS                       //
//==================================================================//

// @enum                Trend direction tracked by the KTE state machine.
// @field bull          Bullish trend (active stop is the lower band).
// @field bear          Bearish trend (active stop is the upper band).
enum eDirection
    bull
    bear

// @enum                Volatility (kinetic energy) estimation method.
// @field atr           Classic Wilder ATR, handles gaps.
// @field parkinson     Range-based estimator, ~5× efficiency.
// @field garman_klass  OHLC-optimised, ~7× efficiency.
// @field rogers_satchell Drift-independent, ~8× efficiency.
// @field yang_zhang    Gap-aware, drift-independent.
enum eVolatilityMeasure
    atr              = 'Average True Range'
    parkinson        = 'Parkinson'
    garman_klass     = 'Garman-Klass'
    rogers_satchell  = 'Rogers-Satchell'
    yang_zhang       = 'Yang-Zhang'

// @enum                Direction switch trigger method.
// @field high_low      Uses high for bullish breach, low for bearish (most responsive, same-bar switch).
// @field on_close      Uses closing price (moderate, switch appears next bar after confirmation).
// @field estimator     Uses smoothed estimator (most conservative, switch appears next bar).
enum eSwitchMethod
    high_low   = 'on high/low'
    on_close   = 'on close'
    estimator  = 'on estimator'

// @enum                Pre-filtering algorithm for price source.
// @field ultimate_2    Ultimate Smoother (2-Pole), zero-lag in passband.
// @field ultimate_3    Ultimate Smoother (3-Pole), smoother with zero lag but overshoots.
// @field super_2       Super Smoother (2-Pole), excellent noise reduction.
// @field super_3       Super Smoother (3-Pole), maximum smoothing.
// @field biquad        BiQuad low pass filter, adjustable Q-factor.
// @field adxvma        ADXvma, adapts to trend strength.
// @field mama          MESA Adaptive Moving Average, tracks cycles.
// @field a2rma         Adaptive Autonomous Recursive MA.
enum eFilterType
    ultimate_2 = 'Ultimate Smoother (2-Pole)'
    ultimate_3 = 'Ultimate Smoother (3-Pole)'
    super_2    = 'Super Smoother (2-Pole)'
    super_3    = 'Super Smoother (3-Pole)'
    biquad     = 'BiQuad (Low Pass)'
    adxvma     = 'ADXvma'
    mama       = 'MAMA (Kaufman)'
    a2rma      = 'A2RMA'


//#endregion


//==================================================================//
//#region                  INPUT PARAMETERS                         //
//==================================================================//

const string g_general    = '🎛️ ────── General Settings ─────── 🎛️'
const string g_estimator  = '📐 ────────── Estimator ────────── 📐'
const string g_volatility = '📊 ────────── Volatility ────────── 📊'
const string g_kte        = '📈 ─────── Envelope (KTE) ──────── 📈'
const string g_visual     = '🎨 ──────── Visualisation ──────── 🎨'
const string g_alerts     = '🔔 ─────────── Alerts ──────────── 🔔'

//──────────────────────────── tooltips ────────────────────────────//

const string tt_source = 'The price source used for the estimator calculation.'

// Estimator (Price Filter)
const string tt_estimator_filter = 'Smoothing filter applied to the price source to produce the estimator.'
  + '\n\nThe estimator can be displayed on the chart (see "Show Estimator") and optionally '
  + 'used for direction switching (see "Direction Switch Method → on estimator").'
  + '\n\n• Ultimate Smoother (2/3-pole): Zero-lag in passband; 3-pole has true zero lag; can overshoot'
  + '\n• Super Smoother (2/3-pole): Ehlers low-lag IIR filter; excellent noise reduction'
  + '\n• BiQuad: 2-pole low-pass with adjustable Q-factor'
  + '\n• ADXvma: Adaptive to trend strength (flat in ranging markets)'
  + '\n• MAMA: MESA Adaptive Moving Average, tracks cycles'
  + '\n• A2RMA: Adaptive Autonomous Recursive MA, adjusts to volatility'

const string tt_estimator_lookback = 'Lookback period for the price estimator filter.'
  + '\n\nLower values = more responsive to price action'
  + '\nHigher values = smoother estimator, fewer direction changes'
  + '\n\nAffects the estimator line display and, when "Direction Switch Method" is set to '
  + '"on estimator", determines direction change sensitivity.'

// Shared between the Estimator and Volatility filter-factor inputs — the parameter has the
// same meaning in both groups (Q-factor for BiQuad, gamma for A2RMA, ignored elsewhere).
const string tt_filter_factor = 'Filter-specific parameter (Q-factor or Gamma). '
  + 'Only BiQuad and A2RMA use this setting; other filters ignore it.'
  + '\n\nBiQuad (Q-factor, typically ~0.7):'
  + '\n• Lower Q (0.3-0.5): Heavily damped, very smooth'
  + '\n• Q ≈ 0.707: Butterworth (maximally flat)'
  + '\n• Higher Q (0.8-1.0): More selective, may overshoot'
  + '\n\nA2RMA (gamma, typically 1.5-3.0):'
  + '\n• Lower gamma: More reactive to small moves'
  + '\n• Higher gamma: Flatter in ranges, responds to breakouts'
  + '\n\nNote: Default 0.70 is tuned for BiQuad. For A2RMA, increase to the typical range above.'

// Volatility Measure
const string tt_volatility_measure = "Selects the volatility (kinetic energy) estimator for band width calculation:"
  + "\n\n• Average True Range: Classic Wilder ATR, handles gaps, industry standard"
  + "\n• Parkinson (High-Low): Range-based, ~5× efficiency, best for ranging markets"
  + "\n• Garman-Klass: Adds open/close dynamics, ~7× efficiency, short-term focus"
  + "\n• Rogers-Satchell: Drift-independent, ~8× efficiency, ideal for trending/crypto"
  + "\n• Yang-Zhang: Most comprehensive, handles gaps + drift, best for daily TF"
  + "\n\nThe estimators measure different quantities, so at one Volatility Factor they would otherwise produce "
  + "different stop distances (ATR runs widest). Each is internally scaled by a per-estimator width factor so one "
  + "Volatility Factor yields comparable band widths across all five — expect only minor fine-tuning when switching."

const string tt_volatility_lookback = 'Lookback period for the selected volatility (kinetic energy) estimator.'
  + '\n\nLower values = more responsive to energy changes'
  + '\nHigher values = smoother, more stable volatility readout'
  + '\n\nNote: Distinct from "Adaptation Speed", which operates on top of this per-bar reading '
  + 'to smooth the bull-vs-bear split.'

const string tt_volatility_filter = 'Smoothing filter applied to the volatility (kinetic energy) calculation.'
  + '\n\nNote: ATR and Yang-Zhang ignore this setting — the dropdown is greyed out when either is selected. '
  + 'ATR always uses RMA (Wilder smoothing); Yang-Zhang uses its internal rolling variance.'
  + '\n\n• SMA / EMA: Traditional moving averages'
  + '\n• Ultimate Smoother: Zero-lag in passband'
  + '\n• Super Smoother: Ehlers low-lag IIR filter; excellent noise reduction'
  + '\n• BiQuad: 2-pole low-pass with adjustable Q-factor'
  + '\n• ADXvma: Adaptive to trend strength (flat in ranging markets)'
  + '\n• MAMA: MESA Adaptive Moving Average, tracks cycles'
  + '\n• A2RMA: Adaptive Autonomous Recursive MA, adjusts to volatility'

// Envelope (KTE)
// Naming note: this control is exposed as "Adaptation Speed" rather than "Adaptation Window".
// The input is a 1-10 scale, not a bar count — higher values mean *faster* adaptation
// (smaller internal lookback) and lower values mean slower adaptation (larger internal lookback).
// "Window" was rejected because it implies a fixed length in bars, which the input is not;
// "Speed" matches the monotonic, scale-like UX the slider provides.
const string tt_adaptation_speed = 'Speed at which the asymmetric bull/bear volatility profile adapts '
  + 'to recent price action.'
  + '\n\nNote: Distinct from "Volatility Lookback", which sets the per-bar estimator period. '
  + 'Adaptation Speed operates on top of that, smoothing the bull-vs-bear split.'
  + '\n\nThough not a direct width multiplier like "Volatility Factor", faster adaptation can '
  + 'visibly widen the bands during volatility spikes, recent samples dominate the smaller averaging window.'
  + '\n\nBecause faster adaptation averages the profile over fewer bars, it also makes the choice of '
  + 'Volatility Measure more visible; at slow settings the estimators look nearly identical.'
  + '\n\nHigher values = faster adaptation'
  + '\nLower values = smoother, more stable bands'
  + '\n\nTip: increase this value when few bars are available, otherwise early readings dominate the profile.'

const string tt_volatility_factor = 'Multiplier applied to the kinetic energy measure to determine envelope width.'
  + '\n\nThe envelope half-width is approximately factor × typical_bar_volatility, in price units. '
  + 'After internal normalisation, the default 5.0 produces sensible widths for most instruments and '
  + 'timeframes; values in the 3–8 range cover most use cases, regardless of the active volatility estimator.'
  + '\n\nHigher values = wider envelope, fewer signals, later entries'
  + '\nLower values = tighter envelope, more signals, earlier entries'

const string tt_switch_method = 'Determines what price must breach the envelope to trigger a direction change:'
  + '\n\n• On high/low: Uses high for bullish breach, low for bearish (most responsive). '
  + 'Switch appears on the same bar as the breach — like a limit order filling immediately.'
  + '\n• On close: Uses the previous bar\'s closing price. '
  + 'Switch appears on the bar after the breach — the breach is confirmed at bar close, acted upon next bar.'
  + '\n• On estimator: Uses the previous bar\'s smoothed estimator (most conservative, reduces whipsaws). '
  + 'Same next-bar timing as "on close".'
  + '\n\nNote: When set to "on estimator", filter configuration lives in the "Estimator" group above.'
  + '\n\nNote: In "on high/low" the flip fires on the breach bar itself, but the thick active line is drawn back to '
  + 'the previous bar, so its colour appears to change one bar before the breaching candle. This is purely a drawing '
  + 'effect — the direction flip, alerts and data streams all update on the breach bar. The two next-bar modes do not '
  + 'show this because their line connects back onto the breach bar.'

// Visualisation
const string tt_palette = 'Selects the colour palette for Kinetic Trend Envelope visualisation.'

const string tt_dark_mode = "Adjusts colour intensity, saturation and brightness for optimal visibility on dark backgrounds."
  + "\n\nDark mode deepens fill colours against a dark chart. Light mode softens and mutes them for legibility on white."

const string tt_fill_transparency = 'Transparency of the fill between price and KTE band.'
  + '\n\n0 = fully opaque, 100 = fully transparent.'

const string tt_show_estimator = 'Display the smoothed price estimator line on the chart.'
  + '\n\nThe estimator provides a smoothed view of price action. When "Direction Switch Method" '
  + 'is set to "on estimator", this line determines when direction changes occur.'

const string tt_opposing = 'Displays the opposing band (target) to visualise the full envelope width.'

// Alerts
const string tt_alerts_bull = "Includes bullish direction changes in the 'Any alert() function call' stream."
  + "\n\nOnly affects the bundled alert stream — the dedicated 'Direction → Bullish' alert condition"
  + " in TradingView's dropdown works independently of this toggle."
  + "\n\nUseful for receiving all events through a single alert slot (saves alert quota)."

const string tt_alerts_bear = "Includes bearish direction changes in the 'Any alert() function call' stream."
  + "\n\nOnly affects the bundled alert stream — the dedicated 'Direction → Bearish' alert condition"
  + " in TradingView's dropdown works independently of this toggle."
  + "\n\nUseful for receiving all events through a single alert slot (saves alert quota)."

//────────────────────────── general inputs ────────────────────────//

i_source = input.source(hlc3, 'Source', group=g_general, tooltip=tt_source)

//────────────────────────────── estimator ─────────────────────────//

i_estimator_filter        = input.enum(eFilterType.ultimate_2, 'Estimator Filter', group=g_estimator, tooltip=tt_estimator_filter)
i_estimator_lookback      = input.int(15, 'Estimator Lookback', minval=2, maxval=200, group=g_estimator, tooltip=tt_estimator_lookback)
i_estimator_filter_factor = input.float(0.7, 'Estimator Filter Factor (Q / Gamma)', minval=0.1, maxval=5.0, step=0.1, group=g_estimator, tooltip=tt_filter_factor, active=(i_estimator_filter == eFilterType.biquad or i_estimator_filter == eFilterType.a2rma))

//────────────────────────────── volatility ────────────────────────//

i_volatility_measure          = input.enum(eVolatilityMeasure.yang_zhang, 'Volatility Measure', group=g_volatility, tooltip=tt_volatility_measure)
i_volatility_lookback         = input.int(14, 'Volatility Lookback', minval=1, maxval=200, group=g_volatility, tooltip=tt_volatility_lookback)
// ATR uses Wilder RMA internally; Yang-Zhang uses rolling variance. Both bypass the filter pipeline,
// so the filter dropdown and its factor do not apply to those two measures.
bool b_volatility_uses_filter = i_volatility_measure != eVolatilityMeasure.atr and i_volatility_measure != eVolatilityMeasure.yang_zhang
i_volatility_filter           = input.enum(vol.eFilterType.sma, 'Volatility Filter', group=g_volatility, tooltip=tt_volatility_filter, active=b_volatility_uses_filter)
i_volatility_filter_factor    = input.float(0.7, 'Volatility Filter Factor (Q / Gamma)', minval=0.1, maxval=5.0, step=0.1, group=g_volatility, tooltip=tt_filter_factor, active=(b_volatility_uses_filter and (i_volatility_filter == vol.eFilterType.biquad or i_volatility_filter == vol.eFilterType.a2rma)))

//──────────────────────────── envelope (kte) ──────────────────────//

i_adaptation_speed  = input.float(3.5, 'Adaptation Speed', minval=1.0, maxval=10.0, step=0.1, group=g_kte, tooltip=tt_adaptation_speed)
i_volatility_factor = input.float(5.0, 'Volatility Factor', minval=0.5, maxval=50.0, step=0.1, group=g_kte, tooltip=tt_volatility_factor)
i_switch_method     = input.enum(eSwitchMethod.estimator, 'Direction Switch Method', group=g_kte, tooltip=tt_switch_method)

//─────────────────────────── visualisation ────────────────────────//

i_palette_name         = input.enum(cu.PaletteNames.gyts_purple, 'Palette', group=g_visual, tooltip=tt_palette)
i_dark_mode            = input.bool(true, 'Dark Mode', group=g_visual, tooltip=tt_dark_mode)

// Regular KTE
i_kte_linewidth        = input.int(3, 'KTE Line Width', minval=1, maxval=10, inline='kte', group=g_visual)
i_kte_line_transp      = input.int(0, 'Transparency', minval=0, maxval=100, inline='kte', group=g_visual)
i_kte_fill_transp      = input.int(80, 'KTE Fill Transparency', minval=0, maxval=100, group=g_visual, tooltip=tt_fill_transparency)

// Opposing Band
i_show_opposing        = input.bool(true, 'Show Opposing Band', group=g_visual, tooltip=tt_opposing)
i_opposing_linewidth   = input.int(1, 'Opposing Band Line Width', minval=1, maxval=10, inline='opposing', group=g_visual, active=i_show_opposing)
i_opposing_line_transp = input.int(75, 'Transparency', minval=0, maxval=100, inline='opposing', group=g_visual, active=i_show_opposing)
i_opposing_fill_transp = input.int(92, 'Opposing Band Fill Transparency', minval=0, maxval=100, group=g_visual, active=i_show_opposing)

// Estimator
i_show_estimator       = input.bool(true, 'Show Estimator', group=g_visual, tooltip=tt_show_estimator)
i_est_width            = input.int(2, 'Estimator Width', minval=1, maxval=10, inline='est', group=g_visual, active=i_show_estimator)
i_est_line_transp      = input.int(20, 'Transparency', minval=0, maxval=100, inline='est', group=g_visual, active=i_show_estimator)

//──────────────────────────── alerts ──────────────────────────────//

i_use_alerts_bull    = input.bool(true,  "Alert: Direction → Bullish", group=g_alerts, tooltip=tt_alerts_bull)
i_use_alerts_bear    = input.bool(true,  "Alert: Direction → Bearish", group=g_alerts, tooltip=tt_alerts_bear)

//#endregion


//==================================================================//
//#region                  USER-DEFINED TYPES                       //
//==================================================================//

// @type                State for tracking volatility (kinetic energy) profiles
// @field arr_positive  <array<float>> Volatility values during bullish candles
// @field arr_negative  <array<float>> Volatility values during bearish candles
// @field arr_direction <array<int>> Recent candle directions (1=bullish, 0=bearish)
// @field n_pos         <int> Current count limit for positive array
// @field n_neg         <int> Current count limit for negative array
type VolatilityProfile
    array<float> arr_positive
    array<float> arr_negative
    array<int>   arr_direction
    int          n_pos
    int          n_neg

// @type                     State for Kinetic Trend Envelope calculation
// @field direction          <eDirection> Current trend direction
// @field lower_band         <float> Current lower band value
// @field upper_band         <float> Current upper band value
type KineticTrendState
    eDirection direction
    float      lower_band
    float      upper_band

//#endregion


//==================================================================//
//#region                     VALIDATION                            //
//==================================================================//

var validator = vu.ValidationFramework.new()

if barstate.isfirst
    validator.init()

    validator.validate_range("Estimator", "Estimator Lookback", i_estimator_lookback, 2, 200, 5, 100, "bars")
    validator.validate_range("Volatility", "Volatility Lookback", i_volatility_lookback, 1, 200, 5, 100, "bars")
    validator.validate_range("Envelope", "Adaptation Speed", i_adaptation_speed, 1.0, 10.0, 2.0, 8.0)
    validator.validate_range("Envelope", "Volatility Factor", i_volatility_factor, 0.5, 50.0, 2.0, 15.0)

    validator.trigger_errors()

//#endregion




//==================================================================//
//#region                VOLATILITY CALCULATIONS                    //
//==================================================================//

//──────────────────── adaptation speed scaling ────────────────────//

// Map the user-facing "Adaptation Speed" (1-10) to an internal lookback in bars. The mapping is logarithmic and inverted:
// speed 1 → ~5000-bar lookback (slow adaptation), speed 10 → ~2-bar lookback (very fast adaptation). The variable is named
// `adaptation_lookback` rather than `profile_lookback` to keep the input label (Adaptation Speed) and its derived value
// greppable from the same root, even though the value is technically the bar-count input to the asymmetric volatility profile.
adaptation_lookback = int(mt.scaler_logarithmic_interpolation(i_adaptation_speed, 1, 10, 5000, 2))

//───────────────────────── estimator calculation ──────────────────//

float estimator = switch i_estimator_filter
    eFilterType.ultimate_2 => filt.ultimate_smoother_2pole(i_source, i_estimator_lookback)
    eFilterType.ultimate_3 => filt.ultimate_smoother_3pole(i_source, i_estimator_lookback)
    eFilterType.super_2    => filt.super_smoother_2pole(i_source, i_estimator_lookback)
    eFilterType.super_3    => filt.super_smoother_3pole(i_source, i_estimator_lookback)
    eFilterType.biquad     => filt.lpf_biquad(i_source, i_estimator_lookback, i_estimator_filter_factor)
    eFilterType.adxvma     => filt.adxvma(i_source, i_estimator_lookback)
    eFilterType.a2rma      => filt.a2rma(i_source, i_estimator_lookback, i_estimator_filter_factor)
    eFilterType.mama       =>
        [m, f, k] = filt.mesa_adaptive_moving_average(i_source, lookback_period=i_estimator_lookback)
        m

//────────────────────── select active volatility ──────────────────//

// Use VolatilityToolkit library for range-based estimators with filter options
// Note: ATR uses ta.atr() for Wilder's RMA smoothing (industry standard)
//       Yang-Zhang uses internal rolling variance, filter_type not applicable
//
// Unit normalisation: ATR returns price units (e.g. ~$45 on a $4500 instrument), while the four range-based estimators
// return per-bar SD in log-return units (dimensionless, e.g. ~0.01). We first express each output as a fraction of price
// (`vol_pct`), then scale back to price units below — so `active_volatility` carries one consistent meaning across all
// five estimators and the user's `Volatility Factor` is asset-scale independent.
float vol_pct = switch i_volatility_measure
    eVolatilityMeasure.atr             => ta.atr(i_volatility_lookback) / math.max(close, 1e-10)  // floor denominator: guards close <= 0 / micro-price (cancels with the later × close on normal data)
    eVolatilityMeasure.parkinson       => vol.parkinson(i_volatility_lookback, i_volatility_filter, i_volatility_filter_factor)
    eVolatilityMeasure.garman_klass    => vol.garman_klass(i_volatility_lookback, i_volatility_filter, i_volatility_filter_factor)
    eVolatilityMeasure.rogers_satchell => vol.rogers_satchell(i_volatility_lookback, i_volatility_filter, i_volatility_filter_factor)
    eVolatilityMeasure.yang_zhang      => vol.yang_zhang(i_volatility_lookback)  // floors to a 2-bar window at lookback = 1 (the other four read a single bar)

// Per-estimator width-scale calibration. The five estimators measure different quantities, so at a fixed Volatility
// Factor they would emit different stop distances: ATR reports mean true range (~1.6× the log-return SD that the four
// range estimators report), so it runs widest. Multiplying by a per-estimator scale brings them onto a common basis so
// one Volatility Factor yields comparable widths; Yang-Zhang (the default) keeps a scale of 1.0, leaving the shipped
// default stop unchanged. ATR is set to 0.64 to offset its wider raw range; the four range estimators stay at 1.0.
const float SCALE_ATR             = 0.64
const float SCALE_PARKINSON       = 1.00
const float SCALE_GARMAN_KLASS    = 1.00
const float SCALE_ROGERS_SATCHELL = 1.00
const float SCALE_YANG_ZHANG      = 1.00
float vol_scale = switch i_volatility_measure
    eVolatilityMeasure.atr             => SCALE_ATR
    eVolatilityMeasure.parkinson       => SCALE_PARKINSON
    eVolatilityMeasure.garman_klass    => SCALE_GARMAN_KLASS
    eVolatilityMeasure.rogers_satchell => SCALE_ROGERS_SATCHELL
    eVolatilityMeasure.yang_zhang      => SCALE_YANG_ZHANG

// Convert back to price units for downstream band-width arithmetic.
float active_volatility = vol_pct * close * vol_scale

//#endregion


//==================================================================//
//#region              KINETIC TREND ENVELOPE CORE                  //
//==================================================================//

// @function                  Calculates adaptive Kinetic Trend Envelope with asymmetric volatility profiling
// @param volatility          <float> Current volatility (kinetic energy) value
// @param factor              <float> Volatility multiplier
// @param lookback            <int> Sensitivity lookback period
// @param band_anchor         <float> Price anchor for band positioning (always the estimator). Decoupled from switch method so the bands stay smooth regardless of switch sensitivity.
// @param switch_src          <float> Price source for direction switching (na for high/low mode)
// @returns                   <KineticTrendState> Updated KTE state
method calculate(KineticTrendState self, VolatilityProfile profile, float volatility, float factor, int lookback, float band_anchor, float switch_src) =>
    // Track candle direction and update volatility profiles
    bool is_bullish = close > open

    // ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    // NON-REPAINTING CONTRACT — three legs, all built on Pine's `var` semantics
    // ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    // 1. VAR-ARRAY ROLLBACK (per-tick safety). Every realtime tick of the live bar re-runs the script, but `var` state
    //    is rolled back to its previous-bar-close snapshot before the next tick fires. The unconditional `arr.push(...)`
    //    below therefore commits exactly ONE sample per bar — the final tick (i.e. with `close` = the bar's eventual
    //    close). This is why pushes are unconditional rather than gated on `barstate.isconfirmed`, and the same reason
    //    this method as a whole does NOT need `barstate.isconfirmed` to be safe.
    //
    // 2. FLIP TEST READS COMMITTED VALUES. The direction-flip comparison further below (`src_min < self.lower_band` /
    //    `src_max > self.upper_band`) reads `self.lower_band` / `self.upper_band` BEFORE this method has reassigned
    //    them — those fields still hold the prior bar's committed values. For `on_close` and `on_estimator`, the
    //    switch source is also from the previous bar (`close[1]` / `estimator[1]`), so BOTH sides of the comparison
    //    are committed values — zero live-bar mutation. For `high_low`, source is the current bar's monotonic
    //    `low`/`high`. No self-reference, no circularity in any mode.
    //
    // 3. BANDS ANCHORED TO ESTIMATOR. `band_anchor = estimator` (set by the caller). Switching `i_switch_method` only
    //    changes *when* flips fire; it does NOT drag the band location around with the wicks. See the SWITCH-METHOD
    //    SEMANTICS note above `switch_source = …` for the timing implications of each option.
    // ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    vu.push_limited(profile.arr_direction, is_bullish ? 1 : 0, lookback)

    // Calculate adaptive array sizes BEFORE the asymmetric push, so the push uses the freshly-computed limits. Reading
    // limits stale here would discard the first opposing-direction sample during a strongly one-directional warmup (e.g.
    // all-bull bars drive n_neg to 0, and the first bear sample is shifted out immediately). Floor both sides at 1.
    //
    // arr_direction is an array<int>: array.avg() would resolve to its integer overload and truncate the 0/1 mean to
    // {0, 1} (1 only when every sample is bullish), collapsing the asymmetric split to all-or-nothing. Compute the
    // proportion of bullish candles in floating point instead, so the bull/bear allocation is genuinely proportional.
    int   dir_count      = profile.arr_direction.size()
    float proportion_pos = dir_count > 0 ? float(profile.arr_direction.sum()) / dir_count : 0.5
    profile.n_pos := math.max(int(proportion_pos * lookback), 1)
    profile.n_neg := math.max(lookback - profile.n_pos, 1)

    if is_bullish
        vu.push_limited(profile.arr_positive, volatility, profile.n_pos)
    else
        vu.push_limited(profile.arr_negative, volatility, profile.n_neg)

    // push_limited() trims only the buffer it pushes to, and only by one element per call, so a buffer whose target
    // shrank while the opposite direction was running would otherwise retain stale samples indefinitely. Trim BOTH
    // buffers down to their current limits every bar to keep each within its window.
    while profile.arr_positive.size() > profile.n_pos
        profile.arr_positive.shift()
    while profile.arr_negative.size() > profile.n_neg
        profile.arr_negative.shift()

    // Asymmetric volatility (kinetic energy) averages. Fall back to the current per-bar volatility while a directional
    // buffer is still empty (a one-sided run that has not yet produced an opposing candle), so the active band is never
    // left na once the estimator and volatility have warmed up.
    float vol_positive = profile.arr_positive.size() > 0 ? profile.arr_positive.avg() : volatility
    float vol_negative = profile.arr_negative.size() > 0 ? profile.arr_negative.avg() : volatility

    // Determine threshold source for direction changes
    float src_min = na(switch_src) ? low : switch_src
    float src_max = na(switch_src) ? high : switch_src

    // ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    // DIRECTION FLIP DETECTION (moved before width calc)
    // ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    // See leg 2 of the NON-REPAINTING CONTRACT. Bands (`self.lower_band` / `self.upper_band`) are read BEFORE
    // reassignment — they hold the prior bar's committed values. For `on_close`/`estimator`, `switch_src` is also from
    // the previous bar (`close[1]`/`estimator[1]`), so the flip fires one bar AFTER the breach — matching confirmed-bar
    // trading semantics. For `high_low`, `src_min`/`src_max` resolve to current `low`/`high` (immediate).
    //
    // Doing the flip first lets every monotonic constraint below be gated on `direction_just_changed`, so
    // prior-direction state cannot leak into the new trend's first bar.
    previous_direction = self.direction
    if self.direction == eDirection.bull and src_min < self.lower_band
        self.direction := eDirection.bear
    else if self.direction == eDirection.bear and src_max > self.upper_band
        self.direction := eDirection.bull
    bool direction_just_changed = self.direction != previous_direction

    // Calculate band widths directly from the asymmetric volatility profile. Widths follow current volatility, so the
    // envelope is free to expand when kinetic energy rises mid-trend (the "expanding energy envelope"). Monotonic
    // trailing-stop behaviour is enforced at band level below — the active band never loosens — so no separate width
    // clamp is needed here; clamping the width too would only over-tighten the stop into a volatility expansion.
    float lower_width = vol_negative * factor
    float upper_width = vol_positive * factor

    // Calculate new band positions. Bands are anchored to the estimator (band_anchor) regardless of switch method, so a
    // sensitive switch source (e.g. high/low) does not drag the envelope around with the wicks. The estimator gives a
    // smooth, consistent reference; the switch method controls only when crossings count.
    float new_lower_band = band_anchor - lower_width
    float new_upper_band = band_anchor + upper_width

    // Update bands with monotonic constraint. Skip on the flip bar: the prior self.lower_band was the BEAR-mode opposing
    // band (set without constraint to band_anchor − lower_width, often close to anchor). Carrying it through math.max
    // would pin the new BULL active band tight to the anchor and make it widen only as the estimator rises past the stuck
    // level. Symmetric reasoning for BEAR.
    // Guard an na new band before the monotonic clamp: if the estimator anchor emits na mid-trend, math.max(x, na) = na would
    // null the active stop for one bar, then next bar nz() would reseed it at the raw new band without the historical max — a
    // transient one-bar loosening of a live stop. Holding the prior committed band through an na carry avoids that drop.
    if not na(new_lower_band)
        if self.direction == eDirection.bull and not direction_just_changed
            self.lower_band := math.max(nz(self.lower_band, new_lower_band), new_lower_band)
        else
            self.lower_band := new_lower_band

    if not na(new_upper_band)
        if self.direction == eDirection.bear and not direction_just_changed
            self.upper_band := math.min(nz(self.upper_band, new_upper_band), new_upper_band)
        else
            self.upper_band := new_upper_band

    self

//───────────────────────── state initialisation ───────────────────//

var KineticTrendState kte_state = KineticTrendState.new(
    direction=eDirection.bull,
    lower_band=na,
    upper_band=na
)

var VolatilityProfile vol_profile = VolatilityProfile.new(
    arr_positive=array.new<float>(0),
    arr_negative=array.new<float>(0),
    arr_direction=array.new<int>(0),
    n_pos=10000,
    n_neg=10000
)

//─────────────────── calculate kinetic trend envelope ─────────────//

// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// SWITCH-METHOD SEMANTICS (critical for downstream strategy authors)
// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// The chosen i_switch_method controls *when* a direction flip fires and how the switch aligns with trading behaviour.
// It does NOT affect band location (bands are anchored to the estimator — see leg 3 of NON-REPAINTING CONTRACT inside
// `calculate`).
//
//   • high_low (switch_src = na → low/high): Uses the current bar's high/low, which are MONOTONIC on the live bar.
//     Once a flip fires intra-bar it stays fired. Behaviour is IDENTICAL to a resting stop/limit order at the band
//     level. The direction switch is visualised on the SAME bar as the breach — matching immediate-fill semantics.
//     RENDERING: the thick active line connects back to the prior bar (see the Active Overlays plot comment), so the new
//     colour appears to BEGIN one bar before the breaching candle. Cosmetic only — the flip, alert and stream all update
//     on the breach bar. The two next-bar modes below do not show this because their back-drawn segment lands on the
//     breach bar itself.
//
//   • on_close (switch_src = close[1]): Compares the PREVIOUS bar's committed close against the previous bar's
//     committed band. The direction switch is visualised on the bar AFTER the breach — matching confirmed-bar trading:
//     the breach is confirmed at bar close, the trader acts at the next bar's open. Because both sides of the
//     comparison are committed values, direction is STABLE throughout the live bar (no flicker).
//
//   • estimator (switch_src = estimator[1]): Same as on_close but uses the smoothed estimator. Direction is STABLE
//     on the live bar.
//
// All three modes are non-flickering on the live bar, eliminating the need for `barstate.isconfirmed` gating on
// direction changes in downstream consumers.
// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

// Determine switch source based on method. `on_close` and `estimator` use the [1] (previous bar's committed value) so
// the direction switch appears on the bar AFTER the breach — matching confirmed-bar trading semantics. `high_low` uses
// the current bar's high/low (immediate, like a resting stop/limit order).
float switch_source = switch i_switch_method
    eSwitchMethod.high_low  => float(na)
    eSwitchMethod.on_close  => close[1]
    eSwitchMethod.estimator => estimator[1]

// Update state. Note `estimator` is passed as `band_anchor` — bands always track the smoothed
// estimator, decoupled from the switch method's sensitivity choice.
kte_state.calculate(vol_profile, active_volatility, i_volatility_factor, adaptation_lookback, estimator, switch_source)

// Extract values for plotting
kte_direction = kte_state.direction
float  kte_lower         = kte_state.lower_band
float  kte_upper         = kte_state.upper_band

//#endregion


//==================================================================//
//#region                       PLOTTING                            //
//==================================================================//

//──────────────────────────── colours ─────────────────────────────//

var p = cu.Palette.new(transparency=0)
if barstate.isfirst
    p.apply_default_palette(i_palette_name)

const color col_transp = color.new(#000000, 100)

// Regular KTE Colors
color c_bull = p.get_colour(6, i_kte_line_transp)
color c_bear = p.get_colour(1, i_kte_line_transp)
color c_bull2 = p.get_colour(5, i_kte_line_transp)  // more mellow colour
color c_bear2 = p.get_colour(2, i_kte_line_transp)
int kte_fill_t = i_dark_mode ? math.max(i_kte_fill_transp - 8, 0) : i_kte_fill_transp
color c_bull_fill_base = p.get_colour(6, kte_fill_t)
color c_bear_fill_base = p.get_colour(1, kte_fill_t)
// Adjust fill colours for dark/light mode — dark deepens towards black, light mutes towards grey
float kte_desat = i_dark_mode ? 0.1 : 0.2
float kte_t_adj = i_dark_mode ? 0.0 : 0.1
color c_bull_fill = cu.adjust_colour(c_bull_fill_base, desaturation_amount=kte_desat, transparency_amount=kte_t_adj, dark_mode=i_dark_mode)
color c_bear_fill = cu.adjust_colour(c_bear_fill_base, desaturation_amount=kte_desat, transparency_amount=kte_t_adj, dark_mode=i_dark_mode)

// Opposing Band Colors — dark mode reduces transparency to keep opposing band visible against dark backgrounds
int opp_line_t = i_dark_mode ? math.max(i_opposing_line_transp - 25, 0) : i_opposing_line_transp
color c_opp_bull_line = p.get_colour(6, opp_line_t)
color c_opp_bear_line = p.get_colour(1, opp_line_t)
int opp_fill_t = i_dark_mode ? math.max(i_opposing_fill_transp - 6, 0) : i_opposing_fill_transp
color c_opp_bull_fill_base = p.get_colour(6, opp_fill_t)
color c_opp_bear_fill_base = p.get_colour(1, opp_fill_t)
float opp_desat = i_dark_mode ? 0.05 : 0.2
float opp_t_adj = i_dark_mode ? 0.0 : 0.1
color c_opp_bull_fill = cu.adjust_colour(c_opp_bull_fill_base, desaturation_amount=opp_desat, transparency_amount=opp_t_adj, dark_mode=i_dark_mode)
color c_opp_bear_fill = cu.adjust_colour(c_opp_bear_fill_base, desaturation_amount=opp_desat, transparency_amount=opp_t_adj, dark_mode=i_dark_mode)
// Estimator tracks the active envelope colour so all KTE lines share the same direction signal
color c_est_bull      = i_show_estimator ? p.get_colour(4, i_est_line_transp) : col_transp
color c_est_bear      = i_show_estimator ? p.get_colour(3, i_est_line_transp) : col_transp
color c_estimator     = kte_direction == eDirection.bull ? c_est_bull : c_est_bear

//────────────────────────── estimator plot ────────────────────────//

p_estimator = plot(estimator, 'Estimator', color=c_estimator, linewidth=i_est_width, force_overlay=true)

//────────────────────── continuous envelope plots ─────────────────//

// We plot Upper and Lower bands continuously to create a seamless envelope. To handle variable linewidths (thick for
// active, thin for opposing), we use a layering technique:
// 1. Base plots (always visible, thin) handle the continuous connection and opposing style.
// 2. Active plots (conditional, thick) overlay the base plots when the band is active.

// Determine dynamic colors for the base plots
// Upper Band: Red (Active Bear) or Cyan (Opposing Bull)
color col_upper_base = kte_direction == eDirection.bear ? c_bear : c_opp_bull_line
// Lower Band: Cyan (Active Bull) or Red (Opposing Bear)
color col_lower_base = kte_direction == eDirection.bull ? c_bull : c_opp_bear_line

// Handle visibility: if show_opposing is false, hide the base plot when it's in opposing mode
color col_upper_final = (i_show_opposing or kte_direction == eDirection.bear) ? col_upper_base : na
color col_lower_final = (i_show_opposing or kte_direction == eDirection.bull) ? col_lower_base : na

// Plot Base Lines (Continuous, thin)
p_upper = plot(kte_upper, 'Upper Band', color=col_upper_final, linewidth=i_opposing_linewidth, style=plot.style_linebr, force_overlay=true)
p_lower = plot(kte_lower, 'Lower Band', color=col_lower_final, linewidth=i_opposing_linewidth, style=plot.style_linebr, force_overlay=true)

// Plot Active Overlays (Conditional, thick). We use conditional colours instead of conditional values (na) to ensure the
// connecting line segment during a trend switch is drawn with the correct thickness (fixing the 1-bar lag visual artifact).
// Side effect: this segment takes the new direction's colour back to the prior bar, so in high_low mode (same-bar flip)
// the colour appears to begin one bar before the breaching candle. Cosmetic — flip/alert/stream are same-bar. See the
// SWITCH-METHOD SEMANTICS note.
color c_active_upper = kte_direction == eDirection.bear ? c_bear : na
color c_active_lower = kte_direction == eDirection.bull ? c_bull : na

p_upper_active = plot(kte_upper, 'Active Upper', color=c_active_upper, linewidth=i_kte_linewidth, style=plot.style_linebr, force_overlay=true)
p_lower_active = plot(kte_lower, 'Active Lower', color=c_active_lower, linewidth=i_kte_linewidth, style=plot.style_linebr, force_overlay=true)

// Fill the channel
// We use a conditional color to switch between bull/bear channel fills
color c_channel_lower_fill = kte_direction == eDirection.bull ? c_bull_fill : (i_show_opposing ? c_opp_bull_fill : na)
color c_channel_upper_fill = kte_direction == eDirection.bear ? c_bear_fill : (i_show_opposing ? c_opp_bear_fill : na)
// Only fill if opposing band is shown, otherwise it looks weird to have a fill with no border
fill(p_lower, p_estimator, c_channel_lower_fill, 'Channel (lower) fill')
fill(p_upper, p_estimator, c_channel_upper_fill, 'Channel (upper) fill')

//#endregion

if barstate.islast
    validator.display_warnings()


//==================================================================//
//#region                        ALERTS                             //
//==================================================================//

bool trigger_bullish      = bar_index >= 1 and kte_direction == eDirection.bull and kte_direction[1] == eDirection.bear
bool trigger_bearish      = bar_index >= 1 and kte_direction == eDirection.bear and kte_direction[1] == eDirection.bull
bool trigger_state_change = trigger_bullish or trigger_bearish

string msg_bullish = str.format("GYTS: {0} ({1}) KTE → BULLISH (Price: {2})", syminfo.ticker, timeframe.period, close)
string msg_bearish = str.format("GYTS: {0} ({1}) KTE → BEARISH (Price: {2})", syminfo.ticker, timeframe.period, close)

// freq_once_per_bar (not _close): the flip is already committed when trigger_* turns true — direction is stable on the
// live bar in every switch mode (on_close/estimator compare committed [1] values; high_low uses monotonic high/low), so
// the alert cannot repaint. Firing on the first qualifying tick rather than at bar close matches the documented timing —
// immediate for high_low, the new bar's open for on_close/estimator. _close would hold the alert back by up to a full bar.
if i_use_alerts_bull and trigger_bullish
    alert(msg_bullish, alert.freq_once_per_bar)

if i_use_alerts_bear and trigger_bearish
    alert(msg_bearish, alert.freq_once_per_bar)

alertcondition(trigger_bullish,      'Direction → Bullish',  'GYTS: {{ticker}} ({{interval}}) KTE → BULLISH')
alertcondition(trigger_bearish,      'Direction → Bearish',  'GYTS: {{ticker}} ({{interval}}) KTE → BEARISH')
alertcondition(trigger_state_change, "Any Direction Change", "GYTS: {{ticker}} ({{interval}}) KTE Direction Changed")

//#endregion


//==================================================================//
//#region                       EXPORTS                             //
//==================================================================//

// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// EXPORTS — public API for downstream consumers
// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
//
// The visible chart plots and the hidden stream below form this indicator's public API. The full downstream wire-up
// guide — consumption patterns, order-type mapping, direction-flip gating and cross-timeframe reads — lives in the Pro
// edition.
//
// ── INVENTORY ─────────────────────────────────────────────────────────────────────────────────────────────────────────────
//
//   Plot title (source dropdown)                   Type   Meaning
//   ─────────────────────────────────────────────   ─────  ──────────────────────────────────────────────────────────────────
//   "Upper Band"                                    float  Upper envelope band (post-monotonic-lock). Active trailing stop in bear trends; opposing
//                                                          target in bull trends. Visible on chart; available as input.source() wire.
//   "Lower Band"                                    float  Lower envelope band (post-monotonic-lock). Active trailing stop in bull trends; opposing
//                                                          target in bear trends. Visible on chart; available as input.source() wire.
//   "Estimator"                                     float  Smoothed price estimator anchoring the envelope centre. Visible on chart; available as input.source().
//   "🔗 STREAM KTE 🪜 Trailing Stoploss"           float  Sign-encoded combined stream: +kte_lower (bull) / −kte_upper (bear). One wire carrying direction ×
//                                                          active band. Assumes a strictly positive active band. Shown in the Data Window (display.data_window).
//   "🔗 STREAM KTE 🪜 Mechanism"                   float  Packed companion: switch method (1 high/low, 2 on close, 3 on estimator) in the 1e7 digit
//                                                          + estimator as a ratio-to-active-band in the low digits, behind an 8e12 marker. Hidden (display.none).
//
// The "Active Upper" / "Active Lower" overlay plots carry identical values to "Upper Band" / "Lower Band" — they exist
// solely for variable-linewidth rendering. Wiring to either is equivalent; prefer the base names.
//
// ── REPAINTING ────────────────────────────────────────────────────────────────────────────────────────────────────────────
//
// Closed bars: all exports are committed and reproducible — reloading the script or pulling history later yields
// identical values. Live bar: the band values (Upper/Lower) drift smoothly per tick like any reactive Pine indicator,
// but direction is STABLE for all three switch methods (`on_close`/`estimator` compare committed [1] values; `high_low`
// uses monotonic high/low), so the stream sign does not flicker. This is NOT post-hoc repainting — closed history is
// fixed. For an active trailing stop the live drift is bounded conservatively: the monotonic lock only tightens the
// active band intra-bar, never loosens it below its committed level. See the NON-REPAINTING CONTRACT and SWITCH-METHOD
// SEMANTICS notes above for the mechanism.
// ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

// WARM-UP: band and trailing-stop exports read `na` until the volatility and estimator filters have warmed up (scales
// with Adaptation Speed → 2–5000 bars, Volatility Lookback, Estimator Lookback). Guard downstream reads, e.g.
// `if not na(kte_lower) and not na(kte_upper)`. The Mechanism stream stays present throughout (ratio 0 until ready).
//
// Combined stream: sign carries direction, magnitude carries the active band level: +kte_lower (bull) / −kte_upper (bear).
// Note that large Volatility Factor can push  envelope's  lower band below zero
float kte_trailing_stoploss = kte_direction == eDirection.bull ? kte_lower : -kte_upper
plot(kte_trailing_stoploss, "🔗 STREAM KTE 🪜 Trailing Stoploss", editable=false, display=display.data_window)

// Stream to be used by downstream GYTS indicators. E.g. lets a strategy (e.g. Order Orchestrator) adopt this KTE's Direction Switch
// Method and draw its estimator runner line automatically, with no duplicated setting. One hidden float packed like the
// WT4D/Flux streams but behind a distinct 8e12 marker: the 1e7 digit holds the method (1 on high/low,
// 2 on close, 3 on estimator) and the low 7 digits hold the estimator as a SCALE-INVARIANT ratio to the active band
// (round(estimator / active_band × 1e6)), so it reconstructs at any price scale. The absolute stop keeps its own
// full-precision wire above — two prices cannot share one float. The method is present even during warm-up (ratio 0
// until the estimator is ready).
int   mech_method_code = switch i_switch_method
    eSwitchMethod.high_low  => 1
    eSwitchMethod.on_close  => 2
    eSwitchMethod.estimator => 3
float mech_active_band = kte_direction == eDirection.bull ? kte_lower : kte_upper
float mech_est_ratio   = not na(estimator) and not na(mech_active_band) and mech_active_band > 0 ? estimator / mech_active_band : na
int   mech_ratio_code  = na(mech_est_ratio) ? 0 : int(math.min(math.max(math.round(mech_est_ratio * 1e6), 0), 9999999))
float kte_mechanism    = 8e12 + mech_method_code * 1e7 + mech_ratio_code
plot(kte_mechanism, "🔗 STREAM KTE 🪜 Mechanism", editable=false, display=display.none)

//#endregion

Overview

Kinetic Trend Envelope estimates price with a selectable filter, measures volatility with a choice of estimators such as Yang-Zhang, and builds an envelope that trails price at a volatility factor scaled by an adaptation speed. The direction can switch by estimator or by price crossing, and an optional opposing band and estimator line can be shown. Alerts fire on bullish and bearish direction changes. Palettes and line styles are configurable.

Details

Published by

vedkanani123 on GitHub

Code credit

GoemonYae, 2026-07, as written in the script's header

Draws

On the price chart

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Sourcesourcehlc3i_source
Estimator FilterenumeFilterType.ultimate_2i_estimator_filter
Estimator Lookbackint15i_estimator_lookback
Estimator Filter Factor (Q / Gamma)float0.7i_estimator_filter_factor
Volatility MeasureenumeVolatilityMeasure.yang_zhangi_volatility_measure
Volatility Lookbackint14i_volatility_lookback
Volatility Filterenumvol.eFilterType.smai_volatility_filter
Volatility Filter Factor (Q / Gamma)float0.7i_volatility_filter_factor
Adaptation Speedfloat3.5i_adaptation_speed
Volatility Factorfloat5.0i_volatility_factor
Direction Switch MethodenumeSwitchMethod.estimatori_switch_method
Paletteenumcu.PaletteNames.gyts_purplei_palette_name
Show all 25 settings
SettingTypeDefaultVariable
Dark Modebooltruei_dark_mode
KTE Line Widthint3i_kte_linewidth
Transparencyint0i_kte_line_transp
KTE Fill Transparencyint80i_kte_fill_transp
Show Opposing Bandbooltruei_show_opposing
Opposing Band Line Widthint1i_opposing_linewidth
Transparencyint75i_opposing_line_transp
Opposing Band Fill Transparencyint92i_opposing_fill_transp
Show Estimatorbooltruei_show_estimator
Estimator Widthint2i_est_width
Transparencyint20i_est_line_transp
Alert: Direction → Bullishbooltruei_use_alerts_bull
Alert: Direction → Bearishbooltruei_use_alerts_bear
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

Chandelier Exit

by everget
947 stars

This indicator plots an ATR-based Chandelier Exit trailing stop on the price chart. It tracks a long stop under recent highs and a short stop above recent lows, then switches direction when price crosses the opposite stop. Buy and sell labels mark those flips.

Indicator v6 Volatility & bandsTrend Repaints
Updated 12 days ago · GPL-3.0 GitHub repository of Chandelier Exit

The Momentum Setup is a multi-indicator visual tool designed for trend and momentum analysis directly on the price chart. It combines Relative Strength Index (RSI), Money Flow Index (MFI), Commodity Channel Index (CCI), Supertrend, and a 250-bar percentage cha

Indicator v5 Oscillators & momentumTrend
Updated 1 year ago · MPL-2.0 GitHub repository of Momentum Setup - Ankush Bajaj Momentum Investing Setup
330 stars

Range Filter [Alorse] is a technical indicator overlay for TradingView designed to filter market volatility and identify price trends. It calculates a smoothed range based on historical volatility using a configurable sampling period and multiplier applied to

Indicator v5 TrendVolatility & bands
Updated 2 months ago · None stated GitHub repository of Range Filter [Alorse]
318 stars

This indicator calculates an ATR trailing stop line derived from the highest high minus an average true range multiple over a specified lookback period. It plots the trailing stop line directly on the price chart and colors price bars based on whether the clos

Indicator v5 TrendVolatility & bands
Updated 2 months ago · MIT GitHub repository of ATR Trailing Stoploss

NVDA Trend Ribbon

by longsurf-ai
280 stars

This indicator plots three exponential moving averages and ATR volatility envelopes on the price chart. It compares the fast and slow EMAs to shade a trend ribbon and marks confirmed upward and downward crossovers with triangle labels. Settings change the thre

Indicator v6 TrendMoving averages
Updated 3 days ago · MIT GitHub repository of NVDA Trend Ribbon

[RS]Donchian Fans

by ricardosantos79
261 stars

This indicator plots Donchian Channel boundary lines along with dynamic fan projections directly on the price chart. It calculates the highest high, lowest low, and median price over a customizable lookback period. Whenever price establishes a new channel high

Indicator v4 Support & resistanceTrend
Updated 4 years ago · None stated GitHub repository of [RS]Donchian Fans

More Trend scripts

Related on ForexR

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers