← К каталогу
Исходный код · ID 4153

Market Gravity

indicator market gravity for tradingview.pine
Вы автор или правообладатель?

Сообщите нам, если считаете, что материал размещён без разрешения.

Краткий обзор

О программе

Этот индикатор Pine Script v6 для TradingView анализирует инфирированные структуры ликвидности выше и ниже текущей цены. Он выявляет подтверждённые развороты, равные максимумы и минимумы, значимые изолированные свечи, уровни предыдущих дня и недели, а также повторяющиеся структуры отторжения, объединяя их в пулы ликвидности. Каждый пул получает структурный показатель силы ликвидности и динамический балл притяжения (Gravity), учитывающий расстояние, свежесть, рыночный режим и направление тренда. Индикатор визуализирует сильнейшие цели по обеим сторонам ценового графика, отображает направление гравитационных потоков кривыми, аннотации жизненного цикла пулов и сводную таблицу с текущим состоянием магнитов.

Настройки программы

НастройкаОписание
pivotLeft Pivot Left Bars — Количество баров слева от свечи для подтверждения верхнего или нижнего пивота при определении структур ликвидности.
pivotRight Pivot Right Bars — Количество баров справа, необходимых для подтверждения разворота перед тем, как он будет учтён как структура ликвидности.
clusterATR Pool Cluster Distance (ATR) — Максимальное расстояние в единицах ATR, при котором совместимые структуры ликвидности объединяются в один пул.
sweepBufferATR Sweep Buffer (ATR) — Дополнительный буфер в ATR, на который цена должна превысить уровень ликвидности, чтобы тот считался пройденным (swept).
maxAgeBars Maximum Liquidity Age — Максимальное количество баров с момента создания пула, после чего он удаляется из анализа.
maxStoredLevels Maximum Stored Levels — Максимальное число сохраняемых уровней ликвидности на каждой стороне (верхней и нижней).
maxTargetDistanceATR Maximum Target Distance (ATR) — Максимальное расстояние в ATR от текущей цены, на котором уровень может быть ранжирован как целевой.
useSwingLiquidity Swing High / Low Liquidity — Включает использование подтверждённых пивотов как базовых структур ликвидности.
useEqualHighLow Equal Highs / Lows — Включает обнаружение повторяющихся подтверждённых пивотов, формирующих равные максимумы или минимумы.
useMajorSwings Major Isolated Swings — Позволяет необычно значимым подтверждённым разворотам квалифицироваться как самостоятельные структуры ликвидности.
usePreviousDay Previous Day High / Low — Использует предыдущий завершённый дневной максимум и минимум при таймфрейме дневном и ниже.
usePreviousWeek Previous Week High / Low — Использует предыдущий завершённый недельный максимум и минимум при таймфрейме недельном и ниже.
useRepeatedRejections Repeated Rejection Structures — Классифицирует повторяющиеся реакции цены вблизи одного уровня как структуры отторжения.
equalToleranceATR Equal High / Low Tolerance (ATR) — Допуск в ATR для определения равенства двух уровней высоких или низких цен.
majorSwingATR Major Swing Threshold (ATR) — Порог в ATR, превышение которого делает свинг значимым (major) для отдельной классификации.
rejectionMinSeparationBars Rejection Minimum Separation — Минимальное число баров между реакциями, необходимое для квалификации как повторяющейся структуры отторжения.
rejectionWickRatio Rejection Wick Ratio — Минимальная доля теневой свечи диапазона свечи, используемая при классификации структур отторжения.
recentPivotMemory Recent Pivot Memory — Число недавних подтверждённых пивотов, сохраняемых для классификации структур ликвидности.
independentTouchMinSeparationBars Independent Touch Minimum Separation — Минимальное число баров между событиями реакции цены, чтобы они считались отдельными касаниями пула.
clusterZonePaddingATR Clustered Pool Zone Padding (ATR) — Отступ в ATR за пределы минимальной и максимальной цен членов сгруппированного пула.
isolatedZoneHalfATR Isolated Pool Half Width (ATR) — Полуширина зоны для пула с одним членом до присоединения дополнительных структур.
maxZoneWidthATR Maximum Pool Zone Width (ATR) — Ограничение ширины зоны пула, предотвращающее чрезмерно широкую кластеризацию.
atrLength ATR Length — Период расчёта Average True Range, используемый для нормализации расстояний и допусков.
fastEMALength Fast EMA Length — Длина быстрой экспоненциальной скользящей средней, используемой для определения направления тренда.
slowEMALength Slow EMA Length — Длина медленной экспоненциальной скользящей средней, используемой для определения направления тренда.
volumeLength Relative Volume Length — Период расчёта среднего объёма для определения относительного объёма свечи.
approachingATR Approaching Distance (ATR) — Расстояние в ATR, при котором уровень считается приближающимся к текущей цене.
accessibilityKATR Accessibility Distance K (ATR) — Коэффициент масштабирования нелинейного затухания доступности с расстоянием в единицах ATR.
swingFreshnessHalfLifeBars Swing Freshness Half-Life (Bars) — Базовый период полураспада свежести для структур, основанных на свинг-пивотах.
clusterFreshnessHalfLifeBars Cluster Freshness Half-Life (Bars) — Базовый период полураспада свежести для кластеризованных пулов с несколькими членами.
previousDayFreshnessHalfLifeBars Previous Day Freshness Half-Life (Bars) — Базовый период полураспада свежести для структур предыдущего дня.
previousWeekFreshnessHalfLifeBars Previous Week Freshness Half-Life (Bars) — Базовый период полураспада свежести для структур предыдущей недели.
activeLiquidityStrengthThreshold Active Liquidity Strength Threshold — Порог структурной силы ликвидности, ниже которого пул остаётся в состоянии формирования и не участвует в ранжировании целей.
rejectionWindowBars Sweep Rejection Window (Bars) — Число баров после подтверждённого прохождения, в течение которых закрытие обратно на исходной стороне зоны классифицирует прохождение как отклонённое.
acceptanceCloseCount Acceptance Consecutive Closes — Число последовательных подтверждённых закрытий за пределами внешней границы зоны, необходимых для классификации пройденной ликвидности как принятой.
lifecycleEventDisplayBars Resolved State Hold (Bars) — Число баров, в течение которых сохраняется визуальное отображение событий жизненного цикла после их разрешения.
showCurves Show Gravity Paths — Включает отображение кривых направления гравитационного потока от текущей цены к целевым уровням.
showOpposingCurve Show Opposing Gravity Path — Включает отображение противоположной кривой гравитационного потока на графике.
projectionBars Projection Length — Число баров вперёд, на которое проецируются кривые гравитационного потока.
displayLevels Levels Per Side — Количество уровней ликвидности, отображаемых на каждой стороне (верхней и нижней) графика.
lineHistoryBars Level Line History — Максимальное число баров назад, в течение которых сохраняются линии уровней на графике.
showGlow Level Glow — Включает свечение (glow) вокруг линий уровней ликвидности для лучшей визуальной заметности.
showTable Market Gravity Table — Включает отображение сводной таблицы с текущими значениями гравитации, давления и состояниями уровней.
showLifecycleEvents Show Lifecycle Events — Включает кратковременное отображение событий жизненного цикла ликвидности (прохождение, отклонение, принятие) на графике.
labelCollisionMode Label Collision Handling — Режим автоматического разрешения визуальных коллизий между метками уровней и аннотациями на графике.
labelCollisionThresholdATR Label Collision Threshold (ATR) — Порог вертикального расстояния в ATR, при котором метки считаются визуально перекрывающимися и получают отдельные горизонтальные полосы.
rankMarkerSpacingBars Label Base Lane Spacing (Bars) — Базовое горизонтальное расстояние между полосами для аннотаций уровней в барах.
lifecycleLabelGapBars Lifecycle Label Gap (Bars) — Дополнительный горизонтальный отступ правее всех активных полос целей перед началом аннотаций событий жизненного цикла.
tablePositionInput Table Position — Позиция сводной таблицы на графике (верхний правый, нижний правый, верхний левый или нижний левый угол).
primaryGravityAlertLevel High Primary Gravity Threshold — Порог значения гравитации, при превышении которого срабатывает оповещение о высоком основном магните.
gravityAlertLevel Strong Net Gravity Threshold — Порог, при превышении которого срабатывают оповещения о сильном направленном рыночном притяжении.
bullColor Bullish Gravity — Цвет, используемый для визуализации бычьего (восходящего) гравитационного притяжения.
bearColor Bearish Gravity — Цвет, используемый для визуализации медвежьего (нисходящего) гравитационного притяжения.
neutralColor Neutral Gravity — Цвет, используемый для отображения нейтрального состояния гравитации.
Получить полный код

Поиск по коду файла

indicator(
     "Market Gravity",
     overlay = true,
     max_lines_count = 100,
     max_labels_count = 100,
     max_polylines_count = 10
 )

max_bars_back(time, 3001)

pivotLeft = input.int(
     5,
     "Pivot Left Bars",
     minval = 1,
     group = "Advanced - Liquidity Detection",
     tooltip = "Bars to the left used when confirming swing liquidity."
 )

pivotRight = input.int(
     5,
     "Pivot Right Bars",
     minval = 1,
     group = "Advanced - Liquidity Detection",
     tooltip = "Bars to the right required before a swing is confirmed."
 )

clusterATR = input.float(
     0.15,
     "Pool Cluster Distance (ATR)",
     minval = 0.01,
     step = 0.01,
     group = "Advanced - Liquidity Detection",
     tooltip = "Compatible liquidity structures within this ATR distance are clustered into one liquidity pool."
 )

sweepBufferATR = input.float(
     0.05,
     "Sweep Buffer (ATR)",
     minval = 0.0,
     step = 0.01,
     group = "Advanced - Liquidity Detection",
     tooltip = "Price must exceed a liquidity level by this ATR amount before the level is considered swept."
 )

maxAgeBars = input.int(
     1500,
     "Maximum Liquidity Age",
     minval = 100,
     maxval = 5000,
     group = "Advanced - Liquidity Detection"
 )

maxStoredLevels = input.int(
     40,
     "Maximum Stored Levels",
     minval = 10,
     maxval = 100,
     group = "Advanced - Liquidity Detection"
 )

maxTargetDistanceATR = input.float(
     12.0,
     "Maximum Target Distance (ATR)",
     minval = 1.0,
     step = 0.5,
     group = "Advanced - Liquidity Detection"
 )

useSwingLiquidity = input.bool(
     true,
     "Swing High / Low Liquidity",
     group = "Advanced - Liquidity Sources",
     tooltip = "Use confirmed swing pivots as baseline inferred liquidity."
 )

useEqualHighLow = input.bool(
     true,
     "Equal Highs / Lows",
     group = "Advanced - Liquidity Sources",
     tooltip = "Detect repeated confirmed pivots forming equal-high or equal-low liquidity."
 )

useMajorSwings = input.bool(
     true,
     "Major Isolated Swings",
     group = "Advanced - Liquidity Sources",
     tooltip = "Allow unusually prominent confirmed swings to qualify as their own liquidity structures."
 )

usePreviousDay = input.bool(
     true,
     "Previous Day High / Low",
     group = "Advanced - Liquidity Sources",
     tooltip = "Use the previous completed daily high and low when the chart timeframe is daily or lower."
 )

usePreviousWeek = input.bool(
     true,
     "Previous Week High / Low",
     group = "Advanced - Liquidity Sources",
     tooltip = "Use the previous completed weekly high and low when the chart timeframe is weekly or lower."
 )

useRepeatedRejections = input.bool(
     true,
     "Repeated Rejection Structures",
     group = "Advanced - Liquidity Sources",
     tooltip = "Classify repeated confirmed reactions near the same price as rejection-based liquidity structures."
 )

equalToleranceATR = input.float(
     0.15,
     "Equal High / Low Tolerance (ATR)",
     minval = 0.01,
     step = 0.01,
     group = "Advanced - Liquidity Sources"
 )

majorSwingATR = input.float(
     2.50,
     "Major Swing Threshold (ATR)",
     minval = 0.50,
     step = 0.25,
     group = "Advanced - Liquidity Sources"
 )

rejectionMinSeparationBars = input.int(
     8,
     "Rejection Minimum Separation",
     minval = 1,
     group = "Advanced - Liquidity Sources",
     tooltip = "Minimum bars between reactions before they can qualify as a repeated rejection structure."
 )

rejectionWickRatio = input.float(
     0.30,
     "Rejection Wick Ratio",
     minval = 0.0,
     maxval = 1.0,
     step = 0.05,
     group = "Advanced - Liquidity Sources",
     tooltip = "Minimum wick share of the pivot candle range used when classifying repeated rejection structures."
 )

recentPivotMemory = input.int(
     30,
     "Recent Pivot Memory",
     minval = 5,
     maxval = 100,
     group = "Advanced - Liquidity Sources",
     tooltip = "Number of recent confirmed pivots retained only for structure classification."
 )

independentTouchMinSeparationBars = input.int(
     8,
     "Independent Touch Minimum Separation",
     minval = 1,
     group = "Advanced - Pool Engine",
     tooltip = "Minimum bars between price-reaction events before they count as separate liquidity touches. Structural confirmations can join a pool without inflating touch count."
 )

clusterZonePaddingATR = input.float(
     0.02,
     "Clustered Pool Zone Padding (ATR)",
     minval = 0.0,
     step = 0.01,
     group = "Advanced - Pool Engine",
     tooltip = "Padding added outside the minimum and maximum member prices of a clustered pool."
 )

isolatedZoneHalfATR = input.float(
     0.05,
     "Isolated Pool Half Width (ATR)",
     minval = 0.01,
     step = 0.01,
     group = "Advanced - Pool Engine",
     tooltip = "Half-width used for a one-member liquidity pool before additional structures join it."
 )

maxZoneWidthATR = input.float(
     0.50,
     "Maximum Pool Zone Width (ATR)",
     minval = 0.10,
     step = 0.05,
     group = "Advanced - Pool Engine",
     tooltip = "Safety bound that prevents an unusually broad cluster from becoming an oversized target zone."
 )

atrLength = input.int(
     14,
     "ATR Length",
     minval = 1,
     group = "Advanced - Gravity Model"
 )

fastEMALength = input.int(
     21,
     "Fast EMA Length",
     minval = 1,
     group = "Advanced - Gravity Model"
 )

slowEMALength = input.int(
     55,
     "Slow EMA Length",
     minval = 2,
     group = "Advanced - Gravity Model"
 )

volumeLength = input.int(
     50,
     "Relative Volume Length",
     minval = 5,
     group = "Advanced - Gravity Model"
 )

approachingATR = input.float(
     0.75,
     "Approaching Distance (ATR)",
     minval = 0.1,
     step = 0.05,
     group = "Advanced - Gravity Model"
 )

primaryRankWeight = 1.00
secondaryRankWeight = 0.65
tertiaryRankWeight = 0.40

accessibilityKATR = input.float(
     2.0,
     "Accessibility Distance K (ATR)",
     minval = 0.25,
     step = 0.25,
     group = "Advanced - Gravity Model",
     tooltip = "Controls nonlinear distance decay. Around K ATR, accessibility is approximately 50%."
 )

swingFreshnessHalfLifeBars = input.int(
     150,
     "Swing Freshness Half-Life (Bars)",
     minval = 10,
     group = "Advanced - Gravity Model"
 )

clusterFreshnessHalfLifeBars = input.int(
     250,
     "Cluster Freshness Half-Life (Bars)",
     minval = 10,
     group = "Advanced - Gravity Model"
 )

previousDayFreshnessHalfLifeBars = input.int(
     300,
     "Previous Day Freshness Half-Life (Bars)",
     minval = 10,
     group = "Advanced - Gravity Model"
 )

previousWeekFreshnessHalfLifeBars = input.int(
     500,
     "Previous Week Freshness Half-Life (Bars)",
     minval = 10,
     group = "Advanced - Gravity Model"
 )

activeLiquidityStrengthThreshold = input.float(
     40.0,
     "Active Liquidity Strength Threshold",
     minval = 0.0,
     maxval = 100.0,
     step = 1.0,
     group = "Advanced - Liquidity Lifecycle",
     tooltip = "Pools below this structural Liquidity Strength remain Forming and are not eligible for target ranking."
 )

rejectionWindowBars = input.int(
     2,
     "Sweep Rejection Window (Bars)",
     minval = 1,
     maxval = 10,
     group = "Advanced - Liquidity Lifecycle",
     tooltip = "After a confirmed sweep, a close back on the original side of the full zone within this many bars classifies the sweep as Rejected."
 )

acceptanceCloseCount = input.int(
     2,
     "Acceptance Consecutive Closes",
     minval = 1,
     maxval = 5,
     group = "Advanced - Liquidity Lifecycle",
     tooltip = "Consecutive confirmed closes beyond the outer zone boundary required to classify swept liquidity as Accepted."
 )

lifecycleEventDisplayBars = input.int(
     3,
     "Resolved State Hold (Bars)",
     minval = 1,
     maxval = 20,
     group = "Advanced - Liquidity Lifecycle",
     tooltip = "Bars to retain Rejected or Accepted state internally before the pool transitions to Retired and its lifecycle event visualization fades."
 )

showCurves = input.bool(
     true,
     "Show Gravity Paths",
     group = "Display - Projection"
 )

showOpposingCurve = input.bool(
     true,
     "Show Opposing Gravity Path",
     group = "Display - Projection"
 )

projectionBars = input.int(
     35,
     "Projection Length",
     minval = 10,
     maxval = 100,
     group = "Display - Projection"
 )

displayLevels = input.int(
     3,
     "Levels Per Side",
     minval = 1,
     maxval = 3,
     group = "Display"
 )

lineHistoryBars = input.int(
     500,
     "Level Line History",
     minval = 50,
     maxval = 3000,
     group = "Display"
 )

showGlow = input.bool(
     true,
     "Level Glow",
     group = "Display"
 )

showTable = input.bool(
     true,
     "Market Gravity Table",
     group = "Display"
 )

showLifecycleEvents = input.bool(
     true,
     "Show Lifecycle Events",
     group = "Display",
     tooltip = "Briefly preserves Sweeping, Rejected, and Accepted liquidity events on the chart after they leave future-target ranking."
 )

labelCollisionMode = input.string(
     "Auto",
     "Label Collision Handling",
     options = [
         "Auto",
         "Off"
     ],
     group = "Display",
     tooltip = "Auto keeps target labels readable with horizontal lanes and repositions Gravity path badges only when they would overlap curved paths, liquidity lines, or another path badge. Target prices and curve geometry never move."
 )

labelCollisionThresholdATR = input.float(
     0.60,
     "Label Collision Threshold (ATR)",
     minval = 0.05,
     maxval = 2.00,
     step = 0.05,
     group = "Display",
     tooltip = "Targets within this ATR-normalized vertical distance are treated as a visual collision and assigned separate horizontal lanes."
 )

rankMarkerSpacingBars = input.int(
     18,
     "Label Base Lane Spacing (Bars)",
     minval = 1,
     maxval = 40,
     group = "Display",
     tooltip = "Base horizontal spacing for collision lanes. Progressively larger X offsets are applied to later collision lanes so wide labels remain readable. True target prices never move vertically."
 )

lifecycleLabelGapBars = input.int(
     18,
     "Lifecycle Label Gap (Bars)",
     minval = 0,
     maxval = 40,
     group = "Display",
     tooltip = "Extra horizontal gap beyond the furthest active target lane before lifecycle-event labels begin, so Accepted, Rejected, and Sweeping annotations remain readable."
 )

tablePositionInput = input.string(
     "Top Right",
     "Table Position",
     options = [
         "Top Right",
         "Bottom Right",
         "Top Left",
         "Bottom Left"
     ],
     group = "Display"
 )

primaryGravityAlertLevel = input.float(
     70.0,
     "High Primary Gravity Threshold",
     minval = 1.0,
     maxval = 100.0,
     step = 1.0,
     group = "Alerts",
     tooltip = "Triggers the High Primary Gravity alert when the current upside or downside Primary Magnet crosses above this Gravity score. Gravity is an attraction score, not a probability."
 )

gravityAlertLevel = input.float(
     60.0,
     "Strong Net Gravity Threshold",
     minval = 1.0,
     maxval = 100.0,
     step = 1.0,
     group = "Alerts",
     tooltip = "Triggers strong directional Market Gravity alerts when Net Gravity crosses above +threshold or below -threshold. Net Gravity measures directional imbalance, not probability."
 )

bullColor = input.color(
     #00E5A8,
     "Bullish Gravity",
     group = "Style"
 )

bearColor = input.color(
     #FF4664,
     "Bearish Gravity",
     group = "Style"
 )

neutralColor = input.color(
     #A8B2C1,
     "Neutral Gravity",
     group = "Style"
 )

pathColor = input.color(
     #00C8FF,
     "Primary Gravity Path",
     group = "Style"
 )

atr = ta.atr(atrLength)

fastEMA = ta.ema(close, fastEMALength)
slowEMA = ta.ema(close, slowEMALength)

volumeAverage = ta.sma(volume, volumeLength)

pivotHigh = ta.pivothigh(
     high,
     pivotLeft,
     pivotRight
 )

pivotLow = ta.pivotlow(
     low,
     pivotLeft,
     pivotRight
 )

pivotWindow = pivotLeft + pivotRight + 1

[previousDayHigh, previousDayLow] = request.security(
     syminfo.tickerid,
     "1D",
     [high[1], low[1]],
     lookahead = barmerge.lookahead_on
 )

[previousWeekHigh, previousWeekLow] = request.security(
     syminfo.tickerid,
     "1W",
     [high[1], low[1]],
     lookahead = barmerge.lookahead_on
 )

chartTimeframeSeconds = timeframe.in_seconds()
canUsePreviousDay = chartTimeframeSeconds <= timeframe.in_seconds("1D")
canUsePreviousWeek = chartTimeframeSeconds <= timeframe.in_seconds("1W")

newDay = timeframe.change("1D")
newWeek = timeframe.change("1W")

LIFECYCLE_FORMING = 0
LIFECYCLE_ACTIVE = 1
LIFECYCLE_APPROACHING = 2
LIFECYCLE_SWEEPING = 3
LIFECYCLE_REJECTED = 4
LIFECYCLE_ACCEPTED = 5
LIFECYCLE_RETIRED = 6

tablePosition =
     tablePositionInput == "Top Right" ? position.top_right :
     tablePositionInput == "Top Left" ? position.top_left :
     tablePositionInput == "Bottom Left" ? position.bottom_left :
     position.bottom_right

var array<float> upperPrices = array.new_float()
var array<int> upperTouches = array.new_int()
var array<int> upperBars = array.new_int()
var array<int> upperActiveBars = array.new_int()
var array<float> upperStrengths = array.new_float()
var array<float> upperVolumes = array.new_float()
var array<string> upperSources = array.new_string()
var array<float> upperZoneLows = array.new_float()
var array<float> upperZoneHighs = array.new_float()
var array<float> upperMemberMins = array.new_float()
var array<float> upperMemberMaxs = array.new_float()
var array<int> upperMemberCounts = array.new_int()
var array<float> upperTotalWeights = array.new_float()
var array<int> upperLastTouchBars = array.new_int()
var array<float> upperPoolATRs = array.new_float()

var array<float> upperMaxSwingProminences = array.new_float()
var array<int> upperSwingEvidenceCounts = array.new_int()
var array<float> upperVolumeEvidenceSums = array.new_float()
var array<int> upperVolumeEvidenceCounts = array.new_int()

var array<int> upperLifecycleStates = array.new_int()
var array<int> upperSweepBars = array.new_int()
var array<int> upperBeyondCloseCounts = array.new_int()
var array<int> upperResolutionBars = array.new_int()
var array<int> upperLifecycleTouchBars = array.new_int()

var array<float> lowerPrices = array.new_float()
var array<int> lowerTouches = array.new_int()
var array<int> lowerBars = array.new_int()
var array<int> lowerActiveBars = array.new_int()
var array<float> lowerStrengths = array.new_float()
var array<float> lowerVolumes = array.new_float()
var array<string> lowerSources = array.new_string()
var array<float> lowerZoneLows = array.new_float()
var array<float> lowerZoneHighs = array.new_float()
var array<float> lowerMemberMins = array.new_float()
var array<float> lowerMemberMaxs = array.new_float()
var array<int> lowerMemberCounts = array.new_int()
var array<float> lowerTotalWeights = array.new_float()
var array<int> lowerLastTouchBars = array.new_int()
var array<float> lowerPoolATRs = array.new_float()

var array<float> lowerMaxSwingProminences = array.new_float()
var array<int> lowerSwingEvidenceCounts = array.new_int()
var array<float> lowerVolumeEvidenceSums = array.new_float()
var array<int> lowerVolumeEvidenceCounts = array.new_int()

var array<int> lowerLifecycleStates = array.new_int()
var array<int> lowerSweepBars = array.new_int()
var array<int> lowerBeyondCloseCounts = array.new_int()
var array<int> lowerResolutionBars = array.new_int()
var array<int> lowerLifecycleTouchBars = array.new_int()

var array<float> recentUpperPivotPrices = array.new_float()
var array<int> recentUpperPivotBars = array.new_int()
var array<float> recentLowerPivotPrices = array.new_float()
var array<int> recentLowerPivotBars = array.new_int()

var array<line> displayLines = array.new_line()
var array<label> displayLabels = array.new_label()

var polyline upperPath = na
var polyline lowerPath = na

var string lastLifecycleEvent = "None"
var int lastLifecycleEventBar = -1

var table gravityTable = table.new(
     tablePosition,
     4,
     9,
     bgcolor = color.new(color.black, 15),
     frame_color = color.new(color.gray, 75),
     frame_width = 1,
     border_color = color.new(color.gray, 90),
     border_width = 1
 )

clamp01(float value) =>
    math.max(
         0.0,
         math.min(1.0, value)
     )

clampSigned(float value) =>
    math.max(
         -1.0,
         math.min(1.0, value)
     )

formatPrice(float value) =>
    na(value) ?
         "-" :
         str.tostring(
              value,
              format.mintick
         )

formatScore(float value) =>
    na(value) ?
         "-" :
         str.tostring(
              int(math.round(value))
         )

appendSource(
     string currentSources,
     string newSource
 ) =>
    wrappedSources =
         " + " +
         currentSources +
         " + "

    wrappedNewSource =
         " + " +
         newSource +
         " + "

    currentSources == "" ?
         newSource :
         str.contains(
              wrappedSources,
              wrappedNewSource
         ) ?
         currentSources :
         currentSources + " + " + newSource

getRecentMatchCount(
     array<float> prices,
     array<int> bars,
     float newPrice,
     int newBar,
     float tolerance,
     int minimumSeparation
 ) =>
    int matches = 0

    if array.size(prices) > 0
        for i = 0 to array.size(prices) - 1
            oldPrice = array.get(
                 prices,
                 i
             )

            oldBar = array.get(
                 bars,
                 i
             )

            farEnough =
                 math.abs(
                      newBar -
                      oldBar
                 ) >=
                 minimumSeparation

            closeEnough =
                 math.abs(
                      newPrice -
                      oldPrice
                 ) <=
                 tolerance

            if farEnough and closeEnough
                matches += 1

    matches

storeRecentPivot(
     array<float> prices,
     array<int> bars,
     float price,
     int pivotBar,
     int maximumSize
 ) =>
    array.push(
         prices,
         price
     )

    array.push(
         bars,
         pivotBar
     )

    while array.size(prices) > maximumSize
        array.shift(prices)
        array.shift(bars)

getPoolZoneBounds(
     float memberMin,
     float memberMax,
     int memberCount,
     float centroid,
     float poolATR
 ) =>
    float zoneLow = na
    float zoneHigh = na

    if poolATR > 0
        if memberCount <= 1
            zoneLow := centroid - poolATR * isolatedZoneHalfATR
            zoneHigh := centroid + poolATR * isolatedZoneHalfATR
        else
            zoneLow := memberMin - poolATR * clusterZonePaddingATR
            zoneHigh := memberMax + poolATR * clusterZonePaddingATR

        maximumWidth = poolATR * maxZoneWidthATR
        currentWidth = zoneHigh - zoneLow

        if maximumWidth > 0 and currentWidth > maximumWidth
            zoneMidpoint = (zoneHigh + zoneLow) / 2.0
            zoneLow := zoneMidpoint - maximumWidth / 2.0
            zoneHigh := zoneMidpoint + maximumWidth / 2.0

    [zoneLow, zoneHigh]

removeLevel(
     array<float> prices,
     array<int> touches,
     array<int> bars,
     array<int> activeBars,
     array<float> strengths,
     array<float> volumes,
     array<string> sources,
     array<float> zoneLows,
     array<float> zoneHighs,
     array<float> memberMins,
     array<float> memberMaxs,
     array<int> memberCounts,
     array<float> totalWeights,
     array<int> lastTouchBars,
     array<float> poolATRs,
     array<float> maxSwingProminences,
     array<int> swingEvidenceCounts,
     array<float> volumeEvidenceSums,
     array<int> volumeEvidenceCounts,
     array<int> lifecycleStates,
     array<int> sweepBars,
     array<int> beyondCloseCounts,
     array<int> resolutionBars,
     array<int> lifecycleTouchBars,
     int index
 ) =>
    array.remove(prices, index)
    array.remove(touches, index)
    array.remove(bars, index)
    array.remove(activeBars, index)
    array.remove(strengths, index)
    array.remove(volumes, index)
    array.remove(sources, index)
    array.remove(zoneLows, index)
    array.remove(zoneHighs, index)
    array.remove(memberMins, index)
    array.remove(memberMaxs, index)
    array.remove(memberCounts, index)
    array.remove(totalWeights, index)
    array.remove(lastTouchBars, index)
    array.remove(poolATRs, index)
    array.remove(maxSwingProminences, index)
    array.remove(swingEvidenceCounts, index)
    array.remove(volumeEvidenceSums, index)
    array.remove(volumeEvidenceCounts, index)
    array.remove(lifecycleStates, index)
    array.remove(sweepBars, index)
    array.remove(beyondCloseCounts, index)
    array.remove(resolutionBars, index)
    array.remove(lifecycleTouchBars, index)

addPoolMember(
     array<float> prices,
     array<int> touches,
     array<int> bars,
     array<int> activeBars,
     array<float> strengths,
     array<float> volumes,
     array<string> sources,
     array<float> zoneLows,
     array<float> zoneHighs,
     array<float> memberMins,
     array<float> memberMaxs,
     array<int> memberCounts,
     array<float> totalWeights,
     array<int> lastTouchBars,
     array<float> poolATRs,
     array<float> maxSwingProminences,
     array<int> swingEvidenceCounts,
     array<float> volumeEvidenceSums,
     array<int> volumeEvidenceCounts,
     array<int> lifecycleStates,
     array<int> sweepBars,
     array<int> beyondCloseCounts,
     array<int> resolutionBars,
     array<int> lifecycleTouchBars,
     float newPrice,
     int newBar,
     int newActiveBar,
     float newStrength,
     float newVolume,
     float newStructuralSwingProminence,
     float newStructuralVolumeScore,
     string newSource,
     float newWeight,
     float newATR,
     bool countsAsTouch,
     int touchBar,
     float tolerance,
     int maximumLevels
 ) =>
    int nearestIndex = -1
    float nearestDistance = na

    if array.size(prices) > 0
        for i = 0 to array.size(prices) - 1
            poolState = array.get(lifecycleStates, i)
            canMerge =
                 poolState == LIFECYCLE_FORMING or
                 poolState == LIFECYCLE_ACTIVE or
                 poolState == LIFECYCLE_APPROACHING

            if canMerge
                poolCentroid = array.get(prices, i)
                distance = math.abs(poolCentroid - newPrice)

                if distance <= tolerance and (
                     na(nearestDistance) or
                     distance < nearestDistance
                 )
                    nearestIndex := i
                    nearestDistance := distance

    safeWeight = math.max(newWeight, 0.01)
    safeATR = newATR > 0 ? newATR : atr

    hasSwingEvidence = not na(newStructuralSwingProminence)
    hasVolumeEvidence = not na(newStructuralVolumeScore)

    structuralSwingProminence =
         hasSwingEvidence ?
         clamp01(newStructuralSwingProminence) :
         0.0

    structuralVolumeScore =
         hasVolumeEvidence ?
         clamp01(newStructuralVolumeScore) :
         0.0

    if nearestIndex == -1
        initialMemberMin = newPrice
        initialMemberMax = newPrice
        initialMemberCount = 1
        initialTouchBar = countsAsTouch ? touchBar : newBar

        [initialZoneLow, initialZoneHigh] = getPoolZoneBounds(
             initialMemberMin,
             initialMemberMax,
             initialMemberCount,
             newPrice,
             safeATR
         )

        array.push(prices, newPrice)

        array.push(touches, 1)
        array.push(bars, newBar)
        array.push(activeBars, newActiveBar)
        array.push(strengths, newStrength)
        array.push(volumes, newVolume)
        array.push(sources, newSource)
        array.push(zoneLows, initialZoneLow)
        array.push(zoneHighs, initialZoneHigh)
        array.push(memberMins, initialMemberMin)
        array.push(memberMaxs, initialMemberMax)
        array.push(memberCounts, initialMemberCount)
        array.push(totalWeights, safeWeight)
        array.push(lastTouchBars, initialTouchBar)
        array.push(poolATRs, safeATR)
        array.push(maxSwingProminences, structuralSwingProminence)
        array.push(swingEvidenceCounts, hasSwingEvidence ? 1 : 0)
        array.push(volumeEvidenceSums, structuralVolumeScore)
        array.push(volumeEvidenceCounts, hasVolumeEvidence ? 1 : 0)
        array.push(lifecycleStates, LIFECYCLE_FORMING)
        array.push(sweepBars, -1)
        array.push(beyondCloseCounts, 0)
        array.push(resolutionBars, -1)
        array.push(lifecycleTouchBars, -1)

    else
        oldTouches = array.get(touches, nearestIndex)
        oldCentroid = array.get(prices, nearestIndex)
        oldBar = array.get(bars, nearestIndex)
        oldActiveBar = array.get(activeBars, nearestIndex)
        oldStrength = array.get(strengths, nearestIndex)
        oldVolume = array.get(volumes, nearestIndex)
        oldSources = array.get(sources, nearestIndex)
        oldMemberMin = array.get(memberMins, nearestIndex)
        oldMemberMax = array.get(memberMaxs, nearestIndex)
        oldMemberCount = array.get(memberCounts, nearestIndex)
        oldTotalWeight = array.get(totalWeights, nearestIndex)
        oldLastTouchBar = array.get(lastTouchBars, nearestIndex)
        oldPoolATR = array.get(poolATRs, nearestIndex)
        oldMaxSwingProminence = array.get(maxSwingProminences, nearestIndex)
        oldSwingEvidenceCount = array.get(swingEvidenceCounts, nearestIndex)
        oldVolumeEvidenceSum = array.get(volumeEvidenceSums, nearestIndex)
        oldVolumeEvidenceCount = array.get(volumeEvidenceCounts, nearestIndex)

        combinedWeight = oldTotalWeight + safeWeight
        newCentroid = (
             oldCentroid * oldTotalWeight +
             newPrice * safeWeight
         ) / combinedWeight

        averagedStrength = (
             oldStrength * oldTotalWeight +
             newStrength * safeWeight
         ) / combinedWeight

        averagedVolume = (
             oldVolume * oldTotalWeight +
             newVolume * safeWeight
         ) / combinedWeight

        averagedATR = (
             oldPoolATR * oldTotalWeight +
             safeATR * safeWeight
         ) / combinedWeight

        updatedMemberMin = math.min(oldMemberMin, newPrice)
        updatedMemberMax = math.max(oldMemberMax, newPrice)
        updatedMemberCount = oldMemberCount + 1

        isIndependentTouch =
             countsAsTouch and
             math.abs(touchBar - oldLastTouchBar) >= independentTouchMinSeparationBars

        updatedTouches = oldTouches + (isIndependentTouch ? 1 : 0)
        updatedLastTouchBar = isIndependentTouch ? touchBar : oldLastTouchBar

        updatedMaxSwingProminence =
             hasSwingEvidence ?
             math.max(
                  oldMaxSwingProminence,
                  structuralSwingProminence
             ) :
             oldMaxSwingProminence

        updatedSwingEvidenceCount =
             oldSwingEvidenceCount +
             (hasSwingEvidence ? 1 : 0)

        updatedVolumeEvidenceSum =
             oldVolumeEvidenceSum +
             (hasVolumeEvidence ? structuralVolumeScore : 0.0)

        updatedVolumeEvidenceCount =
             oldVolumeEvidenceCount +
             (hasVolumeEvidence ? 1 : 0)

        [updatedZoneLow, updatedZoneHigh] = getPoolZoneBounds(
             updatedMemberMin,
             updatedMemberMax,
             updatedMemberCount,
             newCentroid,
             averagedATR
         )

        array.set(prices, nearestIndex, newCentroid)
        array.set(touches, nearestIndex, updatedTouches)

        array.set(
             bars,
             nearestIndex,
             math.min(
                  oldBar,
                  newBar
             )
         )

        array.set(
             activeBars,
             nearestIndex,
             math.min(
                  oldActiveBar,
                  newActiveBar
             )
         )

        array.set(strengths, nearestIndex, averagedStrength)
        array.set(volumes, nearestIndex, averagedVolume)
        array.set(
             sources,
             nearestIndex,
             appendSource(
                  oldSources,
                  newSource
             )
         )
        array.set(zoneLows, nearestIndex, updatedZoneLow)
        array.set(zoneHighs, nearestIndex, updatedZoneHigh)
        array.set(memberMins, nearestIndex, updatedMemberMin)
        array.set(memberMaxs, nearestIndex, updatedMemberMax)
        array.set(memberCounts, nearestIndex, updatedMemberCount)
        array.set(totalWeights, nearestIndex, combinedWeight)
        array.set(lastTouchBars, nearestIndex, updatedLastTouchBar)
        array.set(poolATRs, nearestIndex, averagedATR)
        array.set(maxSwingProminences, nearestIndex, updatedMaxSwingProminence)
        array.set(swingEvidenceCounts, nearestIndex, updatedSwingEvidenceCount)
        array.set(volumeEvidenceSums, nearestIndex, updatedVolumeEvidenceSum)
        array.set(volumeEvidenceCounts, nearestIndex, updatedVolumeEvidenceCount)

    while array.size(prices) > maximumLevels
        int oldestIndex = 0
        int oldestBar = array.get(activeBars, 0)

        if array.size(prices) > 1
            for i = 1 to array.size(prices) - 1
                candidateBar = array.get(activeBars, i)

                if candidateBar < oldestBar
                    oldestBar := candidateBar
                    oldestIndex := i

        removeLevel(
             prices,
             touches,
             bars,
             activeBars,
             strengths,
             volumes,
             sources,
             zoneLows,
             zoneHighs,
             memberMins,
             memberMaxs,
             memberCounts,
             totalWeights,
             lastTouchBars,
             poolATRs,
             maxSwingProminences,
             swingEvidenceCounts,
             volumeEvidenceSums,
             volumeEvidenceCounts,
             lifecycleStates,
             sweepBars,
             beyondCloseCounts,
             resolutionBars,
             lifecycleTouchBars,
             oldestIndex
         )

containsSource(
     string currentSources,
     string sourceName
 ) =>
    wrappedSources =
         " + " +
         currentSources +
         " + "

    wrappedSource =
         " + " +
         sourceName +
         " + "

    str.contains(
         wrappedSources,
         wrappedSource
     )

getLiquidityDensityScore(int memberCount) =>
    memberCount <= 0 ?
         0.0 :
         clamp01(
              0.20 +
              0.80 *
              (
                   1.0 -
                   math.exp(
                        -0.70 *
                        float(
                             math.max(
                                  memberCount - 1,
                                  0
                             )
                        )
                   )
              )
         )

getTouchQualityScore(int independentTouches) =>
    independentTouches <= 0 ?
         0.0 :
         clamp01(
              0.20 +
              0.80 *
              (
                   1.0 -
                   math.exp(
                        -0.80 *
                        float(
                             math.max(
                                  independentTouches - 1,
                                  0
                             )
                        )
                   )
              )
         )

getTimeframeImportanceScore(string sources) =>
    hasPreviousWeek =
         containsSource(
              sources,
              "Previous Week High"
         ) or
         containsSource(
              sources,
              "Previous Week Low"
         )

    hasPreviousDay =
         containsSource(
              sources,
              "Previous Day High"
         ) or
         containsSource(
              sources,
              "Previous Day Low"
         )

    hasPreviousWeek ?
         1.00 :
         hasPreviousDay ?
         0.70 :
         0.35

getLiquidityStrengthComponents(
     int memberCount,
     int independentTouches,
     float maxSwingProminence,
     int swingEvidenceCount,
     float volumeEvidenceSum,
     int volumeEvidenceCount,
     string sources
 ) =>
    densityScore =
         getLiquidityDensityScore(
              memberCount
         )

    swingProminenceScore =
         swingEvidenceCount > 0 ?
         clamp01(
              maxSwingProminence
         ) :
         0.0

    touchQualityScore =
         getTouchQualityScore(
              independentTouches
         )

    timeframeImportanceScore =
         getTimeframeImportanceScore(
              sources
         )

    formationVolumeScore =
         volumeEvidenceCount > 0 ?
         clamp01(
              volumeEvidenceSum /
              float(volumeEvidenceCount)
         ) :
         0.0

    liquidityStrength =
         100.0 *
         (
              densityScore * 0.30 +
              swingProminenceScore * 0.25 +
              touchQualityScore * 0.20 +
              timeframeImportanceScore * 0.15 +
              formationVolumeScore * 0.10
         )

    [densityScore, swingProminenceScore, touchQualityScore, timeframeImportanceScore, formationVolumeScore, liquidityStrength]

getPoolLiquidityStrengthAtIndex(
     array<int> touches,
     array<string> sources,
     array<int> memberCounts,
     array<float> maxSwingProminences,
     array<int> swingEvidenceCounts,
     array<float> volumeEvidenceSums,
     array<int> volumeEvidenceCounts,
     int index
 ) =>
    float result = na

    if index >= 0 and index < array.size(memberCounts)
        [densityScore, prominenceScore, touchScore, timeframeScore, volumeScore, liquidityStrength] = getLiquidityStrengthComponents(
             array.get(memberCounts, index),
             array.get(touches, index),
             array.get(maxSwingProminences, index),
             array.get(swingEvidenceCounts, index),
             array.get(volumeEvidenceSums, index),
             array.get(volumeEvidenceCounts, index),
             array.get(sources, index)
         )
        result := liquidityStrength

    result

getPoolLiquidityComponentsAtIndex(
     array<int> touches,
     array<string> sources,
     array<int> memberCounts,
     array<float> maxSwingProminences,
     array<int> swingEvidenceCounts,
     array<float> volumeEvidenceSums,
     array<int> volumeEvidenceCounts,
     int index
 ) =>
    float densityScore = na
    float prominenceScore = na
    float touchScore = na
    float timeframeScore = na
    float volumeScore = na
    float liquidityStrength = na

    if index >= 0 and index < array.size(memberCounts)
        [componentDensity, componentProminence, componentTouch, componentTimeframe, componentVolume, componentStrength] = getLiquidityStrengthComponents(
             array.get(memberCounts, index),
             array.get(touches, index),
             array.get(maxSwingProminences, index),
             array.get(swingEvidenceCounts, index),
             array.get(volumeEvidenceSums, index),
             array.get(volumeEvidenceCounts, index),
             array.get(sources, index)
         )

        densityScore := componentDensity
        prominenceScore := componentProminence
        touchScore := componentTouch
        timeframeScore := componentTimeframe
        volumeScore := componentVolume
        liquidityStrength := componentStrength

    [densityScore, prominenceScore, touchScore, timeframeScore, volumeScore, liquidityStrength]

getDistanceToZoneATR(
     float zoneLow,
     float zoneHigh,
     float currentATR
 ) =>
    float distanceATR = 0.0

    if currentATR > 0
        distanceATR :=
             close < zoneLow ?
             (zoneLow - close) / currentATR :
             close > zoneHigh ?
             (close - zoneHigh) / currentATR :
             0.0

    distanceATR

getBaseFreshnessHalfLife(
     string sources,
     int memberCount,
     int independentTouches
 ) =>
    hasPreviousWeek =
         containsSource(sources, "Previous Week High") or
         containsSource(sources, "Previous Week Low")

    hasPreviousDay =
         containsSource(sources, "Previous Day High") or
         containsSource(sources, "Previous Day Low")

    hasClusterEvidence =
         memberCount >= 2 or
         independentTouches >= 2 or
         containsSource(sources, "Equal High") or
         containsSource(sources, "Equal Low") or
         containsSource(sources, "Repeated Rejection")

    hasPreviousWeek ?
         float(previousWeekFreshnessHalfLifeBars) :
         hasPreviousDay ?
         float(previousDayFreshnessHalfLifeBars) :
         hasClusterEvidence ?
         float(clusterFreshnessHalfLifeBars) :
         float(swingFreshnessHalfLifeBars)

getFreshnessComponents(
     int activeBar,
     float liquidityStrength,
     string sources,
     int memberCount,
     int independentTouches
 ) =>
    age = math.max(bar_index - activeBar, 0)

    baseHalfLife = getBaseFreshnessHalfLife(
         sources,
         memberCount,
         independentTouches
     )

    strengthNormalized = clamp01(
         liquidityStrength / 100.0
     )

    strengthHalfLifeMultiplier =
         0.75 +
         0.75 *
         strengthNormalized

    effectiveHalfLife = math.max(
         1.0,
         baseHalfLife * strengthHalfLifeMultiplier
     )

    freshness = math.exp(
         -0.6931471805599453 *
         float(age) /
         effectiveHalfLife
     )

    [age, baseHalfLife, effectiveHalfLife, clamp01(freshness)]

var table wm = table.new(position.bottom_right, 1, 1)
if barstate.isfirst
    table.cell(wm, 0, 0, "Telegram @SimpleForexTools", text_color = color.new(color.gray, 70), text_size = size.large)

getAdaptiveTrendInfluenceScale(float timeframeSeconds) =>
    float adaptiveScale = 1.00

    if not na(timeframeSeconds) and timeframeSeconds > 0.0
        fiveMinutes = 5.0 * 60.0
        fifteenMinutes = 15.0 * 60.0
        fourHours = 4.0 * 60.0 * 60.0
        oneDay = 24.0 * 60.0 * 60.0

        if timeframeSeconds <= fiveMinutes
            adaptiveScale := 2.00
        else if timeframeSeconds < fifteenMinutes
            progress = math.log(timeframeSeconds / fiveMinutes) / math.log(fifteenMinutes / fiveMinutes)
            adaptiveScale := 2.00 + 0.50 * progress
        else if timeframeSeconds <= fourHours
            adaptiveScale := 2.50
        else if timeframeSeconds < oneDay
            progress = math.log(timeframeSeconds / fourHours) / math.log(oneDay / fourHours)
            adaptiveScale := 2.50 - 1.50 * progress
        else
            adaptiveScale := 1.00

    math.max(1.00, math.min(2.50, adaptiveScale))

getTrendModifier(
     float trendBias,
     int direction
 ) =>
    directionalTrend = clampSigned(
         float(direction) * trendBias
     )

    adaptiveTrendScale = getAdaptiveTrendInfluenceScale(
         chartTimeframeSeconds
     )

    math.max(
         0.75,
         math.min(
              1.25,
              1.0 + 0.10 * adaptiveTrendScale * directionalTrend
         )
    )

getRegimeModifier(
     string currentRegime,
     float trendBias,
     int direction,
     string sources
 ) =>
    directionalTrend = clampSigned(
         float(direction) * trendBias
     )

    isRangeStructure =
         containsSource(sources, "Equal High") or
         containsSource(sources, "Equal Low") or
         containsSource(sources, "Repeated Rejection")

    rawModifier =
         currentRegime == "Trend Expansion" ?
         1.0 + 0.10 * directionalTrend :
         currentRegime == "Trend" ?
         1.0 + 0.05 * directionalTrend :
         currentRegime == "Range" ?
         (isRangeStructure ? 1.05 : 1.00) :
         currentRegime == "Compression" ?
         0.92 :
         1.00

    boundedModifier = math.max(
         0.90,
         math.min(
              1.10,
              rawModifier
         )
    )

    scaledModifier = 1.0 + (boundedModifier - 1.0) * 2.00

    math.max(
         0.80,
         math.min(
              1.20,
              scaledModifier
         )
    )

getGravityComponents(
     float zoneLow,
     float zoneHigh,
     float liquidityStrength,
     int activeBar,
     int memberCount,
     int independentTouches,
     string sources,
     float currentATR,
     float trendBias,
     int direction,
     string currentRegime
 ) =>
    [age, baseHalfLife, effectiveHalfLife, freshness] = getFreshnessComponents(
         activeBar,
         liquidityStrength,
         sources,
         memberCount,
         independentTouches
     )

    distanceATR = getDistanceToZoneATR(
         zoneLow,
         zoneHigh,
         currentATR
     )

    normalizedDistance =
         accessibilityKATR > 0 ?
         distanceATR / accessibilityKATR :
         distanceATR

    accessibility = 1.0 / (
         1.0 +
         normalizedDistance * normalizedDistance
     )

    distanceMultiplier =
         0.55 +
         0.45 *
         accessibility

    freshnessMultiplier =
         0.70 +
         0.30 *
         freshness

    trendModifier = getTrendModifier(
         trendBias,
         direction
     )

    regimeModifier = getRegimeModifier(
         currentRegime,
         trendBias,
         direction,
         sources
     )

    gravity = math.max(
         0.0,
         math.min(
              100.0,
              liquidityStrength *
              distanceMultiplier *
              freshnessMultiplier *
              trendModifier *
              regimeModifier
         )
    )

    [age, baseHalfLife, effectiveHalfLife, freshness, distanceATR, accessibility, distanceMultiplier, freshnessMultiplier, trendModifier, regimeModifier, gravity]

getGravityScore(
     float zoneLow,
     float zoneHigh,
     float liquidityStrength,
     int activeBar,
     int memberCount,
     int independentTouches,
     string sources,
     float currentATR,
     float trendBias,
     int direction,
     string currentRegime
 ) =>
    [age, baseHalfLife, effectiveHalfLife, freshness, distanceATR, accessibility, distanceMultiplier, freshnessMultiplier, trendModifier, regimeModifier, gravity] = getGravityComponents(
         zoneLow,
         zoneHigh,
         liquidityStrength,
         activeBar,
         memberCount,
         independentTouches,
         sources,
         currentATR,
         trendBias,
         direction,
         currentRegime
     )

    gravity

getBestIndex(
     array<float> prices,
     array<int> touches,
     array<int> activeBars,
     array<string> sources,
     array<float> zoneLows,
     array<float> zoneHighs,
     array<int> memberCounts,
     array<float> maxSwingProminences,
     array<int> swingEvidenceCounts,
     array<float> volumeEvidenceSums,
     array<int> volumeEvidenceCounts,
     array<int> lifecycleStates,
     int exclude1,
     int exclude2,
     float currentATR,
     float trendBias,
     int direction,
     string currentRegime
 ) =>
    int bestIndex = -1
    float bestScore = na

    if array.size(prices) > 0
        for i = 0 to array.size(prices) - 1
            if i != exclude1 and i != exclude2
                levelPrice = array.get(prices, i)
                zoneLow = array.get(zoneLows, i)
                zoneHigh = array.get(zoneHighs, i)
                poolSources = array.get(sources, i)
                memberCount = array.get(memberCounts, i)
                independentTouches = array.get(touches, i)

                lifecycleState = array.get(lifecycleStates, i)
                rankEligible =
                     lifecycleState == LIFECYCLE_ACTIVE or
                     lifecycleState == LIFECYCLE_APPROACHING

                correctSide =
                     direction == 1 ?
                     zoneHigh >= close :
                     zoneLow <= close

                distanceATR = getDistanceToZoneATR(
                     zoneLow,
                     zoneHigh,
                     currentATR
                 )

                if rankEligible and correctSide and distanceATR <= maxTargetDistanceATR
                    [densityScore, swingProminenceScore, touchQualityScore, timeframeImportanceScore, formationVolumeScore, liquidityStrength] = getLiquidityStrengthComponents(
                         memberCount,
                         independentTouches,
                         array.get(maxSwingProminences, i),
                         array.get(swingEvidenceCounts, i),
                         array.get(volumeEvidenceSums, i),
                         array.get(volumeEvidenceCounts, i),
                         poolSources
                     )

                    candidateScore = getGravityScore(
                         zoneLow,
                         zoneHigh,
                         liquidityStrength,
                         array.get(activeBars, i),
                         memberCount,
                         independentTouches,
                         poolSources,
                         currentATR,
                         trendBias,
                         direction,
                         currentRegime
                     )

                    if na(bestScore) or candidateScore > bestScore
                        bestScore := candidateScore
                        bestIndex := i

    [bestIndex, bestScore]

getRankContribution(
     float score,
     float rankWeight
 ) =>
    na(score) ?
         0.0 :
         score * rankWeight

getRankWeightedPull(
     float primaryScore,
     float secondaryScore,
     float tertiaryScore
 ) =>
    getRankContribution(
         primaryScore,
         primaryRankWeight
     ) +
     getRankContribution(
         secondaryScore,
         secondaryRankWeight
     ) +
     getRankContribution(
         tertiaryScore,
         tertiaryRankWeight
     )

getLifecycleStateName(int state) =>
    string stateName = "Forming"

    if state == LIFECYCLE_ACTIVE
        stateName := "Active"
    else if state == LIFECYCLE_APPROACHING
        stateName := "Approaching"
    else if state == LIFECYCLE_SWEEPING
        stateName := "Sweeping"
    else if state == LIFECYCLE_REJECTED
        stateName := "Rejected"
    else if state == LIFECYCLE_ACCEPTED
        stateName := "Accepted"
    else if state == LIFECYCLE_RETIRED
        stateName := "Retired"

    stateName

getLatestLifecycleEventIndex(
     array<int> lifecycleStates,
     array<int> sweepBars,
     array<int> resolutionBars
 ) =>
    int bestIndex = -1
    int bestEventBar = -1

    if array.size(lifecycleStates) > 0
        for i = 0 to array.size(lifecycleStates) - 1
            state = array.get(lifecycleStates, i)
            eventBar =
                 state == LIFECYCLE_SWEEPING ? array.get(sweepBars, i) :
                 (state == LIFECYCLE_REJECTED or state == LIFECYCLE_ACCEPTED) ? array.get(resolutionBars, i) :
                 -1

            if eventBar > bestEventBar
                bestEventBar := eventBar
                bestIndex := i

    [bestIndex, bestEventBar]

getLifecycleEventColor(
     int direction,
     int state
 ) =>
    color result = #F5B942

    if state == LIFECYCLE_ACCEPTED
        result := direction == 1 ? bullColor : bearColor
    else if state == LIFECYCLE_REJECTED
        result := direction == 1 ? bearColor : bullColor

    result

getLifecycleEventText(
     int direction,
     int state
 ) =>
    string result = "SWEEPING"

    if state == LIFECYCLE_ACCEPTED
        result := direction == 1 ? "✓ ACCEPTED ↑" : "✓ ACCEPTED ↓"
    else if state == LIFECYCLE_REJECTED
        result := direction == 1 ? "↩ REJECTED ↓" : "↩ REJECTED ↑"
    else if state == LIFECYCLE_SWEEPING
        result := direction == 1 ? "SWEEPING ↑" : "SWEEPING ↓"

    result

pricesVisuallyCollide(
     float priceA,
     float priceB,
     float currentATR
 ) =>
    (
         labelCollisionMode == "Auto" and
         not na(priceA) and
         not na(priceB) and
         currentATR > 0 and
         math.abs(priceA - priceB) / currentATR <= labelCollisionThresholdATR
     )

reserveTargetLabelLane(
     float targetPrice,
     array<float> assignedPrices,
     array<int> assignedLanes,
     float currentATR
 ) =>
    int lane = 0

    if labelCollisionMode == "Auto" and
         not na(targetPrice) and
         currentATR > 0

        bool lane0Used = false
        bool lane1Used = false
        bool lane2Used = false
        bool lane3Used = false
        bool lane4Used = false
        bool lane5Used = false

        if array.size(assignedPrices) > 0
            for i = 0 to array.size(assignedPrices) - 1
                otherPrice = array.get(assignedPrices, i)
                otherLane = array.get(assignedLanes, i)

                if pricesVisuallyCollide(
                     targetPrice,
                     otherPrice,
                     currentATR
                 )
                    if otherLane == 0
                        lane0Used := true
                    else if otherLane == 1
                        lane1Used := true
                    else if otherLane == 2
                        lane2Used := true
                    else if otherLane == 3
                        lane3Used := true
                    else if otherLane == 4
                        lane4Used := true
                    else if otherLane == 5
                        lane5Used := true

        lane :=
             not lane0Used ? 0 :
             not lane1Used ? 1 :
             not lane2Used ? 2 :
             not lane3Used ? 3 :
             not lane4Used ? 4 :
             not lane5Used ? 5 :
             5

    if not na(targetPrice)
        array.push(assignedPrices, targetPrice)
        array.push(assignedLanes, lane)

    lane

getTargetLaneOffsetBars(
     int lane
 ) =>

    safeLane = math.max(lane, 0)
    safeLane * rankMarkerSpacingBars +
         2 * safeLane * (safeLane - 1)

drawLifecycleEvent(
     float levelPrice,
     float zoneLow,
     float zoneHigh,
     int state,
     int eventBar,
     int direction,
     int markerX,
     color eventColor
 ) =>
    if showLifecycleEvents and
         not na(levelPrice) and
         eventBar >= 0 and
         (
              state == LIFECYCLE_SWEEPING or
              state == LIFECYCLE_REJECTED or
              state == LIFECYCLE_ACCEPTED
         )

        eventPrice =
             direction == 1 ? zoneHigh : zoneLow

        eventStartX = math.max(
             eventBar,
             bar_index - 12
         )

        eventEndX = markerX

        if showGlow
            eventGlow = line.new(
                 eventStartX,
                 eventPrice,
                 eventEndX,
                 eventPrice,
                 xloc = xloc.bar_index,
                 extend = extend.none,
                 color = color.new(eventColor, 88),
                 style = line.style_solid,
                 width = 7
             )

            array.push(displayLines, eventGlow)

        eventLine = line.new(
             eventStartX,
             eventPrice,
             eventEndX,
             eventPrice,
             xloc = xloc.bar_index,
             extend = extend.none,
             color = color.new(eventColor, 5),
             style = state == LIFECYCLE_SWEEPING ? line.style_dashed : line.style_solid,
             width = 2
         )

        eventLabel = label.new(
             eventEndX + 1,
             eventPrice,
             getLifecycleEventText(direction, state) +
             "\n" +
             formatPrice(levelPrice),
             xloc = xloc.bar_index,
             style = label.style_label_left,
             color = color.new(eventColor, 72),
             textcolor = eventColor,
             size = size.tiny,
             textalign = text.align_left
         )

        array.push(displayLines, eventLine)
        array.push(displayLabels, eventLabel)

updateLifecycle(
     array<float> prices,
     array<int> touches,
     array<int> activeBars,
     array<string> sources,
     array<float> zoneLows,
     array<float> zoneHighs,
     array<int> memberCounts,
     array<float> maxSwingProminences,
     array<int> swingEvidenceCounts,
     array<float> volumeEvidenceSums,
     array<int> volumeEvidenceCounts,
     array<int> lifecycleStates,
     array<int> sweepBars,
     array<int> beyondCloseCounts,
     array<int> resolutionBars,
     array<int> lifecycleTouchBars,
     float currentATR,
     int direction,
     bool commitTransitions
 ) =>
    bool touchedEvent = false
    bool sweepEvent = false
    bool rejectedEvent = false
    bool acceptedEvent = false

    if commitTransitions and array.size(prices) > 0 and currentATR > 0
        for i = 0 to array.size(prices) - 1
            currentState = array.get(lifecycleStates, i)
            zoneLow = array.get(zoneLows, i)
            zoneHigh = array.get(zoneHighs, i)
            activeBar = array.get(activeBars, i)
            poolSources = array.get(sources, i)
            memberCount = array.get(memberCounts, i)
            independentTouches = array.get(touches, i)

            [densityScore, swingProminenceScore, touchQualityScore, timeframeImportanceScore, formationVolumeScore, liquidityStrength] = getLiquidityStrengthComponents(
                 memberCount,
                 independentTouches,
                 array.get(maxSwingProminences, i),
                 array.get(swingEvidenceCounts, i),
                 array.get(volumeEvidenceSums, i),
                 array.get(volumeEvidenceCounts, i),
                 poolSources
             )

            distanceATR = getDistanceToZoneATR(
                 zoneLow,
                 zoneHigh,
                 currentATR
             )

            zoneTouched =
                 direction == 1 ?
                 high >= zoneLow :
                 low <= zoneHigh

            sweepBoundary =
                 direction == 1 ?
                 zoneHigh + currentATR * sweepBufferATR :
                 zoneLow - currentATR * sweepBufferATR

            swept =
                 direction == 1 ?
                 high >= sweepBoundary :
                 low <= sweepBoundary

            closeBeyond =
                 direction == 1 ?
                 close > zoneHigh :
                 close < zoneLow

            closeOriginalSide =
                 direction == 1 ?
                 close < zoneLow :
                 close > zoneHigh

            if currentState == LIFECYCLE_FORMING or
                 currentState == LIFECYCLE_ACTIVE or
                 currentState == LIFECYCLE_APPROACHING

                if swept and bar_index >= activeBar
                    initialBeyondCount = closeBeyond ? 1 : 0
                    array.set(sweepBars, i, bar_index)
                    array.set(beyondCloseCounts, i, initialBeyondCount)
                    sweepEvent := true

                    if initialBeyondCount >= acceptanceCloseCount
                        array.set(lifecycleStates, i, LIFECYCLE_ACCEPTED)
                        array.set(resolutionBars, i, bar_index)
                        acceptedEvent := true
                    else
                        array.set(lifecycleStates, i, LIFECYCLE_SWEEPING)

                else
                    nextState =
                         liquidityStrength < activeLiquidityStrengthThreshold ?
                         LIFECYCLE_FORMING :
                         distanceATR <= approachingATR ?
                         LIFECYCLE_APPROACHING :
                         LIFECYCLE_ACTIVE

                    array.set(lifecycleStates, i, nextState)

                    lastLifecycleTouchBar = array.get(lifecycleTouchBars, i)
                    newTouchEvent =
                         zoneTouched and
                         not swept and
                         bar_index > activeBar and
                         (
                              lastLifecycleTouchBar < 0 or
                              bar_index - lastLifecycleTouchBar >= independentTouchMinSeparationBars
                         )

                    if newTouchEvent
                        array.set(lifecycleTouchBars, i, bar_index)
                        touchedEvent := true

            else if currentState == LIFECYCLE_SWEEPING
                sweepBar = array.get(sweepBars, i)
                barsSinceSweep = bar_index - sweepBar
                previousBeyondCount = array.get(beyondCloseCounts, i)
                updatedBeyondCount = closeBeyond ? previousBeyondCount + 1 : 0
                array.set(beyondCloseCounts, i, updatedBeyondCount)

                if updatedBeyondCount >= acceptanceCloseCount
                    array.set(lifecycleStates, i, LIFECYCLE_ACCEPTED)
                    array.set(resolutionBars, i, bar_index)
                    acceptedEvent := true

                else if barsSinceSweep <= rejectionWindowBars and closeOriginalSide
                    array.set(lifecycleStates, i, LIFECYCLE_REJECTED)
                    array.set(resolutionBars, i, bar_index)
                    rejectedEvent := true

                else if barsSinceSweep > rejectionWindowBars
                    array.set(lifecycleStates, i, LIFECYCLE_RETIRED)
                    array.set(resolutionBars, i, bar_index)

            else if currentState == LIFECYCLE_REJECTED or
                 currentState == LIFECYCLE_ACCEPTED
                resolutionBar = array.get(resolutionBars, i)

                if resolutionBar >= 0 and
                     bar_index - resolutionBar >= lifecycleEventDisplayBars
                    array.set(lifecycleStates, i, LIFECYCLE_RETIRED)
                    array.set(resolutionBars, i, bar_index)

    [touchedEvent, sweepEvent, rejectedEvent, acceptedEvent]

drawLevel(
     float levelPrice,
     float score,
     string levelName,
     string state,
     int levelBar,
     color levelColor,
     int markerX
 ) =>
    if not na(levelPrice) and
         not na(score)

        startX = math.max(
             levelBar,
             bar_index -
             lineHistoryBars
         )

        isPrimary = levelName == "Primary Magnet"
        isApproaching = state == "Approaching"
        lineWidth = isPrimary ? 2 : 1
        glowWidth = isPrimary ? 7 : 5
        circleTransparency = isApproaching ? 0 : 10
        labelTransparency = isApproaching ? 66 : 78

        if showGlow
            glowLine = line.new(
                 startX,
                 levelPrice,
                 markerX,
                 levelPrice,
                 xloc = xloc.bar_index,
                 extend = extend.none,
                 color = color.new(
                      levelColor,
                      85
                 ),
                 style = line.style_solid,
                 width = glowWidth
             )

            array.push(
                 displayLines,
      

Показано первые 50% кода (символов: 74821)

Открыть по тарифу

Купите тариф с пакетом открытий или войдите, если тариф уже есть. Открытый файл останется в вашем кабинете навсегда.

Разовая покупка файла

На этой странице откроется полный исходный код файла вместо ознакомительного фрагмента.

Чтобы оформить заказ, введите email и перейдите к оплате.

Стоимость полного кода
7 482,10₽ ≈ $91.45