// ============================================================================ // © 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() 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}}")