This indicator evaluates market structure events, including breaks of structure, liquidity sweeps, and order blocks, by calculating reaction speed, validation, and volatility activity scores.
//@version=6
// ============================================================================
// Time-to-React + Volatility-Time (BOS / Sweep / OB) [Scores + Candle Color] [WavesUnchained]
// Version: 1.2.1
// Author: WavesUnchained
// Build: 2026-10-02 00:00:00
// ============================================================================
// Event reaction scoring (BOS/Sweep/OB) with volatility activity and candle coloring
//
// Features:
// ✓ Auto presets by timeframe with custom overrides
// ✓ BOS/Sweep/OB event prioritization
// ✓ Reaction and validation scoring
// ✓ Volatility activity score
// ✓ Optional chart labels and candle coloring
//
// ============================================================================
indicator("Time-to-React + Volatility-Time (BOS / Sweep / OB) [Scores + Candle Color] [WavesUnchained]", format=format.price, precision=2, overlay=false, max_labels_count=200)
// ═══════════════════════════════════════════════════════════════════════════
// Time-to-React + Volatility-Time
// ═══════════════════════════════════════════════════════════════════════════
// Version: 1.2.0
// Author: WavesUnchained
//
// MODULES:
// - inputs.pine
// - helpers.pine
// - swings_ob.pine
// - events.pine
// - scores.pine
// - plots.pine
// ═══════════════════════════════════════════════════════════════════════════
// NOTE: Modules are combined by the build system.
// ============================================================================
// INLINE MODULES (Combined at build time)
// ============================================================================
// ============================================================================
// MODULE: inputs
// ============================================================================
//──────────────────────────────────────────────────────────────────────────────
// Inputs
//──────────────────────────────────────────────────────────────────────────────
preset = input.string("Auto", "Preset", options=["Auto","15m","1h","4h","D","M","Custom"])
customReactMin = input.int(150, "Custom: Reaction Window (min)", minval=5)
customValidMin = input.int(420, "Custom: Validation Window (min)", minval=10)
atrLen = input.int(14, "ATR Length", minval=1)
atrBaseLen = input.int(50, "ATR Base Length (Activity)", minval=5)
activityK = input.float(1.2, "Activity Curve Sensitivity", minval=0.1, step=0.1)
swingLookback = input.int(10, "Rolling Swing Lookback (BOS/Sweep)", minval=2, maxval=100)
atrMoveMult = input.float(1.2, "B: Minimum Move (ATR Mult)", minval=0.1, step=0.1)
wA = input.float(0.6, "Validation Weight A (Break)", minval=0.0, maxval=1.0, step=0.05)
wB = input.float(0.4, "Validation Weight B (ATR Move)", minval=0.0, maxval=1.0, step=0.05)
wSpeed = input.float(0.45, "Final Weight: Speed", minval=0.0, maxval=1.0, step=0.05)
wVal = input.float(0.55, "Final Weight: Validation", minval=0.0, maxval=1.0, step=0.05)
dispBodyATR = input.float(1.0, "OB: Displacement Body > x*ATR", minval=0.1, step=0.1)
obSearchBars = input.int(12, "OB: Search Back Bars", minval=2, maxval=50)
priority = input.string("BOS>SWEEP>OB", "Event Priority", options=["BOS>SWEEP>OB","SWEEP>BOS>OB","OB>BOS>SWEEP"])
showLabels = input.bool(true, "Show Labels on Chart")
labelAlpha = input.int(0, "Label Background Transparency (0=solid)", minval=0, maxval=90)
// ============================================================================
// MODULE: helpers
// ============================================================================
//──────────────────────────────────────────────────────────────────────────────
// Helpers
//──────────────────────────────────────────────────────────────────────────────
clamp01(x) => math.max(0.0, math.min(1.0, x))
clamp100(x) => math.max(0.0, math.min(100.0, x))
tfSec = timeframe.in_seconds(timeframe.period)
tfMin = tfSec / 60.0
// Preset resolver (minutes)
// Tier 0=15m, 1=1h, 2=4h, 3=D, 4=M
f_presetTier(_preset, _tfMin) =>
int tier = 0
if _preset == "1h"
tier := 1
else if _preset == "4h"
tier := 2
else if _preset == "D"
tier := 3
else if _preset == "M"
tier := 4
else if _preset == "Auto"
if _tfMin <= 20
tier := 0
else if _tfMin <= 90
tier := 1
else if _tfMin <= 300
tier := 2
else if _tfMin <= 10080
tier := 3
else
tier := 4
tier
f_presets(_preset, _tfMin) =>
tier = f_presetTier(_preset, _tfMin)
float r = 150
float v = 420
float atrM = 1.2
float _wA = 0.6
float _wB = 0.4
float _wSpeed = 0.45
float _wVal = 0.55
if tier == 1
r := 480
v := 1200
atrM := 1.6
_wA := 0.7
_wB := 0.3
_wSpeed := 0.40
_wVal := 0.60
else if tier == 2
r := 2160
v := 5760
atrM := 2.0
_wA := 0.75
_wB := 0.25
_wSpeed := 0.35
_wVal := 0.65
else if tier == 3
r := 7200
v := 17280
atrM := 2.4
_wA := 0.8
_wB := 0.2
_wSpeed := 0.30
_wVal := 0.70
else if tier == 4
r := 86400
v := 172800
atrM := 3.0
_wA := 0.85
_wB := 0.15
_wSpeed := 0.25
_wVal := 0.75
[r, v, atrM, _wA, _wB, _wSpeed, _wVal]
[pReactMin, pValidMin, pAtrMoveMult, pWA, pWB, pWSpeed, pWVal] = f_presets(preset, tfMin)
reactMin = preset == "Custom" ? customReactMin : int(pReactMin)
validMin = preset == "Custom" ? customValidMin : int(pValidMin)
atrMult = preset == "Custom" ? atrMoveMult : pAtrMoveMult
wAeff = preset == "Custom" ? wA : pWA
wBeff = preset == "Custom" ? wB : pWB
wSpeedEff = preset == "Custom" ? wSpeed : pWSpeed
wValEff = preset == "Custom" ? wVal : pWVal
sumValW = math.max(0.0001, wAeff + wBeff)
wAeffN = wAeff / sumValW
wBeffN = wBeff / sumValW
sumFinalW = math.max(0.0001, wSpeedEff + wValEff)
wSpeedN = wSpeedEff / sumFinalW
wValN = wValEff / sumFinalW
reactBars = int(math.ceil((reactMin * 60.0) / tfSec))
validBars = int(math.ceil((validMin * 60.0) / tfSec))
// ============================================================================
// MODULE: swings_ob
// ============================================================================
//──────────────────────────────────────────────────────────────────────────────
// Rolling swing levels (for BOS & Sweep) + OB (simple displacement-based)
//──────────────────────────────────────────────────────────────────────────────
var float lastSwingHigh = na
var int lastSwingHighBar = na
var float lastSwingLow = na
var int lastSwingLowBar = na
lastSwingHigh := ta.highest(high, swingLookback)[1]
lastSwingHighBar := bar_index - 1
lastSwingLow := ta.lowest(low, swingLookback)[1]
lastSwingLowBar := bar_index - 1
atr = ta.atr(atrLen)
isDispUp = (close > open) and (math.abs(close - open) > dispBodyATR * atr) and (close > high[1])
isDispDn = (close < open) and (math.abs(close - open) > dispBodyATR * atr) and (close < low[1])
var float bullObHigh = na
var float bullObLow = na
var int bullObBar = na
var float bearObHigh = na
var float bearObLow = na
var int bearObBar = na
if isDispUp
float hi = na
float lo = na
int obB = na
for i = 1 to obSearchBars
if close[i] < open[i]
hi := high[i]
lo := low[i]
obB := bar_index - i
break
if not na(obB)
bullObHigh := hi
bullObLow := lo
bullObBar := obB
if isDispDn
float hi = na
float lo = na
int obB = na
for i = 1 to obSearchBars
if close[i] > open[i]
hi := high[i]
lo := low[i]
obB := bar_index - i
break
if not na(obB)
bearObHigh := hi
bearObLow := lo
bearObBar := obB
bullObTap = not na(bullObHigh) and (low <= bullObHigh) and (high >= bullObLow)
bearObTap = not na(bearObLow) and (high >= bearObLow) and (low <= bearObHigh)
// First cross: the previous bar must not have closed above ITS own reference level
// (lastSwingHigh already contains high[1], so close[1] <= lastSwingHigh is always true)
bosUp = not na(lastSwingHigh) and bar_index > lastSwingHighBar and close > lastSwingHigh and close[1] <= nz(lastSwingHigh[1], lastSwingHigh)
bosDn = not na(lastSwingLow) and bar_index > lastSwingLowBar and close < lastSwingLow and close[1] >= nz(lastSwingLow[1], lastSwingLow)
sweepUp = not na(lastSwingHigh) and bar_index > lastSwingHighBar and high > lastSwingHigh and close < lastSwingHigh
sweepDn = not na(lastSwingLow) and bar_index > lastSwingLowBar and low < lastSwingLow and close > lastSwingLow
// ============================================================================
// MODULE: events
// ============================================================================
//──────────────────────────────────────────────────────────────────────────────
// Event engine (single active event with priority)
//──────────────────────────────────────────────────────────────────────────────
var bool eventActive = false
var string eventType = ""
var int eventDir = 0
var int eventBar = na
var float eventRef = na
var float eventAtr = na
var bool reacFound = false
var int reacBar = na
var float reacHi = na
var float reacLo = na
var bool valA = false
var float valBProgress = 0.0
var float speedScore = na
var float valScore = na
var float reactionScore = na
f_pickEvent() =>
bool fire = false
string t = ""
int d = 0
bool cBOS = (bosUp or bosDn)
bool cSWP = (sweepUp or sweepDn)
bool cOB = (bullObTap or bearObTap)
bool pickBOS = false
bool pickSWP = false
bool pickOB = false
if priority == "BOS>SWEEP>OB"
pickBOS := cBOS
pickSWP := not pickBOS and cSWP
pickOB := not pickBOS and not pickSWP and cOB
else if priority == "SWEEP>BOS>OB"
pickSWP := cSWP
pickBOS := not pickSWP and cBOS
pickOB := not pickSWP and not pickBOS and cOB
else
pickOB := cOB
pickBOS := not pickOB and cBOS
pickSWP := not pickOB and not pickBOS and cSWP
// Direction is determined purely from current-bar price action, not the event label:
// BOS: the break direction is which extreme was broken on this bar's close
// SWEEP: the recovery direction is encoded in whether low/high swept and closed back inside
// OB: the OB type (bull/bear) was created by the displacement; confirm via bar close
if pickBOS
fire := true
t := "BOS"
d := bosUp ? 1 : -1
else if pickSWP
fire := true
t := "SWEEP"
// sweepDn: low wicked below swing low and closed above → bullish (+1)
// sweepUp: high wicked above swing high and closed below → bearish (-1)
d := sweepDn ? 1 : -1
else if pickOB
fire := true
t := "OB"
// Confirm OB direction by current-bar close vs open (price action body bias)
// If bull OB tapped but bar closes red, direction is ambiguous → use OB type
bool paUp = close >= open
bool paDn = close < open
d := bullObTap and not (bearObTap and paDn) ? 1 : -1
[fire, t, d]
[fireEvt, tEvt, dEvt] = f_pickEvent()
if not eventActive and fireEvt
eventActive := true
eventType := tEvt
eventDir := dEvt
eventBar := bar_index
eventRef := close
eventAtr := atr
// Reset per-event state — without this, speedScore from a previous event
// survives and the speed score is never recomputed for later events
reacFound := false
reacBar := na
reacHi := na
reacLo := na
valA := false
valBProgress := 0.0
speedScore := na
valScore := na
reactionScore := na
if showLabels
colBg = eventDir == 1 ? color.new(color.lime, labelAlpha) : color.new(color.red, labelAlpha)
label.new(bar_index, close, text=eventType + (eventDir == 1 ? " ↑" : " ↓"),
style=label.style_label_up, textcolor=color.white, color=colBg, size=size.normal)
if eventActive
ageBars = bar_index - eventBar
// Reaction search starts on the bar AFTER the event — the trigger bar
// cannot be its own reaction
if not reacFound and ageBars >= 1
bullReac = (close > open) or (low >= low[1])
bearReac = (close < open) or (high <= high[1])
if (eventDir == 1 and bullReac)
reacFound := true
reacBar := bar_index
reacHi := high
reacLo := low
if showLabels
label.new(bar_index, high, text="Reac", style=label.style_label_down,
textcolor=color.black, color=color.new(color.yellow, labelAlpha), size=size.normal)
if (eventDir == -1 and bearReac)
reacFound := true
reacBar := bar_index
reacHi := high
reacLo := low
if showLabels
label.new(bar_index, low, text="Reac", style=label.style_label_up,
textcolor=color.black, color=color.new(color.yellow, labelAlpha), size=size.normal)
if reacFound and na(speedScore)
// Reaction on the first bar after the event = 0 bars delay = 100
barsToReact = reacBar - eventBar - 1
speedScore := 100.0 * clamp01(1.0 - (barsToReact / math.max(1.0, reactBars - 1)))
if reacFound
barsSinceReac = bar_index - reacBar
inValidWindow = barsSinceReac <= validBars
if not valA and inValidWindow
if eventDir == 1
valA := close > reacHi
else
valA := close < reacLo
moveTarget = atrMult * eventAtr
// Measured from the event bar's close, so the trigger bar's own range
// does not count as follow-through
moveNow = eventDir == 1 ? (high - eventRef) : (eventRef - low)
valBProgress := clamp01(moveTarget > 0 ? (moveNow / moveTarget) : 0)
valScore := 100.0 * (wAeffN * (valA ? 1.0 : 0.0) + wBeffN * valBProgress)
reactionScore := clamp100(wSpeedN * speedScore + wValN * valScore)
if barsSinceReac > validBars
eventActive := false
if not reacFound and ageBars > reactBars
speedScore := 0
valScore := 0
reactionScore := 0
eventActive := false
// ============================================================================
// MODULE: scores
// ============================================================================
//──────────────────────────────────────────────────────────────────────────────
// Scores
//──────────────────────────────────────────────────────────────────────────────
plotScore = eventActive or (not na(reactionScore) and (bar_index - nz(reacBar, bar_index)) <= 3) ? reactionScore : na
atrBase = ta.sma(atr, atrBaseLen)
ratio = atrBase > 0 ? (atr / atrBase) : 1.0
activityScore = 50.0 + 50.0 * math.tanh((ratio - 1.0) * activityK)
activityScore := clamp100(activityScore)
// ============================================================================
// MODULE: plots
// ============================================================================
//──────────────────────────────────────────────────────────────────────────────
// Plots (panel) + candle coloring
//──────────────────────────────────────────────────────────────────────────────
hline(50, "Mid (50)", color=color.new(color.gray, 65))
hline(70, "Strong (70)", color=color.new(color.gray, 75))
hline(30, "Weak (30)", color=color.new(color.gray, 75))
reacCol = not na(plotScore) ? color.from_gradient(plotScore, 0, 100, color.red, color.lime) : color.new(color.gray, 90)
plot(plotScore, "ReactionScore (0-100)", color=reacCol, linewidth=3)
actCol = color.from_gradient(activityScore, 0, 100, color.new(color.gray, 10), color.aqua)
plot(activityScore, "ActivityScore (0-100)", color=actCol, linewidth=2, style=plot.style_line)
color barCol = na
if eventActive and not na(reactionScore)
base = eventDir == 1 ? color.lime : color.red
barCol := color.from_gradient(reactionScore, 0, 100, color.new(base, 70), color.new(base, 0))
barcolor(barCol)
Overview
Time-to-React + Volatility-Time (BOS / Sweep / OB) plots composite scores in a separate pane while offering optional candle coloring and chart labels. Configurable inputs allow traders to adjust preset timeframes, reaction and validation time windows, ATR lengths, swing lookback periods, and event prioritization. The script serves as an analytical measurement tool for structural reactions, so it does not include automated trade execution rules.
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Preset
string
Auto
preset
Custom: Reaction Window (min)
int
150
customReactMin
Custom: Validation Window (min)
int
420
customValidMin
ATR Length
int
14
atrLen
ATR Base Length (Activity)
int
50
atrBaseLen
Activity Curve Sensitivity
float
1.2
activityK
Rolling Swing Lookback (BOS/Sweep)
int
10
swingLookback
B: Minimum Move (ATR Mult)
float
1.2
atrMoveMult
Validation Weight A (Break)
float
0.6
wA
Validation Weight B (ATR Move)
float
0.4
wB
Final Weight: Speed
float
0.45
wSpeed
Final Weight: Validation
float
0.55
wVal
Show all 17 settings
Setting
Type
Default
Variable
OB: Displacement Body > x*ATR
float
1.0
dispBodyATR
OB: Search Back Bars
int
12
obSearchBars
Event Priority
string
BOS>SWEEP>OB
priority
Show Labels on Chart
bool
true
showLabels
Label Background Transparency (0=solid)
int
0
labelAlpha
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.
Calculates CRT session range boundaries and detects Turtle Soup setups to identify potential breakout signals. The script tracks session high, low, and midpoint values to determine intraday bias while measuring price break distances against ATR thresholds. It
Indicatorv5SMC / ICT / price actionSessions & time
AmiComStrategy is an indicator that calculates correlations between securities using the Corell indicator over specified timeframes such as 5D. It filters trading opportunities based on session times like 1300-1700 and security up and down times such as 3H. Th
This indicator calculates and visualizes trend strength in a separate pane by combining a Kalman filter, a Follow Line trend tracker with ATR and Bollinger Bands filtering, Zig Zag swings, and higher-timeframe Power of 3 (PO3) ghost candles. It provides option
This indicator plots HTF SMC confluence signals in a separate pane. It visualizes ATR regime states from compression through expansion and exhaustion using custom colors. Squeeze markers indicate on or released states while reversal context displays scores for
This indicator calculates volume anomaly metrics using rolling z-scores of window volume, trend cleanliness from directional price consistency, and relative price impact. It plots these in a separate pane alongside signals: yellow dots for volume building with
This overlay indicator marks ICT liquidity void fills on the price chart and can show long and short signals. It places a Fibonacci level inside a displacement candle, defaulting to the midpoint, and draws lines in a chosen style. Optional filters limit the fi