|
//@version=6 |
|
indicator("SFP Detector - LTF Pivot Confirmed (v1.7)", overlay=true) |
|
|
|
// Core - Pivot Geometry |
|
grpCore = "Core - Pivot Geometry" |
|
leftBars = input.int(3, "Pivot Left", minval=1, group=grpCore, tooltip="Bars to the LEFT of the pivot high or low. Higher means stronger swing but slower confirmation.") |
|
rightBars = input.int(3, "Pivot Right", minval=1, group=grpCore, tooltip="Bars to the RIGHT of the pivot. Controls confirmation delay. Use >0 to avoid repaint after print.") |
|
maxAge = input.int(200, "Max Bars Since Pivot", minval=1, group=grpCore, tooltip="Ignore pivots older than this many bars. Keeps signals near recent structure.") |
|
|
|
// Filters - Sweep and Re-entry |
|
grpSweep = "Filters - Sweep and Re-entry" |
|
penetrationPct = input.float(0.20, "Min Sweep Penetration (%)", minval=0.0, step=0.05, group=grpSweep, tooltip="Price must TAKE the pivot by at least this percent to count as a sweep.") |
|
reentryPct = input.float(0.00, "Min Re-entry Distance (%)", minval=0.0, step=0.05, group=grpSweep, tooltip="After the sweep, CLOSE must return inside by at least this percent beyond the pivot level.") |
|
minWickFrac = input.float(0.25, "Min Wick Fraction (0-1)", minval=0.0, maxval=1.0, step=0.05, group=grpSweep, tooltip="Require at least this fraction of the bar to be wick in the sweep direction.") |
|
minSepBars = input.int(0, "Min Bars Between Signals", minval=0, group=grpSweep, tooltip="Optional throttle. Require this many bars between same type signals.") |
|
|
|
// Optional - Volatility Filter |
|
grpVol = "Optional - Volatility Filter" |
|
useATR = input.bool(false, "Use ATR Filter", group=grpVol, tooltip="If enabled, require current bar range to be at least X times ATR.") |
|
atrLen = input.int(14, "ATR Length", minval=1, group=grpVol, tooltip="ATR lookback length.") |
|
minAtrMult = input.float(0.80, "Min Range as ATR multiple", minval=0.0, step=0.05, group=grpVol, tooltip="Require (high - low) >= this multiple of ATR. Set 0 to disable effect.") |
|
|
|
// Optional - Session Filter |
|
grpSess = "Optional - Session Filter" |
|
useSession = input.bool(false, "Filter by Session", group=grpSess, tooltip="If enabled, signals only during the specified session window.") |
|
sessionStr = input.session("0900-2200", "Session (exchange time)", group=grpSess, tooltip="Format HHMM-HHMM. Example 0900-1700. Uses exchange time.") |
|
|
|
// Optional - HTF Bias Filter |
|
grpHTF = "Optional - HTF Bias Filter" |
|
useHTF = input.bool(false, "Use HTF Bias", group=grpHTF, tooltip="If enabled, confirm signals with a higher timeframe bias (close vs HTF MA).") |
|
htfTF = input.timeframe("60", "HTF Timeframe", group=grpHTF, tooltip="Higher timeframe used for bias sampling. Example 60 for 1h.") |
|
htfMaLen= input.int(50, "HTF MA Length", minval=1, group=grpHTF, tooltip="Moving average length used to define HTF bias.") |
|
|
|
// Visualization and Alerts |
|
grpVis = "Visualization and Alerts" |
|
showSwingLines = input.bool(true, "Show Last Swing Levels", group=grpVis, tooltip="Plot the most recent confirmed pivot high and low as level lines.") |
|
shapeSizeStr = input.string("small", "Signal Marker Size", options=["tiny","small","normal","large"], group=grpVis, tooltip="Marker size for SFP triangles. Pine needs const sizes, so we gate by this string.") |
|
labelMode = input.string("None", "Signal Label Mode", options=["None","Side label"], group=grpVis, tooltip="Choose None for clean markers or Side label for a readable text box with background.") |
|
enableAlerts = input.bool(true, "Enable Alerts", group=grpVis, tooltip="Create alert conditions for bullish and bearish SFP confirmations.") |
|
|
|
// Pre-computations |
|
rng = math.max(1e-10, high - low) |
|
wickTop = high - math.max(open, close) |
|
wickBottom = math.min(open, close) - low |
|
atr = ta.atr(atrLen) |
|
|
|
// Pivots - confirmed, non-repainting (print after rightBars) |
|
ph = ta.pivothigh(high, leftBars, rightBars) |
|
pl = ta.pivotlow(low, leftBars, rightBars) |
|
lastPH = ta.valuewhen(not na(ph), ph, 0) |
|
lastPL = ta.valuewhen(not na(pl), pl, 0) |
|
barsSincePH = ta.barssince(not na(ph)) |
|
barsSincePL = ta.barssince(not na(pl)) |
|
phValid = not na(lastPH) and barsSincePH <= maxAge |
|
plValid = not na(lastPL) and barsSincePL <= maxAge |
|
|
|
// HTF bias - lookahead off |
|
htfClose = useHTF ? request.security(syminfo.tickerid, htfTF, close, barmerge.gaps_off, barmerge.lookahead_off) : na |
|
htfMA = useHTF ? ta.sma(htfClose, htfMaLen) : na |
|
bullBias = useHTF ? (htfClose > htfMA) : true |
|
bearBias = useHTF ? (htfClose < htfMA) : true |
|
|
|
// Session and ATR filters |
|
inSess = useSession ? not na(time(timeframe.period, sessionStr)) : true |
|
rangeOK = useATR ? (rng >= (minAtrMult * atr)) : true |
|
|
|
// SFP Logic - bar close confirmed |
|
bearishSweep = phValid and (high > lastPH * (1.0 + penetrationPct / 100.0)) |
|
bearishRein = phValid and (close < lastPH * (1.0 - reentryPct / 100.0)) |
|
bearishWickOK = (wickTop / rng) >= minWickFrac |
|
|
|
bullishSweep = plValid and (low < lastPL * (1.0 - penetrationPct / 100.0)) |
|
bullishRein = plValid and (close > lastPL * (1.0 + reentryPct / 100.0)) |
|
bullishWickOK = (wickBottom / rng) >= minWickFrac |
|
|
|
rawBearSFP = barstate.isconfirmed and bearishSweep and bearishRein and bearishWickOK |
|
rawBullSFP = barstate.isconfirmed and bullishSweep and bullishRein and bullishWickOK |
|
|
|
// Optional filters |
|
bearSFP = rawBearSFP and bearBias and inSess and rangeOK |
|
bullSFP = rawBullSFP and bullBias and inSess and rangeOK |
|
|
|
// Separation logic - compute on every bar for consistency |
|
barsSinceBullPrev = ta.barssince(bullSFP[1]) |
|
barsSinceBearPrev = ta.barssince(bearSFP[1]) |
|
bullGapOK = minSepBars <= 0 or (na(barsSinceBullPrev) ? true : barsSinceBullPrev >= minSepBars) |
|
bearGapOK = minSepBars <= 0 or (na(barsSinceBearPrev) ? true : barsSinceBearPrev >= minSepBars) |
|
bullSig = bullSFP and bullGapOK |
|
bearSig = bearSFP and bearGapOK |
|
|
|
// Plotting - colors |
|
colUp = color.new(color.teal, 0) |
|
colDown = color.new(color.red, 0) |
|
|
|
// Swing levels |
|
plot(showSwingLines and plValid ? lastPL : na, title="Last Pivot Low", color=color.new(color.teal, 70), linewidth=2, style=plot.style_linebr) |
|
plot(showSwingLines and phValid ? lastPH : na, title="Last Pivot High", color=color.new(color.red, 70), linewidth=2, style=plot.style_linebr) |
|
|
|
// Marker size gating - Pine requires const sizes |
|
bullTiny = bullSig and shapeSizeStr == "tiny" |
|
bullSmall = bullSig and shapeSizeStr == "small" |
|
bullNormal = bullSig and shapeSizeStr == "normal" |
|
bullLarge = bullSig and shapeSizeStr == "large" |
|
bearTiny = bearSig and shapeSizeStr == "tiny" |
|
bearSmall = bearSig and shapeSizeStr == "small" |
|
bearNormal = bearSig and shapeSizeStr == "normal" |
|
bearLarge = bearSig and shapeSizeStr == "large" |
|
|
|
// Plot markers - no text on markers to avoid const string issues and halos |
|
plotshape(bullTiny, title="Bullish SFP tiny", style=shape.triangleup, location=location.belowbar, color=colUp, size=size.tiny, text="") |
|
plotshape(bullSmall, title="Bullish SFP small", style=shape.triangleup, location=location.belowbar, color=colUp, size=size.small, text="") |
|
plotshape(bullNormal, title="Bullish SFP normal", style=shape.triangleup, location=location.belowbar, color=colUp, size=size.normal, text="") |
|
plotshape(bullLarge, title="Bullish SFP large", style=shape.triangleup, location=location.belowbar, color=colUp, size=size.large, text="") |
|
plotshape(bearTiny, title="Bearish SFP tiny", style=shape.triangledown, location=location.abovebar, color=colDown, size=size.tiny, text="") |
|
plotshape(bearSmall, title="Bearish SFP small", style=shape.triangledown, location=location.abovebar, color=colDown, size=size.small, text="") |
|
plotshape(bearNormal, title="Bearish SFP normal", style=shape.triangledown, location=location.abovebar, color=colDown, size=size.normal, text="") |
|
plotshape(bearLarge, title="Bearish SFP large", style=shape.triangledown, location=location.abovebar, color=colDown, size=size.large, text="") |
|
|
|
// Side labels with background for readability |
|
if labelMode == "Side label" and bullSig |
|
label.new(bar_index, low, "SFP Buy", style=label.style_label_up, textcolor=color.white, color=color.new(colUp, 0), yloc=yloc.belowbar) |
|
if labelMode == "Side label" and bearSig |
|
label.new(bar_index, high, "SFP Sell", style=label.style_label_down, textcolor=color.white, color=color.new(colDown, 0), yloc=yloc.abovebar) |
|
|
|
// Alerts - const messages and global scope |
|
alertcondition(enableAlerts and bullSig, title="Bullish SFP", message="Bullish SFP on {{ticker}} {{interval}} close {{close}}") |
|
alertcondition(enableAlerts and bearSig, title="Bearish SFP", message="Bearish SFP on {{ticker}} {{interval}} close {{close}}") |
|
|
|
// Notes |
|
// Signals are generated on confirmed bars only. Pivot logic with rightBars > 0 avoids repaint after print. |
|
// HTF filter uses lookahead off to avoid future leaks. HTF values finalize on HTF bar close. |
|
// For earlier but noisier signals, lower rightBars and increase penetration thresholds. |