This strategy evaluates the dynamic relationship between price and a chosen moving average across six diagnostic dimensions: trend, respect, extension, compression, acceleration, and exhaustion.
//@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.
Multi-dimensional relationship oscillator from -100 to +100
Supports nine moving average calculation families
Evaluates trend persistence, compression, and exhaustion scores
Signal cooldown and confirmed closed bar filtering
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
MA family
string
EMA
maTypeInput
MA length
int
50
maLengthInput
Source
source
close
sourceInput
ALMA offset
float
0.85
almaOffsetInput
Sigma
float
6.0
almaSigmaInput
ATR length
int
14
atrLengthInput
MA slope lookback
int
10
slopeLengthInput
Full slope strength (ATR)
float
0.50
fullSlopeAtrInput
Direction persistence
int
20
persistenceLengthInput
Price efficiency lookback
int
14
efficiencyLengthInput
Statistical reference
int
100
statisticsLengthInput
Normal extension (ATR)
float
0.75
extensionNormalAtrInput
Show all 40 settings
Setting
Type
Default
Variable
Extreme
float
2.50
extensionExtremeAtrInput
MA interaction zone (ATR)
float
0.35
proximityAtrInput
Respect sample lookback
int
80
respectLookbackInput
Outcome bars
int
4
respectResponseBarsInput
Compression lookback
int
20
compressionLengthInput
Band (ATR)
float
0.45
compressionBandAtrInput
Acceleration lookback
int
5
accelerationLengthInput
Exhaustion lookback
int
8
exhaustionLengthInput
Only confirm events on closed bars
bool
true
confirmedOnlyInput
Signal cooldown
int
8
signalCooldownInput
Minimum trend quality
float
55.0
minimumTrendInput
Minimum respect
float
55.0
minimumRespectInput
Pullback prior extension (ATR)
float
0.70
pullbackPriorExtensionInput
Maximum pullback exhaustion
float
65.0
maximumPullbackExhaustionInput
Momentum acceleration threshold
float
60.0
accelerationThresholdInput
Compression threshold
float
65.0
compressionThresholdInput
Do-not-chase extension
float
75.0
chaseExtensionThresholdInput
Exhaustion warning
float
68.0
exhaustionThresholdInput
Relationship-break confirmation
int
3
breakConfirmationInput
Divergence pivot bars (left)
int
5
divergencePivotLeftInput
Right
int
5
divergencePivotRightInput
Extreme lookback
int
30
turnLookbackInput
Extreme level
float
55.0
turnExtremeLevelInput
Turn confirmation bars
int
3
turnConfirmBarsInput
Show selected MA in price chart
bool
true
showMaInput
Show event markers in price chart
bool
true
showPriceMarkersInput
Show event markers in oscillator
bool
true
showPaneMarkersInput
Create event tooltip labels
bool
true
showEventTooltipsInput
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.
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
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
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
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
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
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