EQH/EQL Liquidity Zones [LuxAlgo]

LuxAlgo · study · 239 行 · 点赞 6,542 · TradingView 原页

本页源码来自 TradingView 公开发布的开源脚本,版权归原作者所有, 请遵循其原始许可(Pine 脚本常见 CC BY-NC-SA / MPL-2.0 / MIT)。 本项目仅用于研究检索与许可范围内的移植。

Pine Script

// This work is licensed under a Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) https://creativecommons.org/licenses/by-nc-sa/4.0/
// © LuxAlgo
//@version=6
indicator("EQH/EQL Liquidity Zones [LuxAlgo]", overlay = true, max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500)
 
//---------------------------------------------------------------------------------------------------------------------}
// Constants
//---------------------------------------------------------------------------------------------------------------------{
const string G1 = "Liquidity Detection"
const string G2 = "Visuals"
const color BULL_COLOR = #089981
const color BEAR_COLOR = #f23645
 
//---------------------------------------------------------------------------------------------------------------------}
// Inputs
//---------------------------------------------------------------------------------------------------------------------{
int leftLenInput    = input.int(10, "Pivot Left Length", minval = 1, group = G1, tooltip = "Number of bars to the left required for a pivot.")
int rightLenInput   = input.int(2, "Pivot Right Length", minval = 1, group = G1, tooltip = "Number of bars to the right required for a pivot.")
float thresholdPct  = input.float(0.03, "Equality Threshold (%)", minval = 0, step = 0.01, group = G1, tooltip = "Maximum percentage difference to consider two highs/lows 'equal'.")
int maxZones        = input.int(60, "Max Active Zones", minval = 1, maxval = 100, group = G1, tooltip = "Maximum number of active (unswept) liquidity lines to track. Increase for 1m charts.")
 
color bullColor     = input.color(BULL_COLOR, "Bullish Zone Color", group = G2)
color bearColor     = input.color(BEAR_COLOR, "Bearish Zone Color", group = G2)
int zoneTransp      = input.int(85, "Zone Transparency", minval = 0, maxval = 100, group = G2)
bool showMidline    = input.bool(false, "Show Midline", group = G2)
color midlineColor  = input.color(#787b86, "Midline Color", group = G2)
bool showVolume     = input.bool(true, "Show Volume", group = G2)
bool deleteOnSweep  = input.bool(false, "Delete on Sweep", group = G2)
 
//---------------------------------------------------------------------------------------------------------------------}
// Types
//---------------------------------------------------------------------------------------------------------------------{
type LiquidityZone
    box   id
    line  midline
    label lbl
    label b1     
    label b2     
    label v1     
    label v2     
    float sweepLevel
    float totalVol 
    int   createdIdx 
    bool  isHigh
    bool  isSwept
 
type PivotPoint
    float price
    int   idx
    float vol
 
//---------------------------------------------------------------------------------------------------------------------}
// Storage
//---------------------------------------------------------------------------------------------------------------------{
var activeZones     = array.new<LiquidityZone>()
var historicalHighs = array.new<PivotPoint>()
var historicalLows  = array.new<PivotPoint>()
 
//---------------------------------------------------------------------------------------------------------------------}
// Functions
//---------------------------------------------------------------------------------------------------------------------{
f_formatVol(float v) =>
    string res = ""
    if v >= 1000000
        res := str.format("{0,number,#.#}M", v / 1000000)
    else if v >= 1000
        res := str.format("{0,number,#.#}K", v / 1000)
    else
        res := str.tostring(v, "#")
    res
 
method update(LiquidityZone zone, bool deleteOnSweep) =>
    bool shouldRemove = false
    if not zone.isSwept
        // Extend to current bar
        box.set_right(zone.id, bar_index)
        if not na(zone.midline)
            line.set_x2(zone.midline, bar_index)
        label.set_x(zone.lbl, bar_index)
        
        // Sweep check starts the bar AFTER confirmation to handle 1m volatility
        if bar_index > zone.createdIdx
            bool sweepOccurred = zone.isHigh ? high > zone.sweepLevel : low < zone.sweepLevel
            
            if sweepOccurred
                zone.isSwept := true
                shouldRemove := true
                if deleteOnSweep
                    box.delete(zone.id)
                    line.delete(zone.midline)
                    label.delete(zone.lbl)
                    label.delete(zone.b1)
                    label.delete(zone.b2)
                    label.delete(zone.v1)
                    label.delete(zone.v2)
                else
                    box.set_bgcolor(zone.id, color.new(chart.fg_color, 95))
                    box.set_border_color(zone.id, color.new(chart.fg_color, 80))
                    if not na(zone.midline)
                        line.set_color(zone.midline, color.new(chart.fg_color, 80))
                    
                    label.set_textcolor(zone.lbl, color.new(chart.fg_color, 75))
                    string volStr = showVolume ? str.format(" ({0})", f_formatVol(zone.totalVol)) : ""
                    label.set_text(zone.lbl, "Swept " + (zone.isHigh ? "EQH" : "EQL") + volStr)
                    
                    label.set_color(zone.b1, color.new(chart.fg_color, 85))
                    label.set_color(zone.b2, color.new(chart.fg_color, 85))
                    label.set_text(zone.v1, "")
                    label.set_text(zone.v2, "")
    
    shouldRemove
 
f_consolidateLabels(array<LiquidityZone> zones) =>
    int sz = zones.size()
    if sz > 0
        for i = 0 to sz - 1
            LiquidityZone z = zones.get(i)
            if not z.isSwept
                label.set_text(z.lbl, "")
        
        array<int> processed = array.new<int>()
        for i = 0 to sz - 1
            if processed.includes(i) or zones.get(i).isSwept
                continue
            
            LiquidityZone base = zones.get(i)
            processed.push(i)
            float clusterVol = base.totalVol
            int clusterCount = 1
            
            if i < sz - 1
                for j = i + 1 to sz - 1
                    if processed.includes(j)
                        continue
                    
                    LiquidityZone comp = zones.get(j)
                    if comp.isSwept
                        continue
                        
                    if base.isHigh == comp.isHigh and math.abs(base.sweepLevel - comp.sweepLevel) / base.sweepLevel * 100 <= thresholdPct * 3
                        clusterVol += comp.totalVol
                        clusterCount += 1
                        processed.push(j)
            
            string typeStr  = base.isHigh ? "EQH" : "EQL"
            string countStr = clusterCount > 1 ? str.format("{0}x ", clusterCount) : ""
            string volStr   = showVolume ? str.format(" ({0})", f_formatVol(clusterVol)) : ""
            label.set_text(base.lbl, countStr + typeStr + volStr)
 
//---------------------------------------------------------------------------------------------------------------------}
// Core Logic
//---------------------------------------------------------------------------------------------------------------------{
float pH = ta.pivothigh(leftLenInput, rightLenInput)
float pL = ta.pivotlow(leftLenInput, rightLenInput)
 
// Handle Highs (EQH)
if not na(pH)
    int currentPivotIdx = bar_index - rightLenInput
    float currentVol = volume[rightLenInput]
    
    if historicalHighs.size() > 0
        for i = 0 to historicalHighs.size() - 1
            PivotPoint prev = historicalHighs.get(i)
            float diff = math.abs(pH - prev.price) / prev.price * 100
            
            if diff <= thresholdPct
                float top    = math.max(pH, prev.price)
                float bottom = math.min(pH, prev.price)
                float mid    = (top + bottom) / 2
                float totalVol = prev.vol + currentVol
                
                box b = box.new(prev.idx, top, bar_index, bottom, border_color = bearColor, bgcolor = color.new(bearColor, zoneTransp))
                line ml = showMidline ? line.new(prev.idx, mid, bar_index, mid, color = midlineColor, style = line.style_dashed) : na
                
                label lb = label.new(bar_index, mid, "", color = #00000000, textcolor = bearColor, style = label.style_label_left, size = size.small)
                label b1 = label.new(prev.idx, prev.price, "", color = bearColor, style = label.style_circle, size = size.small)
                label b2 = label.new(currentPivotIdx, pH, "", color = bearColor, style = label.style_circle, size = size.small)
                label v1 = showVolume ? label.new(prev.idx, prev.price, f_formatVol(prev.vol), color = #00000000, textcolor = bearColor, style = label.style_label_down, size = size.small) : na
                label v2 = showVolume ? label.new(currentPivotIdx, pH, f_formatVol(currentVol), color = #00000000, textcolor = bearColor, style = label.style_label_down, size = size.small) : na
                
                activeZones.push(LiquidityZone.new(b, ml, lb, b1, b2, v1, v2, top, totalVol, bar_index, true, false))
                break 
    
    historicalHighs.unshift(PivotPoint.new(pH, currentPivotIdx, currentVol))
    if historicalHighs.size() > 50
        historicalHighs.pop()
 
// Handle Lows (EQL)
if not na(pL)
    int currentPivotIdx = bar_index - rightLenInput
    float currentVol = volume[rightLenInput]
    
    if historicalLows.size() > 0
        for i = 0 to historicalLows.size() - 1
            PivotPoint prev = historicalLows.get(i)
            float diff = math.abs(pL - prev.price) / prev.price * 100
            
            if diff <= thresholdPct
                float top    = math.max(pL, prev.price)
                float bottom = math.min(pL, prev.price)
                float mid    = (top + bottom) / 2
                float totalVol = prev.vol + currentVol
                
                box b = box.new(prev.idx, top, bar_index, bottom, border_color = bullColor, bgcolor = color.new(bullColor, zoneTransp))
                line ml = showMidline ? line.new(prev.idx, mid, bar_index, mid, color = midlineColor, style = line.style_dashed) : na
                
                label lb = label.new(bar_index, mid, "", color = #00000000, textcolor = bullColor, style = label.style_label_left, size = size.small)
                label b1 = label.new(prev.idx, prev.price, "", color = bullColor, style = label.style_circle, size = size.small)
                label b2 = label.new(currentPivotIdx, pL, "", color = bullColor, style = label.style_circle, size = size.small)
                label v1 = showVolume ? label.new(prev.idx, prev.price, f_formatVol(prev.vol), color = #00000000, textcolor = bullColor, style = label.style_label_up, size = size.small) : na
                label v2 = showVolume ? label.new(currentPivotIdx, pL, f_formatVol(currentVol), color = #00000000, textcolor = bullColor, style = label.style_label_up, size = size.small) : na
                
                activeZones.push(LiquidityZone.new(b, ml, lb, b1, b2, v1, v2, bottom, totalVol, bar_index, false, false))
                break
    
    historicalLows.unshift(PivotPoint.new(pL, currentPivotIdx, currentVol))
    if historicalLows.size() > 50
        historicalLows.pop()
 
//---------------------------------------------------------------------------------------------------------------------}
// Manage Active Zones
//---------------------------------------------------------------------------------------------------------------------{
if activeZones.size() > 0
    // 1. Process active updates and identify swept zones
    for i = activeZones.size() - 1 to 0
        LiquidityZone zone = activeZones.get(i)
        bool swept = zone.update(deleteOnSweep)
        if swept
            activeZones.remove(i)
    
    // 2. Enforce limit by removing OLDEST zones only if still over capacity
    while activeZones.size() > maxZones
        activeZones.remove(0)
    
    // 3. Consolidate labels for remaining live zones
    if activeZones.size() > 0
        f_consolidateLabels(activeZones)
 
//---------------------------------------------------------------------------------------------------------------------}

← 返回列表