bos-algonexta.pine · 304 lines · 20.5 KB
Not yet verified on the native platform — test it before relying on it.
The original uploaded source.
// ============================================================================
// © algonexta
// This script is for education and research; it does not guarantee results.
// Verify this source in TradingView before use.
// ============================================================================
//@version=6
// AlgoNexta Pro-BOS Structure Signals
// Version 3.1
// Pine Script v6
// UI and risk-projection lifecycle remediation
indicator("AlgoNexta Pro-BOS Structure Signals", "AN Pro-BOS", overlay = true, max_labels_count = 300, max_lines_count = 500, max_boxes_count = 200)
string G_STRUCTURE = "1. Structure Discovery"
string G_BOS = "2. BOS Confirmation"
string G_FILTER = "3. Higher-Timeframe Filter"
string G_RISK = "4. ATR Risk Projection"
string G_STRUCTURE_DISPLAY = "5. Structure Display"
string G_SIGNAL_DISPLAY = "6. Signal Display and Alerts"
int swingStrength = input.int(3, "Swing strength", minval = 2, maxval = 20, group = G_STRUCTURE, tooltip = "Bars required on both sides of a candidate pivot before it becomes confirmed.")
string priceUnitMode = input.string("Custom Pip Size", "Price unit mode", options = ["Custom Pip Size", "Minimum Tick"], group = G_STRUCTURE, tooltip = "Select the unit used by minimum structure move and BOS buffer.")
float customPipSize = input.float(0.1, "Custom pip size", minval = 0.0000001, step = 0.01, group = G_STRUCTURE, tooltip = "Used only in Custom Pip Size mode. XAUUSD conventions vary by feed.")
float minMovePips = input.float(40.0, "Minimum structure move (units)", minval = 0.0, step = 1.0, group = G_STRUCTURE, tooltip = "Applied between the last accepted pivot and an alternating candidate. Same-side candidates replace only when more extreme.")
float bosBufferPips = input.float(0.0, "BOS buffer (units)", minval = 0.0, step = 0.1, group = G_BOS, tooltip = "Extra distance beyond the active level required from the confirmed candle close.")
bool allowThresholdEquality = input.bool(false, "Allow close equal to threshold", group = G_BOS)
int minSignalGap = input.int(0, "Minimum bars between same-side signals", minval = 0, maxval = 1000, group = G_BOS)
bool requireHtf = input.bool(true, "Require confirmed HTF EMA alignment", group = G_FILTER, tooltip = "Uses only the previous fully closed higher-timeframe close and EMA.")
string higherTf = input.timeframe("15", "Higher timeframe", group = G_FILTER)
int emaPeriod = input.int(240, "EMA period", minval = 5, group = G_FILTER)
bool showRisk = input.bool(true, "Show risk projection", group = G_RISK)
string riskDisplayMode = input.string("Latest Signal Only", "Risk display mode", options = ["Latest Signal Only", "Active Projections", "Historical Projections"], group = G_RISK, tooltip = "Latest keeps one projection; Active keeps unresolved projections; Historical keeps a bounded audit trail.")
int atrLength = input.int(14, "ATR length", minval = 1, group = G_RISK)
float atrSlMultiplier = input.float(1.5, "ATR SL multiplier", minval = 0.1, step = 0.1, group = G_RISK)
float tp1Rr = input.float(1.5, "TP1 RR", minval = 0.1, step = 0.1, group = G_RISK)
float tp2Rr = input.float(2.0, "TP2 RR", minval = 0.1, step = 0.1, group = G_RISK)
float tp3Rr = input.float(3.0, "TP3 RR", minval = 0.1, step = 0.1, group = G_RISK)
int projectionBars = input.int(15, "Projection width (bars)", minval = 1, maxval = 300, group = G_RISK, tooltip = "Horizontal width and Active-mode expiry measured from the signal bar.")
int maxActiveProjections = input.int(3, "Maximum active projections", minval = 1, maxval = 10, group = G_RISK)
int maxHistoricalProjections = input.int(5, "Maximum historical projections", minval = 1, maxval = 20, group = G_RISK)
bool showRiskBoxes = input.bool(true, "Show risk/reward boxes", group = G_RISK)
bool showEntryComponent = input.bool(true, "Show Entry line and label", group = G_RISK)
bool showStopComponent = input.bool(true, "Show Stop line and label", group = G_RISK)
bool showTp1Component = input.bool(true, "Show TP1", group = G_RISK)
bool showTp2Component = input.bool(true, "Show TP2", group = G_RISK)
bool showTp3Component = input.bool(true, "Show TP3", group = G_RISK)
string riskLabelStyle = input.string("Compact", "Risk label style", options = ["Full", "Compact"], group = G_RISK, tooltip = "Compact uses E, SL and RR values; Full includes Entry, ATR and TP names.")
bool showActiveLevels = input.bool(true, "Show active structure levels", group = G_STRUCTURE_DISPLAY, tooltip = "Shows the current confirmed high and low levels available for a future BOS.")
bool showBrokenLevels = input.bool(false, "Show broken structure levels", group = G_STRUCTURE_DISPLAY, tooltip = "Keeps a bounded diagnostic history of consumed levels.")
bool showPivotMarkers = input.bool(false, "Show confirmed pivot markers", group = G_STRUCTURE_DISPLAY, tooltip = "Visual offset only; logical activation remains at confirmation.")
string levelLineStart = input.string("Confirmation Bar", "Active line starts at", options = ["Confirmation Bar", "Pivot Bar"], group = G_STRUCTURE_DISPLAY)
int maxBrokenLevelLines = input.int(50, "Broken level lines to keep", minval = 1, maxval = 200, group = G_STRUCTURE_DISPLAY)
bool showSignalText = input.bool(true, "Show filtered signal text", group = G_SIGNAL_DISPLAY)
bool showRawBosEvents = input.bool(false, "Show raw structural BOS events", group = G_SIGNAL_DISPLAY, tooltip = "Shows every confirmed structural break before HTF alignment and signal-gap filters. These markers are diagnostic events, not filtered BUY/SELL signals.")
bool showEma = input.bool(false, "Show chart EMA", group = G_SIGNAL_DISPLAY)
float effectivePipSize = priceUnitMode == "Minimum Tick" ? syminfo.mintick : customPipSize
float minStructureMove = minMovePips * effectivePipSize
float bosBuffer = bosBufferPips * effectivePipSize
float chartEma = ta.ema(close, emaPeriod)
float atr = ta.atr(atrLength)
int chartSeconds = timeframe.in_seconds(timeframe.period)
int htfSeconds = timeframe.in_seconds(higherTf)
bool validHtfSelection = not na(chartSeconds) and not na(htfSeconds) and htfSeconds > chartSeconds
if barstate.isfirst and requireHtf and not validHtfSelection
runtime.error("AN Pro-BOS: Higher timeframe must be strictly higher than the chart timeframe.")
if barstate.isfirst and not (tp1Rr < tp2Rr and tp2Rr < tp3Rr)
runtime.error("AN Pro-BOS: RR targets must satisfy TP1 < TP2 < TP3.")
// Last fully closed HTF values only. lookahead_off (the default) never reads future HTF data,
// and the [1] offset excludes the still-developing HTF bar, so historical and live values match.
float confirmedHtfClose = request.security(syminfo.tickerid, higherTf, close[1], gaps = barmerge.gaps_off, lookahead = barmerge.lookahead_off)
float confirmedHtfEma = request.security(syminfo.tickerid, higherTf, ta.ema(close, emaPeriod)[1], gaps = barmerge.gaps_off, lookahead = barmerge.lookahead_off)
bool htfBullAligned = validHtfSelection and not na(confirmedHtfClose) and not na(confirmedHtfEma) and confirmedHtfClose > confirmedHtfEma
bool htfBearAligned = validHtfSelection and not na(confirmedHtfClose) and not na(confirmedHtfEma) and confirmedHtfClose < confirmedHtfEma
float confirmedPivotHigh = ta.pivothigh(high, swingStrength, swingStrength)
float confirmedPivotLow = ta.pivotlow(low, swingStrength, swingStrength)
bool hasConfirmedHigh = not na(confirmedPivotHigh)
bool hasConfirmedLow = not na(confirmedPivotLow)
int confirmedPivotBar = bar_index - swingStrength
var float activeHighPrice = na
var int activeHighPivotBar = na
var int activeHighConfirmationBar = na
var bool activeHighBroken = true
var float activeLowPrice = na
var int activeLowPivotBar = na
var int activeLowConfirmationBar = na
var bool activeLowBroken = true
var float lastAcceptedPivotPrice = na
var int lastAcceptedPivotType = 0
var int lastBullSignalBar = na
var int lastBearSignalBar = na
var line activeHighLine = na
var line activeLowLine = na
var line[] retiredLevelLines = array.new_line()
var label[] bosEventLabels = array.new_label()
f_retireLevel(line levelLine, bool broken) =>
if not na(levelLine)
line.set_extend(levelLine, extend.none)
line.set_x2(levelLine, bar_index)
if broken and showBrokenLevels
line.set_color(levelLine, color.new(color.gray, 25))
line.set_style(levelLine, line.style_dashed)
array.push(retiredLevelLines, levelLine)
else
line.delete(levelLine)
while array.size(retiredLevelLines) > maxBrokenLevelLines
line.delete(array.shift(retiredLevelLines))
f_newLevelLine(bool isHigh, int pivotBar, int confirmationBar, float price) =>
int startBar = levelLineStart == "Pivot Bar" ? pivotBar : confirmationBar
color levelColor = isHigh ? color.new(color.red, 15) : color.new(color.lime, 15)
showActiveLevels ? line.new(startBar, price, bar_index, price, xloc = xloc.bar_index, extend = extend.right, color = levelColor, width = 1) : na
// State before this bar is evaluated first. A pivot confirmed on this bar cannot
// break itself and cannot backdate a BOS.
bool rawBullishBos = barstate.isconfirmed and not na(activeHighPrice) and not activeHighBroken and bar_index > activeHighConfirmationBar and (allowThresholdEquality ? close >= activeHighPrice + bosBuffer : close > activeHighPrice + bosBuffer)
bool rawBearishBos = barstate.isconfirmed and not na(activeLowPrice) and not activeLowBroken and bar_index > activeLowConfirmationBar and (allowThresholdEquality ? close <= activeLowPrice - bosBuffer : close < activeLowPrice - bosBuffer)
bool ambiguousDualBos = rawBullishBos and rawBearishBos
// Structural consumption is independent from trade filters and frequency gaps.
if rawBullishBos
activeHighBroken := true
f_retireLevel(activeHighLine, true)
activeHighLine := na
if rawBearishBos
activeLowBroken := true
f_retireLevel(activeLowLine, true)
activeLowLine := na
bool bullishGapOk = na(lastBullSignalBar) or bar_index - lastBullSignalBar >= minSignalGap
bool bearishGapOk = na(lastBearSignalBar) or bar_index - lastBearSignalBar >= minSignalGap
bool bullishBosSignal = rawBullishBos and not ambiguousDualBos and (not requireHtf or htfBullAligned) and bullishGapOk
bool bearishBosSignal = rawBearishBos and not ambiguousDualBos and (not requireHtf or htfBearAligned) and bearishGapOk
if bullishBosSignal
lastBullSignalBar := bar_index
if bearishBosSignal
lastBearSignalBar := bar_index
if showRawBosEvents and rawBullishBos and not ambiguousDualBos
label eventLabel = label.new(bar_index, high, "BOS", xloc = xloc.bar_index, yloc = yloc.abovebar, style = label.style_label_down, color = color.new(color.green, 55), textcolor = color.white, size = size.tiny)
array.push(bosEventLabels, eventLabel)
if showRawBosEvents and rawBearishBos and not ambiguousDualBos
label eventLabel = label.new(bar_index, low, "BOS", xloc = xloc.bar_index, yloc = yloc.belowbar, style = label.style_label_up, color = color.new(color.red, 55), textcolor = color.white, size = size.tiny)
array.push(bosEventLabels, eventLabel)
while array.size(bosEventLabels) > 100
label.delete(array.shift(bosEventLabels))
// Minimum move applies to alternating pivots. Consecutive same-side candidates
// replace only when strictly more extreme. High is processed first if both exist.
if barstate.isconfirmed and hasConfirmedHigh
bool sameSideHigh = lastAcceptedPivotType == 1
bool moreExtremeHigh = sameSideHigh and (na(lastAcceptedPivotPrice) or confirmedPivotHigh > lastAcceptedPivotPrice)
bool alternatingHighAccepted = lastAcceptedPivotType == 0 or (lastAcceptedPivotType == -1 and math.abs(confirmedPivotHigh - lastAcceptedPivotPrice) >= minStructureMove)
if moreExtremeHigh or alternatingHighAccepted
f_retireLevel(activeHighLine, false)
activeHighPrice := confirmedPivotHigh
activeHighPivotBar := confirmedPivotBar
activeHighConfirmationBar := bar_index
activeHighBroken := false
activeHighLine := f_newLevelLine(true, activeHighPivotBar, activeHighConfirmationBar, activeHighPrice)
lastAcceptedPivotPrice := confirmedPivotHigh
lastAcceptedPivotType := 1
if barstate.isconfirmed and hasConfirmedLow
bool sameSideLow = lastAcceptedPivotType == -1
bool moreExtremeLow = sameSideLow and (na(lastAcceptedPivotPrice) or confirmedPivotLow < lastAcceptedPivotPrice)
bool alternatingLowAccepted = lastAcceptedPivotType == 0 or (lastAcceptedPivotType == 1 and math.abs(confirmedPivotLow - lastAcceptedPivotPrice) >= minStructureMove)
if moreExtremeLow or alternatingLowAccepted
f_retireLevel(activeLowLine, false)
activeLowPrice := confirmedPivotLow
activeLowPivotBar := confirmedPivotBar
activeLowConfirmationBar := bar_index
activeLowBroken := false
activeLowLine := f_newLevelLine(false, activeLowPivotBar, activeLowConfirmationBar, activeLowPrice)
lastAcceptedPivotPrice := confirmedPivotLow
lastAcceptedPivotType := -1
plotshape(showPivotMarkers and hasConfirmedHigh, "Confirmed pivot high", shape.circle, location.abovebar, color = color.new(color.red, 20), size = size.tiny, offset = -swingStrength)
plotshape(showPivotMarkers and hasConfirmedLow, "Confirmed pivot low", shape.circle, location.belowbar, color = color.new(color.lime, 20), size = size.tiny, offset = -swingStrength)
plotshape(showSignalText and bullishBosSignal, "Bullish BOS Signal", shape.labelup, location.belowbar, color = color.lime, textcolor = color.black, text = "BOS\nBUY", size = size.small)
plotshape(showSignalText and bearishBosSignal, "Bearish BOS Signal", shape.labeldown, location.abovebar, color = color.red, textcolor = color.white, text = "BOS\nSELL", size = size.small)
plotshape(not showSignalText and bullishBosSignal, "Bullish BOS Arrow", shape.triangleup, location.belowbar, color = color.lime, size = size.small)
plotshape(not showSignalText and bearishBosSignal, "Bearish BOS Arrow", shape.triangledown, location.abovebar, color = color.red, size = size.small)
plot(showEma ? chartEma : na, "Chart EMA", color = color.blue, linewidth = 2)
type RiskProjection
bool isBuy
int signalBar
int expirationBar
float entry
float stop
float tp3
box riskBox
box rewardBox
line entryLine
line stopLine
line tp1Line
line tp2Line
line tp3Line
label entryLabel
label stopLabel
label tp1Label
label tp2Label
label tp3Label
var RiskProjection[] riskProjections = array.new<RiskProjection>()
f_deleteProjection(RiskProjection projection) =>
if not na(projection.riskBox)
box.delete(projection.riskBox)
if not na(projection.rewardBox)
box.delete(projection.rewardBox)
if not na(projection.entryLine)
line.delete(projection.entryLine)
if not na(projection.stopLine)
line.delete(projection.stopLine)
if not na(projection.tp1Line)
line.delete(projection.tp1Line)
if not na(projection.tp2Line)
line.delete(projection.tp2Line)
if not na(projection.tp3Line)
line.delete(projection.tp3Line)
if not na(projection.entryLabel)
label.delete(projection.entryLabel)
if not na(projection.stopLabel)
label.delete(projection.stopLabel)
if not na(projection.tp1Label)
label.delete(projection.tp1Label)
if not na(projection.tp2Label)
label.delete(projection.tp2Label)
if not na(projection.tp3Label)
label.delete(projection.tp3Label)
f_clearProjections() =>
while array.size(riskProjections) > 0
f_deleteProjection(array.pop(riskProjections))
f_removeOldestProjection() =>
if array.size(riskProjections) > 0
f_deleteProjection(array.shift(riskProjections))
f_createProjection(bool isBuy) =>
float entry = close
float risk = atr * atrSlMultiplier
float stopPrice = isBuy ? entry - risk : entry + risk
float tp1Price = isBuy ? entry + risk * tp1Rr : entry - risk * tp1Rr
float tp2Price = isBuy ? entry + risk * tp2Rr : entry - risk * tp2Rr
float tp3Price = isBuy ? entry + risk * tp3Rr : entry - risk * tp3Rr
int projectionEnd = bar_index + projectionBars
box riskBox = showRiskBoxes ? box.new(bar_index, math.max(entry, stopPrice), projectionEnd, math.min(entry, stopPrice), xloc = xloc.bar_index, border_color = color.new(color.red, 55), bgcolor = color.new(color.red, 88)) : na
box rewardBox = showRiskBoxes ? box.new(bar_index, math.max(entry, tp3Price), projectionEnd, math.min(entry, tp3Price), xloc = xloc.bar_index, border_color = color.new(color.teal, 55), bgcolor = color.new(color.teal, 88)) : na
line entryLine = showEntryComponent ? line.new(bar_index, entry, projectionEnd, entry, xloc = xloc.bar_index, color = color.white, style = line.style_dotted) : na
line stopLine = showStopComponent ? line.new(bar_index, stopPrice, projectionEnd, stopPrice, xloc = xloc.bar_index, color = color.red) : na
line tp1Line = showTp1Component ? line.new(bar_index, tp1Price, projectionEnd, tp1Price, xloc = xloc.bar_index, color = color.lime, style = line.style_dotted) : na
line tp2Line = showTp2Component ? line.new(bar_index, tp2Price, projectionEnd, tp2Price, xloc = xloc.bar_index, color = color.lime, style = line.style_dashed) : na
line tp3Line = showTp3Component ? line.new(bar_index, tp3Price, projectionEnd, tp3Price, xloc = xloc.bar_index, color = color.lime) : na
string entryText = riskLabelStyle == "Compact" ? "E" : "Entry"
string stopText = riskLabelStyle == "Compact" ? "SL" : "SL " + str.tostring(atrSlMultiplier) + " ATR"
string tp1Text = riskLabelStyle == "Compact" ? str.tostring(tp1Rr) + "R" : "TP1 " + str.tostring(tp1Rr) + "R"
string tp2Text = riskLabelStyle == "Compact" ? str.tostring(tp2Rr) + "R" : "TP2 " + str.tostring(tp2Rr) + "R"
string tp3Text = riskLabelStyle == "Compact" ? str.tostring(tp3Rr) + "R" : "TP3 " + str.tostring(tp3Rr) + "R"
label entryLabel = showEntryComponent ? label.new(projectionEnd, entry, entryText, xloc = xloc.bar_index, style = label.style_label_left, color = color.white, textcolor = color.black, size = size.small) : na
label stopLabel = showStopComponent ? label.new(projectionEnd, stopPrice, stopText, xloc = xloc.bar_index, style = label.style_label_left, color = color.red, textcolor = color.white, size = size.small) : na
label tp1Label = showTp1Component ? label.new(projectionEnd, tp1Price, tp1Text, xloc = xloc.bar_index, style = label.style_label_left, color = color.lime, textcolor = color.black, size = size.small) : na
label tp2Label = showTp2Component ? label.new(projectionEnd, tp2Price, tp2Text, xloc = xloc.bar_index, style = label.style_label_left, color = color.lime, textcolor = color.black, size = size.small) : na
label tp3Label = showTp3Component ? label.new(projectionEnd, tp3Price, tp3Text, xloc = xloc.bar_index, style = label.style_label_left, color = color.lime, textcolor = color.black, size = size.small) : na
RiskProjection.new(isBuy, bar_index, projectionEnd, entry, stopPrice, tp3Price, riskBox, rewardBox, entryLine, stopLine, tp1Line, tp2Line, tp3Line, entryLabel, stopLabel, tp1Label, tp2Label, tp3Label)
// Risk lifecycle is downstream from BOS and cannot change signals or alerts.
if not showRisk
f_clearProjections()
else if riskDisplayMode == "Active Projections" and array.size(riskProjections) > 0
for projectionIndex = array.size(riskProjections) - 1 to 0
RiskProjection projection = array.get(riskProjections, projectionIndex)
bool afterSignal = bar_index > projection.signalBar
bool stopTouched = projection.isBuy ? low <= projection.stop : high >= projection.stop
bool tp3Touched = projection.isBuy ? high >= projection.tp3 : low <= projection.tp3
bool expired = bar_index >= projection.expirationBar
if afterSignal and (stopTouched or tp3Touched or expired)
f_deleteProjection(projection)
array.remove(riskProjections, projectionIndex)
bool riskReady = not na(atr) and atr > 0 and not na(close)
if showRisk and riskReady and (bullishBosSignal or bearishBosSignal)
if riskDisplayMode == "Latest Signal Only"
f_clearProjections()
RiskProjection newProjection = f_createProjection(bullishBosSignal)
array.push(riskProjections, newProjection)
int projectionLimit = riskDisplayMode == "Latest Signal Only" ? 1 : riskDisplayMode == "Active Projections" ? maxActiveProjections : maxHistoricalProjections
while array.size(riskProjections) > projectionLimit
f_removeOldestProjection()
alertcondition(bullishBosSignal, "AlgoNexta Bullish BOS", "AlgoNexta confirmed bullish BOS on {{ticker}} {{interval}} at close {{close}}")
alertcondition(bearishBosSignal, "AlgoNexta Bearish BOS", "AlgoNexta confirmed bearish BOS on {{ticker}} {{interval}} at close {{close}}")
alertcondition(bullishBosSignal or bearishBosSignal, "AlgoNexta Any Confirmed BOS", "AlgoNexta confirmed BOS on {{ticker}} {{interval}} at close {{close}}")