What it does
SD Projections finds the manipulation leg behind each change in the state of delivery and projects it forward in standard deviations. It draws targets and confluence; it does not give buy or sell signals in the main edition.
How it works
- A run of down candles makes a low; the first candle's open is the CISD level.
- When a candle closes above that open, the run's low becomes level 1 and its high level 0 (mirror for bearish legs).
- Optional filters: the leg must sweep the lowest low of the previous 20 bars and must be at least 0.5 × ATR(14).
- Levels 1, 0, −1, −2, −2.5 and −4 are drawn. The −2/−2.5 block is the primary reaction area; the −4/−4.5 block is maximum expansion.
- Events are marked: −2 reached, a close beyond −2.5 (continuation to −4 becomes more likely), −4 hit, and invalidation on a close back beyond 1.
- Unmitigated fair value gaps and equal highs/lows are drawn; a ◆ marks a projection level that lines up with one of them, or a −4 that sits inside another projection's −2/−2.5.
How to use it
Work top-down: use a higher timeframe for direction, put the indicator on the middle timeframe to find legs and targets, and execute on a lower timeframe. Treat −2/−2.5 as the first objective and give most weight to levels marked ◆. The ProSuite edition adds an order-block retest entry with a stop beyond the sweep and targets at −2 and −4.
Settings
| Input | Default | What it changes |
|---|---|---|
| Levels | 1, 0, −1, −2, −2.5, −4 | Which projection lines are drawn |
| Leg must sweep liquidity | On | Only legs that take a prior low/high qualify |
| Min leg size | 0.5 × ATR | Filters out small, noisy legs |
| Points of interest | On | FVGs and equal highs/lows used for ◆ confluence |
Limitations
Projections are a framework, not a forecast; a trending market can run through every level. Average legs tend to reach −2/−2.5, small expanding legs −4, and very large legs often only −1, so read the leg size before expecting a target.
Pine Script source
In TradingView open Pine Editor, paste the code, click Save and then Add to chart.
SD_Projections_v1.0.pine
393 lines
//@version=6
// SD Projections v1.0: standard deviation projections of manipulation legs, level blocks and points of interest.
// Method (TTrades, "Standard Deviation Projections"):
// Manipulation leg : the swing that sweeps liquidity just before a change in the state of delivery (CISD):
// the final low before a bullish CISD, the final high before a bearish CISD.
// Anchor : bullish → 1 = the low, 0 = the high that made that low; bearish → 1 = the high, 0 = the low.
// Levels : 1, 0, -1, -2, -2.5, -4. -2 / -2.5 = primary reaction area ("low-hanging fruit");
// a body close beyond -2 / -2.5 → continuation and max expansion more likely; -4 = max expansion.
// Confluence : a level that lines up with a PD array (FVG, equal highs / lows); the -4 of one projection
// inside the -2 / -2.5 of another = high-probability reaction zone.
// Leg size : average leg → -2 / -2.5, expanding → -4 / -4.5, large leg → expect only -1.
// Projections are targets and confluence, not signals. Decisions on bar close.
indicator("SD Projections v1.0", "SD Proj", overlay = true, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500, max_bars_back = 500)
// ───────────────────────────── inputs
grpL = "Manipulation legs"
needSweep = input.bool(true, "Leg must sweep liquidity (prior low / high)", group = grpL)
sweepLook = input.int(20, "Liquidity lookback (bars before the leg)", minval = 3, maxval = 200, group = grpL)
minLegAtr = input.float(0.5, "Min leg size (x ATR14)", minval = 0, step = 0.1, group = grpL)
showBull = input.bool(true, "Bullish projections", inline = "dir", group = grpL)
showBear = input.bool(true, "Bearish projections", inline = "dir", group = grpL)
keepN = input.int(6, "Projections to keep", minval = 1, maxval = 30, group = grpL)
grpP = "Projection"
levelsIn = input.string("1, 0, -1, -2, -2.5, -4", "Levels", group = grpP)
projBars = input.int(60, "Length to the right (bars)", minval = 5, maxval = 400, group = grpP)
showBand = input.bool(true, "Shade the manipulation leg", group = grpP)
blockPri = input.bool(true, "Block −2 to −2.5 (primary reaction)", group = grpP)
blockMax = input.bool(true, "Block −4 to −4.5 (max expansion)", group = grpP)
showEvt = input.bool(true, "Events: −2 reached, continuation, −4 hit, invalidated", group = grpP)
legSize = input.bool(true, "Leg size note (large leg → −1, expanding → −4)", group = grpP)
grpC = "Points of interest and confluence"
showFvg = input.bool(true, "Fair value gaps (unmitigated)", group = grpC)
fvgKeep = input.int(8, "FVGs to keep", minval = 1, maxval = 40, group = grpC)
showEq = input.bool(true, "Equal highs / lows", group = grpC)
pivLen = input.int(3, "Swing strength (bars each side)", minval = 1, maxval = 10, group = grpC)
eqTolAtr = input.float(0.1, "Equal highs / lows tolerance (x ATR14)", minval = 0, step = 0.05, group = grpC)
confTolAtr = input.float(0.15, "Confluence tolerance (x ATR14)", minval = 0, step = 0.05, group = grpC)
showStack = input.bool(true, "−4 inside another −2 / −2.5 (stacked zone)", group = grpC)
grpV = "Style"
themeIn = input.string("Auto", "Theme", options = ["Auto", "Light", "Dark"], group = grpV)
labelLeft = input.bool(true, "Level labels on the left (TTrades style)", group = grpV)
showPanel = input.bool(true, "Panel", group = grpV)
panelPos = input.string("Top right", "Panel position", options = ["Top right", "Bottom right", "Top left", "Bottom left"], group = grpV)
panelSize = input.string("Small", "Panel size", options = ["Tiny", "Small", "Normal"], group = grpV)
alOn = input.bool(true, "Alerts (create one alert with 'Any alert() function call')", group = "Alerts")
// ───────────────────────────── palette
bgLum = 0.299 * color.r(chart.bg_color) + 0.587 * color.g(chart.bg_color) + 0.114 * color.b(chart.bg_color)
isDark = themeIn == "Dark" or (themeIn == "Auto" and bgLum < 128)
cBull = isDark ? #4C9AFF : #1E63D6
cBear = isDark ? #FF5C7A : #D02F4A
cLevel = isDark ? #9AA0AC : #6B7080
cText = isDark ? #D1D4DC : #131722
cMuted = isDark ? #787B86 : #8A8E99
cBand = isDark ? #FFFFFF : #000000
cPanel = isDark ? #1E222D : #FFFFFF
cHead = isDark ? #2A2E39 : #F0F3FA
cBorder = isDark ? #363A45 : #E0E3EB
cGold = isDark ? #FFD54F : #E0A800
cPoi = isDark ? #B39DDB : #7E57C2
atr = ta.atr(14)
ph = ta.pivothigh(high, pivLen, pivLen)
pl = ta.pivotlow(low, pivLen, pivLen)
// ───────────────────────────── types and state
type Proj
int id
bool bull
float zero // the high (low) that made the low (high)
float one // the sweep extreme
int bLeg // first bar of the leg
int bExt // bar of the extreme
int bCisd
int bEnd
bool live = true
bool hit2 = false
bool cont = false
bool hit4 = false
bool failed = false
string note = ""
array<line> lines
array<label> labels
array<box> boxes
type Poi
bool bull // bullish FVG / equal lows (support), bearish FVG / equal highs (resistance)
bool isFvg
float top
float bottom
int b1
box bx
line ln
label lb
var lv = array.new<float>()
if barstate.isfirst
for s in str.split(str.replace_all(levelsIn, " ", ""), ",")
v = str.tonumber(s)
if not na(v)
lv.push(v)
var projs = array.new<Proj>()
var fvgs = array.new<Poi>()
var eqs = array.new<Poi>()
var legs = array.new<float>() // recent leg sizes, for the leg-size note
var int nextId = 0
var int nStack = 0
var int nPoiConf = 0
// CISD engine
var bool prevDn = false
var bool prevUp = false
var bool pendBull = false
var bool pendBear = false
var float bullLevel = na
var float bearLevel = na
var float legLow = na
var float legHigh = na
var int bLow = na
var int bHigh = na
var int bRunDn = na
var int bRunUp = na
// swing points for equal highs / lows
var float lastPH = na
var int lastPHb = na
var float lastPL = na
var int lastPLb = na
// ───────────────────────────── helpers
lvl(Proj p, float k) =>
p.zero + k * (p.one - p.zero)
fmtK(float k) =>
str.tostring(k, "#.#")
notify(string msg) =>
if alOn
alert("SD Proj · " + syminfo.ticker + " · " + msg, alert.freq_once_per_bar_close)
method wipe(Proj p) =>
for l in p.lines
l.delete()
for lb in p.labels
lb.delete()
for b in p.boxes
b.delete()
// a point of interest near price y that a projection in this direction is reaching for
poiNear(bool bullProj, float y) =>
tol = confTolAtr * nz(atr)
string hit = ""
for q in fvgs
if q.bull != bullProj and y <= q.top + tol and y >= q.bottom - tol
hit := "FVG"
for q in eqs
if q.bull != bullProj and math.abs(y - q.top) <= tol
hit := q.bull ? "EQL" : "EQH"
hit
drawProj(Proj p) =>
c = p.bull ? cBull : cBear
x1 = p.bLeg
xr = p.bCisd + projBars
if showBand
p.boxes.push(box.new(x1, math.max(p.zero, p.one), p.bCisd, math.min(p.zero, p.one), border_color = #00000000, bgcolor = color.new(cBand, 94)))
if blockPri
a = lvl(p, -2)
b = lvl(p, -2.5)
p.boxes.push(box.new(p.bCisd, math.max(a, b), xr, math.min(a, b), border_color = color.new(c, 70), bgcolor = color.new(c, 86), text = "−2 / −2.5", text_size = size.tiny, text_color = color.new(c, 20), text_halign = text.align_right, text_valign = text.align_center))
if blockMax
a = lvl(p, -4)
b = lvl(p, -4.5)
p.boxes.push(box.new(p.bCisd, math.max(a, b), xr, math.min(a, b), border_color = color.new(c, 70), bgcolor = color.new(c, 80), text = "max expansion", text_size = size.tiny, text_color = color.new(c, 20), text_halign = text.align_right, text_valign = text.align_center))
for k in lv
y = lvl(p, k)
anch = k == 1 or k == 0
key = k == -2 or k == -2.5 or k == -4
lc = anch ? c : key ? color.new(c, 10) : cLevel
p.lines.push(line.new(anch ? x1 : p.bCisd, y, xr, y, color = lc, style = anch or key ? line.style_solid : line.style_dotted, width = key ? 2 : 1))
poi = key ? poiNear(p.bull, y) : ""
txt = fmtK(k) + (poi != "" ? " ◆ " + poi : "")
p.labels.push(label.new(labelLeft ? (anch ? x1 : p.bCisd) : xr, y, txt, color = #00000000, style = labelLeft ? label.style_label_right : label.style_label_left, textcolor = poi != "" ? cGold : anch ? c : key ? c : cLevel, size = size.small))
// leg line from 0 to 1
p.lines.push(line.new(x1, p.zero, p.bExt, p.one, color = color.new(c, 30), width = 2))
if legSize and p.note != ""
p.labels.push(label.new(p.bExt, p.one, p.note, color = #00000000, style = p.bull ? label.style_label_up : label.style_label_down, textcolor = c, size = size.tiny))
// TTrades stacked zone: -4 of one projection inside the -2 / -2.5 of another (same direction)
stack(Proj p) =>
n = 0
if showStack
for q in projs
if q.id != p.id and q.live and q.bull == p.bull
for pair in array.from(0, 1)
a = pair == 0 ? p : q
b = pair == 0 ? q : p
y = lvl(a, -4)
lo = math.min(lvl(b, -2), lvl(b, -2.5))
hi = math.max(lvl(b, -2), lvl(b, -2.5))
if y >= lo and y <= hi
n += 1
p.boxes.push(box.new(math.max(p.bCisd, q.bCisd), hi, math.max(p.bEnd, q.bEnd), lo, border_color = cGold, border_style = line.style_dashed, bgcolor = color.new(cGold, 80), text = "◆ −4 / −2 stack", text_color = cGold, text_size = size.tiny, text_halign = text.align_left, text_valign = text.align_top))
n
// ───────────────────────────── main (closed bars only)
if barstate.isconfirmed
// ---- points of interest: fair value gaps
if showFvg
if high[2] < low
fvgs.push(Poi.new(true, true, low, high[2], bar_index - 2, box.new(bar_index - 2, low, bar_index + 5, high[2], border_color = #00000000, bgcolor = color.new(cPoi, 85), text = "FVG", text_size = size.tiny, text_color = color.new(cPoi, 20), text_halign = text.align_left)))
if low[2] > high
fvgs.push(Poi.new(false, true, low[2], high, bar_index - 2, box.new(bar_index - 2, low[2], bar_index + 5, high, border_color = #00000000, bgcolor = color.new(cPoi, 85), text = "FVG", text_size = size.tiny, text_color = color.new(cPoi, 20), text_halign = text.align_left)))
if fvgs.size() > 0
for i = fvgs.size() - 1 to 0
q = fvgs.get(i)
// mitigated: a close through the far side
if q.bull ? close < q.bottom : close > q.top
q.bx.delete()
fvgs.remove(i)
else
q.bx.set_right(bar_index + 5)
while fvgs.size() > fvgKeep
fvgs.shift().bx.delete()
// ---- points of interest: equal highs / lows (swing points within tolerance)
if showEq
tol = eqTolAtr * nz(atr)
if not na(ph)
if not na(lastPH) and math.abs(ph - lastPH) <= tol and bar_index - pivLen - lastPHb < 150
lvlEq = math.max(ph, lastPH)
eqs.push(Poi.new(false, false, lvlEq, lvlEq, lastPHb, na, line.new(lastPHb, lvlEq, bar_index - pivLen, lvlEq, color = cPoi, width = 2), label.new(bar_index - pivLen, lvlEq, "EQH", color = #00000000, style = label.style_label_left, textcolor = cPoi, size = size.tiny)))
lastPH := ph
lastPHb := bar_index - pivLen
if not na(pl)
if not na(lastPL) and math.abs(pl - lastPL) <= tol and bar_index - pivLen - lastPLb < 150
lvlEq = math.min(pl, lastPL)
eqs.push(Poi.new(true, false, lvlEq, lvlEq, lastPLb, na, line.new(lastPLb, lvlEq, bar_index - pivLen, lvlEq, color = cPoi, width = 2), label.new(bar_index - pivLen, lvlEq, "EQL", color = #00000000, style = label.style_label_left, textcolor = cPoi, size = size.tiny)))
lastPL := pl
lastPLb := bar_index - pivLen
if eqs.size() > 0
for i = eqs.size() - 1 to 0
q = eqs.get(i)
// swept: liquidity taken, no longer a target
if q.bull ? low < q.top : high > q.top
q.ln.set_style(line.style_dotted)
q.ln.set_color(color.new(cPoi, 60))
q.lb.set_text((q.bull ? "EQL" : "EQH") + " swept")
eqs.remove(i)
while eqs.size() > 8
old = eqs.shift()
old.ln.delete()
old.lb.delete()
// ---- track every live projection
for p in projs
if p.live
sg = p.bull ? 1.0 : -1.0
c = p.bull ? cBull : cBear
if bar_index > p.bEnd
p.live := false
else if sg * (close - p.one) < 0
p.live := false
p.failed := true
for l in p.lines
l.set_color(color.new(cMuted, 70))
for b in p.boxes
b.set_bgcolor(color.new(cMuted, 94))
if showEvt
p.labels.push(label.new(bar_index, p.one, "invalidated", color = #00000000, style = p.bull ? label.style_label_down : label.style_label_up, textcolor = cMuted, size = size.tiny))
else
if not p.hit2 and sg * ((p.bull ? high : low) - lvl(p, -2)) >= 0
p.hit2 := true
notify((p.bull ? "bullish" : "bearish") + " projection reached −2 (reaction area)")
if not p.cont and sg * (close - lvl(p, -2.5)) > 0
p.cont := true
if showEvt
p.labels.push(label.new(bar_index, p.bull ? high : low, "close beyond −2.5 → −4", color = #00000000, style = p.bull ? label.style_label_down : label.style_label_up, textcolor = c, size = size.tiny))
notify((p.bull ? "bullish" : "bearish") + " body close beyond −2.5: continuation to −4 more likely")
if not p.hit4 and sg * ((p.bull ? high : low) - lvl(p, -4)) >= 0
p.hit4 := true
if showEvt
p.lines.push(line.new(p.bCisd + projBars, lvl(p, -4), bar_index, lvl(p, -4), color = cGold, width = 2))
p.labels.push(label.new(bar_index, lvl(p, -4), "−4 max expansion", color = #00000000, style = label.style_label_left, textcolor = cGold, size = size.tiny))
notify((p.bull ? "bullish" : "bearish") + " projection hit −4 (max expansion)")
// ---- manipulation legs: the swing before a CISD
isDn = close < open
isUp = close > open
minLeg = minLegAtr * nz(atr)
for dir in array.from(1, -1)
bull = dir == 1
pend = bull ? pendBull : pendBear
if pend and (bull ? low < legLow : high > legHigh)
if bull
legLow := low
bLow := bar_index
else
legHigh := high
bHigh := bar_index
trig = bull ? close > bullLevel : close < bearLevel
if pend and trig
if bull
pendBull := false
else
pendBear := false
ext = bull ? legLow : legHigh
bExt = bull ? bLow : bHigh
bRun = bull ? bRunDn : bRunUp
// 0 = the high (low) that made the low (high): extreme of the leg from the candle before the run
float zero = bull ? high[bar_index - bExt] : low[bar_index - bExt]
for i = bar_index - bExt to math.min(bar_index - bRun + 1, 400)
zero := bull ? math.max(zero, high[i]) : math.min(zero, low[i])
// liquidity: the leg took the extreme of the bars before it
float prior = na
for i = bar_index - bRun + 1 to math.min(bar_index - bRun + sweepLook, 450)
prior := na(prior) ? (bull ? low[i] : high[i]) : bull ? math.min(prior, low[i]) : math.max(prior, high[i])
swept = not needSweep or (bull ? ext < prior : ext > prior)
legSz = math.abs(zero - ext)
if swept and legSz >= minLeg and (bull ? showBull : showBear)
// leg size against recent legs: large → −1, expanding → −4
avgLeg = legs.size() > 0 ? legs.avg() : legSz
note = legSz > 1.8 * avgLeg ? "large leg → expect −1" : legSz < 0.6 * avgLeg ? "small leg → −4 / −4.5 possible" : "average leg → −2 / −2.5"
legs.push(legSz)
if legs.size() > 12
legs.shift()
p = Proj.new(nextId, bull, zero, ext, bRun - 1, bExt, bar_index, bar_index + projBars, note = note, lines = array.new<line>(), labels = array.new<label>(), boxes = array.new<box>())
nextId += 1
drawProj(p)
for k in array.from(-2.0, -2.5, -4.0)
if poiNear(bull, lvl(p, k)) != ""
nPoiConf += 1
projs.push(p)
nStack += stack(p)
notify((bull ? "bullish" : "bearish") + " manipulation leg + CISD: −2 " + str.tostring(lvl(p, -2), format.mintick) + " · −2.5 " + str.tostring(lvl(p, -2.5), format.mintick) + " · −4 " + str.tostring(lvl(p, -4), format.mintick))
if isDn and not prevDn
bullLevel := open
legLow := low
bLow := bar_index
bRunDn := bar_index
pendBull := true
if isUp and not prevUp
bearLevel := open
legHigh := high
bHigh := bar_index
bRunUp := bar_index
pendBear := true
if isDn or isUp
prevDn := isDn
prevUp := isUp
while projs.size() > keepN
projs.shift().wipe()
// ───────────────────────────── panel
tPos = panelPos == "Top right" ? position.top_right : panelPos == "Bottom right" ? position.bottom_right : panelPos == "Top left" ? position.top_left : position.bottom_left
tSz = panelSize == "Tiny" ? size.tiny : panelSize == "Normal" ? size.normal : size.small
var table panel = table.new(tPos, 2, 7, bgcolor = cPanel, frame_color = cBorder, frame_width = 1, border_color = cPanel, border_width = 3)
cell(int c, int r, string s, color tc, color bg) =>
table.cell(panel, c, r, s, text_color = tc, text_size = tSz, text_halign = c == 0 ? text.align_left : text.align_right, bgcolor = bg)
where(bool bull) =>
string s = "—"
for p in projs
if p.bull == bull
k = (close - p.zero) / (p.one - p.zero)
nxt = float(na)
for l in lv
if l < k and (na(nxt) or l > nxt)
nxt := l
s := str.tostring(k, "0.0") + " SD" + (p.failed ? " · invalid" : p.hit4 ? " · max expansion" : p.cont ? " · continuation" : p.hit2 ? " · in −2 zone" : "") + (na(nxt) or p.failed ? "" : " · next " + fmtK(nxt) + " " + str.tostring(lvl(p, nxt), format.mintick))
s
if showPanel and barstate.islast
cell(0, 0, "SD Projections", cText, cHead)
cell(1, 0, "manipulation leg → −2 / −2.5 / −4", cMuted, cHead)
cell(0, 1, "Bullish (latest)", cMuted, cPanel)
cell(1, 1, where(true), cBull, cPanel)
cell(0, 2, "Bearish (latest)", cMuted, cPanel)
cell(1, 2, where(false), cBear, cPanel)
cell(0, 3, "Levels on a PD array", cMuted, cPanel)
cell(1, 3, str.tostring(nPoiConf) + " ◆", nPoiConf > 0 ? cGold : cMuted, cPanel)
cell(0, 4, "Stacked −4 / −2 zones", cMuted, cPanel)
cell(1, 4, str.tostring(nStack), nStack > 0 ? cGold : cMuted, cPanel)
cell(0, 5, "Read", cMuted, cPanel)
cell(1, 5, "−2/−2.5 react · close beyond → −4 · −4 max", cText, cPanel)
cell(0, 6, "Not a signal", cMuted, cPanel)
cell(1, 6, "targets + confluence for your model", cMuted, cPanel)

