A classic Ichimoku cloud is built from the midpoints of highs and lows over fixed windows. It works, but it produces a blocky, laggy envelope, and – more to the point – it treats every cloud break the same way. Price pokes above the cloud, you get a signal, and a good share of those signals are noise.
This indicator, adapted to ProBuilder from the “Smart Ichimoku | GainzAlgo” pinescript, changes two things. First, it rebuilds the whole cloud with Hull moving averages instead of the classic max/min midpoints, which gives a smoother and more responsive envelope. Second, and this is the real idea, it does not accept every cloud break at face value: each break is scored by a small classifier that weighs four momentum and volatility features, and only the breaks whose probability clears a threshold are marked.
On top of that, every fresh break projects a three-level target plan (Conservative / Standard / Aggressive) derived from the recent distribution of price moves, and a quality badge rates how convincing the setup is.
The smoothed cloud. All four Ichimoku lines are Hull moving averages (average[n, 7] in ProBuilder) over the median price hl2:
The cloud used for the logic is displaced by disp bars (senkouA[disp], senkouB[disp]), so what you see over the current candle is the cloud computed disp bars ago – exactly the segment the original plots with its forward offset. cloudTop and cloudBot are the upper and lower edges of that band.
Cloud breaks. A bearish break is a close below cloudBot when the previous close was at or above it; a bullish break is the mirror above cloudTop. Nothing is drawn yet – a break is only a candidate.
The breakout classifier. Four features are computed and normalised so they live on a comparable scale:
f_rsi – RSI(14) recentred and scaled around 50f_stoch – stochastic %K(14) recentred and scaled around 50f_zscore – z-score of close vs its 20-bar meanf_dist – distance from price to the cloud edge, measured in ATR(14)They are combined into a single linear score and squashed through a logistic (sigmoid) function into a probability between 0 and 1, one for the sell side and one for the buy side:
prob = 1 / (1 + exp(-z))
The interesting part is where the weights come from. They are self-calibrated: each feature’s weight is the absolute value of its recent rolling correlation with the next-bar return, over calibLen bars, scaled by 4. A feature that has actually been predictive lately gets more say; a feature that hasn’t is turned down toward zero. During warm-up, or when a feature has no variance, its weight falls back to 1. A break is confirmed only when its probability is at or above threshold.
The target plan. When a break happens, the indicator looks at the last tpLookback bars of absolute per-bar moves and extracts three magnitudes: the mean move, an approximate median (found by counting how many moves fall under successive fractions of the mean), and the modal move (found by binning the moves in buckets of 0.25 x ATR). Each is projected from the breakout close, scaled by tpScale, giving three levels labelled Conservative, Standard and Aggressive.
The quality badge. The latest break is graded by how close its probability sits to the threshold: Bullish / Bearish Breakout (confirmed), Nearly Confirmed (within 0.10 below), Watch Setup (within 0.25), or Weak Setup.
//--------------------------------------------
// PRC_Smart-Ichimoku (by GainzAlgo)
// version = 0
// 21.07.2026
// Iván González @ www.prorealcode.com
// Sharing ProRealTime knowledge
//--------------------------------------------
// Ichimoku suavizado (HMA) + clasificador de rupturas de nube
// con pesos autocalibrados por correlacion, y plan de targets
// del ultimo cruce.
//--------------------------------------------
DEFPARAM DRAWONLASTBARONLY = TRUE
//=== INPUTS ===
lenTenkan = 9 // Tenkan
lenKijun = 26 // Kijun
lenSenkou = 52 // Senkou
disp = 26 // Displacement
calibLen = 100 // ventana de autocalibracion (20..300)
threshold = 0.60 // umbral de probabilidad de ruptura (0.5..1.0)
tpLookback = 60 // lookback de estadistica de movimiento (10..200)
tpScale = 3.0 // multiplicador de target (0.5..10.0)
//=== ICHIMOKU SUAVIZADO (nube HMA) ===
srcHL = (high + low) / 2
tenkan = average[lenTenkan, 7](srcHL)
kijun = average[lenKijun, 7](srcHL)
tkAvg = (tenkan + kijun) / 2
senkouA = average[lenKijun, 7](tkAvg)
senkouB = average[lenSenkou, 7](srcHL)
// nube "actual" = senkou calculado hace disp barras (equivale al offset de Pine)
senkouAd = senkouA[disp]
senkouBd = senkouB[disp]
cloudTop = max(senkouAd, senkouBd)
cloudBot = min(senkouAd, senkouBd)
//=== CRUCES DE NUBE ===
crossBelow = close < cloudBot and close[1] >= cloudBot[1]
crossAbove = close > cloudTop and close[1] <= cloudTop[1]
anyCross = crossBelow or crossAbove
//=== FEATURES DEL CLASIFICADOR ===
myatr = averagetruerange[14](close)
frsi = (rsi[14](close) - 50) / 25
llow = lowest[14](low)
hhigh = highest[14](high)
if hhigh - llow <> 0 then
stochK = 100 * (close - llow) / (hhigh - llow)
else
stochK = 50
endif
fstoch = (stochK - 50) / 25
sd20 = std[20](close)
if sd20 = 0 then
fzscore = 0
else
fzscore = (close - average[20](close)) / sd20
endif
if myatr = 0 then
fdistBear = 0
fdistBull = 0
else
fdistBear = (cloudBot - close) / myatr
fdistBull = (close - cloudTop) / myatr
endif
//=== PESOS AUTOCALIBRADOS ===
// r = (avg(x*y) - avg(x)*avg(y)) / (std(x)*std(y))
fwdret = close - close[1]
avgFwd = average[calibLen](fwdret)
stdFwd = std[calibLen](fwdret)
xrsi = frsi[1]
prsi = xrsi * fwdret
if barindex > calibLen and std[calibLen](xrsi) * stdFwd <> 0 then
corrRsi = (average[calibLen](prsi) - average[calibLen](xrsi) * avgFwd) / (std[calibLen](xrsi) * stdFwd)
wrsi = abs(corrRsi) * 4
else
wrsi = 1
endif
xstoch = fstoch[1]
pstoch = xstoch * fwdret
if barindex > calibLen and std[calibLen](xstoch) * stdFwd <> 0 then
corrStoch = (average[calibLen](pstoch) - average[calibLen](xstoch) * avgFwd) / (std[calibLen](xstoch) * stdFwd)
wstoch = abs(corrStoch) * 4
else
wstoch = 1
endif
xzscore = fzscore[1]
pzscore = xzscore * fwdret
if barindex > calibLen and std[calibLen](xzscore) * stdFwd <> 0 then
corrZscore = (average[calibLen](pzscore) - average[calibLen](xzscore) * avgFwd) / (std[calibLen](xzscore) * stdFwd)
wzscore = abs(corrZscore) * 4
else
wzscore = 1
endif
xdbull = fdistBull[1]
pdbull = xdbull * fwdret
if barindex > calibLen and std[calibLen](xdbull) * stdFwd <> 0 then
corrDbull = (average[calibLen](pdbull) - average[calibLen](xdbull) * avgFwd) / (std[calibLen](xdbull) * stdFwd)
wdbull = abs(corrDbull) * 4
else
wdbull = 1
endif
xdbear = fdistBear[1]
pdbear = xdbear * fwdret
if barindex > calibLen and std[calibLen](xdbear) * stdFwd <> 0 then
corrDbear = (average[calibLen](pdbear) - average[calibLen](xdbear) * avgFwd) / (std[calibLen](xdbear) * stdFwd)
wdbear = abs(corrDbear) * 4
else
wdbear = 1
endif
//=== COMBINACION LINEAL + SIGMOIDE ===
zsell = wrsi * frsi + wstoch * fstoch + wzscore * fzscore + wdbear * fdistBear
zbuy = wdbull * fdistBull - wrsi * frsi - wstoch * fstoch - wzscore * fzscore
probSell = 1 / (1 + exp(-zsell))
probBuy = 1 / (1 + exp(-zbuy))
confirmedBull = crossAbove and probBuy >= threshold
confirmedBear = crossBelow and probSell >= threshold
//=== ESTADISTICA DE MOVIMIENTO ===
barMove = abs(close - close[1])
meanMove = average[tpLookback](barMove)
once lastCrossBar = 0
once lastClose = 0
once lastIsBull = 0
once lastProb = 0.0
once lastBadgeY = 0.0
once lastMean = 0.0
once lastMedian = 0.0
once lastMode = 0.0
if anyCross then
lastCrossBar = barindex
lastClose = close
if crossAbove then
lastIsBull = 1
lastProb = probBuy
lastBadgeY = low - myatr * 0.8
else
lastIsBull = 0
lastProb = probSell
lastBadgeY = high + myatr * 0.8
endif
lastMean = meanMove
// -- mediana aproximada por conteo de umbrales --
cnt25 = 0
cnt50 = 0
cnt75 = 0
cnt100 = 0
cnt150 = 0
for i = 0 to tpLookback - 1 do
mv = abs(close[i] - close[i + 1])
if mv < meanMove * 0.25 then
cnt25 = cnt25 + 1
endif
if mv < meanMove * 0.50 then
cnt50 = cnt50 + 1
endif
if mv < meanMove * 0.75 then
cnt75 = cnt75 + 1
endif
if mv < meanMove * 1.00 then
cnt100 = cnt100 + 1
endif
if mv < meanMove * 1.50 then
cnt150 = cnt150 + 1
endif
next
halfN = tpLookback / 2
medianMult = 1.75
if cnt150 >= halfN then
medianMult = 1.25
endif
if cnt100 >= halfN then
medianMult = 0.875
endif
if cnt75 >= halfN then
medianMult = 0.625
endif
if cnt50 >= halfN then
medianMult = 0.375
endif
if cnt25 >= halfN then
medianMult = 0.25
endif
lastMedian = meanMove * medianMult
// -- moda por binning en cubos de 0.25*ATR --
binW = myatr * 0.25
bb0 = 0
bb1 = 0
bb2 = 0
bb3 = 0
bb4 = 0
bb5 = 0
for i = 0 to tpLookback - 1 do
mv = abs(close[i] - close[i + 1])
if binW > 0 then
idx = floor(mv / binW)
else
idx = 0
endif
if idx = 0 then
bb0 = bb0 + 1
elsif idx = 1 then
bb1 = bb1 + 1
elsif idx = 2 then
bb2 = bb2 + 1
elsif idx = 3 then
bb3 = bb3 + 1
elsif idx = 4 then
bb4 = bb4 + 1
else
bb5 = bb5 + 1
endif
next
modeBin = 5
if bb4 >= bb5 then
modeBin = 4
endif
if bb3 >= bb4 and bb3 >= bb5 then
modeBin = 3
endif
if bb2 >= bb3 and bb2 >= bb4 and bb2 >= bb5 then
modeBin = 2
endif
if bb1 >= bb2 and bb1 >= bb3 and bb1 >= bb4 and bb1 >= bb5 then
modeBin = 1
endif
if bb0 >= bb1 and bb0 >= bb2 and bb0 >= bb3 and bb0 >= bb4 and bb0 >= bb5 then
modeBin = 0
endif
lastMode = (modeBin + 0.5) * binW
endif
//=== REGISTRO DE SEÑALES CONFIRMADAS (arrays, X fija) ===
once nSig = 0
if confirmedBull then
$sigX[nSig] = barindex
$sigY[nSig] = low - myatr * 0.5
$sigType[nSig] = 1
nSig = nSig + 1
elsif confirmedBear then
$sigX[nSig] = barindex
$sigY[nSig] = high + myatr * 0.5
$sigType[nSig] = 2
nSig = nSig + 1
endif
//=== NUBE: relleno dinamico ===
if senkouAd > senkouBd then
cr = 0
cg = 229
cb = 255
else
cr = 255
cg = 0
cb = 127
endif
colorbetween(senkouAd, senkouBd, cr, cg, cb, 20)
//=== DIBUJO EN LA ULTIMA BARRA ===
if islastbarupdate then
// -- flechas de rupturas confirmadas (redibujo desde arrays) --
// solo las ultimas 250 señales
startK = 0
if nSig > 250 then
startK = nSig - 250
endif
for k = startK to nSig - 1 do
if $sigType[k] = 1 then
drawtext("▲", $sigX[k], $sigY[k]) coloured(57, 255, 20)
else
drawtext("▼", $sigX[k], $sigY[k]) coloured(255, 0, 127)
endif
next
// -- plan de targets del ultimo cruce --
if lastCrossBar > 0 then
if lastIsBull = 1 then
dirSign = 1
else
dirSign = -1
endif
tCons = lastClose + dirSign * lastMean * tpScale
tStd = lastClose + dirSign * lastMedian * tpScale
tAggr = lastClose + dirSign * lastMode * tpScale
lvlTop = max(tCons, max(tStd, tAggr))
lvlBot = min(tCons, min(tStd, tAggr))
lvlMid = tCons + tStd + tAggr - lvlTop - lvlBot
x1 = lastCrossBar
x2 = lastCrossBar + 60
if lastIsBull = 1 then
// bull: top=Aggressive(rosa), mid=Standard(aqua), bot=Conservative(amarillo)
drawsegment(x1, lvlTop, x2, lvlTop) coloured(255, 0, 127) style(dottedline, 2)
drawsegment(x1, lvlMid, x2, lvlMid) coloured(0, 229, 255) style(dottedline2, 2)
drawsegment(x1, lvlBot, x2, lvlBot) coloured(255, 234, 0) style(line, 2)
drawtext("Aggressive", x2, lvlTop+0.15*myatr) coloured(255, 0, 127)
drawtext("Standard", x2, lvlMid+0.15*myatr) coloured(0, 229, 255)
drawtext("Conservative", x2, lvlBot+0.15*myatr) coloured(255, 234, 0)
else
// bear: top=Conservative(amarillo), mid=Standard(aqua), bot=Aggressive(rosa)
drawsegment(x1, lvlTop, x2, lvlTop) coloured(255, 234, 0) style(line, 2)
drawsegment(x1, lvlMid, x2, lvlMid) coloured(0, 229, 255) style(dottedline2, 2)
drawsegment(x1, lvlBot, x2, lvlBot) coloured(255, 0, 127) style(dottedline, 2)
drawtext("Conservative", x2, lvlTop+0.15*myatr) coloured(255, 234, 0)
drawtext("Standard", x2, lvlMid+0.15*myatr) coloured(0, 229, 255)
drawtext("Aggressive", x2, lvlBot+0.15*myatr) coloured(255, 0, 127)
endif
// -- badge de calidad de la ultima señal --
pctv = round(lastProb * 100)
gap = threshold - lastProb
if lastProb >= threshold then
if lastIsBull = 1 then
drawtext("Bullish Breakout #pctv#%", x1, lastBadgeY) coloured(57, 255, 20)
else
drawtext("Bearish Breakout #pctv#%", x1, lastBadgeY) coloured(255, 0, 127)
endif
elsif gap <= 0.10 then
drawtext("Nearly Confirmed #pctv#%", x1, lastBadgeY) coloured(0, 176, 255)
elsif gap <= 0.25 then
drawtext("Watch Setup #pctv#%", x1, lastBadgeY) coloured(255, 109, 0)
else
drawtext("Weak Setup #pctv#%", x1, lastBadgeY) coloured(255, 193, 7)
endif
endif
endif
return senkouAd coloured(0, 229, 255, 89) style(line, 1) as "Senkou A", senkouBd coloured(255, 0, 127, 89) style(line, 1) as "Senkou B", kijun coloured(255, 255, 255, 102) style(line, 1) as "Kijun"