1. Ranked Support & Resistance Zones Description (Zeiierman)
An indicator that turns every swing high/low into a scored support/resistance zone, ranks all zones every bar, and shows only the strongest ones on the chart
1.1 Indicator Concept
Most support/resistance (S/R) indicators simply mark every swing high and swing low as a level, leaving the chart cluttered with dozens of lines of unknown quality. This indicator is built on a different idea: not every S/R zone deserves equal attention. A zone that formed on high volume, aligns with the prevailing trend, was born from a sharp/clean swing, and has already proven itself by being touched and defended multiple times, is structurally more important than a random pivot that appeared once and was never revisited.
- Zones, not lines: every S/R level is stored as a price zone (a band of width proportional to ATR) rather than a single price, because in real markets price rarely reacts to one exact tick.
- Scoring engine: each zone is continuously scored from 0-100 using five inputs - zone size, volume at creation, trend alignment, swing sharpness, and how many times it has been revisited - minus penalties for age and for being "eaten into" by price (mitigation).
- Ranking, not accumulation: every bar, all stored zones are re-sorted by score and only the Top-N zones are actually drawn. Lower ranked zones stay in memory (in case they climb back up) but are hidden from view, keeping the chart clean.
- Lifecycle awareness: a zone is not permanent. It can be absorbed into an existing nearby zone (reducing duplicate clutter), mitigated (price has partly filled through it, weakening it), broken (price closed convincingly through it, invalidating it), or simply expire from old age.
1.2 Indicator Features
-
Zone Boxes:
- Teal boxes = support zones (price floors), orange boxes = resistance zones (price ceilings).
- Each box spans a price band (top/bottom) and stretches across time, continuously extending to the right while the zone is still active.
- Only the top-ranked zones (controlled by "Show Top Zones") are drawn in full color; the rest exist internally but are invisible.
-
Strength Bars:
- Inside every visible zone, the upper half can show an orange bar with a bear % label (sell-side conviction) and the lower half a teal bar with a bull % label (buy-side conviction).
- The bar's length is proportional to its percentage - a 90% bull bar visually stretches almost across the whole zone width, a 20% bar stays short.
- Zone Text: a short label such as "Strong Support", "Support", "Weak Support" or "Mitigated Support" (and the resistance equivalents) is printed inside the zone, summarizing its current quality in words.
- Broken Zone Marker: when a zone is invalidated, its box turns gray and its centerline becomes a visible dotted gray line marking exactly where the old zone used to sit. A limited number of the most recently broken zones are kept on the chart before being fully removed.
- Direction Filter: the indicator can be set to show "All" zones, "Support Only", or "Resistance Only".
- Alerts: 12 alert conditions covering zone creation, top-rank changes, price touching/mitigating a zone, and zone breaks - split separately for support and resistance.
1.3 How to Use the Indicator
- Trade the Top-Ranked Zones First: because zones are re-ranked every bar, the ones currently drawn on your chart are (by the indicator's own scoring) the most relevant S/R levels right now - treat these as your primary reaction zones for entries, take-profit, or stop-loss placement.
- Read the Strength Bars Before Reacting: a support zone with a 85% bull bar is a much higher-conviction "buy the dip" zone than one sitting at 30%. Compare bull vs bear % inside the same zone to judge which side is currently winning the fight for that price area.
- Watch the Zone Text for Early Warning: once a zone's label flips to "Weak Support" or "Mitigated Support", price has already chewed through a large portion of it - treat it as less reliable than a fresh "Strong Support".
- Use Break Alerts as Breakout Confirmation: a "Resistance Broken" alert means price closed convincingly above a ranked ceiling (a bullish break), while "Support Broken" means a bearish break below a ranked floor - both can be used as breakout/momentum entry triggers.
- Use the Top-Rank Alerts to Track Leadership: "New Top-Ranked S/R Zone" fires whenever the #1-scored zone changes, letting you know the single most important level on the chart has shifted.
- Tune to Your Market/Timeframe: increase "Pivot Length" and "Min Pivot ATR" on noisy/lower timeframes to keep only clean, well-confirmed swings; widen "Zone Width ATR" on volatile instruments so zones aren't too thin to react to.
1.4 How the Indicator Works
Inputs & Roles
- visibleLimit ("Show Top Zones", Integer, default: 8, range 1-30): how many of the highest-ranked zones are actually drawn on the chart. Example: with visibleLimit=8, if the engine currently holds 40 zones in memory, only the top 8 by score get colored boxes - the other 32 stay invisible until they rank high enough to enter the top 8. Increase → more zones visible at once (busier chart). Decrease → only the very best zones shown (cleaner chart, but you may miss secondary levels).
- storedLimit ("Max Stored Zones", Integer, default: 60, range 10-250): the maximum number of zones kept in memory at all (visible + hidden). Once exceeded, the lowest-scored zone is deleted every bar until the count is back at the limit. Increase → more historical zones remembered, better chance a hidden zone can climb back into the visible top-N later. Decrease → less memory used, but old lower-ranked zones are forgotten faster.
- pivotSpan ("Pivot Length", Integer, default: 5, range 2-50): number of bars required on both sides of a swing high/low for it to be confirmed as a pivot (using ta.pivothigh/ta.pivotlow). Example: pivotSpan=5 means a candle only becomes a confirmed swing high once the 5 bars before AND the 5 bars after it are all lower - so confirmation always arrives 5 bars late. Increase → fewer but more reliable pivots (major swings only). Decrease → more pivots detected, more (and noisier) zones created, but confirmed faster.
- minSwingAtr ("Min Pivot ATR", Float, default: 0.15, range 0-5): minimum "sharpness" a pivot must have, measured in ATR multiples, before it is allowed to become a zone. Sharpness (swing quality) is how far the pivot price sticks out above/below the candles immediately next to it. Example: if ATR=$10 and minSwingAtr=0.15, a swing high must protrude at least $1.5 above its neighboring bars' highs to qualify; a flatter, barely-there pivot is ignored. Increase → only sharp, obvious swings create zones (fewer, cleaner zones). Decrease (toward 0) → even very mild pivots create zones (more zones, more noise).
- absorbAtr ("Absorb Similar Zones", Float, default: 0.55, range 0-3): maximum distance (in ATR) between a new pivot's midpoint and an existing zone's midpoint for the new pivot to be merged into that zone instead of creating a duplicate. Example: ATR=$10, absorbAtr=0.55 → any new pivot within $5.5 of an existing zone's center gets absorbed (reconfirming/widening that zone) rather than drawing a brand-new box right next to it. Increase → more aggressive merging, fewer total zones, each one more "battle tested". Decrease → more separate zones created even when close together.
- zoneAtrWidth ("Zone Width ATR", Float, default: 0.40, range 0.05-2): thickness of a zone box, in ATR multiples, split evenly above and below the pivot price. Example: ATR=$10, zoneAtrWidth=0.40 → zone half-width = 0.40×10×0.5 = $2, so a pivot at $100 creates a zone spanning $98-$102. Increase → thicker zones (price reacts "inside" a wider band, but zones overlap and absorb more easily). Decrease → thinner, more precise zones, but more easily missed or immediately broken by a wick.
- volLen ("Volume MA Length", Integer, default: 20): lookback for the average-volume baseline (ta.sma(volume, volLen)) that each new zone's creation-volume is compared against. Example: if the 20-bar average volume is 1,000 and the pivot bar's volume was 2,000, the zone's volume score = 2.0 (double the average), which strengthens its rank. Increase → smoother, slower-changing baseline. Decrease → baseline reacts faster to recent volume spikes/dips.
- trendLen ("Trend EMA Length", Integer, default: 50): length of the EMA used to judge whether a zone is aligned with the broader trend. A support zone that formed above this EMA, or a resistance zone that formed below it, gets a trend-alignment bonus to its score (the idea: a support pullback within an uptrend, or a resistance rejection within a downtrend, is more meaningful). Increase → trend filter reacts to a longer-term bias. Decrease → reacts to a shorter-term bias.
- breakAtr ("Break Close Buffer ATR", Float, default: 0.12, range 0-3): how far (in ATR) price must close beyond a zone's edge before that zone is declared broken - a buffer that prevents a zone from being invalidated by a marginal close right at its border. Example: ATR=$10, breakAtr=0.12 → a support zone with bottom at $98 is only broken once close < 98 - (0.12×10) = $96.8. Increase → harder to break a zone (more buffer, fewer false breaks). Decrease → easier to break, more sensitive to marginal closes.
- keepBrokenCount ("Keep Broken Zones", Integer, default: 4, range 0-30): how many of the most recently broken zones stay visible (as gray boxes + dotted lines) on the chart before being fully deleted, oldest first. Set to 0 → broken zones disappear immediately, no visual history of past breaks. Increase → more broken-zone history kept visible as reference (e.g., a former resistance that could now act as support).
- directionFilter ("Direction Filter", Options: All / Support Only / Resistance Only, default: All): restricts which zones are eligible to be shown. Example: setting "Support Only" hides all resistance zones from the visible top-N (they still exist and are still scored internally, just never drawn).
- showStrengthBars (Boolean, default: true): shows/hides the bull%/bear% strength bars inside each visible zone. If turned off, zones are drawn as plain colored boxes with no internal bars.
- showZoneText (Boolean, default: true): shows/hides the text label ("Strong Support", etc.) inside each visible zone.
- strengthTextSize / zoneTextSize (Options: auto/tiny/small/normal/large/huge, default: auto): controls the font size of the strength % labels and the zone text label respectively. Purely cosmetic, no effect on calculation.
- supportColor / resistanceColor / brokenColor: the fill/border colors used for support zones, resistance zones, and broken zones. Purely cosmetic.
Main Logic Blocks
🎯 Flow 1: What You See — The Top-Ranked Zone Display
- Every bar, all zones currently stored in memory are sorted from highest to lowest score (0-100 scale, see Flow 2).
- The engine walks down this sorted list and marks a zone "visible" only if it (a) passes the directionFilter and (b) is still within the first visibleLimit zones counted so far. Example: visibleLimit=8 → only the 8 highest-scoring qualifying zones get "visible=true"; the 9th best onward stays hidden even though it still exists internally.
- A visible zone is drawn as a colored box (teal for support, orange for resistance) sized to its top/bottom price band. Inside it: the top half can show a bear-strength bar sized by bearStrength%, the bottom half a bull-strength bar sized by bullStrength%, and (if enabled) a text summary such as "Strong Support".
- A hidden (ranked-out) zone is not deleted - its box is simply painted fully transparent and its text cleared, so it can silently re-appear the moment its score climbs back into the top-N.
🔧 Flow 2: How a Zone's Score and Strength % Are Calculated
-
Score (0-100), recomputed every bar for every zone:
- Five positive components are combined: zone size relative to ATR (up to 20 points), creation volume relative to average (up to 18 points), trend alignment (12 points flat if aligned), swing sharpness (up to 28 points - the single biggest factor), and touch count (up to 16 points) — plus a flat 10-point base, for a maximum raw total around 104.
- Two penalties are then subtracted: mitigation × 22 (how much price has already filled through the zone) and an age penalty of up to 10 points as the zone approaches 450 bars old.
- Example: a zone with a strong 1.4-ATR sharp swing (near-maximum swing score ≈ 27/28), 2× average volume (max 18/18), trend-aligned (12/12), full zone width (20/20), 2 touches (8/16) and base 10, and no mitigation/age penalty yet, scores roughly 27+18+12+20+8+10 = 95/100 - a very high-conviction zone.
-
Bull%/Bear% strength, derived from the score:
- The raw score is reshaped through a curve that stretches high scores (≥75) toward 100% and compresses low scores, then a trend-alignment bonus (+6) is added and a mitigation penalty (up to -28) is subtracted, giving the "zone-side" percentage (the strength on the zone's own side - bull for support, bear for resistance).
- The opposite side's percentage starts from a small 5% floor and rises mainly with mitigation (up to +65 as the zone gets filled through) plus a small score contribution - representing how much the "wrong side" of the trade is gaining ground as the zone weakens.
- Example: a support zone (dir=-1) with score=95, trendScore=1, mitigation=0 → zoneSide ≈ 72+(95-75)×1.12 = 94.4, +6 trend bonus, capped at 100 → bullStrength=100%; oppositeSide ≈ 5+0+95×0.08=12.6% → bearStrength=12%. If the same zone later gets 80% mitigated, bullStrength drops sharply (mitigation penalty -22.4) while bearStrength jumps toward 5+52+7.6≈64%.
- Zone text label, derived from mitigation and the zone-side strength: "Mitigated" if mitigation ≥ 0.75, else "Strong" if strength ≥ 70%, else plain if strength ≥ 45%, otherwise "Weak".
🌱 Flow 3: How a Zone Is Born — Pivot Detection & Absorption
- Step 1 - Detect a raw pivot: ta.pivothigh/ta.pivotlow confirm a swing high/low once pivotSpan bars have closed on both sides. Because confirmation needs future bars, the pivot's actual bar is always pivotSpan bars in the past by the time it's processed.
- Step 2 - Filter weak pivots: the pivot's "swing quality" (how far it sticks out above/below its immediate neighbors, in ATR) must be ≥ minSwingAtr, otherwise it's discarded and no zone is created.
- Step 3 - Build the candidate zone: a price band is built around the pivot price (± half of zoneAtrWidth×ATR), and its volume score, trend score, swing score, and initial score are all computed as in Flow 2.
- Step 4 - Absorb or create: the candidate is compared against every existing zone on the same side (support vs. support, resistance vs. resistance). If it's close enough (midpoints within absorbAtr×ATR) or overlaps it by at least 35% of the smaller zone's width, it is merged into that existing zone: the zone widens to cover both, its mitigation resets to 0 (a fresh retest "heals" the zone), its touch count rises by 1, and its score gets the better of the two scores plus a +3 reconfirmation bonus. If no match is found, a brand-new zone is created instead and pushed into the active zone list.
🔄 Flow 4: How a Zone Lives and Dies — Touch, Mitigation, Break, Expiry
- Touch & mitigation: every bar, if the current candle's range overlaps a zone's price band, that zone's touch count nudges up by 0.20 and its mitigation ratio is recalculated as how far price has pushed through the zone from the outer edge (0 = just tagged the edge, 1 = pushed all the way through to the far side). Crossing the 0.75 mitigation threshold for the first time fires a "Mitigated" alert.
- Break: a support zone is broken once close falls below its bottom edge by more than breakAtr×ATR (a resistance zone breaks the mirrored way, above its top). A broken zone is flagged, its box turns gray and its centerline becomes a visible dotted marker frozen at the break point, its bull/bear bars and text are cleared, and it is moved out of the active zone list into a small "recently broken" buffer capped at keepBrokenCount zones (oldest deleted first).
- Expiry: regardless of score or touches, a zone is silently deleted once it is older than 450 bars since its (last-merged) creation bar - old, stale zones eventually clear themselves out even without being broken.
- Pruning by storage limit: after re-ranking, if the total number of active zones exceeds storedLimit, the lowest-scoring zones are deleted one by one until the count is back within the limit.
🔔 Flow 5: Alerts
- Creation: "Support Created" / "Resistance Created" fire the instant a brand-new (non-absorbed) zone is added.
- Top-rank: "Top-Ranked Support/Resistance Created" and "New Top-Ranked S/R Zone" fire whenever the #1 zone by score changes identity (a new zone takes over as the single most important level on the chart).
- Touch/Mitigation: "Support/Resistance Mitigation Started" fires on any touch; "Support/Resistance Mitigated" fires the moment mitigation first crosses 75%.
- Break: "Support Broken" / "Resistance Broken" fire when a zone is invalidated by close, as described in Flow 4.
Outputs & Usage Roles
- Top-ranked zone boxes: the primary output - the current best S/R levels to react to, already filtered and ranked so you don't have to judge zone quality yourself.
- Bull%/Bear% strength bars: a quick visual read of how strongly each zone currently favors buyers vs. sellers, useful for grading entry conviction.
- Zone text label: a plain-language summary ("Strong Support" → "Mitigated Support") of how much a zone has been worn down, useful as an at-a-glance early-warning system.
- Broken zone markers: a short visual memory of recently invalidated levels, useful for spotting support/resistance role-reversal (old resistance becoming new support, and vice versa).
- Alerts: let the 12 events above be monitored without watching the chart live.
// This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
// https://creativecommons.org/licenses/by-nc-sa/4.0/
// © Zeiierman {
//@version=6
indicator("Ranked Support & Resistance Zones (Zeiierman)", shorttitle = "Ranked S/R Zones (Zeiierman)", overlay = true, max_lines_count = 500, max_boxes_count = 300, max_labels_count = 200)
//}
// ~~ Tooltips (Đã Việt hóa) {
t1 = "Điều chỉnh số lượng vùng hỗ trợ/kháng cự xếp hạng cao nhất được hiển thị trên biểu đồ. Giá trị nhỏ hơn = biểu đồ gọn hơn nhưng có thể bỏ lỡ vùng phụ. VD: 8 = chỉ hiện 8 vùng điểm cao nhất."
t2 = "Số lượng vùng S/R tối đa được lưu trong bộ nhớ trước khi các vùng xếp hạng thấp nhất bị xoá bớt. Không ảnh hưởng số vùng hiển thị, chỉ ảnh hưởng vùng dự trữ ngầm."
t3 = "Độ dài xác nhận pivot (số nến 2 bên cần thấp/cao hơn để xác nhận đỉnh/đáy). Giá trị cao hơn = ít mức hơn nhưng được xác nhận chắc chắn hơn. Khuyến nghị: 5-10 cho khung H1-H4."
t4 = "Độ nhô tối thiểu của pivot, tính theo ATR. Giúp lọc bỏ các pivot yếu, không đủ rõ ràng. VD: 0.15 = pivot phải nhô ít nhất 0.15×ATR so với nến lân cận."
t5 = "Nếu vùng mới nằm gần vùng cũ hơn khoảng cách này (tính theo ATR), nó sẽ được gộp vào vùng cũ thay vì tạo vùng trùng lặp. Giá trị cao hơn = gộp mạnh tay hơn, ít vùng hơn."
t6 = "Độ dày của vùng giá, tính theo ATR. Giá trị cao hơn = vùng dày hơn, dễ phản ứng nhưng dễ chồng lấn nhau."
t7 = "Số nến dùng để tính khối lượng trung bình nền, làm cơ sở so sánh khi chấm điểm vùng."
t8 = "Độ dài EMA dùng để chấm điểm mức độ thuận xu hướng. Vùng hỗ trợ nằm trên EMA và vùng kháng cự nằm dưới EMA sẽ được cộng điểm mạnh hơn."
t9 = "Điều chỉnh mức giá phải đóng cửa vượt qua vùng bao xa (tính theo ATR) trước khi vùng đó bị coi là phá vỡ. Giá trị cao hơn = khó vỡ hơn, ít phá vỡ giả hơn."
t10 = "Hiện hoặc ẩn các thanh sức mạnh bên mua/bên bán bên trong mỗi vùng S/R."
t11 = "Hiện hoặc ẩn nhãn chữ thông minh (VD: Strong Support) bên trong mỗi vùng S/R."
t12 = "Bộ lọc tuỳ chọn cho các vùng được hiển thị (Tất cả / Chỉ hỗ trợ / Chỉ kháng cự)."
t13 = "Xoá vùng sau số nến này. Cũng góp phần vào việc suy giảm điểm xếp hạng theo tuổi." // Lưu ý: chưa gắn với input nào trong code gốc, đang cố định 450 nến — giữ nguyên như bản gốc, không bổ sung input mới.
t14 = "Hiện hoặc ẩn bảng dashboard thu gọn." // Lưu ý: tính năng dashboard chưa được cài đặt trong code gốc — giữ nguyên như bản gốc.
t15 = "Hiện hoặc ẩn nhãn khi vùng bị phá vỡ." // Lưu ý: tính năng nhãn phá vỡ chưa được cài đặt trong code gốc — giữ nguyên như bản gốc.
t16 = "Điều chỉnh cỡ chữ của nhãn phần trăm sức mạnh."
t17 = "Điều chỉnh cỡ chữ của nhãn chữ trong vùng."
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Inputs (Đã Việt hóa) {
gRank = "🏆 Xếp Hạng"
gDetect = "📐 Nhận Diện Swing"
gScore = "🧮 Cỗ Máy Tính Điểm"
gDisplay = "👁️ Hiển Thị"
gColors = "🎨 Màu Sắc"
gBreaks = "🧱 Phá Vỡ"
visibleLimit = input.int(8, "Hiện Top Bao Nhiêu Vùng", minval = 1, maxval = 30, tooltip = t1, group = gRank)
storedLimit = input.int(60, "Số Vùng Lưu Tối Đa", minval = 10, maxval = 250, tooltip = t2, group = gRank)
pivotSpan = input.int(5, "Độ Dài Pivot", minval = 2, maxval = 50, tooltip = t3, group = gDetect)
minSwingAtr = input.float(0.15, "ATR Pivot Tối Thiểu", minval = 0.0, maxval = 5.0, step = 0.05, tooltip = t4, group = gDetect)
absorbAtr = input.float(0.55, "Ngưỡng Hấp Thụ Vùng Gần", minval = 0.0, maxval = 3.0, step = 0.05, tooltip = t5, group = gDetect)
zoneAtrWidth = input.float(0.40, "Độ Dày Vùng (ATR)", minval = 0.05, maxval = 2.0, step = 0.05, tooltip = t6, group = gDetect)
volLen = input.int(20, "Độ Dài MA Khối Lượng", minval = 1, tooltip = t7, group = gScore)
trendLen = input.int(50, "Độ Dài EMA Xu Hướng", minval = 1, tooltip = t8, group = gScore)
breakAtr = input.float(0.12, "Vùng Đệm Phá Vỡ (ATR)", minval = 0.0, maxval = 3.0, step = 0.05, tooltip = t9, group = gBreaks)
keepBrokenCount = input.int(4, "Số Vùng Vỡ Giữ Lại", minval = 0, maxval = 30, group = gBreaks)
directionFilter = input.string("All", "Bộ Lọc Hướng", options = ["All", "Support Only", "Resistance Only"], tooltip = t12, group = gDisplay)
showStrengthBars = input.bool(true, "Hiện Thanh Sức Mạnh", tooltip = t10, group = gDisplay)
showZoneText = input.bool(true, "Hiện Chữ Trong Vùng", tooltip = t11, group = gDisplay)
strengthTextSize = input.string("auto", "Cỡ Chữ % Sức Mạnh", options = ["auto", "tiny", "small", "normal", "large", "huge"], tooltip = t16, group = gDisplay)
zoneTextSize = input.string("auto", "Cỡ Chữ Nhãn Vùng", options = ["auto", "tiny", "small", "normal", "large", "huge"], tooltip = t17, group = gDisplay)
supportColor = input.color(color.rgb(26, 216, 194, 50), "Hỗ Trợ", inline = "c", group = gColors)
resistanceColor = input.color(color.rgb(216, 76, 26, 50), "Kháng Cự", inline = "c", group = gColors)
brokenColor = input.color(color.rgb(148, 163, 184, 72), "Đã Vỡ", group = gColors)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Core calculations {
atrRaw = ta.atr(14)
atr = na(atrRaw) or atrRaw == 0 ? syminfo.mintick * 10.0 : atrRaw
volBase = ta.sma(volume, volLen)
trendBase = ta.ema(close, trendLen)
barMs = timeframe.in_seconds(timeframe.period) * 1000
pivotHigh = ta.pivothigh(high, pivotSpan, pivotSpan)
pivotLow = ta.pivotlow(low, pivotSpan, pivotSpan)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Zone type {
type RankedZone
float score
float top
float bottom
float mid
int dir
int bornBar
int leftTime
float width
float mitigation
float touchCount
float volScore
float trendScore
float swingScore
int bullStrength
int bearStrength
bool broken
line centerLine
box bodyBox
box bullBar
box bearBar
label eventLabel
var array srZones = array.new()
var array brokenZones = array.new()
var string previousLeaderKey = ""
var string lastBreakSide = "None"
var int lastBreakIndex = na
newTopRankAlert = false
supportCreatedAlert = false
resistanceCreatedAlert = false
topSupportAlert = false
topResistanceAlert = false
supportBreakAlert = false
resistanceBreakAlert = false
supportTouchAlert = false
resistanceTouchAlert = false
supportMitigatedAlert = false
resistanceMitigatedAlert = false
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Methods {
method canDisplay(RankedZone z) =>
directionFilter == "All" or directionFilter == "Support Only" and z.dir == -1 or directionFilter == "Resistance Only" and z.dir == 1
method overlaps(RankedZone z, float newTop, float newBottom, int newDir) =>
float overlapTop = math.min(z.top, newTop)
float overlapBottom = math.max(z.bottom, newBottom)
float overlap = math.max(overlapTop - overlapBottom, 0)
float smaller = math.min(math.max(z.top - z.bottom, syminfo.mintick), math.max(newTop - newBottom, syminfo.mintick))
bool sameSide = z.dir == newDir
bool closeMid = math.abs(z.mid - math.avg(newTop, newBottom)) <= absorbAtr * atr
bool meaningfulOverlap = overlap / smaller >= 0.35
sameSide and (closeMid or meaningfulOverlap)
method refreshGeometry(RankedZone z) =>
line.set_y1(z.centerLine, z.mid)
line.set_y2(z.centerLine, z.mid)
line.set_x2(z.centerLine, bar_index + 1)
box.set_top(z.bodyBox, z.top)
box.set_bottom(z.bodyBox, z.bottom)
box.set_right(z.bodyBox, time + barMs * 25)
method deleteZone(RankedZone z) =>
line.delete(z.centerLine)
box.delete(z.bodyBox)
box.delete(z.bullBar)
box.delete(z.bearBar)
if not na(z.eventLabel)
label.delete(z.eventLabel)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Scoring helpers {
swing_quality(float price, int dir) =>
float quality = 0.0
if dir == 1
float localMax = math.max(high[pivotSpan - 1], high[pivotSpan + 1])
quality := math.max((price - localMax) / atr, 0)
else
float localMin = math.min(low[pivotSpan - 1], low[pivotSpan + 1])
quality := math.max((localMin - price) / atr, 0)
quality
score(float width, float volScore, float trendScore, float swingScore, float mitigation, float touches, int age) =>
float sizeNorm = math.min(width / atr, 1.0)
float volNorm = math.min(volScore / 2.0, 1.0)
float swingNorm = math.min(swingScore / 1.5, 1.0)
float touchNorm = math.min(touches / 4.0, 1.0)
float ageNorm = math.min(age / math.max(450, 1), 1.0)
float raw = sizeNorm * 20.0 + volNorm * 18.0 + trendScore * 12.0 + swingNorm * 28.0 + touchNorm * 16.0 + 10.0
float penalty = mitigation * 22.0 + ageNorm * 10.0
math.max(math.min(raw - penalty, 100.0), 0.0)
strengths(int dir, float score, float trendScore, float mitigation) =>
float scoreNorm = math.max(math.min(score, 100.0), 0.0)
float zoneSide = scoreNorm
if scoreNorm >= 75
zoneSide := 72 + (scoreNorm - 75) * 1.12
else if scoreNorm >= 45
zoneSide := 48 + (scoreNorm - 45) * 0.75
else
zoneSide := scoreNorm * 1.05
zoneSide += trendScore * 6.0
zoneSide -= mitigation * 28.0
zoneSide := math.max(math.min(zoneSide, 100.0), 0.0)
float oppositeSide = 5.0 + mitigation * 65.0 + scoreNorm * 0.08
oppositeSide := math.max(math.min(oppositeSide, 100.0), 0.0)
int bull = dir == -1 ? int(zoneSide) : int(oppositeSide)
int bear = dir == 1 ? int(zoneSide) : int(oppositeSide)
[bull, bear]
zone_text(RankedZone z) =>
string txt = ""
if z.dir == -1
txt := z.mitigation >= 0.75 ? "Mitigated Support" : z.bullStrength >= 70 ? "Strong Support" : z.bullStrength >= 45 ? "Support" : "Weak Support"
else
txt := z.mitigation >= 0.75 ? "Mitigated Resistance" : z.bearStrength >= 70 ? "Strong Resistance" : z.bearStrength >= 45 ? "Resistance" : "Weak Resistance"
txt
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Create zones {
add_zone(float pivotPrice, int dir, int pivotBar, int pivotTime) =>
float half = zoneAtrWidth * atr * 0.5
float top = pivotPrice + half
float bottom = pivotPrice - half
float mid = pivotPrice
float width = math.max(top - bottom, syminfo.mintick)
float volScore = volBase != 0 ? volume[pivotSpan] / volBase : 1.0
float trendScore = dir == -1 and pivotPrice > trendBase ? 1.0 : dir == 1 and pivotPrice < trendBase ? 1.0 : 0.0
float swingScore = swing_quality(pivotPrice, dir)
float mitigation = 0.0
float touches = 0.0
int age = bar_index - pivotBar
float score = score(width, volScore, trendScore, swingScore, mitigation, touches, age)
[bullStrength, bearStrength] = strengths(dir, score, trendScore, mitigation)
bool absorbed = false
bool created = false
if srZones.size() > 0
for i = 0 to srZones.size() - 1
RankedZone old = srZones.get(i)
if old.overlaps(top, bottom, dir) and not absorbed
old.top := math.max(old.top, top)
old.bottom := math.min(old.bottom, bottom)
old.mid := math.avg(old.top, old.bottom)
old.width := math.max(old.top - old.bottom, syminfo.mintick)
old.bornBar := math.min(old.bornBar, pivotBar)
old.leftTime := math.min(old.leftTime, pivotTime)
old.volScore := math.max(old.volScore, volScore)
old.trendScore := math.max(old.trendScore, trendScore)
old.swingScore := math.max(old.swingScore, swingScore)
old.mitigation := 0.0
old.touchCount += 1.0
old.score := math.max(old.score, score) + 3.0
old.bullStrength := math.max(old.bullStrength, bullStrength)
old.bearStrength := math.max(old.bearStrength, bearStrength)
old.refreshGeometry()
srZones.set(i, old)
absorbed := true
if not absorbed
color baseColor = dir == -1 ? supportColor : resistanceColor
line ln = line.new(pivotBar, mid, bar_index + 1, mid, extend = extend.right, color = color.new(baseColor, 100), width = 1)
box bx = box.new(left = pivotTime, right = time + barMs * 5, top = top, bottom = bottom, xloc = xloc.bar_time, bgcolor = color.new(baseColor, 78), border_color = color.new(baseColor, 30))
float zoneMid = math.avg(top, bottom)
box bearBox = box.new(left = pivotTime, right = pivotTime, top = top, bottom = zoneMid, xloc = xloc.bar_time, bgcolor = resistanceColor, border_color = na)
box bullBox = box.new(left = pivotTime, right = pivotTime, top = zoneMid, bottom = bottom, xloc = xloc.bar_time, bgcolor = supportColor, border_color = na)
srZones.push(RankedZone.new(score, top, bottom, mid, dir, pivotBar, pivotTime, width, mitigation, touches, volScore, trendScore, swingScore, bullStrength, bearStrength, false, ln, bx, bullBox, bearBox, na))
created := true
created
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Add confirmed pivots {
if not na(pivotHigh)
float sq = swing_quality(pivotHigh, 1)
if sq >= minSwingAtr
bool createdResistance = add_zone(pivotHigh, 1, bar_index - pivotSpan, time[pivotSpan])
resistanceCreatedAlert := createdResistance
if not na(pivotLow)
float sq = swing_quality(pivotLow, -1)
if sq >= minSwingAtr
bool createdSupport = add_zone(pivotLow, -1, bar_index - pivotSpan, time[pivotSpan])
supportCreatedAlert := createdSupport
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Update zones {
if srZones.size() > 0
for i = srZones.size() - 1 to 0
RankedZone z = srZones.get(i)
int age = bar_index - z.bornBar
bool expired = age > 450
bool touched = high >= z.bottom and low <= z.top
bool brokenSupport = z.dir == -1 and close < z.bottom - breakAtr * atr
bool brokenResistance = z.dir == 1 and close > z.top + breakAtr * atr
if touched and not brokenSupport and not brokenResistance
bool wasUnderMitigationThreshold = z.mitigation < 0.75
supportTouchAlert := supportTouchAlert or z.dir == -1
resistanceTouchAlert := resistanceTouchAlert or z.dir == 1
z.touchCount += 0.20
float fillDistance = z.dir == -1 ? z.top - low : high - z.bottom
z.mitigation := math.min(math.max(fillDistance / math.max(z.width, syminfo.mintick), 0.0), 1.0)
supportMitigatedAlert := supportMitigatedAlert or z.dir == -1 and wasUnderMitigationThreshold and z.mitigation >= 0.75
resistanceMitigatedAlert := resistanceMitigatedAlert or z.dir == 1 and wasUnderMitigationThreshold and z.mitigation >= 0.75
z.score := score(z.width, z.volScore, z.trendScore, z.swingScore, z.mitigation, z.touchCount, age)
[newBull, newBear] = strengths(z.dir, z.score, z.trendScore, z.mitigation)
z.bullStrength := newBull
z.bearStrength := newBear
if expired
z.deleteZone()
srZones.remove(i)
else if brokenSupport or brokenResistance
z.broken := true
lastBreakSide := brokenResistance ? "Bullish Break" : "Bearish Break"
lastBreakIndex := bar_index
supportBreakAlert := supportBreakAlert or brokenSupport
resistanceBreakAlert := resistanceBreakAlert or brokenResistance
line.set_color(z.centerLine, color.new(brokenColor, 0))
line.set_style(z.centerLine, line.style_dotted)
line.set_extend(z.centerLine, extend.none)
line.set_x2(z.centerLine, bar_index)
box.set_bgcolor(z.bodyBox, color.new(brokenColor, 88))
box.set_border_color(z.bodyBox, color.new(brokenColor, 100))
box.set_right(z.bodyBox, time)
box.set_bgcolor(z.bullBar, color.new(color.gray, 100))
box.set_bgcolor(z.bearBar, color.new(color.gray, 100))
box.set_text(z.bodyBox, "")
box.set_text(z.bullBar, "")
box.set_text(z.bearBar, "")
if keepBrokenCount > 0
brokenZones.push(z)
while brokenZones.size() > keepBrokenCount
RankedZone oldBroken = brokenZones.shift()
oldBroken.deleteZone()
else
z.deleteZone()
srZones.remove(i)
else
z.refreshGeometry()
srZones.set(i, z)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Sort and limit zones {
srZones.sort(order.descending, sort_field = "score")
if srZones.size() > 0
RankedZone leader = srZones.get(0)
string leaderKey = str.tostring(leader.leftTime) + "_" + str.tostring(leader.dir)
bool newLeader = leaderKey != previousLeaderKey
newTopRankAlert := newLeader
topSupportAlert := newLeader and leader.dir == -1
topResistanceAlert := newLeader and leader.dir == 1
previousLeaderKey := leaderKey
while srZones.size() > storedLimit
RankedZone removed = srZones.pop()
removed.deleteZone()
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Display zones {
int visibleCount = 0
float nearestSupport = na
float nearestResistance = na
if srZones.size() > 0
for i = 0 to srZones.size() - 1
RankedZone z = srZones.get(i)
bool pass = z.canDisplay()
bool visible = pass and visibleCount < visibleLimit
if visible
visibleCount += 1
color baseColor = z.dir == -1 ? supportColor : resistanceColor
int left = z.leftTime
int right = time + barMs * 25
int maxBarWidth = int((right - left) * 0.82)
int bullWidth = int(maxBarWidth * z.bullStrength / 100)
int bearWidth = int(maxBarWidth * z.bearStrength / 100)
float middle = math.avg(z.top, z.bottom)
box.set_bgcolor(z.bodyBox, visible ? color.new(baseColor, 78) : color.new(color.gray, 100))
box.set_border_color(z.bodyBox, visible ? color.new(baseColor, 25) : color.new(color.gray, 100))
line.set_color(z.centerLine, color.new(baseColor, 100))
line.set_width(z.centerLine, 1)
if visible and showStrengthBars
box.set_left(z.bearBar, left)
box.set_right(z.bearBar, left + bearWidth)
box.set_top(z.bearBar, z.top)
box.set_bottom(z.bearBar, middle)
box.set_bgcolor(z.bearBar, color.new(resistanceColor, 10))
box.set_border_color(z.bearBar, na)
box.set_text(z.bearBar, str.tostring(z.bearStrength) + "%")
box.set_text_color(z.bearBar, color.white)
box.set_text_halign(z.bearBar, text.align_center)
box.set_text_valign(z.bearBar, text.align_center)
box.set_text_size(z.bearBar, strengthTextSize)
box.set_left(z.bullBar, left)
box.set_right(z.bullBar, left + bullWidth)
box.set_top(z.bullBar, middle)
box.set_bottom(z.bullBar, z.bottom)
box.set_bgcolor(z.bullBar, color.new(supportColor, 10))
box.set_border_color(z.bullBar, na)
box.set_text(z.bullBar, str.tostring(z.bullStrength) + "%")
box.set_text_color(z.bullBar, color.white)
box.set_text_halign(z.bullBar, text.align_center)
box.set_text_valign(z.bullBar, text.align_center)
box.set_text_size(z.bullBar, strengthTextSize)
box.set_text(z.bodyBox, showZoneText ? zone_text(z) : "")
box.set_text_halign(z.bodyBox, text.align_right)
box.set_text_valign(z.bodyBox, text.align_center)
box.set_text_color(z.bodyBox, chart.fg_color)
box.set_text_size(z.bodyBox, zoneTextSize)
else
box.set_bgcolor(z.bullBar, color.new(color.gray, 100))
box.set_bgcolor(z.bearBar, color.new(color.gray, 100))
box.set_border_color(z.bullBar, color.new(color.gray, 100))
box.set_border_color(z.bearBar, color.new(color.gray, 100))
box.set_text(z.bullBar, "")
box.set_text(z.bearBar, "")
box.set_text(z.bodyBox, "")
if z.dir == -1 and z.mid < close
nearestSupport := na(nearestSupport) or z.mid > nearestSupport ? z.mid : nearestSupport
if z.dir == 1 and z.mid > close
nearestResistance := na(nearestResistance) or z.mid < nearestResistance ? z.mid : nearestResistance
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
// ~~ Alerts (Đã Việt hóa) {
alertcondition(supportCreatedAlert, "Tạo Vùng Hỗ Trợ", "Một vùng hỗ trợ được xếp hạng mới vừa được tạo.")
alertcondition(resistanceCreatedAlert, "Tạo Vùng Kháng Cự", "Một vùng kháng cự được xếp hạng mới vừa được tạo.")
alertcondition(topSupportAlert, "Vùng Hỗ Trợ Dẫn Đầu Mới", "Một vùng hỗ trợ vừa trở thành vùng đang hoạt động có điểm xếp hạng cao nhất.")
alertcondition(topResistanceAlert, "Vùng Kháng Cự Dẫn Đầu Mới", "Một vùng kháng cự vừa trở thành vùng đang hoạt động có điểm xếp hạng cao nhất.")
alertcondition(newTopRankAlert, "Vùng S/R Dẫn Đầu Mới", "Một vùng hỗ trợ/kháng cự vừa trở thành vùng đang hoạt động có điểm xếp hạng cao nhất.")
alertcondition(supportBreakAlert, "Vỡ Vùng Hỗ Trợ", "Một vùng hỗ trợ được xếp hạng đã bị phá vỡ bởi giá đóng cửa.")
alertcondition(resistanceBreakAlert, "Vỡ Vùng Kháng Cự", "Một vùng kháng cự được xếp hạng đã bị phá vỡ bởi giá đóng cửa.")
alertcondition(supportTouchAlert, "Bắt Đầu Mitigation Vùng Hỗ Trợ", "Giá đã chạm hoặc bắt đầu ăn sâu vào một vùng hỗ trợ được xếp hạng.")
alertcondition(resistanceTouchAlert, "Bắt Đầu Mitigation Vùng Kháng Cự", "Giá đã chạm hoặc bắt đầu ăn sâu vào một vùng kháng cự được xếp hạng.")
alertcondition(supportMitigatedAlert, "Vùng Hỗ Trợ Đã Bị Mitigation", "Một vùng hỗ trợ được xếp hạng đã đạt ngưỡng mitigation.")
alertcondition(resistanceMitigatedAlert, "Vùng Kháng Cự Đã Bị Mitigation", "Một vùng kháng cự được xếp hạng đã đạt ngưỡng mitigation.")
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~}
Indicator Insider

HTF Candle Profile Indicator – Strategy
An indicator that builds a Volume Profile chart for exactly one higher-timeframe (HTF) candle currently forming, based on the volume of the current-timeframe bars falling inside it, then projects the whole analysis as a visual diagram right next to the chart.

Bollinger Bands Regression Forecast Indicator– Strategy
Traditional Bollinger Bands combined with a projected “forecast cone” — linearly extrapolating the trend of the middle line and the band width, showing what the bands might look like in the future.

Bollinger Bands Forecast with Signals Indicator – Strategy
Standard Bollinger Bands plus a forward-extrapolated “forecast cone”, an illustrative zigzag+random-walk price path threading through that cone, and an independent mean-reversion signal engine that fires when price gets rejected at the upper/lower band.
