Dynamic Support & Resistance [UAlgo]Dynamic Support & Resistance is a pivot driven structure indicator that detects recurring reaction prices and converts them into live support and resistance zones. The script does not treat every pivot as an isolated event. Instead, it groups nearby pivots into shared price areas, counts how many times the market has respected each area, and only promotes a level visually once it reaches the required minimum number of touches.
This creates a cleaner and more practical market structure map. When price reacts again near an existing zone, the level is updated rather than duplicated. As a result, the plotted areas represent repeated interaction and growing structural significance instead of a large collection of disconnected swing points.
Each active level is displayed as a channel centered on the level price. The size of that channel is based on the user selected tolerance percentage, so every level is shown as a reaction zone rather than a single exact line only. A center line and a text label are also added, which makes the structure easier to read in live chart conditions.
The script also includes a clear invalidation model. Support becomes invalid when price closes meaningfully below the zone by more than the selected break threshold. Resistance follows the opposite logic. Once invalidated, the level is removed from the active structure map, which keeps the chart focused on areas that are still relevant.
This makes the indicator useful for traders who want a simple but adaptive framework for mapping horizontal support and resistance. It works well for identifying repeated reaction zones, tracking the growth of structural importance through multiple touches, and recognizing when a level has finally lost validity.
🔹 Features
🔸 Pivot Based Structure Detection
The script starts from confirmed pivot highs and pivot lows. This means support and resistance zones are built from meaningful swing points rather than from arbitrary rolling highs and lows. As a result, the detected levels are more closely aligned with actual market turning areas.
🔸 Level Clustering Instead of Raw Pivot Plotting
A newly detected pivot is not always turned into a brand new level. The script first checks whether that pivot belongs to an existing active zone within the allowed tolerance. If it does, the existing level is updated. If it does not, a new level is created. This prevents unnecessary duplication and keeps the structure map organized.
🔸 Touch Counting and Strength Filtering
Every level tracks how many times price has reacted around it. A zone becomes visually important only after it reaches the required minimum number of touches. This helps filter out weak one time reactions and highlights price areas that have shown repeated acceptance or rejection.
🔸 Adaptive Reaction Zones
Each level is displayed as a channel rather than as a single price only. The channel width is calculated from the level price and the selected tolerance percentage. This makes the plotted area more realistic because support and resistance usually behave as zones rather than exact ticks.
🔸 Dynamic Price Recentering
When a new pivot is assigned to an existing level, the script updates the level price using an average based on the previous stored price and the new pivot. This gradually shifts the zone toward the center of actual reaction activity, which makes the level more representative over time.
🔸 Separate Support and Resistance Maps
Support and resistance are stored independently in their own arrays. This allows the script to manage bullish and bearish reaction zones separately while preserving clean logic for visualization, touch counting, invalidation, and cleanup.
🔸 Live Visual Rendering
Once a level becomes strong enough, the script draws:
a reaction channel,
a center line,
and a label showing the role and price.
This produces a chart friendly display that is easy to interpret during live trading or post analysis.
🔸 Invalidation by Closing Break
Levels are not removed by random intrabar noise alone. Instead, support is invalidated only when closing price moves below the level by more than the selected break percentage, and resistance is invalidated only when closing price moves above it by more than that threshold. This helps reduce premature removals.
🔸 Automatic Cleanup
Broken levels are removed from the active arrays after invalidation. This keeps the script efficient and prevents the internal structure store from filling with irrelevant levels.
🔸 Label Position Refresh
On the last bar, active labels are shifted slightly forward so they remain readable and do not sit directly on top of current candles. This small detail improves chart presentation significantly.
🔹 Calculations
1) Pivot Detection
float ph = ta.pivothigh(high, pivotLen, pivotLen)
float pl = ta.pivotlow(low, pivotLen, pivotLen)
int pIdx = bar_index - pivotLen
if not na(ph)
resistances.processPoint(ph, -1, pIdx)
if not na(pl)
supports.processPoint(pl, 1, pIdx)
This is the starting point of the script.
The code uses ta.pivothigh and ta.pivotlow to detect confirmed swing highs and swing lows. Because pivots are only confirmed after pivotLen bars on both sides, the actual pivot bar is not the current bar. That is why the script calculates:
pIdx = bar_index - pivotLen
This gives the real bar index where the pivot occurred.
Then the pivot is passed into the correct structure map:
pivot highs go into the resistance array,
pivot lows go into the support array.
So at this stage, the script is transforming raw swing points into candidate support or resistance events.
2) Grouping New Pivots Into Existing Levels
method processPoint(array levels, float price, int role, int idx) =>
bool found = false
for in levels
if lvl.active
float tolerance = lvl.price * zonePct
if math.abs(price - lvl.price) <= tolerance
lvl.price := (lvl.price * lvl.count + price) / (lvl.count + 1)
lvl.count += 1
lvl.updateVisuals()
found := true
break
if not found
SRLevel newLvl = SRLevel.new(price, 1, role, true, na, na, na, idx)
levels.push(newLvl)
This method decides whether a new pivot should strengthen an existing level or create a completely new one.
For every active level in the relevant array, the script calculates a tolerance band:
tolerance = lvl.price * zonePct
Then it checks whether the new pivot price is close enough to that level:
math.abs(price - lvl.price) <= tolerance
If the pivot falls inside the allowed zone, the script treats it as another touch of the same structure. It then updates the stored level price using an average weighted by the existing touch count:
lvl.price := (lvl.price * lvl.count + price) / (lvl.count + 1)
This is important. The level does not stay frozen forever. It gradually recenters as more pivots are absorbed into it. At the same time, the touch count increases, which strengthens the zone statistically.
If no active level is close enough, the script creates a fresh support or resistance level with an initial count of one.
So this method is the core clustering engine of the whole indicator.
3) Minimum Touch Logic and Zone Construction
method updateVisuals(SRLevel this) =>
if this.active and this.count >= minStrength
color c = this.role == 1 ? colSup : colRes
float tolerance = this.price * zonePct
float top = this.price + tolerance
float bot = this.price - tolerance
This is the first visual gate.
A level is drawn only if two conditions are true:
the level must still be active,
and its touch count must be greater than or equal to the minimum strength input.
That means weak single touch levels can exist internally, but they are not shown until they prove themselves.
Once the level qualifies, the script calculates the channel boundaries around the center price:
top = this.price + tolerance
bot = this.price - tolerance
So the plotted zone is always centered on the current level price and expands above and below it by the selected tolerance percentage.
This is what turns the raw pivot cluster into an actual support or resistance zone.
4) Drawing the Channel, Center Line, and Label
if na(this.bx)
this.bx := box.new(
left=this.start_idx,
top=top,
right=bar_index,
bottom=bot,
border_color=color.new(c, 0),
border_style=line.style_dotted,
bgcolor=color.new(c, 80),
extend=extend.right
)
else
this.bx.set_top(top)
this.bx.set_bottom(bot)
if na(this.ln)
this.ln := line.new(
x1=this.start_idx,
y1=this.price,
x2=bar_index,
y2=this.price,
color=c,
width=1,
extend=extend.right
)
else
this.ln.set_y1(this.price)
this.ln.set_y2(this.price)
Once a level is strong enough, the script renders its visual structure.
The box represents the full support or resistance channel from the starting pivot index to the current bar, and it is extended to the right so the zone remains visible into future bars.
The line is drawn exactly at the stored level price, which serves as the center of the zone. Because the price can shift slightly over time as new pivots are absorbed, the center line is updated dynamically as well.
So visually, each confirmed level contains:
a shaded reaction area,
a center reference line,
and an ongoing extension into the future.
This gives the user both a zone view and a central price reference at the same time.
5) Text Label Calculation
string txt = str.format("{0} - {1,number,#.##}", this.role == 1 ? "Support" : "Resistance", this.price)
if na(this.lbl)
this.lbl := label.new(
x=bar_index,
y=this.price,
text=txt,
style=label.style_label_left,
color=color.new(color.black, 100),
textcolor=colTxt,
size=size.small
)
else
this.lbl.set_xy(bar_index, this.price)
this.lbl.set_text(txt)
This snippet builds and updates the text label for each visible level.
The label text is generated from the level role and the current averaged level price. So if the level belongs to the bullish structure map, the label shows Support and the current price. If it belongs to the bearish structure map, it shows Resistance and the current price.
The label is then either created or updated at the latest bar.
This means the label always reflects the most current state of the zone instead of staying attached to an outdated price.
6) Invalidation Logic
method checkInvalidation(array levels, float currentPrice) =>
for in levels
if lvl.active
float threshold = lvl.price * breakPct
bool broken = false
if lvl.role == 1
if currentPrice < lvl.price - threshold
broken := true
else
if currentPrice > lvl.price + threshold
broken := true
if broken
lvl.active := false
lvl.updateVisuals()
This method decides when a level should stop being considered valid.
For every active level, the script calculates a break threshold based on the level price and the selected invalidation percentage:
threshold = lvl.price * breakPct
Then it applies directional logic.
For support:
price must close below the level by more than the threshold.
For resistance:
price must close above the level by more than the threshold.
This is important because it means the script does not remove zones on a tiny touch or minor overshoot. A meaningful closing break is required.
When that break happens, the level is marked inactive and its visuals are refreshed so the box, line, and label are deleted.
7) Visual Removal of Broken Levels
else if not this.active
if not na(this.bx)
this.bx.delete()
this.bx := na
if not na(this.ln)
this.ln.delete()
this.ln := na
if not na(this.lbl)
this.lbl.delete()
this.lbl := na
This is the part of updateVisuals that handles invalidated levels.
Once a level is no longer active, the script deletes all visual objects associated with it:
the channel box,
the center line,
and the text label.
This keeps the chart focused on only those support and resistance zones that are still valid. It also prevents broken levels from continuing to influence the user visually after the market has already moved through them.
8) Cleanup of Inactive Objects From Memory
method cleanup(array levels) =>
int n = levels.size()
if n > 0
for i = n - 1 to 0
SRLevel lvl = levels.get(i)
if not lvl.active
levels.remove(i)
Deleting a level visually is only one part of the process. The script also removes inactive levels from the calculation arrays.
This reverse loop is important because removing array elements while looping forward can shift indices and create logic errors. By iterating from the end toward zero, the script safely removes inactive items.
This keeps the internal storage efficient and prevents the structure engine from wasting time checking already broken zones on future bars.
9) Last Bar Label Position Update
if barstate.islast
for lvl in supports
if lvl.active and not na(lvl.lbl)
lvl.lbl.set_x(bar_index + 5)
for lvl in resistances
if lvl.active and not na(lvl.lbl)
lvl.lbl.set_x(bar_index + 5)
This small block improves readability on the chart.
On the last visible bar, the script shifts active labels slightly to the right of the current candle. That creates a cleaner presentation and reduces overlap between labels and price bars.
Although this does not change the structural logic of the indicator, it improves usability in live analysis and makes the levels easier to read at a glance.
10) Full Execution Flow
if not na(ph)
resistances.processPoint(ph, -1, pIdx)
if not na(pl)
supports.processPoint(pl, 1, pIdx)
supports.checkInvalidation(close)
resistances.checkInvalidation(close)
supports.cleanup()
resistances.cleanup()
This compact block summarizes how the script behaves on every bar.
First, it checks whether a new pivot high or pivot low has been confirmed.
Second, it sends that pivot into the correct level array.
Third, it checks whether any active support or resistance has been invalidated by the current closing price.
Finally, it removes inactive levels from memory.
So the script is constantly cycling through four steps:
detect,
cluster,
invalidate,
clean.
That is why the indicator remains dynamic. It is always updating the active structure map as the market evolves.
Индикатор Pine Script®















