Volumized Order Block 2026-3.txt
Copy
// This Pine Script™ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © fluxchart
//@version=5
//- Chart TF ≤ 14400s (4H and below) → LTF runs
//- Chart TF > 14400s (6H, 8H, 12H, 16H, daily, weekly…) → LTF skipped
//The ltfMaxChartSecs input lets you raise that ceiling if needed (e.g. set to 21600 for 6H on SPXL), but the default of 14400 is the safe line for
//long-history assets like SPY/QQQ.
const bool DEBUG = true
const int maxBoxesCount = 500
const float overlapThresholdPercentage = 0
const int maxDistanceToLastBar = 1750 // Affects Running Time
const int maxOrderBlocks = 30
indicator(title = 'Volumized Order Blocks 2026-3', overlay = true, max_boxes_count = maxBoxesCount, max_labels_count = maxBoxesCount, max_lines_count = maxBoxesCount, max_bars_back = 5000)
showInvalidated = input.bool(false, "Show Historic Zones", group = "General Configuration", display = display.none)
OBsEnabled = true
orderBlockVolumetricInfo = input.bool(true, "Volumetric Info", group = "General Configuration", inline="EV", display = display.none)
obEndMethod = input.string("Close", "Zone Invalidation", options = ["Wick", "Close"], group = "General Configuration", display = display.none)
combineOBs = DEBUG ? input.bool(true, "Combine Zones", group = "General Configuration", display = display.none) : true
maxATRMult = DEBUG ? input.float(1.618,"Max Atr Multiplier", group = "General Configuration") : 3.5
swingLength = input.int(5, 'Swing Length', minval = 3, tooltip="Swing length is used when finding order block formations. Smaller values will result in finding smaller order blocks.",group = "General Configuration", display = display.none)
zoneCount = input.string("3", 'Zone Count', options = ["High", "Medium", "Low", "3", "2", "One"], tooltip = "Number of Order Block Zones to be rendered. Higher options will result in older Order Blocks shown.", group = "General Configuration", display = display.none)
bullOrderBlockColor = input(color.rgb(26, 169, 145, 87), 'Bullish', inline = 'obColor', group = 'General Configuration', display = display.none)
bearOrderBlockColor = input(color.rgb(184, 41, 127, 50), 'Bearish', inline = 'obColor', group = 'General Configuration', display = display.none)
// LTF Configuration
ltfEnabled = input.bool(true, "Show LTF Blocks Within Range", group = "LTF Configuration", display = display.none)
ltfAutoEnabled = input.bool(true, "Auto (2 TFs Lower)", group = "LTF Configuration", tooltip = "Automatically selects the 2 timeframes below the current chart timeframe. E.g. on 4H → searches 2H and 1H.", display = display.none)
ltfTimeframe = input.timeframe("15", "Lower Timeframe (Manual)", group = "LTF Configuration", display = display.none)
ltfThresholdPct = input.float(15.0, "Price Threshold %", minval = 0.1, maxval = 50.0, step = 0.5, group = "LTF Configuration", tooltip = "Only show LTF order blocks within this % of current price", display = display.none)
ltfProximityPct = input.float(5.0, "Zone Proximity %", minval = 0.1, maxval = 50.0, step = 0.5, group = "LTF Configuration", tooltip = "Suppress an LTF block if it is within this % of price distance from any existing zone of the same type (0 = overlapping only).", display = display.none)
ltfLookbackBars = input.int(500, "LTF Lookback Bars", minval = 50, maxval = 1750, step = 50, group = "LTF Configuration", tooltip = "How many recent lower-TF bars to scan for order blocks. Smaller = faster. ~410 bars ≈ 1 year of 4H data. Reduce this to fix crashes on long-history assets like SPY on daily charts.", display = display.none)
ltfBullColor = input(color.rgb(0, 255, 200, 60), 'LTF Bullish', inline = 'ltfColor', group = 'LTF Configuration', display = display.none)
ltfBearColor = input(color.rgb(255, 50, 160, 50), 'LTF Bearish', inline = 'ltfColor', group = 'LTF Configuration', display = display.none)
// Auto LTF: map current timeframe → 1st and 2nd lower timeframe strings.
// Use string detection for D/W/M because timeframe.in_seconds() can return 0 for monthly
// and may be unreliable for session-based timeframes on some symbols (e.g. SPY).
_tfPeriod = timeframe.period
_isDaily = str.contains(_tfPeriod, "D")
_isWeekly = str.contains(_tfPeriod, "W")
_isMonthly = str.contains(_tfPeriod, "M")
_ltfSecs = _isMonthly ? 2592000 : _isWeekly ? 604800 : _isDaily ? 86400 : timeframe.in_seconds(_tfPeriod)
// Maximum chart timeframe (in seconds) at which LTF is active.
// Default 23400 = 390 min (1 RTH session). Safe for SPY/QQQ because TradingView caps
// intraday (minute-string) data differently than daily bars, so 390-min charts don't
// carry the full 30-year history that crashes the request.security calls.
ltfMaxChartSecs = input.int(23400, "LTF Max Chart TF (sec)", minval = 3600, maxval = 86400, step = 3600, group = "LTF Configuration", tooltip = "LTF is disabled when the chart timeframe exceeds this value in seconds. 23400 = 390 min (1 RTH session). 14400 = 4H (more conservative).", display = display.none)
_ltfFeatActive = ltfEnabled and _ltfSecs <= ltfMaxChartSecs
_autoTF1 = _isMonthly ? "1W" : _isWeekly ? "1D" : _isDaily ? "240" : _ltfSecs >= 14400 ? "120" : _ltfSecs >= 7200 ? "60" : _ltfSecs >= 3600 ? "30" : _ltfSecs >= 1800 ? "15" : _ltfSecs >= 900 ? "5" : _ltfSecs >= 300 ? "3" : "1"
_autoTF2 = _isMonthly ? "1D" : _isWeekly ? "240" : _isDaily ? "120" : _ltfSecs >= 14400 ? "60" : _ltfSecs >= 7200 ? "30" : _ltfSecs >= 3600 ? "15" : _ltfSecs >= 1800 ? "5" : _ltfSecs >= 900 ? "3" : "1"
activeLTF1 = ltfAutoEnabled ? _autoTF1 : ltfTimeframe
activeLTF2 = ltfAutoEnabled ? _autoTF2 : ltfTimeframe
bullishOrderBlocks = zoneCount == "One" ? 1 : zoneCount == "2" ? 2 : zoneCount == "3" ? 3 : zoneCount == "Low" ? 4 : zoneCount == "Medium" ? 6 : 10
bearishOrderBlocks = zoneCount == "One" ? 1 : zoneCount == "2" ? 2 : zoneCount == "3" ? 3 : zoneCount == "Low" ? 4 : zoneCount == "Medium" ? 6 : 10
timeframe1Enabled = true
timeframe1 = ""
textColor = input.color(#ffffff80, "Text Color", group = "Style")
labelSizeInput = input.string("Tiny", "Label Text Size", options = ["Tiny", "Small", "Normal", "Large", "Huge"], group = "Style")
selectedTextSize = labelSizeInput == "Tiny" ? size.tiny :
labelSizeInput == "Small" ? size.small :
labelSizeInput == "Normal" ? size.normal :
labelSizeInput == "Large" ? size.large :
size.huge
extendZonesBy = DEBUG ? input.int(15, "Extend Zones", group = "Style", minval = 1, maxval = 30, inline = "ExtendZones") : 15
extendZonesDynamic = DEBUG ? input.bool(true, "Dynamic", group = "Style", inline = "ExtendZones") : true
combinedText = DEBUG ? input.bool(false, "Combined Text", group = "Style", inline = "CombinedColor") : false
// --- MODIFIED: Added variable to shift text to the left ---
textShiftBars = input.int(5, "Text Shift Left (Bars)", group = "Style", tooltip = "Shifts the text label to the left by this many bars")
volumeBarsPlace = "Right"
mirrorVolumeBars = DEBUG ? input.bool(true, "Mirror Volume Bars", group = "Style", inline = "volumebars") : true
volumeBarsLeftSide = (volumeBarsPlace == "Left")
extendZonesByTime = extendZonesBy * timeframe.in_seconds(timeframe.period) * 1000
atr = ta.atr(10)
type orderBlockInfo
float top
float bottom
float obVolume
string obType
int startTime
float bbVolume
float obLowVolume
float obHighVolume
bool breaker
int breakTime
string timeframeStr
bool disabled = false
string combinedTimeframesStr = na
bool combined = false
type orderBlock
orderBlockInfo info
bool isRendered = false
box orderBox = na
box breakerBox = na
line orderBoxLineTop = na
line orderBoxLineBottom = na
line breakerBoxLineTop = na
line breakerBoxLineBottom = na
//
box orderBoxText = na
box orderBoxPositive = na
box orderBoxNegative = na
line orderSeperator = na
line orderTextSeperator = na
createOrderBlock (orderBlockInfo orderBlockInfoF) =>
orderBlock newOrderBlock = orderBlock.new(orderBlockInfoF)
newOrderBlock
safeDeleteOrderBlock (orderBlock orderBlockF) =>
orderBlockF.isRendered := false
box.delete(orderBlockF.orderBox)
box.delete(orderBlockF.breakerBox)
box.delete(orderBlockF.orderBoxText)
box.delete(orderBlockF.orderBoxPositive)
box.delete(orderBlockF.orderBoxNegative)
line.delete(orderBlockF.orderBoxLineTop)
line.delete(orderBlockF.orderBoxLineBottom)
line.delete(orderBlockF.breakerBoxLineTop)
line.delete(orderBlockF.breakerBoxLineBottom)
line.delete(orderBlockF.orderSeperator)
line.delete(orderBlockF.orderTextSeperator)
type timeframeInfo
int index = na
string timeframeStr = na
bool isEnabled = false
orderBlockInfo[] bullishOrderBlocksList = na
orderBlockInfo[] bearishOrderBlocksList = na
newTimeframeInfo (index, timeframeStr, isEnabled) =>
newTFInfo = timeframeInfo.new()
newTFInfo.index := index
newTFInfo.isEnabled := isEnabled
newTFInfo.timeframeStr := timeframeStr
newTFInfo
type obSwing
int x = na
float y = na
float swingVolume = na
bool crossed = false
var timeframeInfo[] timeframeInfos = array.from(newTimeframeInfo(1, timeframe1, timeframe1Enabled))
var bullishOrderBlocksList = array.new<orderBlockInfo>(0)
var bearishOrderBlocksList = array.new<orderBlockInfo>(0)
// Separate arrays for LTF requests — use a limited lookback to avoid replaying
// thousands of historical bars for long-history assets like SPY/QQQ on daily charts.
var ltfBullOBList = array.new<orderBlockInfo>(0)
var ltfBearOBList = array.new<orderBlockInfo>(0)
var allOrderBlocksList = array.new<orderBlock>(0)
moveLine(_line, _x, _y, _x2) =>
line.set_xy1(_line, _x, _y)
line.set_xy2(_line, _x2, _y)
moveBox (_box, _topLeftX, _topLeftY, _bottomRightX, _bottomRightY) =>
box.set_lefttop(_box, _topLeftX, _topLeftY)
box.set_rightbottom(_box, _bottomRightX, _bottomRightY)
isTimeframeLower (timeframe1F, timeframe2F) =>
timeframe.in_seconds(timeframe1F) < timeframe.in_seconds(timeframe2F)
getMinTimeframe (timeframe1F, timeframe2F) =>
if isTimeframeLower(timeframe1F, timeframe2F)
timeframe1F
else
timeframe2F
getMaxTimeframe (timeframe1F, timeframe2F) =>
if isTimeframeLower(timeframe1F, timeframe2F)
timeframe2F
else
timeframe1F
formatTimeframeString (formatTimeframe) =>
timeframeF = formatTimeframe == "" ? timeframe.period : formatTimeframe
if str.contains(timeframeF, "D") or str.contains(timeframeF, "W") or str.contains(timeframeF, "S") or str.contains(timeframeF, "M")
timeframeF
else
seconds = timeframe.in_seconds(timeframeF)
if seconds >= 3600
hourCount = int(seconds / 3600)
str.tostring(hourCount) + " Hour" + (hourCount > 1 ? "s" : "")
else
timeframeF + " Min"
betterCross(s1, s2) =>
string ret = na
if s1 >= s2 and s1[1] < s2
ret := "Bull"
if s1 < s2 and s1[1] >= s2
ret := "Bear"
ret
colorWithTransparency (colorF, transparencyX) =>
color.new(colorF, color.t(colorF) * transparencyX)
createOBBox (boxColor, transparencyX = 1.0, xlocType = xloc.bar_time) =>
box.new(na, na, na, na, text_size = selectedTextSize, xloc = xlocType, extend = extend.none, bgcolor = colorWithTransparency(boxColor, transparencyX), text_color = textColor, text_halign = text.align_right, border_color = #00000000)
renderOrderBlock (orderBlock ob) =>
orderBlockInfo info = ob.info
ob.isRendered := true
isLTFBlock = ltfEnabled and (ob.info.timeframeStr == activeLTF1 or ob.info.timeframeStr == activeLTF2) and timeframe.in_seconds(ob.info.timeframeStr) < timeframe.in_seconds(timeframe.period)
orderColor = ob.info.obType == "Bull" ? (isLTFBlock ? ltfBullColor : bullOrderBlockColor) : (isLTFBlock ? ltfBearColor : bearOrderBlockColor)
if OBsEnabled and (not false or not (false and info.breaker)) and not (not showInvalidated and info.breaker)
ob.orderBox := createOBBox(orderColor, 1.5)
if ob.info.combined
ob.orderBox.set_bgcolor(colorWithTransparency(orderColor, 1.1))
ob.orderBoxText := createOBBox(color.new(color.white, 100))
if orderBlockVolumetricInfo
ob.orderBoxPositive := createOBBox(bullOrderBlockColor)
ob.orderBoxNegative := createOBBox(bearOrderBlockColor)
ob.orderSeperator := line.new(na,na,na,na,xloc.bar_time,extend.none,textColor,line.style_dashed,1)
ob.orderTextSeperator := line.new(na,na,na,na,xloc.bar_time,extend.none,textColor,line.style_solid,1)
zoneSize = extendZonesDynamic ? na(info.breakTime) ? extendZonesByTime : (info.breakTime - info.startTime) : extendZonesByTime
if na(info.breakTime)
zoneSize := (time + 1) - info.startTime
startX = volumeBarsLeftSide ? info.startTime : info.startTime + zoneSize - zoneSize / 3
maxEndX = volumeBarsLeftSide ? info.startTime + zoneSize / 3 : info.startTime + zoneSize
moveBox(ob.orderBox, info.startTime, info.top, info.startTime + zoneSize, info.bottom)
// --- MODIFIED: Convert textShiftBars to time and subtract from X coordinates ---
shiftTime = textShiftBars * timeframe.in_seconds(timeframe.period) * 1000
moveBox(ob.orderBoxText, startX - shiftTime, info.top, maxEndX - shiftTime, info.bottom)
percentage = int((math.min(info.obHighVolume, info.obLowVolume) / math.max(info.obHighVolume, info.obLowVolume)) * 100.0)
OBText = (na(ob.info.combinedTimeframesStr) ? formatTimeframeString(ob.info.timeframeStr) : ob.info.combinedTimeframesStr) + " OB"
box.set_text(ob.orderBoxText, (orderBlockVolumetricInfo ? str.tostring(ob.info.obVolume, format.volume) + " (" + str.tostring(percentage) + "%)\n" : "") + (combinedText and ob.info.combined ? "[Combined]\n" : "") + OBText)
if orderBlockVolumetricInfo
showHighLowBoxText = false
highVolumeWidth = (info.obHighVolume / info.obVolume) * (maxEndX - startX)
lowVolumeWidth = (info.obLowVolume / info.obVolume) * (maxEndX - startX)
curStartXHigh = int(math.floor(maxEndX - highVolumeWidth))
curStartXLow = int(math.floor(maxEndX - lowVolumeWidth))
moveBox(ob.orderBoxPositive, curStartXHigh, info.top, maxEndX, (info.bottom + info.top) / 2)
box.set_text(ob.orderBoxPositive, showHighLowBoxText ? str.tostring(info.obHighVolume, format.volume) : "")
moveBox(ob.orderBoxNegative, curStartXLow, info.bottom, maxEndX, (info.bottom + info.top) / 2)
box.set_text(ob.orderBoxNegative, showHighLowBoxText ? str.tostring(info.obLowVolume, format.volume) : "")
moveLine(ob.orderSeperator, volumeBarsLeftSide ? startX : maxEndX, (info.bottom + info.top) / 2, volumeBarsLeftSide ? maxEndX : startX)
line.set_xy1(ob.orderTextSeperator, volumeBarsLeftSide ? maxEndX : startX, info.top)
line.set_xy2(ob.orderTextSeperator, volumeBarsLeftSide ? maxEndX : startX, info.bottom)
findOBSwings(len) =>
var swingType = 0
var obSwing top = obSwing.new(na, na)
var obSwing bottom = obSwing.new(na, na)
upper = ta.highest(len)
lower = ta.lowest(len)
swingType := high[len] > upper ? 0 : low[len] < lower ? 1 : swingType
if swingType == 0 and swingType[1] != 0
top := obSwing.new(bar_index[len], high[len], volume[len])
if swingType == 1 and swingType[1] != 1
bottom := obSwing.new(bar_index[len], low[len], volume[len])
[top, bottom]
findOrderBlocks () =>
if bar_index > last_bar_index - maxDistanceToLastBar
[top, btm] = findOBSwings(swingLength)
useBody = false
max = useBody ? math.max(close, open) : high
min = useBody ? math.min(close, open) : low
// Bullish Order Block
bullishBreaked = 0
if bullishOrderBlocksList.size() > 0
for i = bullishOrderBlocksList.size() - 1 to 0
currentOB = bullishOrderBlocksList.get(i)
if not currentOB.breaker
if (obEndMethod == "Wick" ? low : math.min(open, close)) < currentOB.bottom
currentOB.breaker := true
currentOB.breakTime := time
currentOB.bbVolume := volume
else
if high > currentOB.top
bullishOrderBlocksList.remove(i)
else if i < bullishOrderBlocks and top.y < currentOB.top and top.y > currentOB.bottom
bullishBreaked := 1
if close > top.y and not top.crossed
top.crossed := true
boxBtm = max[1]
boxTop = min[1]
boxLoc = time[1]
for i = 1 to (bar_index - top.x) - 1
boxBtm := math.min(min[i], boxBtm)
boxTop := boxBtm == min[i] ? max[i] : boxTop
boxLoc := boxBtm == min[i] ? time[i] : boxLoc
newOrderBlockInfo = orderBlockInfo.new(boxTop, boxBtm, volume + volume[1] + volume[2], "Bull", boxLoc)
newOrderBlockInfo.obLowVolume := volume[2]
newOrderBlockInfo.obHighVolume := volume + volume[1]
obSize = math.abs(newOrderBlockInfo.top - newOrderBlockInfo.bottom)
if obSize <= atr * maxATRMult
bullishOrderBlocksList.unshift(newOrderBlockInfo)
if bullishOrderBlocksList.size() > maxOrderBlocks
bullishOrderBlocksList.pop()
// Bearish Order Block
bearishBreaked = 0
if bearishOrderBlocksList.size() > 0
for i = bearishOrderBlocksList.size() - 1 to 0
currentOB = bearishOrderBlocksList.get(i)
if not currentOB.breaker
if (obEndMethod == "Wick" ? high : math.max(open, close)) > currentOB.top
currentOB.breaker := true
currentOB.breakTime := time
currentOB.bbVolume := volume
else
if low < currentOB.bottom
bearishOrderBlocksList.remove(i)
else if i < bearishOrderBlocks and btm.y > currentOB.bottom and btm.y < currentOB.top
bearishBreaked := 1
if close < btm.y and not btm.crossed
btm.crossed := true
boxBtm = min[1]
boxTop = max[1]
boxLoc = time[1]
for i = 1 to (bar_index - btm.x) - 1
boxTop := math.max(max[i], boxTop)
boxBtm := boxTop == max[i] ? min[i] : boxBtm
boxLoc := boxTop == max[i] ? time[i] : boxLoc
newOrderBlockInfo = orderBlockInfo.new(boxTop, boxBtm, volume + volume[1] + volume[2], "Bear", boxLoc)
newOrderBlockInfo.obLowVolume := volume + volume[1]
newOrderBlockInfo.obHighVolume := volume[2]
obSize = math.abs(newOrderBlockInfo.top - newOrderBlockInfo.bottom)
if obSize <= atr * maxATRMult
bearishOrderBlocksList.unshift(newOrderBlockInfo)
if bearishOrderBlocksList.size() > maxOrderBlocks
bearishOrderBlocksList.pop()
true
// LTF-specific OB detection — identical logic but uses ltfBullOBList/ltfBearOBList
// and ltfLookbackBars so request.security only does real work on recent bars.
// Separate var state in findOBSwingsLTF ensures no cross-contamination with the main swing tracker.
findOBSwingsLTF(len) =>
var swingType = 0
var obSwing top = obSwing.new(na, na)
var obSwing bottom = obSwing.new(na, na)
upper = ta.highest(len)
lower = ta.lowest(len)
swingType := high[len] > upper ? 0 : low[len] < lower ? 1 : swingType
if swingType == 0 and swingType[1] != 0
top := obSwing.new(bar_index[len], high[len], volume[len])
if swingType == 1 and swingType[1] != 1
bottom := obSwing.new(bar_index[len], low[len], volume[len])
[top, bottom]
findOrderBlocksLTF () =>
if bar_index > last_bar_index - ltfLookbackBars
[top, btm] = findOBSwingsLTF(swingLength)
useBody = false
max = useBody ? math.max(close, open) : high
min = useBody ? math.min(close, open) : low
bullishBreaked = 0
if ltfBullOBList.size() > 0
for i = ltfBullOBList.size() - 1 to 0
currentOB = ltfBullOBList.get(i)
if not currentOB.breaker
if (obEndMethod == "Wick" ? low : math.min(open, close)) < currentOB.bottom
currentOB.breaker := true
currentOB.breakTime := time
currentOB.bbVolume := volume
else
if high > currentOB.top
ltfBullOBList.remove(i)
else if i < bullishOrderBlocks and top.y < currentOB.top and top.y > currentOB.bottom
bullishBreaked := 1
if close > top.y and not top.crossed
top.crossed := true
boxBtm = max[1]
boxTop = min[1]
boxLoc = time[1]
for i = 1 to (bar_index - top.x) - 1
boxBtm := math.min(min[i], boxBtm)
boxTop := boxBtm == min[i] ? max[i] : boxTop
boxLoc := boxBtm == min[i] ? time[i] : boxLoc
newOrderBlockInfo = orderBlockInfo.new(boxTop, boxBtm, volume + volume[1] + volume[2], "Bull", boxLoc)
newOrderBlockInfo.obLowVolume := volume[2]
newOrderBlockInfo.obHighVolume := volume + volume[1]
obSize = math.abs(newOrderBlockInfo.top - newOrderBlockInfo.bottom)
if obSize <= atr * maxATRMult
ltfBullOBList.unshift(newOrderBlockInfo)
if ltfBullOBList.size() > maxOrderBlocks
ltfBullOBList.pop()
bearishBreaked = 0
if ltfBearOBList.size() > 0
for i = ltfBearOBList.size() - 1 to 0
currentOB = ltfBearOBList.get(i)
if not currentOB.breaker
if (obEndMethod == "Wick" ? high : math.max(open, close)) > currentOB.top
currentOB.breaker := true
currentOB.breakTime := time
currentOB.bbVolume := volume
else
if low < currentOB.bottom
ltfBearOBList.remove(i)
else if i < bearishOrderBlocks and btm.y > currentOB.bottom and btm.y < currentOB.top
bearishBreaked := 1
if close < btm.y and not btm.crossed
btm.crossed := true
boxBtm = min[1]
boxTop = max[1]
boxLoc = time[1]
for i = 1 to (bar_index - btm.x) - 1
boxTop := math.max(max[i], boxTop)
boxBtm := boxTop == max[i] ? min[i] : boxBtm
boxLoc := boxTop == max[i] ? time[i] : boxLoc
newOrderBlockInfo = orderBlockInfo.new(boxTop, boxBtm, volume + volume[1] + volume[2], "Bear", boxLoc)
newOrderBlockInfo.obLowVolume := volume + volume[1]
newOrderBlockInfo.obHighVolume := volume[2]
obSize = math.abs(newOrderBlockInfo.top - newOrderBlockInfo.bottom)
if obSize <= atr * maxATRMult
ltfBearOBList.unshift(newOrderBlockInfo)
if ltfBearOBList.size() > maxOrderBlocks
ltfBearOBList.pop()
true
areaOfOB (orderBlockInfo OBInfoF) =>
float XA1 = OBInfoF.startTime
float XA2 = na(OBInfoF.breakTime) ? time + 1 : OBInfoF.breakTime
float YA1 = OBInfoF.top
float YA2 = OBInfoF.bottom
float edge1 = math.sqrt((XA2 - XA1) * (XA2 - XA1) + (YA2 - YA2) * (YA2 - YA2))
float edge2 = math.sqrt((XA2 - XA2) * (XA2 - XA2) + (YA2 - YA1) * (YA2 - YA1))
float totalArea = edge1 * edge2
totalArea
doOBsTouch (orderBlockInfo OBInfo1, orderBlockInfo OBInfo2) =>
float XA1 = OBInfo1.startTime
float XA2 = na(OBInfo1.breakTime) ? time + 1 : OBInfo1.breakTime
float YA1 = OBInfo1.top
float YA2 = OBInfo1.bottom
float XB1 = OBInfo2.startTime
float XB2 = na(OBInfo2.breakTime) ? time + 1 : OBInfo2.breakTime
float YB1 = OBInfo2.top
float YB2 = OBInfo2.bottom
float intersectionArea = math.max(0, math.min(XA2, XB2) - math.max(XA1, XB1)) * math.max(0, math.min(YA1, YB1) - math.max(YA2, YB2))
float unionArea = areaOfOB(OBInfo1) + areaOfOB(OBInfo2) - intersectionArea
float overlapPercentage = (intersectionArea / unionArea) * 100.0
if overlapPercentage > overlapThresholdPercentage
true
else
false
isOBValid (orderBlockInfo OBInfo) =>
valid = true
if OBInfo.disabled
valid := false
valid
combineOBsFunc () =>
if allOrderBlocksList.size() > 0
lastCombinations = 999
while lastCombinations > 0
lastCombinations := 0
for i = 0 to allOrderBlocksList.size() - 1
curOB1 = allOrderBlocksList.get(i)
for j = 0 to allOrderBlocksList.size() - 1
curOB2 = allOrderBlocksList.get(j)
if i == j
continue
if not isOBValid(curOB1.info) or not isOBValid(curOB2.info)
continue
if curOB1.info.obType != curOB2.info.obType
continue
if doOBsTouch(curOB1.info, curOB2.info)
curOB1.info.disabled := true
curOB2.info.disabled := true
orderBlock newOB = createOrderBlock(orderBlockInfo.new(math.max(curOB1.info.top, curOB2.info.top), math.min(curOB1.info.bottom, curOB2.info.bottom), curOB1.info.obVolume + curOB2.info.obVolume, curOB1.info.obType))
newOB.info.startTime := math.min(curOB1.info.startTime, curOB2.info.startTime)
newOB.info.breakTime := math.max(nz(curOB1.info.breakTime), nz(curOB2.info.breakTime))
newOB.info.breakTime := newOB.info.breakTime == 0 ? na : newOB.info.breakTime
newOB.info.timeframeStr := curOB1.info.timeframeStr
newOB.info.obVolume := curOB1.info.obVolume + curOB2.info.obVolume
newOB.info.obLowVolume := curOB1.info.obLowVolume + curOB2.info.obLowVolume
newOB.info.obHighVolume := curOB1.info.obHighVolume + curOB2.info.obHighVolume
newOB.info.bbVolume := nz(curOB1.info.bbVolume, 0) + nz(curOB2.info.bbVolume, 0)
newOB.info.breaker := curOB1.info.breaker or curOB2.info.breaker
newOB.info.combined := true
if timeframe.in_seconds(curOB1.info.timeframeStr) != timeframe.in_seconds(curOB2.info.timeframeStr)
newOB.info.combinedTimeframesStr := (na(curOB1.info.combinedTimeframesStr) ? formatTimeframeString(curOB1.info.timeframeStr) : curOB1.info.combinedTimeframesStr) + " & " + (na(curOB2.info.combinedTimeframesStr) ? formatTimeframeString(curOB2.info.timeframeStr) : curOB2.info.combinedTimeframesStr)
allOrderBlocksList.unshift(newOB)
lastCombinations += 1
reqSeq (timeframeStr) =>
[bullishOrderBlocksListF, bearishOrderBlocksListF] = request.security(syminfo.tickerid, timeframeStr, [bullishOrderBlocksList, bearishOrderBlocksList])
[bullishOrderBlocksListF, bearishOrderBlocksListF]
reqSeqLTF (timeframeStr) =>
[bullF, bearF] = request.security(syminfo.tickerid, timeframeStr, [ltfBullOBList, ltfBearOBList])
[bullF, bearF]
getTFData (timeframeInfo timeframeInfoF, timeframeStr) =>
if not isTimeframeLower(timeframeInfoF.timeframeStr, timeframe.period) and timeframeInfoF.isEnabled
[bullishOrderBlocksListF, bearishOrderBlocksListF] = reqSeq(timeframeStr)
[bullishOrderBlocksListF, bearishOrderBlocksListF]
else
[na, na]
handleTimeframeInfo (timeframeInfo timeframeInfoF, bullishOrderBlocksListF, bearishOrderBlocksListF) =>
if not isTimeframeLower(timeframeInfoF.timeframeStr, timeframe.period) and timeframeInfoF.isEnabled
timeframeInfoF.bullishOrderBlocksList := bullishOrderBlocksListF
timeframeInfoF.bearishOrderBlocksList := bearishOrderBlocksListF
isWithinThresholdPct (float currentPrice, orderBlockInfo obInfo, float thresholdPct) =>
float midPrice = (obInfo.top + obInfo.bottom) / 2.0
float pctDiff = math.abs(midPrice - currentPrice) / currentPrice * 100.0
pctDiff <= thresholdPct
handleOrderBlocksFinal () =>
if DEBUG
log.info("Bullish OB Count " + str.tostring(bullishOrderBlocksList.size()))
log.info("Bearish OB Count " + str.tostring(bearishOrderBlocksList.size()))
if allOrderBlocksList.size () > 0
for i = 0 to allOrderBlocksList.size() - 1
safeDeleteOrderBlock(allOrderBlocksList.get(i))
allOrderBlocksList.clear()
for i = 0 to timeframeInfos.size() - 1
curTimeframe = timeframeInfos.get(i)
if not curTimeframe.isEnabled
continue
if curTimeframe.bullishOrderBlocksList.size() > 0
for j = 0 to math.min(curTimeframe.bullishOrderBlocksList.size() - 1, bullishOrderBlocks - 1)
orderBlockInfoF = curTimeframe.bullishOrderBlocksList.get(j)
orderBlockInfoF.timeframeStr := curTimeframe.timeframeStr
allOrderBlocksList.unshift(createOrderBlock(orderBlockInfo.copy(orderBlockInfoF)))
if curTimeframe.bearishOrderBlocksList.size() > 0
for j = 0 to math.min(curTimeframe.bearishOrderBlocksList.size() - 1, bearishOrderBlocks - 1)
orderBlockInfoF = curTimeframe.bearishOrderBlocksList.get(j)
orderBlockInfoF.timeframeStr := curTimeframe.timeframeStr
allOrderBlocksList.unshift(createOrderBlock(orderBlockInfo.copy(orderBlockInfoF)))
if combineOBs
combineOBsFunc()
if allOrderBlocksList.size() > 0
for i = 0 to allOrderBlocksList.size() - 1
curOB = allOrderBlocksList.get(i)
if isOBValid(curOB.info)
renderOrderBlock(curOB)
findOrderBlocks()
findOrderBlocksLTF()
[bullishOrderBlocksListTimeframe1, bearishOrderBlocksListTimeframe1] = getTFData(timeframeInfos.get(0), timeframe1)
// Conditionally fetch LTF data — avoids request.security on daily/weekly/monthly charts
// which would compute across decades of intraday bars and crash Pine for long-history assets.
getLTFData(string tfStr, bool shouldFetch) =>
if shouldFetch
reqSeqLTF(tfStr)
else
[array.new<orderBlockInfo>(0), array.new<orderBlockInfo>(0)]
[ltfBullBlocks1, ltfBearBlocks1] = getLTFData(activeLTF1, _ltfFeatActive)
[ltfBullBlocks2, ltfBearBlocks2] = getLTFData(activeLTF2, _ltfFeatActive)
tfToSeconds(string tf) =>
tf == "" ? timeframe.in_seconds(timeframe.period) : timeframe.in_seconds(tf)
// Returns true if the candidate should be suppressed.
// If an existing zone is the lower-TF one, it is removed and deleted so the candidate can take its place.
ltfCheckConflicts(orderBlockInfo ltfOBInfo) =>
suppress = false
int k = 0
while k < allOrderBlocksList.size() and not suppress
existingOB = allOrderBlocksList.get(k)
if isOBValid(existingOB.info) and existingOB.info.obType == ltfOBInfo.obType
float gap = math.max(0.0, math.max(ltfOBInfo.bottom, existingOB.info.bottom) - math.min(ltfOBInfo.top, existingOB.info.top))
if gap / close * 100.0 <= ltfProximityPct
int candidateSecs = tfToSeconds(ltfOBInfo.timeframeStr)
int existingSecs = tfToSeconds(existingOB.info.timeframeStr)
if candidateSecs < existingSecs
// Candidate is the lower TF → suppress it
suppress := true
else if existingSecs < candidateSecs
// Existing is the lower TF → remove it, keep checking for more conflicts
safeDeleteOrderBlock(existingOB)
existingOB.info.disabled := true
allOrderBlocksList.remove(k)
k := k - 1 // compensate for removal so next element isn't skipped
else
// Same TF → keep existing, suppress candidate
suppress := true
k := k + 1
suppress
renderLTFList(orderBlockInfo[] bullList, orderBlockInfo[] bearList, string tfStr) =>
if not na(bullList) and bullList.size() > 0
for j = 0 to bullList.size() - 1
ltfOBInfo = bullList.get(j)
ltfOBInfo.timeframeStr := tfStr // set before conflict check so TF comparison is accurate
if not ltfOBInfo.breaker and isWithinThresholdPct(close, ltfOBInfo, ltfThresholdPct) and not ltfCheckConflicts(ltfOBInfo)
newOB = createOrderBlock(orderBlockInfo.copy(ltfOBInfo))
renderOrderBlock(newOB)
allOrderBlocksList.unshift(newOB)
if not na(bearList) and bearList.size() > 0
for j = 0 to bearList.size() - 1
ltfOBInfo = bearList.get(j)
ltfOBInfo.timeframeStr := tfStr
if not ltfOBInfo.breaker and isWithinThresholdPct(close, ltfOBInfo, ltfThresholdPct) and not ltfCheckConflicts(ltfOBInfo)
newOB = createOrderBlock(orderBlockInfo.copy(ltfOBInfo))
renderOrderBlock(newOB)
allOrderBlocksList.unshift(newOB)
if barstate.isconfirmed
handleTimeframeInfo(timeframeInfos.get(0), bullishOrderBlocksListTimeframe1, bearishOrderBlocksListTimeframe1)
handleOrderBlocksFinal()
if _ltfFeatActive
if timeframe.in_seconds(activeLTF1) < _ltfSecs
renderLTFList(ltfBullBlocks1, ltfBearBlocks1, activeLTF1)
if ltfAutoEnabled and timeframe.in_seconds(activeLTF2) < _ltfSecs
renderLTFList(ltfBullBlocks2, ltfBearBlocks2, activeLTF2)
Show all 691 lines