Indicator · Pine Script v6 · Own pane

Time-to-React + Volatility-Time (BOS / Sweep / OB)

by casoon

This indicator evaluates market structure events, including breaks of structure, liquidity sweeps, and order blocks, by calculating reaction speed, validation, and volatility activity scores.

View on GitHub

GitHub stars9on the repository
Pine versionv6current
Lines of code452
Settings17inputs
LicenceMIT
Last updateyesterday27 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

452 lines of Pine Script v6 from indicators/volatility/time_to_react_volatility_time/time_to_react_volatility_time.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

time_to_react_volatility_time.pine
//@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.

Details

Published by

casoon on GitHub

Type

Indicator, Pine Script v6

Draws

Separate pane below the chart

Category

Volatility & bands, SMC / ICT / price action

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Topics

pine-scriptpinescripttechnical-analysistrading-indicatorstrading-strategiestradingview

Settings

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

SettingTypeDefaultVariable
PresetstringAutopreset
Custom: Reaction Window (min)int150customReactMin
Custom: Validation Window (min)int420customValidMin
ATR Lengthint14atrLen
ATR Base Length (Activity)int50atrBaseLen
Activity Curve Sensitivityfloat1.2activityK
Rolling Swing Lookback (BOS/Sweep)int10swingLookback
B: Minimum Move (ATR Mult)float1.2atrMoveMult
Validation Weight A (Break)float0.6wA
Validation Weight B (ATR Move)float0.4wB
Final Weight: Speedfloat0.45wSpeed
Final Weight: Validationfloat0.55wVal
Show all 17 settings
SettingTypeDefaultVariable
OB: Displacement Body > x*ATRfloat1.0dispBodyATR
OB: Search Back Barsint12obSearchBars
Event PrioritystringBOS>SWEEP>OBpriority
Show Labels on ChartbooltrueshowLabels
Label Background Transparency (0=solid)int0labelAlpha
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

CRT + Turtle Soup Scanner

by alboogycOdR
6 stars

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

Indicator v5 SMC / ICT / price actionSessions & time
Updated 7 months ago · None stated GitHub repository of CRT + Turtle Soup Scanner

AmiComStrategy

by amico1234
1 stars

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

Indicator v4 Sessions & timeVolatility & bands
Updated 2 years ago · None stated GitHub repository of AmiComStrategy

SMC HTF Confluence

by skipp-dev
1 stars

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

Indicator v6 SMC / ICT / price actionVolatility & bands
Updated 2 days ago · None stated GitHub repository of SMC HTF Confluence
1 stars

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

Indicator v6 VolumeVolatility & bands
Updated 7 months ago · None stated GitHub repository of Volume Flow Anomaly AnomIQ Preview

ICT Liquidity Void Fill

by ArunKBhaskar
575 stars

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

Indicator v6 SMC / ICT / price actionSupport & resistance
Updated 1 year ago · MPL-2.0 GitHub repository of ICT Liquidity Void Fill

More indicators

Trade Your Charts With a Regulated Broker

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

Compare brokers