//@version=6
// ============================================================================
// MTF StochRSI Pair Score [WavesUnchained]
// Version: 2.1.1
// Author: WavesUnchained
// Build: 2026-10-02 00:00:00
// ============================================================================
// Multi-timeframe StochRSI confluence scorer — pair-weighted with sync bonus
// and conflict penalty.
//
// Features:
// ✓ Per-TF StochRSI score with exhaustion dampening
// ✓ Pair confluence with sync bonus and conflict penalty
// ✓ Weighted total across up to 5 configurable TF pairs
// ✓ Smoothed total score (EMA / RMA / SMA / WMA / HMA / JMA)
// ✓ TF Agreement Index — directional consensus across all 6 TFs
// ✓ Signal markers — edge-triggered, optional confirmed-bar gate
// ✓ Bias / Quality / Timing state engine — single-glance decision readout
// ✓ Dashboard table — compact state view by default, per-TF/per-pair debug view optional
//
// ============================================================================
indicator("MTF StochRSI Pair Score [WavesUnchained]", format=format.price, precision=2, shorttitle="MTF StochRSI Score", overlay=false, max_labels_count=500)
// ─────────────────────────────────────
// Inputs
// ─────────────────────────────────────
groupStoch = "StochRSI Settings"
rsiLen = input.int(14, "RSI Length", minval=1, group=groupStoch)
stochLen = input.int(14, "Stoch Length", minval=1, group=groupStoch)
kSmooth = input.int(3, "K Smoothing", minval=1, group=groupStoch)
dSmooth = input.int(3, "D Smoothing", minval=1, group=groupStoch)
upper = input.float(80, "Upper Zone", minval=50, maxval=100, group=groupStoch)
lower = input.float(20, "Lower Zone", minval=0, maxval=50, group=groupStoch)
groupTF = "Timeframes"
tf1 = input.timeframe("15", "TF 1", group=groupTF)
tf2 = input.timeframe("30", "TF 2", group=groupTF)
tf3 = input.timeframe("60", "TF 3", group=groupTF)
tf4 = input.timeframe("120", "TF 4", group=groupTF)
tf5 = input.timeframe("240", "TF 5", group=groupTF)
tf6 = input.timeframe("D", "TF 6", group=groupTF)
groupPair = "Pair Settings"
minorWeight = input.float(0.4, "Lower TF Weight in Pair", minval=0, maxval=1, step=0.05, group=groupPair)
syncBonus = input.float(1.15, "Sync Bonus", minval=1.0, maxval=2.0, step=0.05, group=groupPair,
tooltip="Multiplier when both TFs of a pair point the same way. Pairs are adjacent TFs (1+2, 2+3, 3+4 …), so each middle TF sits in two pairs — this overlap is intentional: the score measures sync between neighbouring timeframes.")
conflictPenalty = input.float(0.75, "Conflict Penalty", minval=0.1, maxval=1.0, step=0.05, group=groupPair)
useP12 = input.bool(true, "Use Pair 1: TF1 + TF2", group=groupPair)
useP23 = input.bool(true, "Use Pair 2: TF2 + TF3", group=groupPair)
useP34 = input.bool(true, "Use Pair 3: TF3 + TF4", group=groupPair)
useP45 = input.bool(true, "Use Pair 4: TF4 + TF5", group=groupPair)
useP56 = input.bool(false, "Use Pair 5: TF5 + TF6", group=groupPair)
wP12 = input.float(1.0, "Weight Pair 1", minval=0, step=0.1, group=groupPair)
wP23 = input.float(1.5, "Weight Pair 2", minval=0, step=0.1, group=groupPair)
wP34 = input.float(1.3, "Weight Pair 3", minval=0, step=0.1, group=groupPair)
wP45 = input.float(1.2, "Weight Pair 4", minval=0, step=0.1, group=groupPair)
wP56 = input.float(1.0, "Weight Pair 5", minval=0, step=0.1, group=groupPair)
groupSmooth = "Smoothing"
smoothMode = input.string("JMA", "Mode", options=["EMA", "RMA", "SMA", "WMA", "HMA", "JMA"], group=groupSmooth)
smoothLen = input.int(5, "Length", minval=1, maxval=50, group=groupSmooth)
jmaPhase = input.int(50, " JMA Phase", minval=-100, maxval=100, group=groupSmooth)
jmaPower = input.float(2.0, " JMA Power", minval=1.0, maxval=4.0, step=0.25, group=groupSmooth)
groupDisplay = "Display"
showTable = input.bool(true, "Show Table", group=groupDisplay)
showDebugTable = input.bool(false, "Show Debug Table (per-TF/per-pair)", group=groupDisplay,
tooltip="Adds the per-TF and per-pair breakdown below the Bias/Quality/Timing state block")
showSignals = input.bool(true, "Show Signal Markers", group=groupDisplay)
confirmedOnly = input.bool(true, "Only Confirmed Bar Signals", group=groupDisplay,
tooltip="Prevents signals from appearing and disappearing on open HTF bars — strongly recommended")
longLevel = input.float(60, "Strong Long Level", minval=0, maxval=100, group=groupDisplay)
shortLevel = input.float(-60, "Strong Short Level", minval=-100, maxval=0, group=groupDisplay)
buildingLevel = input.float(30, "Building Level", minval=0, maxval=100, group=groupDisplay,
tooltip="Score threshold (± ) for the BUILDING state, below the Strong Long/Short Level")
minAgreement = input.float(33, "Min Agreement for Signals", minval=0, maxval=100, step=33, group=groupDisplay,
tooltip="33 = at least 4:2 majority across 6 TFs. 0 = any majority (3:2 with one neutral).")
freshWindow = input.int(3, "Fresh Window (bars)", minval=1, group=groupDisplay,
tooltip="Bars after a Confirmed signal during which Timing shows FRESH")
// ─────────────────────────────────────
// Helpers
// ─────────────────────────────────────
f_clamp(x, minVal, maxVal) =>
math.max(minVal, math.min(maxVal, x))
f_bg(score) =>
color c = color.new(color.gray, 80)
if score >= 70
c := color.new(#00c853, 65)
else if score >= 30
c := color.new(#00c853, 82)
else if score <= -70
c := color.new(#d50000, 65)
else if score <= -30
c := color.new(#d50000, 82)
c
f_tc_cell(score) =>
math.abs(score) >= 30 ? color.white : color.new(color.gray, 20)
f_jma(src, length, phase, power) =>
var float jma = na
var float e0 = na
var float e1 = na
var float e2 = na
_pr = phase < -100 ? 0.5 : phase > 100 ? 2.5 : phase / 100.0 + 1.5
_beta = 0.45 * (length - 1) / (0.45 * (length - 1) + 2)
_alpha = math.pow(_beta, power)
e0 := (1 - _alpha) * src + _alpha * nz(e0[1], src)
e1 := (src - e0) * (1 - _beta) + _beta * nz(e1[1], 0)
e2 := (e0 + _pr * e1 - nz(jma[1], src)) * math.pow(1 - _alpha, 2) + math.pow(_alpha, 2) * nz(e2[1], 0)
jma := nz(jma[1], src) + e2
jma
f_smooth(src, length, mode) =>
switch mode
'SMA' => ta.sma(src, length)
'RMA' => ta.rma(src, length)
'HMA' => ta.hma(src, length)
'WMA' => ta.wma(src, length)
'JMA' => f_jma(src, length, jmaPhase, jmaPower)
=> ta.ema(src, length)
f_score() =>
r = ta.rsi(close, rsiLen)
raw = ta.stoch(r, r, r, stochLen)
k = ta.sma(raw, kSmooth)
d = ta.sma(k, dSmooth)
kAboveD = k > d
kRising = k > k[1]
crossUp = ta.crossover(k, d)
crossDn = ta.crossunder(k, d)
score = 0.0
score += kAboveD ? 20 : -20
score += kRising ? 20 : -20
score += (k - d) * 0.5
score += (k - k[1]) * 0.8
// Tiered, exclusive cross bonus: an in-zone cross earns 30 instead of the plain
// cross's 15 (not 15 + 30 on top of the K-above-D and K-rising flips it already causes).
score += crossUp ? (k < lower ? 30 : 15) : 0
score += crossDn ? (k > upper ? -30 : -15) : 0
score += k > upper and kRising ? 10 : 0
score += k < lower and not kRising ? -10 : 0
// Reduce score when both lines are already deep in extreme zone with no fresh counter-cross
exhaustedHigh = k > upper and d > upper and not crossDn
exhaustedLow = k < lower and d < lower and not crossUp
score := exhaustedHigh or exhaustedLow ? score * 0.65 : score
[f_clamp(score, -100, 100), k, d]
f_pair(sMinor, sMajor) =>
majorWeight = 1.0 - minorWeight
base = sMinor * minorWeight + sMajor * majorWeight
sameDirection = sMinor > 0 and sMajor > 0 or sMinor < 0 and sMajor < 0
conflict = sMinor > 0 and sMajor < 0 or sMinor < 0 and sMajor > 0
adjusted = sameDirection ? base * syncBonus : conflict ? base * conflictPenalty : base
f_clamp(adjusted, -100, 100)
// ─────────────────────────────────────
// MTF Values
// ─────────────────────────────────────
[s1, k1, d1] = request.security(syminfo.tickerid, tf1, f_score(), barmerge.gaps_off, barmerge.lookahead_off)
[s2, k2, d2] = request.security(syminfo.tickerid, tf2, f_score(), barmerge.gaps_off, barmerge.lookahead_off)
[s3, k3, d3] = request.security(syminfo.tickerid, tf3, f_score(), barmerge.gaps_off, barmerge.lookahead_off)
[s4, k4, d4] = request.security(syminfo.tickerid, tf4, f_score(), barmerge.gaps_off, barmerge.lookahead_off)
[s5, k5, d5] = request.security(syminfo.tickerid, tf5, f_score(), barmerge.gaps_off, barmerge.lookahead_off)
[s6, k6, d6] = request.security(syminfo.tickerid, tf6, f_score(), barmerge.gaps_off, barmerge.lookahead_off)
// ─────────────────────────────────────
// Pair Scores
// ─────────────────────────────────────
p12 = f_pair(s1, s2)
p23 = f_pair(s2, s3)
p34 = f_pair(s3, s4)
p45 = f_pair(s4, s5)
p56 = f_pair(s5, s6)
weightedSum = 0.0
weightSum = 0.0
if useP12
weightedSum += p12 * wP12
weightSum += wP12
if useP23
weightedSum += p23 * wP23
weightSum += wP23
if useP34
weightedSum += p34 * wP34
weightSum += wP34
if useP45
weightedSum += p45 * wP45
weightSum += wP45
if useP56
weightedSum += p56 * wP56
weightSum += wP56
totalScore = weightSum > 0 ? weightedSum / weightSum : na
// ─────────────────────────────────────
// Derived Series
// ─────────────────────────────────────
// Smoothed total — tames crossover spikes from f_score
smoothedTotal = f_smooth(totalScore, smoothLen, smoothMode)
// Agreement Index: how many of the 6 TFs point in the same direction
// Steps of ±33.3 per TF: +100 = all 6 bullish, -100 = all 6 bearish
bullCount = (s1 > 0 ? 1 : 0) + (s2 > 0 ? 1 : 0) + (s3 > 0 ? 1 : 0) + (s4 > 0 ? 1 : 0) + (s5 > 0 ? 1 : 0) + (s6 > 0 ? 1 : 0)
bearCount = (s1 < 0 ? 1 : 0) + (s2 < 0 ? 1 : 0) + (s3 < 0 ? 1 : 0) + (s4 < 0 ? 1 : 0) + (s5 < 0 ? 1 : 0) + (s6 < 0 ? 1 : 0)
agreementIdx = (bullCount - bearCount) / 6.0 * 100
// Confirmed-bar gate: block intrabar state changes on open HTF candles
sigOk = not confirmedOnly or barstate.isconfirmed
// Threshold crossing: fires once, exactly when the score enters the zone (+ min agreement)
longTrigger = ta.crossover(smoothedTotal, longLevel) and agreementIdx >= minAgreement and sigOk
shortTrigger = ta.crossunder(smoothedTotal, shortLevel) and agreementIdx <= -minAgreement and sigOk
// ─────────────────────────────────────
// State Engine — Bias / Quality / Timing
// (pure readout over the scores/triggers above — no new gating, alerts unchanged)
// ─────────────────────────────────────
biasDir = smoothedTotal > 0 ? 1 : smoothedTotal < 0 ? -1 : 0
agreeDir = agreementIdx > 0 ? 1 : agreementIdx < 0 ? -1 : 0
conflict = biasDir != 0 and agreeDir != 0 and biasDir != agreeDir
string biasState = "NO EDGE"
if smoothedTotal >= longLevel and agreementIdx >= minAgreement
biasState := "LONG CONFIRMED"
else if smoothedTotal <= shortLevel and agreementIdx <= -minAgreement
biasState := "SHORT CONFIRMED"
else if conflict
biasState := "CONFLICT"
else if smoothedTotal > buildingLevel and agreeDir >= 0
biasState := "LONG BUILDING"
else if smoothedTotal < -buildingLevel and agreeDir <= 0
biasState := "SHORT BUILDING"
string qualityState = "WEAK"
if math.abs(agreementIdx) >= 66
qualityState := "CLEAN"
else if math.abs(agreementIdx) >= 33
qualityState := "MIXED"
// Exhaustion majority: how many TFs already sit deep in the zone in the bias direction
lateLongCount = (k1 > upper ? 1 : 0) + (k2 > upper ? 1 : 0) + (k3 > upper ? 1 : 0) + (k4 > upper ? 1 : 0) + (k5 > upper ? 1 : 0) + (k6 > upper ? 1 : 0)
lateShortCount = (k1 < lower ? 1 : 0) + (k2 < lower ? 1 : 0) + (k3 < lower ? 1 : 0) + (k4 < lower ? 1 : 0) + (k5 < lower ? 1 : 0) + (k6 < lower ? 1 : 0)
isLateLong = lateLongCount >= 4
isLateShort = lateShortCount >= 4
barsSinceLong = ta.barssince(longTrigger)
barsSinceShort = ta.barssince(shortTrigger)
string timingState = "-"
if biasState == "LONG CONFIRMED" and isLateLong
timingState := "LATE"
else if biasState == "SHORT CONFIRMED" and isLateShort
timingState := "LATE"
else if biasState == "LONG CONFIRMED" and barsSinceLong <= freshWindow
timingState := "FRESH"
else if biasState == "SHORT CONFIRMED" and barsSinceShort <= freshWindow
timingState := "FRESH"
else if biasState == "LONG CONFIRMED" or biasState == "SHORT CONFIRMED"
timingState := "ACTIVE"
// ─────────────────────────────────────
// Plots
// ─────────────────────────────────────
hline(0, "Neutral", color=color.new(color.gray, 60))
hline(longLevel, "Strong Long", color=color.new(color.green, 70))
hline(shortLevel, "Strong Short", color=color.new(color.red, 70))
plot(smoothedTotal, "Total Score", linewidth=2, color=color.new(color.teal, 0))
plotshape(showSignals and longTrigger, title="Long Bias", style=shape.triangleup, location=location.bottom, size=size.tiny, color=color.green)
plotshape(showSignals and shortTrigger, title="Short Bias", style=shape.triangledown, location=location.top, size=size.tiny, color=color.red)
bool alertsOnBarClose = input.bool(true, "Alerts only on bar close (confirmed)", group="Alerts")
bool alertOk = not alertsOnBarClose or barstate.isconfirmed
alertcondition(alertOk and (longTrigger), "MTF StochRSI Long Bias", "MSRP · LONG BIAS · {{ticker}} {{interval}}")
alertcondition(alertOk and (shortTrigger), "MTF StochRSI Short Bias", "MSRP · SHORT BIAS · {{ticker}} {{interval}}")
// ─────────────────────────────────────
// Table
// ─────────────────────────────────────
color tc = color.new(color.gray, 20)
color hd = color.new(color.gray, 90)
var table t = table.new(position.top_right, 4, 13,
bgcolor=color.new(color.white, 5),
border_color=color.new(color.gray, 60),
border_width=1,
frame_color=color.new(color.gray, 40),
frame_width=1)
// Fills two adjacent columns: label | score-colored-cell
f_cell(col, row, name, score, enabled=true) =>
cellTc = enabled ? f_tc_cell(score) : color.new(color.gray, 60)
cellBg = enabled ? f_bg(score) : color.new(color.gray, 85)
table.cell(t, col, row, name,
text_color=tc, bgcolor=na, text_size=size.small)
table.cell(t, col + 1, row, enabled ? str.tostring(score, "#") : "off",
text_color=cellTc, bgcolor=cellBg, text_size=size.small)
f_sep(row) =>
table.cell(t, 0, row, "", bgcolor=color.new(color.gray, 80))
table.cell(t, 1, row, "", bgcolor=color.new(color.gray, 80))
table.cell(t, 2, row, "", bgcolor=color.new(color.gray, 80))
table.cell(t, 3, row, "", bgcolor=color.new(color.gray, 80))
// State row: label | value, value cell merged across the remaining 3 columns
f_stateRow(row, label, value, bg, txt) =>
table.cell(t, 0, row, label, text_color=tc, bgcolor=na, text_size=size.small)
table.cell(t, 1, row, value, text_color=txt, bgcolor=bg, text_size=size.small)
table.cell(t, 2, row, "", bgcolor=bg)
table.cell(t, 3, row, "", bgcolor=bg)
table.merge_cells(t, 1, row, 3, row)
f_biasBg(state) =>
switch state
"LONG CONFIRMED" => color.new(#00c853, 15)
"LONG BUILDING" => color.new(#00c853, 55)
"SHORT CONFIRMED" => color.new(#d50000, 15)
"SHORT BUILDING" => color.new(#d50000, 55)
"CONFLICT" => color.new(#ff6f00, 35)
=> color.new(color.gray, 80)
f_biasTxt(state) =>
state == "NO EDGE" ? tc : color.white
f_qualityBg(state) =>
switch state
"CLEAN" => color.new(#00c853, 55)
"MIXED" => color.new(#ff6f00, 55)
=> color.new(color.gray, 75)
f_qualityTxt(state) =>
state == "WEAK" ? tc : color.white
f_timingBg(state) =>
switch state
"FRESH" => color.new(#ffd600, 45)
"LATE" => color.new(#ff6f00, 45)
"ACTIVE" => color.new(color.gray, 70)
=> color.new(color.gray, 85)
f_timingTxt(state) =>
state == "ACTIVE" or state == "-" ? tc : color.white
if showTable and barstate.islast
// Bias / Quality / Timing — single-glance decision readout
f_stateRow(0, "BIAS", biasState, f_biasBg(biasState), f_biasTxt(biasState))
f_stateRow(1, "QUALITY", qualityState, f_qualityBg(qualityState), f_qualityTxt(qualityState))
f_stateRow(2, "TIMING", timingState, f_timingBg(timingState), f_timingTxt(timingState))
f_sep(3)
if showDebugTable
// Header
table.cell(t, 0, 4, "TF", text_color=tc, bgcolor=hd, text_size=size.small)
table.cell(t, 1, 4, "Scr", text_color=tc, bgcolor=hd, text_size=size.small)
table.cell(t, 2, 4, "TF", text_color=tc, bgcolor=hd, text_size=size.small)
table.cell(t, 3, 4, "Scr", text_color=tc, bgcolor=hd, text_size=size.small)
// TFs — 2 per row
f_cell(0, 5, tf1, s1)
f_cell(2, 5, tf2, s2)
f_cell(0, 6, tf3, s3)
f_cell(2, 6, tf4, s4)
f_cell(0, 7, tf5, s5)
f_cell(2, 7, tf6, s6)
// Pairs — 2 per row, named by TF tier (Fast → Macro+) instead of raw TF pair
f_cell(0, 8, "Fast", p12, useP12)
f_cell(2, 8, "Mid", p23, useP23)
f_cell(0, 9, "Slow", p34, useP34)
f_cell(2, 9, "Macro", p45, useP45)
f_cell(0, 10, "Macro+", p56, useP56)
table.cell(t, 2, 10, "", bgcolor=color.new(color.gray, 92))
table.cell(t, 3, 10, "", bgcolor=color.new(color.gray, 92))
f_sep(11)
// Totals
rowTotals = showDebugTable ? 12 : 4
f_cell(0, rowTotals, "Agree", agreementIdx)
f_cell(2, rowTotals, "Total", smoothedTotal)
Overview
MTF StochRSI Pair Score [WavesUnchained] calculates a StochRSI reading on six configurable timeframes, dampens exhaustion, then combines adjacent timeframe pairs with a sync bonus or conflict penalty and user pair weights. The weighted total can be smoothed with EMA, RMA, SMA, WMA, HMA, or JMA. It also plots an agreement index, edge-triggered signal markers, and a bias, quality, and timing state. A table shows the compact state or optional per-timeframe debug detail. It does not place trades.
Per-timeframe StochRSI with exhaustion dampening
Pair sync bonus and conflict penalty
Weighted score across up to five pairs
Smoothed total with EMA, RMA, SMA, WMA, HMA, or JMA
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
RSI Length
int
14
rsiLen
Stoch Length
int
14
stochLen
K Smoothing
int
3
kSmooth
D Smoothing
int
3
dSmooth
Upper Zone
float
80
upper
Lower Zone
float
20
lower
TF 1
timeframe
15
tf1
TF 2
timeframe
30
tf2
TF 3
timeframe
60
tf3
TF 4
timeframe
120
tf4
TF 5
timeframe
240
tf5
TF 6
timeframe
D
tf6
Show all 39 settings
Setting
Type
Default
Variable
Lower TF Weight in Pair
float
0.4
minorWeight
Sync Bonus
float
1.15
syncBonus
Conflict Penalty
float
0.75
conflictPenalty
Use Pair 1: TF1 + TF2
bool
true
useP12
Use Pair 2: TF2 + TF3
bool
true
useP23
Use Pair 3: TF3 + TF4
bool
true
useP34
Use Pair 4: TF4 + TF5
bool
true
useP45
Use Pair 5: TF5 + TF6
bool
false
useP56
Weight Pair 1
float
1.0
wP12
Weight Pair 2
float
1.5
wP23
Weight Pair 3
float
1.3
wP34
Weight Pair 4
float
1.2
wP45
Weight Pair 5
float
1.0
wP56
Mode
string
JMA
smoothMode
Length
int
5
smoothLen
JMA Phase
int
50
jmaPhase
JMA Power
float
2.0
jmaPower
Show Table
bool
true
showTable
Show Debug Table (per-TF/per-pair)
bool
false
showDebugTable
Show Signal Markers
bool
true
showSignals
Only Confirmed Bar Signals
bool
true
confirmedOnly
Strong Long Level
float
60
longLevel
Strong Short Level
float
-60
shortLevel
Building Level
float
30
buildingLevel
Min Agreement for Signals
float
33
minAgreement
Fresh Window (bars)
int
3
freshWindow
Alerts only on bar close (confirmed)
bool
true
alertsOnBarClose
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.
This indicator scans a pasted list of symbols for RSI directional momentum and shows matching long and short signals in a table in a separate pane. The user chooses continuous break, continuous retracement, flip break, flip retracement, divergence, or hidden d
This indicator plots the RSI oscillator on the main chart and displays its value distribution in a table positioned at the top right. The script calculates the relative strength index using the input source and length, then plots horizontal lines at 0, 50, and
Market Speedometer plots a 0-100 market speed reading in a separate pane. It measures movement by bar-to-bar change, true range, or a short average of recent bars, then ranks that movement against ATR and a percentile lookback. An EMA smooths the reading, and
KamaFama is an indicator that translates a Freqtrade trading strategy into Pine Script for visualization. It combines Kaufman Adaptive Moving Average (KAMA), MAMA/FAMA calculations derived via Hilbert Transform, Williams %R, and correlation trend filters to ev
Calculates normalized reversal metrics across up to ten Forex currency pairs using user-selected moving average methods, including HarmonicMA, EMA, SMA, or WMA. The indicator identifies overbought, oversold, and mean-crossing conditions using a configurable th
The 8-Factor Composite Screener evaluates equities across fundamental and technical criteria, including valuation ratios, earnings and revenue growth, 12-month return momentum, proximity to 52-week highs, Stage 2 moving average alignment, benchmark relative st