Strategy · Pine Script v6 · Own pane

Market–Average Relationship Engine [WavesUnchained]

by casoon

This strategy evaluates the dynamic relationship between price and a chosen moving average across six diagnostic dimensions: trend, respect, extension, compression, acceleration, and exhaustion.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code1,480
Settings40inputs
LicenceMIT
Last updateyesterday27 Oct 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 strategy, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart. The trades and results appear in the Strategy Tester tab; adjust the settings with the gear icon.

Source Code

1,480 lines of Pine Script v6 from strategies/market_average_relationship_engine/market_average_relationship_engine_strategy.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

market_average_relationship_engine_strategy.pine
//@version=6
// ============================================================================
// Market–Average Relationship Engine [WavesUnchained] — Strategy (standalone)
// Version: 1.2.4
// Author: WavesUnchained
// Build: 2026-10-12
// ============================================================================
// Purpose
// -------
// Evaluates the current relationship between price and ONE consistently
// selected moving-average family. The MA is treated as a market reference,
// not as a standalone crossover signal.
//
// Six diagnostic dimensions:
//   1. Trend        — directional quality and persistence
//   2. Respect      — whether reactions around the MA have historically worked
//   3. Extension    — how unusually far price is from the MA
//   4. Compression  — whether price is clustering around the MA
//   5. Acceleration — whether separation from the MA is increasing
//   6. Exhaustion   — whether an extended move is losing quality
//
// Main outputs:
//   • Relationship oscillator (-100 .. +100)
//   • Pullback, momentum-release, do-not-chase, and relationship-break events
//   • Price/relationship divergence and turn-from-extreme events
//   • Optional MA and event markers in the price chart
//   • All diagnostics in the Data Window and event-label tooltips
//
// Design rules:
//   • Extension is NOT treated as trend strength.
//   • A far-away price can remain directional while becoming a poor new entry.
//   • Signals are confirmed on closed bars by default.
//   • No future bars or lookahead are used. Price pivots (divergence only) are
//     a diagnostic reference — they never gate the pullback/momentum/chase/
//     exhaustion events, which stay pivot-free.
// ============================================================================

strategy("Market–Average Relationship Engine [WavesUnchained] — Strategy", overlay=false,
     precision=1,
     max_lines_count=50,
     max_labels_count=500,
     default_qty_type=strategy.percent_of_equity, default_qty_value=10,
     commission_type=strategy.commission.percent, commission_value=0.02,
     slippage=1, max_bars_back=500)

// Synthetische Bar-Preise (Heikin Ashi, Renko …): Orders füllen zu Marktpreisen,
// der Backtest gilt dort nicht. Kurzer Hinweis unten rechts statt auf der Kerze.
var table chartTypeWarn = table.new(position.bottom_right, 1, 1)
if barstate.islast and not chart.is_standard
    table.cell(chartTypeWarn, 0, 0, "MARE Strategy · backtest invalid on this chart type",
         bgcolor = color.new(color.red, 10), text_color = color.white, text_size = size.small)

// ============================================================================
// 01. INPUTS
// ============================================================================

string GROUP_MA        = "01 · Moving Average"
string GROUP_NORM      = "02 · Normalization"
string GROUP_SCORES    = "03 · Score Model"
string GROUP_EVENTS    = "04 · Event Logic"
string GROUP_DIVERGE   = "05 · Divergence & Extremes"
string GROUP_VISUALS   = "06 · Visuals"
string GROUP_ALERTS    = "07 · Alerts"
string GROUP_STRATEGY  = "08 · Strategy"

// — Moving average ------------------------------------------------------------

string maTypeInput = input.string(
     "EMA",
     "MA family",
     options = ["EMA", "SMA", "WMA", "HMA", "VWMA", "RMA", "DEMA", "TEMA", "ALMA"],
     group = GROUP_MA,
     tooltip = "One MA family is used consistently throughout the complete model.")

int maLengthInput = input.int(
     50,
     "MA length",
     minval = 2,
     maxval = 1000,
     group = GROUP_MA)

sourceInput = input.source(
     close,
     "Source",
     group = GROUP_MA)

float almaOffsetInput = input.float(
     0.85,
     "ALMA offset",
     minval = 0.0,
     maxval = 1.0,
     step = 0.05,
     group = GROUP_MA,
     inline = "alma")

float almaSigmaInput = input.float(
     6.0,
     "Sigma",
     minval = 0.5,
     maxval = 20.0,
     step = 0.5,
     group = GROUP_MA,
     inline = "alma")

// — Normalization -------------------------------------------------------------

int atrLengthInput = input.int(
     14,
     "ATR length",
     minval = 2,
     maxval = 200,
     group = GROUP_NORM)

int slopeLengthInput = input.int(
     10,
     "MA slope lookback",
     minval = 1,
     maxval = 100,
     group = GROUP_NORM)

float fullSlopeAtrInput = input.float(
     0.50,
     "Full slope strength (ATR)",
     minval = 0.05,
     maxval = 5.0,
     step = 0.05,
     group = GROUP_NORM,
     tooltip = "MA movement across the slope lookback that counts as full slope strength.")

int persistenceLengthInput = input.int(
     20,
     "Direction persistence",
     minval = 3,
     maxval = 200,
     group = GROUP_NORM)

int efficiencyLengthInput = input.int(
     14,
     "Price efficiency lookback",
     minval = 3,
     maxval = 100,
     group = GROUP_NORM)

int statisticsLengthInput = input.int(
     100,
     "Statistical reference",
     minval = 20,
     maxval = 500,
     group = GROUP_NORM,
     tooltip = "Historical reference for unusual extension and volatility contraction.")

// — Score model ---------------------------------------------------------------

float extensionNormalAtrInput = input.float(
     0.75,
     "Normal extension (ATR)",
     minval = 0.05,
     maxval = 10.0,
     step = 0.05,
     group = GROUP_SCORES,
     inline = "ext")

float extensionExtremeAtrInput = input.float(
     2.50,
     "Extreme",
     minval = 0.10,
     maxval = 20.0,
     step = 0.10,
     group = GROUP_SCORES,
     inline = "ext")

float proximityAtrInput = input.float(
     0.35,
     "MA interaction zone (ATR)",
     minval = 0.05,
     maxval = 3.0,
     step = 0.05,
     group = GROUP_SCORES)

int respectLookbackInput = input.int(
     80,
     "Respect sample lookback",
     minval = 20,
     maxval = 250,
     group = GROUP_SCORES,
     inline = "respect")

int respectResponseBarsInput = input.int(
     4,
     "Outcome bars",
     minval = 1,
     maxval = 20,
     group = GROUP_SCORES,
     inline = "respect",
     tooltip = "A historical MA interaction is judged after this many bars.")

int compressionLengthInput = input.int(
     20,
     "Compression lookback",
     minval = 5,
     maxval = 100,
     group = GROUP_SCORES,
     inline = "comp")

float compressionBandAtrInput = input.float(
     0.45,
     "Band (ATR)",
     minval = 0.05,
     maxval = 3.0,
     step = 0.05,
     group = GROUP_SCORES,
     inline = "comp")

int accelerationLengthInput = input.int(
     5,
     "Acceleration lookback",
     minval = 1,
     maxval = 50,
     group = GROUP_SCORES)

int exhaustionLengthInput = input.int(
     8,
     "Exhaustion lookback",
     minval = 2,
     maxval = 50,
     group = GROUP_SCORES)

// — Event logic ---------------------------------------------------------------

bool confirmedOnlyInput = input.bool(
     true,
     "Only confirm events on closed bars",
     group = GROUP_EVENTS)

int signalCooldownInput = input.int(
     8,
     "Signal cooldown",
     minval = 0,
     maxval = 100,
     group = GROUP_EVENTS)

float minimumTrendInput = input.float(
     55.0,
     "Minimum trend quality",
     minval = 0.0,
     maxval = 100.0,
     step = 1.0,
     group = GROUP_EVENTS,
     inline = "quality")

float minimumRespectInput = input.float(
     55.0,
     "Minimum respect",
     minval = 0.0,
     maxval = 100.0,
     step = 1.0,
     group = GROUP_EVENTS,
     inline = "quality")

float pullbackPriorExtensionInput = input.float(
     0.70,
     "Pullback prior extension (ATR)",
     minval = 0.0,
     maxval = 10.0,
     step = 0.05,
     group = GROUP_EVENTS)

float maximumPullbackExhaustionInput = input.float(
     65.0,
     "Maximum pullback exhaustion",
     minval = 0.0,
     maxval = 100.0,
     step = 1.0,
     group = GROUP_EVENTS)

float accelerationThresholdInput = input.float(
     60.0,
     "Momentum acceleration threshold",
     minval = 0.0,
     maxval = 100.0,
     step = 1.0,
     group = GROUP_EVENTS,
     inline = "momentum")

float compressionThresholdInput = input.float(
     65.0,
     "Compression threshold",
     minval = 0.0,
     maxval = 100.0,
     step = 1.0,
     group = GROUP_EVENTS,
     inline = "momentum")

float chaseExtensionThresholdInput = input.float(
     75.0,
     "Do-not-chase extension",
     minval = 0.0,
     maxval = 100.0,
     step = 1.0,
     group = GROUP_EVENTS,
     inline = "risk")

float exhaustionThresholdInput = input.float(
     68.0,
     "Exhaustion warning",
     minval = 0.0,
     maxval = 100.0,
     step = 1.0,
     group = GROUP_EVENTS,
     inline = "risk")

int breakConfirmationInput = input.int(
     3,
     "Relationship-break confirmation",
     minval = 1,
     maxval = 20,
     group = GROUP_EVENTS)

// — Divergence & extremes ------------------------------------------------------

int divergencePivotLeftInput = input.int(
     5,
     "Divergence pivot bars (left)",
     minval = 2,
     maxval = 30,
     group = GROUP_DIVERGE,
     inline = "divpiv")

int divergencePivotRightInput = input.int(
     5,
     "Right",
     minval = 2,
     maxval = 30,
     group = GROUP_DIVERGE,
     inline = "divpiv",
     tooltip = "Price pivots used only to detect price/relationship divergence — diagnostic, never a gate on the pullback, momentum, or exhaustion events above.")

int turnLookbackInput = input.int(
     30,
     "Extreme lookback",
     minval = 5,
     maxval = 150,
     group = GROUP_DIVERGE,
     inline = "turn",
     tooltip = "How far back to check whether the relationship line recently touched an extreme.")

float turnExtremeLevelInput = input.float(
     55.0,
     "Extreme level",
     minval = 10.0,
     maxval = 100.0,
     step = 1.0,
     group = GROUP_DIVERGE,
     inline = "turn")

int turnConfirmBarsInput = input.int(
     3,
     "Turn confirmation bars",
     minval = 2,
     maxval = 15,
     group = GROUP_DIVERGE,
     tooltip = "Consecutive bars the relationship line must rise (or fall) off the extreme before the turn counts as confirmed.")

// — Visuals -------------------------------------------------------------------

bool showMaInput = input.bool(
     true,
     "Show selected MA in price chart",
     group = GROUP_VISUALS)

bool showPriceMarkersInput = input.bool(
     true,
     "Show event markers in price chart",
     group = GROUP_VISUALS)

bool showPaneMarkersInput = input.bool(
     true,
     "Show event markers in oscillator",
     group = GROUP_VISUALS)

bool showEventTooltipsInput = input.bool(
     true,
     "Create event tooltip labels",
     group = GROUP_VISUALS,
     tooltip = "Labels are created only on events. Hover them to inspect all six diagnostics.")

bool showStateBackgroundInput = input.bool(
     true,
     "Shade oscillator state",
     group = GROUP_VISUALS)

bool showRelationshipFillInput = input.bool(
     true,
     "Fill relationship to zero",
     group = GROUP_VISUALS)

bool colorBarsInput = input.bool(
     false,
     "Color price bars by relationship",
     group = GROUP_VISUALS)

int visualTransparencyInput = input.int(
     78,
     "Background transparency",
     minval = 0,
     maxval = 100,
     group = GROUP_VISUALS)

// — Alerts --------------------------------------------------------------------

bool enablePullbackAlertsInput = input.bool(true, "Pullback confirmations", group = GROUP_ALERTS)
bool enableMomentumAlertsInput = input.bool(true, "Compression releases", group = GROUP_ALERTS)
bool enableRiskAlertsInput = input.bool(true, "Do-not-chase / exhaustion", group = GROUP_ALERTS)
bool enableBreakAlertsInput = input.bool(true, "Relationship breaks", group = GROUP_ALERTS)
bool enableDivergenceAlertsInput = input.bool(true, "Price/relationship divergence", group = GROUP_ALERTS)
bool enableTurnAlertsInput = input.bool(true, "Turn from extreme", group = GROUP_ALERTS)

// — Strategy hook --------------------------------------------------------------
// Drives the trailing stop below; carried over from the indicator, where it has
// no effect on scoring or display.

float stopAtrMultInput = input.float(
     2.5,
     "Stop distance from reference MA (ATR×)",
     minval = 0.5,
     maxval = 10.0,
     step = 0.1,
     group = GROUP_STRATEGY,
     tooltip = "Only used by the generated strategy's trailing stop — not part of the indicator's own scoring or display.")

// ============================================================================
// 02. HELPERS
// ============================================================================

clamp(float value, float minimum, float maximum) =>
    math.max(minimum, math.min(maximum, value))

safeDiv(float numerator, float denominator, float fallback = 0.0) =>
    math.abs(denominator) > 1e-10 ? numerator / denominator : fallback

normalize(float value, float low, float high) =>
    high > low ? clamp((value - low) / (high - low), 0.0, 1.0) : 0.0

signedDirection(float value, float deadZone = 0.0) =>
    value > deadZone ? 1.0 : value < -deadZone ? -1.0 : 0.0

formatScore(float value) =>
    str.tostring(value, "#.0")

formatSigned(float value) =>
    str.tostring(value, value >= 0.0 ? "+#.00" : "#.00")

movingAverage(float source, int length, string averageType) =>
    float ema1 = ta.ema(source, length)
    float ema2 = ta.ema(ema1, length)
    float ema3 = ta.ema(ema2, length)
    switch averageType
        "EMA"  => ema1
        "SMA"  => ta.sma(source, length)
        "WMA"  => ta.wma(source, length)
        "HMA"  => ta.hma(source, length)
        "VWMA" => ta.vwma(source, length)
        "RMA"  => ta.rma(source, length)
        "DEMA" => 2.0 * ema1 - ema2
        "TEMA" => 3.0 * ema1 - 3.0 * ema2 + ema3
        "ALMA" => ta.alma(source, length, almaOffsetInput, almaSigmaInput)
        => ema1

// Historical interaction quality. A touch is evaluated only after enough bars
// have elapsed, so it cannot use future information on the currently executing bar.
respectStatistics(
     float average,
     float atrValue,
     int lookback,
     int responseBars,
     float proximityAtr) =>
    float successes = 0.0
    float failures = 0.0
    float weightedOutcome = 0.0

    for i = responseBars to lookback
        float atrAtTouch = atrValue[i]
        float maAtTouch = average[i]
        float distanceAtTouch = math.abs(close[i] - maAtTouch)
        bool valid = not na(atrAtTouch) and not na(maAtTouch)
        bool touched = valid and distanceAtTouch <= atrAtTouch * proximityAtr
        // One sample per touch episode: skip bars whose predecessor was already
        // within proximity, so a multi-bar hug of the MA counts once.
        float atrBefore = atrValue[i + 1]
        float maBefore = average[i + 1]
        bool touchedBefore = not na(atrBefore) and not na(maBefore) and math.abs(close[i + 1] - maBefore) <= atrBefore * proximityAtr

        if touched and not touchedBefore
            float localSlope = maAtTouch - average[i + 1]
            float trendSide = localSlope >= 0.0 ? 1.0 : -1.0
            float outcomeClose = close[i - responseBars]
            float outcomeMa = average[i - responseBars]
            float outcomeAtr = atrValue[i - responseBars]
            float progress = safeDiv((outcomeClose - close[i]) * trendSide, outcomeAtr, 0.0)
            bool heldCorrectSide = (outcomeClose - outcomeMa) * trendSide > 0.0
            bool succeeded = heldCorrectSide and progress > 0.0

            successes += succeeded ? 1.0 : 0.0
            failures += succeeded ? 0.0 : 1.0
            weightedOutcome += clamp(progress, -1.0, 1.0)

    float samples = successes + failures
    float hitRate = samples > 0.0 ? successes / samples : 0.5
    float outcomeQuality = samples > 0.0 ? normalize(weightedOutcome / samples, -0.35, 0.50) : 0.5
    float confidence = normalize(samples, 2.0, 10.0)
    float rawScore = 100.0 * (0.70 * hitRate + 0.30 * outcomeQuality)
    float confidenceAdjusted = 50.0 + (rawScore - 50.0) * confidence
    [clamp(confidenceAdjusted, 0.0, 100.0), samples, 100.0 * hitRate]

// ============================================================================
// 03. CORE SERIES
// ============================================================================

float ma = movingAverage(sourceInput, maLengthInput, maTypeInput)
float atr = ta.atr(atrLengthInput)
float safeAtr = math.max(atr, syminfo.mintick)

float signedDistanceAtr = safeDiv(close - ma, safeAtr, 0.0)
float absoluteDistanceAtr = math.abs(signedDistanceAtr)

float maSlopeAtr = safeDiv(ma - ma[slopeLengthInput], safeAtr, 0.0)
float maSlopeStrength = 100.0 * normalize(math.abs(maSlopeAtr), 0.0, fullSlopeAtrInput)
float maDirection = signedDirection(maSlopeAtr, fullSlopeAtrInput * 0.05)
float priceSide = signedDirection(close - ma, safeAtr * 0.03)

// Directional persistence: proportion of recent bars aligned with the current side.
float bullishSideRatio = ta.sma(close > ma ? 1.0 : 0.0, persistenceLengthInput)
float bearishSideRatio = ta.sma(close < ma ? 1.0 : 0.0, persistenceLengthInput)
float sidePersistence = priceSide > 0.0 ? bullishSideRatio : priceSide < 0.0 ? bearishSideRatio : 0.5

float bullishSlopeRatio = ta.sma(ma > ma[1] ? 1.0 : 0.0, persistenceLengthInput)
float bearishSlopeRatio = ta.sma(ma < ma[1] ? 1.0 : 0.0, persistenceLengthInput)
float slopePersistence = maDirection > 0.0 ? bullishSlopeRatio : maDirection < 0.0 ? bearishSlopeRatio : 0.5

// Price efficiency ratio: directional progress relative to total travelled path.
float pathTravel = ta.sma(math.abs(close - close[1]), efficiencyLengthInput) * efficiencyLengthInput
float netTravel = math.abs(close - close[efficiencyLengthInput])
float priceEfficiency = clamp(safeDiv(netTravel, pathTravel, 0.0), 0.0, 1.0)
float efficiencyScore = 100.0 * priceEfficiency

float directionalAgreement = if priceSide != 0.0 and maDirection != 0.0 and priceSide == maDirection
    1.0
else if priceSide != 0.0 and maDirection == 0.0
    0.45
else if priceSide == 0.0
    0.25
else
    0.0

float trendQuality =
     100.0 * clamp(
         0.30 * (maSlopeStrength / 100.0) +
         0.25 * sidePersistence +
         0.20 * slopePersistence +
         0.15 * priceEfficiency +
         0.10 * directionalAgreement,
         0.0,
         1.0)

float trendDirection = if directionalAgreement >= 1.0
    priceSide
else if maDirection != 0.0
    maDirection
else
    priceSide

float signedTrend = trendQuality * trendDirection

// ============================================================================
// 04. SIX DIAGNOSTIC SCORES
// ============================================================================

// — 1. TREND ------------------------------------------------------------------
// trendQuality is already 0..100; signedTrend carries direction.

// — 2. RESPECT ----------------------------------------------------------------

[respectScore, respectSamples, respectHitRate] = respectStatistics(
     ma,
     safeAtr,
     respectLookbackInput,
     respectResponseBarsInput,
     proximityAtrInput)

// Add a small current-regime component: the MA is more usable when price does
// not repeatedly alternate sides.
float sideFlipRate = ta.sma(ta.cross(close, ma) ? 1.0 : 0.0, persistenceLengthInput)
float currentStability = 100.0 * (1.0 - clamp(sideFlipRate * 4.0, 0.0, 1.0))
respectScore := clamp(0.80 * respectScore + 0.20 * currentStability, 0.0, 100.0)

// — 3. EXTENSION --------------------------------------------------------------

float extensionAtrComponent =
     100.0 * normalize(
         absoluteDistanceAtr,
         extensionNormalAtrInput,
         extensionExtremeAtrInput)

float distanceMean = ta.sma(absoluteDistanceAtr, statisticsLengthInput)
float distanceDeviation = ta.stdev(absoluteDistanceAtr, statisticsLengthInput)
float extensionZ = safeDiv(absoluteDistanceAtr - distanceMean, distanceDeviation, 0.0)
float extensionRarityComponent = 100.0 * normalize(extensionZ, 0.0, 2.5)

float extensionScore = clamp(
     0.70 * extensionAtrComponent +
     0.30 * extensionRarityComponent,
     0.0,
     100.0)

// — 4. COMPRESSION ------------------------------------------------------------

float nearMaRatio = ta.sma(absoluteDistanceAtr <= compressionBandAtrInput ? 1.0 : 0.0, compressionLengthInput)
float averageDistance = ta.sma(absoluteDistanceAtr, compressionLengthInput)
float tightness = 1.0 - normalize(averageDistance, compressionBandAtrInput * 0.40, compressionBandAtrInput * 1.80)

float atrBaseline = ta.sma(safeAtr, statisticsLengthInput)
float volatilityContraction = 1.0 - normalize(safeDiv(safeAtr, atrBaseline, 1.0), 0.70, 1.20)

float compressionScore = 100.0 * clamp(
     0.50 * nearMaRatio +
     0.30 * tightness +
     0.20 * volatilityContraction,
     0.0,
     1.0)

// — 5. ACCELERATION -----------------------------------------------------------

float directedDistance = signedDistanceAtr * trendDirection
float separationChange = directedDistance - directedDistance[accelerationLengthInput]
float separationVelocity = safeDiv(separationChange, accelerationLengthInput, 0.0)

float currentSlopeVelocity = safeDiv(maSlopeAtr, slopeLengthInput, 0.0) * trendDirection
float priorSlopeAtr = safeDiv(ma[accelerationLengthInput] - ma[accelerationLengthInput + slopeLengthInput], safeAtr[accelerationLengthInput], 0.0)
float priorSlopeVelocity = safeDiv(priorSlopeAtr, slopeLengthInput, 0.0) * trendDirection
float slopeAcceleration = currentSlopeVelocity - priorSlopeVelocity

float recentReturn = safeDiv(close - close[accelerationLengthInput], safeAtr, 0.0) * trendDirection
float priorReturn = safeDiv(close[accelerationLengthInput] - close[accelerationLengthInput * 2], safeAtr[accelerationLengthInput], 0.0) * trendDirection
float returnAcceleration = recentReturn - priorReturn

float separationAccelerationScore = 100.0 * normalize(separationVelocity, 0.00, 0.25)
float slopeAccelerationScore = 100.0 * normalize(slopeAcceleration, 0.00, 0.04)
float returnAccelerationScore = 100.0 * normalize(returnAcceleration, 0.00, 1.50)

float accelerationScore = clamp(
     0.50 * separationAccelerationScore +
     0.20 * slopeAccelerationScore +
     0.30 * returnAccelerationScore,
     0.0,
     100.0)

// — 6. EXHAUSTION -------------------------------------------------------------

float efficiencyNow = priceEfficiency
float efficiencyPrior = priceEfficiency[exhaustionLengthInput]
float efficiencyDecay = normalize(efficiencyPrior - efficiencyNow, 0.0, 0.35)

float candleRange = math.max(high - low, syminfo.mintick)
float upperWick = high - math.max(open, close)
float lowerWick = math.min(open, close) - low
float opposingWickRatio = if trendDirection > 0.0
    upperWick / candleRange
else if trendDirection < 0.0
    lowerWick / candleRange
else
    0.0
float wickPressure = ta.sma(opposingWickRatio, exhaustionLengthInput)

float directionalProgress = safeDiv((close - close[exhaustionLengthInput]) * trendDirection, safeAtr, 0.0)
float progressFailure = 1.0 - normalize(directionalProgress, 0.0, 1.50)

float accelerationLoss = normalize(accelerationScore[exhaustionLengthInput] - accelerationScore, 0.0, 50.0)
float extensionPrecondition = extensionScore / 100.0

float exhaustionScore = 100.0 * clamp(
     extensionPrecondition * (
         0.35 * efficiencyDecay +
         0.25 * wickPressure +
         0.25 * progressFailure +
         0.15 * accelerationLoss),
     0.0,
     1.0)

// ============================================================================
// 05. RELATIONSHIP OSCILLATOR
// ============================================================================

// Extension above the chase threshold and exhaustion reduce entry quality,
// but do not automatically reverse the directional sign.
float extensionPenalty = normalize(extensionScore, chaseExtensionThresholdInput, 100.0)
float exhaustionPenalty = normalize(exhaustionScore, exhaustionThresholdInput, 100.0)
float qualityPenalty = clamp(0.18 * extensionPenalty + 0.32 * exhaustionPenalty, 0.0, 0.50)

float relationshipRaw = signedTrend * (1.0 - qualityPenalty)
float relationship = ta.ema(relationshipRaw, 3)
float relationshipSignal = ta.ema(relationship, 5)

bool bullishEnvironment = relationship >= 20.0
bool bearishEnvironment = relationship <= -20.0
bool neutralEnvironment = not bullishEnvironment and not bearishEnvironment

// ============================================================================
// 06. STATE ENGINE
// ============================================================================

bool isConfirmed = not confirmedOnlyInput or barstate.isconfirmed

bool compressionState =
     compressionScore >= compressionThresholdInput and
     trendQuality < minimumTrendInput

bool bullishAccelerationState =
     trendDirection > 0.0 and
     trendQuality >= minimumTrendInput and
     accelerationScore >= accelerationThresholdInput and
     exhaustionScore < exhaustionThresholdInput

bool bearishAccelerationState =
     trendDirection < 0.0 and
     trendQuality >= minimumTrendInput and
     accelerationScore >= accelerationThresholdInput and
     exhaustionScore < exhaustionThresholdInput

bool overextendedState =
     trendQuality >= minimumTrendInput and
     extensionScore >= chaseExtensionThresholdInput and
     exhaustionScore < exhaustionThresholdInput

bool exhaustionState =
     trendQuality >= minimumTrendInput and
     exhaustionScore >= exhaustionThresholdInput

bool healthyTrendState =
     trendQuality >= minimumTrendInput and
     respectScore >= minimumRespectInput and
     extensionScore < chaseExtensionThresholdInput and
     exhaustionScore < exhaustionThresholdInput and
     not bullishAccelerationState and
     not bearishAccelerationState

string stateName = if compressionState
    "MA Compression"
else if exhaustionState and trendDirection > 0.0
    "Bullish Exhaustion Risk"
else if exhaustionState and trendDirection < 0.0
    "Bearish Exhaustion Risk"
else if overextendedState and trendDirection > 0.0
    "Bullish — Do Not Chase"
else if overextendedState and trendDirection < 0.0
    "Bearish — Do Not Chase"
else if bullishAccelerationState
    "Bullish Acceleration"
else if bearishAccelerationState
    "Bearish Acceleration"
else if healthyTrendState and trendDirection > 0.0
    "Healthy Bullish Trend"
else if healthyTrendState and trendDirection < 0.0
    "Healthy Bearish Trend"
else if trendDirection > 0.0 and trendQuality >= 35.0
    "Developing Bullish Relationship"
else if trendDirection < 0.0 and trendQuality >= 35.0
    "Developing Bearish Relationship"
else
    "Neutral / Unstructured"

// ============================================================================
// 07. EVENT ENGINE
// ============================================================================

bool priorBullishTrend = trendDirection[1] > 0.0 and trendQuality[1] >= minimumTrendInput
bool priorBearishTrend = trendDirection[1] < 0.0 and trendQuality[1] >= minimumTrendInput

// Prior extension is measured in the trade's direction — a dip below the MA
// does not count as a prior bullish extension (and vice versa).
int priorExtensionLength = math.max(2, accelerationLengthInput + 1)
float recentBullishExtension = ta.highest(signedDistanceAtr[1], priorExtensionLength)
float recentBearishExtension = -ta.lowest(signedDistanceAtr[1], priorExtensionLength)

bool bullishRejection =
     low <= ma + safeAtr * proximityAtrInput and
     close > ma and
     close > open and
     close >= (high + low) * 0.5

bool bearishRejection =
     high >= ma - safeAtr * proximityAtrInput and
     close < ma and
     close < open and
     close <= (high + low) * 0.5

bool bullishPullbackSetup =
     isConfirmed and
     priorBullishTrend and
     trendDirection > 0.0 and
     trendQuality >= minimumTrendInput and
     respectScore >= minimumRespectInput and
     recentBullishExtension >= pullbackPriorExtensionInput and
     bullishRejection and
     exhaustionScore <= maximumPullbackExhaustionInput

bool bearishPullbackSetup =
     isConfirmed and
     priorBearishTrend and
     trendDirection < 0.0 and
     trendQuality >= minimumTrendInput and
     respectScore >= minimumRespectInput and
     recentBearishExtension >= pullbackPriorExtensionInput and
     bearishRejection and
     exhaustionScore <= maximumPullbackExhaustionInput

bool compressionWasPresent =
     ta.highest(compressionScore[1], compressionLengthInput) >= compressionThresholdInput

bool bullishMomentumSetup =
     isConfirmed and
     compressionWasPresent and
     ta.crossover(signedDistanceAtr, compressionBandAtrInput) and
     maSlopeAtr > 0.0 and
     accelerationScore >= accelerationThresholdInput and
     priceEfficiency >= 0.35 and
     exhaustionScore < exhaustionThresholdInput

bool bearishMomentumSetup =
     isConfirmed and
     compressionWasPresent and
     ta.crossunder(signedDistanceAtr, -compressionBandAtrInput) and
     maSlopeAtr < 0.0 and
     accelerationScore >= accelerationThresholdInput and
     priceEfficiency >= 0.35 and
     exhaustionScore < exhaustionThresholdInput

bool bullishDoNotChaseSetup =
     isConfirmed and
     trendDirection > 0.0 and
     trendQuality >= minimumTrendInput and
     ta.crossover(extensionScore, chaseExtensionThresholdInput)

bool bearishDoNotChaseSetup =
     isConfirmed and
     trendDirection < 0.0 and
     trendQuality >= minimumTrendInput and
     ta.crossover(extensionScore, chaseExtensionThresholdInput)

bool bullishExhaustionSetup =
     isConfirmed and
     trendDirection > 0.0 and
     ta.crossover(exhaustionScore, exhaustionThresholdInput)

bool bearishExhaustionSetup =
     isConfirmed and
     trendDirection < 0.0 and
     ta.crossover(exhaustionScore, exhaustionThresholdInput)

// A relationship break requires sustained acceptance on the opposite side;
// a one-bar MA cross is insufficient.
float recentBullishAcceptance = ta.sma(close > ma ? 1.0 : 0.0, breakConfirmationInput)
float recentBearishAcceptance = ta.sma(close < ma ? 1.0 : 0.0, breakConfirmationInput)

bool bullishRelationshipBrokenRaw =
     isConfirmed and
     trendDirection[breakConfirmationInput] > 0.0 and
     trendQuality[breakConfirmationInput] >= minimumTrendInput and
     recentBearishAcceptance >= 0.80 and
     relationship < 10.0 and
     maSlopeAtr <= fullSlopeAtrInput * 0.10

bool bearishRelationshipBrokenRaw =
     isConfirmed and
     trendDirection[breakConfirmationInput] < 0.0 and
     trendQuality[breakConfirmationInput] >= minimumTrendInput and
     recentBullishAcceptance >= 0.80 and
     relationship > -10.0 and
     maSlopeAtr >= -fullSlopeAtrInput * 0.10

// Fire only on the transition into the break condition.
bool bullishRelationshipBrokenSetup = bullishRelationshipBrokenRaw and not bullishRelationshipBrokenRaw[1]
bool bearishRelationshipBrokenSetup = bearishRelationshipBrokenRaw and not bearishRelationshipBrokenRaw[1]

// — Divergence: price pivots stay a pure diagnostic here — they detect the
// divergence event itself, but never gate the pullback/momentum/chase/exhaustion
// setups above. Regular divergence only (price extreme vs. relationship extreme
// at the matching prior pivot of the same type).
float pivotLowPrice = ta.pivotlow(low, divergencePivotLeftInput, divergencePivotRightInput)
float pivotHighPrice = ta.pivothigh(high, divergencePivotLeftInput, divergencePivotRightInput)
bool newPivotLow = not na(pivotLowPrice)
bool newPivotHigh = not na(pivotHighPrice)

var float lastPivotLowPrice = na
var float lastPivotLowRelationship = na
var float lastPivotHighPrice = na
var float lastPivotHighRelationship = na

bool bullishDivergenceSetup =
     isConfirmed and
     newPivotLow and
     not na(lastPivotLowPrice) and
     pivotLowPrice < lastPivotLowPrice and
     relationship[divergencePivotRightInput] > lastPivotLowRelationship

bool bearishDivergenceSetup =
     isConfirmed and
     newPivotHigh and
     not na(lastPivotHighPrice) and
     pivotHighPrice > lastPivotHighPrice and
     relationship[divergencePivotRightInput] < lastPivotHighRelationship

if newPivotLow
    lastPivotLowPrice := pivotLowPrice
    lastPivotLowRelationship := relationship[divergencePivotRightInput]

if newPivotHigh
    lastPivotHighPrice := pivotHighPrice
    lastPivotHighRelationship := relationship[divergencePivotRightInput]

// — Turn from extreme: pivot-free — the relationship line itself curling off its
// own recent high/low. Depending on what preceded it, this reads either as a new
// trend starting (from a neutral/opposite prior state) or an existing trend
// recognizably losing steam (from an already-extended same-side state); both are
// the same underlying event, the surrounding state carries the interpretation.
float relationshipRecentLow = ta.lowest(relationship, turnLookbackInput)
float relationshipRecentHigh = ta.highest(relationship, turnLookbackInput)

bool bullishTurnRaw =
     isConfirmed and
     relationshipRecentLow <= -turnExtremeLevelInput and
     ta.rising(relationship, turnConfirmBarsInput)

bool bearishTurnRaw =
     isConfirmed and
     relationshipRecentHigh >= turnExtremeLevelInput and
     ta.falling(relationship, turnConfirmBarsInput)

// Fire only on the transition into a confirmed turn, not every bar it keeps rising/falling.
bool bullishTurnSetup = bullishTurnRaw and not bullishTurnRaw[1]
bool bearishTurnSetup = bearishTurnRaw and not bearishTurnRaw[1]

// Shared cooldown by event family.
// Cooldown counts from the last FIRED event of the family, not from blocked
// candidates — a setup suppressed by the cooldown does not restart the clock.
var int lastPullbackEventBar = na
var int lastMomentumEventBar = na
var int lastChaseEventBar = na
var int lastExhaustionEventBar = na
var int lastBreakEventBar = na
var int lastDivergenceEventBar = na
var int lastTurnEventBar = na
bool pullbackAllowed = na(lastPullbackEventBar) or bar_index - lastPullbackEventBar - 1 > signalCooldownInput
bool momentumAllowed = na(lastMomentumEventBar) or bar_index - lastMomentumEventBar - 1 > signalCooldownInput
bool chaseAllowed = na(lastChaseEventBar) or bar_index - lastChaseEventBar - 1 > signalCooldownInput
bool exhaustionAllowed = na(lastExhaustionEventBar) or bar_index - lastExhaustionEventBar - 1 > signalCooldownInput
bool breakAllowed = na(lastBreakEventBar) or bar_index - lastBreakEventBar - 1 > signalCooldownInput
bool divergenceAllowed = na(lastDivergenceEventBar) or bar_index - lastDivergenceEventBar - 1 > signalCooldownInput
bool turnAllowed = na(lastTurnEventBar) or bar_index - lastTurnEventBar - 1 > signalCooldownInput

bool bullishPullback = bullishPullbackSetup and pullbackAllowed
bool bearishPullback = bearishPullbackSetup and pullbackAllowed
bool bullishMomentum = bullishMomentumSetup and momentumAllowed
bool bearishMomentum = bearishMomentumSetup and momentumAllowed
bool bullishDoNotChase = bullishDoNotChaseSetup and chaseAllowed
bool bearishDoNotChase = bearishDoNotChaseSetup and chaseAllowed
bool bullishExhaustion = bullishExhaustionSetup and exhaustionAllowed
bool bearishExhaustion = bearishExhaustionSetup and exhaustionAllowed
bool bullishRelationshipBroken = bullishRelationshipBrokenSetup and breakAllowed
bool bearishRelationshipBroken = bearishRelationshipBrokenSetup and breakAllowed
bool bullishDivergence = bullishDivergenceSetup and divergenceAllowed
bool bearishDivergence = bearishDivergenceSetup and divergenceAllowed
bool bullishTurn = bullishTurnSetup and turnAllowed
bool bearishTurn = bearishTurnSetup and turnAllowed

if bullishPullback or bearishPullback
    lastPullbackEventBar := bar_index
if bullishMomentum or bearishMomentum
    lastMomentumEventBar := bar_index
if bullishDoNotChase or bearishDoNotChase
    lastChaseEventBar := bar_index
if bullishExhaustion or bearishExhaustion
    lastExhaustionEventBar := bar_index
if bullishRelationshipBroken or bearishRelationshipBroken
    lastBreakEventBar := bar_index
if bullishDivergence or bearishDivergence
    lastDivergenceEventBar := bar_index
if bullishTurn or bearishTurn
    lastTurnEventBar := bar_index

// ============================================================================
// 08. VISUALS — OSCILLATOR
// ============================================================================

color bullishColor = color.rgb(46, 160, 100)
color bearishColor = color.rgb(214, 78, 78)
color neutralColor = color.rgb(135, 140, 150)
color warningColor = color.rgb(229, 168, 55)
color compressionColor = color.rgb(110, 105, 180)

color relationshipColor = if relationship > 0.0
    color.from_gradient(relationship, 0.0, 100.0, neutralColor, bullishColor)
else if relationship < 0.0
    color.from_gradient(relationship, -100.0, 0.0, bearishColor, neutralColor)
else
    neutralColor

color stateBackground = if not showStateBackgroundInput
    na
else if compressionState
    color.new(compressionColor, visualTransparencyInput)
else if exhaustionState
    color.new(bearishColor, visualTransparencyInput)
else if overextendedState
    color.new(warningColor, visualTransparencyInput)
else if bullishAccelerationState or bearishAccelerationState
    color.new(warningColor, math.min(95, visualTransparencyInput + 8))
else if healthyTrendState and trendDirection > 0.0
    color.new(bullishColor, math.min(95, visualTransparencyInput + 8))
else if healthyTrendState and trendDirection < 0.0
    color.new(bearishColor, math.min(95, visualTransparencyInput + 8))
else
    na

bgcolor(stateBackground, title = "Relationship state")

hline(75.0, "Strong bullish", color = color.new(bullishColor, 65), linestyle = hline.style_dotted)
hline(20.0, "Bullish threshold", color = color.new(bullishColor, 82), linestyle = hline.style_dashed)
hline(0.0, "Equilibrium", color = color.new(neutralColor, 45))
hline(-20.0, "Bearish threshold", color = color.new(bearishColor, 82), linestyle = hline.style_dashed)
hline(-75.0, "Strong bearish", color = color.new(bearishColor, 65), linestyle = hline.style_dotted)

relationshipPlot = plot(
     relationship,
     "MA Relationship",
     color = relationshipColor,
     linewidth = 3)

signalPlot = plot(
     relationshipSignal,
     "Relationship signal",
     color = color.new(neutralColor, 35),
     linewidth = 1)

zeroPlot = plot(
     0.0,
     "Zero",
     color = color.new(neutralColor, 100),
     editable = false)

fill(
     relationshipPlot,
     zeroPlot,
     color = showRelationshipFillInput ? color.new(relationshipColor, 84) : na,
     title = "Relationship fill")

// Event markers in pane.
plotshape(
     showPaneMarkersInput and bullishMomentum,
     title = "Bullish compression release",
     style = shape.diamond,
     location = location.bottom,
     color = bullishColor,
     size = size.tiny)

plotshape(
     showPaneMarkersInput and bearishMomentum,
     title = "Bearish compression release",
     style = shape.diamond,
     location = location.top,
     color = bearishColor,
     size = size.tiny)

plotshape(
     showPaneMarkersInput and (bullishDoNotChase or bearishDoNotChase) ? relationship : na,
     title = "Do not chase",
     style = shape.circle,
     location = location.absolute,
     color = warningColor,
     size = size.tiny)

plotshape(
     showPaneMarkersInput and (bullishExhaustion or bearishExhaustion) ? relationship : na,
     title = "Exhaustion warning",
     style = shape.xcross,
     location = location.absolute,
     color = bearishColor,
     size = size.tiny)

plotshape(
     showPaneMarkersInput and (bullishRelationshipBroken or bearishRelationshipBroken) ? relationship : na,
     title = "Relationship broken",
     style = shape.square,
     location = location.absolute,
     color = neutralColor,
     size = size.tiny)

plotshape(
     showPaneMarkersInput and bullishDivergence ? relationship : na,
     title = "Bullish divergence",
     style = shape.circle,
     location = location.absolute,
     color = bullishColor,
     size = size.tiny)

plotshape(
     showPaneMarkersInput and bearishDivergence ? relationship : na,
     title = "Bearish divergence",
     style = shape.circle,
     location = location.absolute,
     color = bearishColor,
     size = size.tiny)

plotshape(
     showPaneMarkersInput and bullishTurn ? relationship : na,
     title = "Turning bullish from extreme",
     style = shape.arrowup,
     location = location.absolute,
     color = bullishColor,
     size = size.tiny)

plotshape(
     showPaneMarkersInput and bearishTurn ? relationship : na,
     title = "Turning bearish from extreme",
     style = shape.arrowdown,
     location = location.absolute,
     color = bearishColor,
     size = size.tiny)

// ============================================================================
// 09. VISUALS — PRICE CHART
// ============================================================================

plot(
     showMaInput ? ma : na,
     "Selected MA",
     color = relationshipColor,
     linewidth = 2,
     force_overlay = true)

plotshape(
     showPriceMarkersInput and bullishPullback,
     title = "Chart: Bullish pullback",
     style = shape.triangleup,
     location = location.belowbar,
     color = bullishColor,
     size = size.tiny,
     force_overlay = true)

plotshape(
     showPriceMarkersInput and bearishPullback,
     title = "Chart: Bearish pullback",
     style = shape.triangledown,
     location = location.abovebar,
     color = bearishColor,
     size = size.tiny,
     force_overlay = true)

plotshape(
     showPriceMarkersInput and bullishMomentum,
     title = "Chart: Bullish momentum release",
     style = shape.diamond,
     location = location.belowbar,
     color = bullishColor,
     size = size.tiny,
     force_overlay = true)

plotshape(
     showPriceMarkersInput and bearishMomentum,
     title = "Chart: Bearish momentum release",
     style = shape.diamond,
     location = location.abovebar,
     color = bearishColor,
     size = size.tiny,
     force_overlay = true)

plotshape(
     showPriceMarkersInput and bullishDoNotChase,
     title = "Chart: Bullish do not chase",
     style = shape.circle,
     location = location.abovebar,
     color = warningColor,
     size = size.tiny,
     force_overlay = true)

plotshape(
     showPriceMarkersInput and bearishDoNotChase,
     title = "Chart: Bearish do not chase",
     style = shape.circle,
     location = location.belowbar,
     color = warningColor,
     size = size.tiny,
     force_overlay = true)

plotshape(
     showPriceMarkersInput and bullishRelationshipBroken,
     title = "Chart: Bullish relationship broken",
     style = shape.xcross,
     location = location.abovebar,
     color = bearishColor,
     size = size.tiny,
     force_overlay = true)

plotshape(
     showPriceMarkersInput and bearishRelationshipBroken,
     title = "Chart: Bearish relationship broken",
     style = shape.xcross,
     location = location.belowbar,
     color = bullishColor,
     size = size.tiny,
     force_overlay = true)

// Drawn at the real pivot bar (offset back by the confirmation delay), not the
// later bar where the pivot itself confirms.
plotshape(
     showPriceMarkersInput and bullishDivergence,
     title = "Chart: Bullish divergence",
     style = shape.circle,
     location = location.belowbar,
     color = bullishColor,
     size = size.tiny,
     offset = -divergencePivotRightInput,
     force_overlay = true)

plotshape(
     showPriceMarkersInput and bearishDivergence,
     title = "Chart: Bearish divergence",
     style = shape.circle,
     location = location.abovebar,
     color = bearishColor,
     size = size.tiny,
     offset = -divergencePivotRightInput,
     force_overlay = true)

plotshape(
     showPriceMarkersInput and bullishTurn,
     title = "Chart: Turning bullish from extreme",
     style = shape.arrowup,
     location = location.belowbar,
     color = bullishColor,
     size = size.tiny,
     force_overlay = true)

plotshape(
     showPriceMarkersInput and bearishTurn,
     title = "Chart: Turning bearish from extreme",
     style = shape.arrowdown,
     location = location.abovebar,
     color = bearishColor,
     size = size.tiny,
     force_overlay = true)

color barRelationshipColor = if not colorBarsInput
    na
else if relationship >= 20.0
    color.new(bullishColor, 35)
else if relationship <= -20.0
    color.new(bearishColor, 35)
else
    color.new(neutralColor, 55)

barcolor(barRelationshipColor)

// ============================================================================
// 10. EVENT TOOLTIPS
// ============================================================================

string diagnosticsText =
     "State: " + stateName +
     "\nReference: " + maTypeInput + " " + str.tostring(maLengthInput) +
     "\nRelationship: " + formatSigned(relationship) +
     "\nTrend: " + formatScore(trendQuality) +
     "\nRespect: " + formatScore(respectScore) +
     "\nExtension: " + formatScore(extensionScore) + "  (" + formatScore(absoluteDistanceAtr) + " ATR)" +
     "\nCompression: " + formatScore(compressionScore) +
     "\nAcceleration: " + formatScore(accelerationScore) +
     "\nExhaustion: " + formatScore(exhaustionScore) +
     "\nMA slope: " + formatSigned(maSlopeAtr) + " ATR / " + str.tostring(slopeLengthInput) + " bars" +
     "\nRespect samples: " + str.tostring(respectSamples, "#") +
     "\nRespect hit rate: " + formatScore(respectHitRate) + "%"

bool anyBullishPriceEvent = bullishPullback or bullishMomentum or bearishRelationshipBroken or bullishTurn
bool anyBearishPriceEvent = bearishPullback or bearishMomentum or bullishRelationshipBroken or bearishTurn
bool anyWarningPriceEvent = bullishDoNotChase or bearishDoNotChase or bullishExhaustion or bearishExhaustion

// Invisible hit-areas: the visible marker is the plotshape above; these labels
// exist only to carry the hover tooltip at the same spot, so they stay fully
// transparent instead of drawing a second, larger shape on top.
if showEventTooltipsInput and anyBullishPriceEvent
    label.new(
         bar_index,
         low,
         "",
         yloc = yloc.belowbar,
         style = label.style_circle,
         color = color.new(bullishColor, 100),
         textcolor = color.new(color.white, 100),
         size = size.tiny,
         tooltip = diagnosticsText,
         force_overlay = true)

if showEventTooltipsInput and anyBearishPriceEvent
    label.new(
         bar_index,
         high,
         "",
         yloc = yloc.abovebar,
         style = label.style_circle,
         color = color.new(bearishColor, 100),
         textcolor = color.new(color.white, 100),
         size = size.tiny,
         tooltip = diagnosticsText,
         force_overlay = true)

if showEventTooltipsInput and anyWarningPriceEvent
    label.new(
         bar_index,
         trendDirection >= 0.0 ? high : low,
         "",
         yloc = trendDirection >= 0.0 ? yloc.abovebar : yloc.belowbar,
         style = label.style_circle,
         color = color.new(warningColor, 100),
         textcolor = color.new(color.black, 100),
         size = size.tiny,
         tooltip = diagnosticsText,
         force_overlay = true)

// Divergence markers are drawn back at the historical pivot bar (see the
// offset plotshape above), so their hit-area needs the same offset — a hit-area
// on the current bar would sit next to an empty spot, not on the visible flag.
if showEventTooltipsInput and bullishDivergence
    label.new(
         bar_index - divergencePivotRightInput,
         low[divergencePivotRightInput],
         "",
         yloc = yloc.belowbar,
         style = label.style_circle,
         color = color.new(bullishColor, 100),
         textcolor = color.new(color.white, 100),
         size = size.tiny,
         tooltip = diagnosticsText,
         force_overlay = true)

if showEventTooltipsInput and bearishDivergence
    label.new(
         bar_index - divergencePivotRightInput,
         high[divergencePivotRightInput],
         "",
         yloc = yloc.abovebar,
         style = label.style_circle,
         color = color.new(bearishColor, 100),
         textcolor = color.new(color.white, 100),
         size = size.tiny,
         tooltip = diagnosticsText,
         force_overlay = true)

// ============================================================================
// 11. DATA WINDOW
// ============================================================================

plot(trendQuality, "Diagnostic · Trend", display = display.data_window)
plot(respectScore, "Diagnostic · Respect", display = display.data_window)
plot(extensionScore, "Diagnostic · Extension", display = display.data_window)
plot(compressionScore, "Diagnostic · Compression", display = display.data_window)
plot(accelerationScore, "Diagnostic · Acceleration", display = display.data_window)
plot(exhaustionScore, "Diagnostic · Exhaustion", display = display.data_window)
plot(absoluteDistanceAtr, "Diagnostic · Distance from MA (ATR)", display = display.data_window)
plot(maSlopeAtr, "Diagnostic · MA slope (ATR)", display = display.data_window)
plot(respectSamples, "Diagnostic · Respect samples", display = display.data_window)
plot(respectHitRate, "Diagnostic · Respect hit rate (%)", display = display.data_window)
plot(relationshipRecentLow, "Diagnostic · Relationship recent low", display = display.data_window)
plot(relationshipRecentHigh, "Diagnostic · Relationship recent high", display = display.data_window)

// ============================================================================
// 12. ALERTS
// ============================================================================

// ============================================================================
// 13. STRATEGY HOOKS
// ============================================================================
// These variables exist solely as the entry/stop declaration for the standalone
// strategy derived from this file (see @strategy-config below). They play no
// role in the indicator's own scoring, state, or display logic.
//
// Entries are limited to Pullback and Momentum Release — both already gated on
// an established, quality-checked trend (see the Event Engine). Divergence and
// Turn From Extreme are reversal-style, early signals and are deliberately kept
// out of this trend-following entry model rather than OR'd in alongside it
// (mixing trend-following and reversal logic into one signal is the anti-pattern
// this repo's indicator-design rules call out).

int atrLen = atrLengthInput

bool strategyLongSignal = bullishPullback or bullishMomentum
bool strategyShortSignal = bearishPullback or bearishMomentum

float longStop = ma - safeAtr * stopAtrMultInput
float shortStop = ma + safeAtr * stopAtrMultInput
// ─── Strategy inputs ─────────────────────────────────────────────
g_strat       = "Strategy"
tradeDirInput = input.string("Both", "Trade Direction",
     options=["Both", "Long Only", "Short Only"], group=g_strat,
     tooltip="Filter entries by direction. Useful for asymmetric instruments.")
confirmClose  = input.bool(true,  "Entries on Confirmed Bar Only", group=g_strat,
     tooltip="Only trigger entries on closed bars. Prevents repainting in live trading.")
cooldownBars  = input.int(0,     "Cooldown Bars After Exit", minval=0, group=g_strat,
     tooltip="Block new entries for N bars after a position closes. Set 0 to disable.")
enableBE      = input.bool(false, "Break-Even Stop", group=g_strat,
     tooltip="Move SL to entry price once the trade is N×ATR in profit.")
beATRInput    = input.float(1.0,  "Break-Even Trigger (ATR×)", minval=0.1, step=0.1, group=g_strat,
     tooltip="ATR distance from entry that triggers the break-even move.")

g_filters     = "Filters"
useSession    = input.bool(false, "Session Filter", group=g_filters)
sessionInput  = input.session("0000-2345", "Session", group=g_filters)
useDateRange  = input.bool(false, "Date Range", group=g_filters)
dateFrom      = input.time(timestamp("01 Jan 2020 00:00 +0000"), "From", group=g_filters)
dateTo        = input.time(timestamp("31 Dec 2030 23:59 +0000"), "To",   group=g_filters)
useMaxDD      = input.bool(false, "Max Drawdown", group=g_filters)
maxDD         = input.float(20.0, "Max Drawdown (%)", minval=1, step=1, group=g_filters)
useMaxIntraDD = input.bool(false, "Max Intraday Loss", group=g_filters)
maxIntraDD    = input.float(5.0,  "Max Intraday Loss (%)", minval=1, step=1, group=g_filters)
useLossStreak = input.bool(false, "Losing Streak Limit", group=g_filters)
maxLossStreak = input.int(5,      "Max Consecutive Losses", minval=1, group=g_filters)

strategy.risk.max_drawdown(useMaxDD ? maxDD : 100, strategy.percent_of_equity)
strategy.risk.max_intraday_loss(useMaxIntraDD ? maxIntraDD : 100, strategy.percent_of_equity)

var int _genLossStreak = 0
if strategy.losstrades > strategy.losstrades[1]
    _genLossStreak := _genLossStreak + 1
if strategy.wintrades > strategy.wintrades[1]
    _genLossStreak := 0
_genFilterOk = (not useSession or not na(time(timeframe.period, sessionInput))) and
     (not useDateRange or (time >= dateFrom and time <= dateTo)) and
     (not useLossStreak or _genLossStreak < maxLossStreak)

var int _lastExitBar = na
_justClosed = strategy.closedtrades > 0 and strategy.closedtrades.exit_bar_index(strategy.closedtrades - 1) == bar_index
if _justClosed
    _lastExitBar := bar_index
_inCooldown = cooldownBars > 0 and not na(_lastExitBar) and (bar_index - _lastExitBar) < cooldownBars
_canEntry   = not _inCooldown and (not confirmClose or barstate.isconfirmed) and _genFilterOk
g_exits       = "Strategy — Advanced Exits"
enableVolExit = input.bool(true,  "Volatility Exit (ATR spike)", group=g_exits,
     tooltip="Close the position when current ATR jumps above its baseline by the configured ratio — captures climax / exhaustion bars where the rest of the move is unlikely to continue.")
volExitRatio  = input.float(1.6, "  Volatility Spike Ratio",   minval=1.2, maxval=5.0, step=0.1, group=g_exits,
     tooltip="Exit when ta.atr(atrLen) >= ratio × ema(ta.atr(atrLen), 50). Default 1.6 = current ATR is 60% above its 50-bar average.")

// ─── Strategy execution ──────────────────────────────────────────
_canLong  = tradeDirInput != "Short Only"
_canShort = tradeDirInput != "Long Only"

if (strategyLongSignal) and _canLong and _canEntry
    strategy.entry("Long", strategy.long)
if (strategyShortSignal) and _canShort and _canEntry
    strategy.entry("Short", strategy.short)

// Volatility exit — close on ATR spike (climax / exhaustion bars)
// Baseline computed every bar — inside the position block it would only average in-position bars
_atrCur  = ta.atr(atrLen)
_atrBase = ta.ema(_atrCur, 50)
if enableVolExit and strategy.position_size != 0
    _spike   = not na(_atrBase) and _atrCur >= _atrBase * volExitRatio
    if _spike and strategy.position_size > 0
        strategy.close("Long",  comment="Vol Exit (ATR×" + str.tostring(volExitRatio, '#.#') + ")")
    if _spike and strategy.position_size < 0
        strategy.close("Short", comment="Vol Exit (ATR×" + str.tostring(volExitRatio, '#.#') + ")")

// Break-even integrated into the same exit (single stop level — no exit races)
_beAtr   = ta.atr(14) * beATRInput
_beLong  = enableBE and strategy.position_size > 0 and close >= strategy.position_avg_price + _beAtr
_beShort = enableBE and strategy.position_size < 0 and close <= strategy.position_avg_price - _beAtr

_finalLongStop  = _beLong  ? math.max(longStop,  strategy.position_avg_price) : longStop
_finalShortStop = _beShort ? math.min(shortStop, strategy.position_avg_price) : shortStop

if strategy.position_size > 0
    strategy.exit("Long Exit", "Long", stop=_finalLongStop)
if strategy.position_size < 0
    strategy.exit("Short Exit", "Short", stop=_finalShortStop)

Overview

Market–Average Relationship Engine [WavesUnchained] plots a normalized relationship oscillator bounded between -100 and +100 in a separate indicator pane. Trading signals trigger on specific relationship events like pullbacks and momentum releases, regulated by signal cooldowns and trend quality thresholds. Key settings adjust moving average types, lookback periods, ATR multipliers, and diagnostic scoring sensitivities. By default, the strategy operates without an automatic hard stop loss.

Details

Published by

casoon on GitHub

Type

Strategy, Pine Script v6

Draws

Separate pane below the chart

Category

Trend, Moving averages, Oscillators & momentum

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

  • The strategy enters trades without a stop loss

Topics

pine-scriptpinescripttechnical-analysistrading-indicatorstrading-strategiestradingview

Settings

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

SettingTypeDefaultVariable
MA familystringEMAmaTypeInput
MA lengthint50maLengthInput
SourcesourceclosesourceInput
ALMA offsetfloat0.85almaOffsetInput
Sigmafloat6.0almaSigmaInput
ATR lengthint14atrLengthInput
MA slope lookbackint10slopeLengthInput
Full slope strength (ATR)float0.50fullSlopeAtrInput
Direction persistenceint20persistenceLengthInput
Price efficiency lookbackint14efficiencyLengthInput
Statistical referenceint100statisticsLengthInput
Normal extension (ATR)float0.75extensionNormalAtrInput
Show all 40 settings
SettingTypeDefaultVariable
Extremefloat2.50extensionExtremeAtrInput
MA interaction zone (ATR)float0.35proximityAtrInput
Respect sample lookbackint80respectLookbackInput
Outcome barsint4respectResponseBarsInput
Compression lookbackint20compressionLengthInput
Band (ATR)float0.45compressionBandAtrInput
Acceleration lookbackint5accelerationLengthInput
Exhaustion lookbackint8exhaustionLengthInput
Only confirm events on closed barsbooltrueconfirmedOnlyInput
Signal cooldownint8signalCooldownInput
Minimum trend qualityfloat55.0minimumTrendInput
Minimum respectfloat55.0minimumRespectInput
Pullback prior extension (ATR)float0.70pullbackPriorExtensionInput
Maximum pullback exhaustionfloat65.0maximumPullbackExhaustionInput
Momentum acceleration thresholdfloat60.0accelerationThresholdInput
Compression thresholdfloat65.0compressionThresholdInput
Do-not-chase extensionfloat75.0chaseExtensionThresholdInput
Exhaustion warningfloat68.0exhaustionThresholdInput
Relationship-break confirmationint3breakConfirmationInput
Divergence pivot bars (left)int5divergencePivotLeftInput
Rightint5divergencePivotRightInput
Extreme lookbackint30turnLookbackInput
Extreme levelfloat55.0turnExtremeLevelInput
Turn confirmation barsint3turnConfirmBarsInput
Show selected MA in price chartbooltrueshowMaInput
Show event markers in price chartbooltrueshowPriceMarkersInput
Show event markers in oscillatorbooltrueshowPaneMarkersInput
Create event tooltip labelsbooltrueshowEventTooltipsInput
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

330 stars

MACD + Divergences [Alorse] is a momentum strategy that calculates the MACD oscillator and identifies regular and hidden divergences using pivot high and low points. It filters trade signals with dual Exponential Moving Averages (EMA 150 and 600) to ensure tra

Strategy v4 Oscillators & momentumDivergence Repaints
Updated 2 months ago · None stated GitHub repository of MACD + Divergences [Alorse]

MACD Crossover (tutorial)

by pineforge-4pass
193 stars

This strategy calculates the Moving Average Convergence Divergence indicator and plots the MACD line, signal line, and histogram in a separate pane. It enters long positions when the MACD line crosses above the signal line and enters short positions when it cr

Strategy v6 Oscillators & momentumMoving averages
Updated today · Apache-2.0 GitHub repository of MACD Crossover (tutorial)

ADX Strategy

by hasnocool
106 stars

An ADX-based trend-following strategy that enters long positions when the positive directional indicator (DI+) crosses above the Average Directional Index (ADX) while ADX remains below a preset threshold and a fast EMA is above a slow EMA. It allows adding to

Strategy v4 TrendOscillators & momentum
Updated 2 years ago · GPL-3.0 GitHub repository of ADX Strategy

EMA Ribbon MACD

by iamrichardD
57 stars

This strategy combines an EMA ribbon with MACD confirmation and plots in a separate pane. It enters when ribbon alignment, optional MACD zero or signal crosses, volume, smart-money, and multi-timeframe filters agree, then exits with percentage stop loss and ta

Strategy v6 TrendMoving averages
Updated 1 year ago · Apache-2.0 GitHub repository of EMA Ribbon MACD
7 stars

This strategy combines the Moving Average Convergence Divergence oscillator with an Exponential Moving Average trend filter. It enters long positions when the MACD histogram crosses above zero while price stays above the long-term EMA filter. Long trades close

Strategy v5 TrendOscillators & momentum
Updated 1 year ago · MPL-2.0 GitHub repository of MACD EMA Filter Strategy

Cerebr

by jerryinyang
5 stars

This strategy combines optional Hull moving average, SuperTrend, and MACD conditions in a separate pane. It marks HMA rising or falling over a chosen period and SuperTrend long or short direction, and it treats MACD above or below its signal line as buy or sel

Strategy v4 TrendMoving averages
Updated 2 years ago · None stated GitHub repository of Cerebr

More strategys

Trade Your Charts With a Regulated Broker

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

Compare brokers