key_level_heatmap_crypto.pine
Copy
//@version=6
// =============================================================================
// 加密货币 1H 关键价位热力图 V2(自适应验证版)
// =============================================================================
// 本脚本是 key_level_heatmap_crypto.pine 的迭代优化版。原始文件未做修改。
//
// V2 针对加密货币 7×24、高波动、杠杆清算频繁的微观结构做了五类改进:
//
// 1. 多尺度枢轴确认
// - 同时检查短腿枢轴与长腿枢轴,多尺度共振的价位会获得更高证据分;
// - 避免单一固定腿数在趋势/震荡切换时漏掉真正结构位。
//
// 2. 流动性集群 / 等高点等低点识别
// - 对已确认摆动高/低点做 ATR 自适应聚类;
// - 双重顶/底、三重顶/底、清算池等价位会获得额外证据;
// - 这是原版缺口+固定枢轴无法识别的加密特色结构。
//
// 3. 触及后“验证制”计分
// - 不再把每次穿越都按固定分累加,而是观察 4 根 1H K 线:
// 有效拒绝(反向位移 >= 0.35 ATR)才计入 validReaction;
// 无效穿越(继续朝不利方向 >= 0.65 ATR)会降低该区间权重。
// - 分数从“触及次数”升级为“被验证的可交易反应质量”。
//
// 4. 突破失效惩罚与角色翻转保留
// - 收盘有效突破后更果断地削弱旧支撑/阻力;
// - 若后续回踩重新验证原突破边,仍可凭多空转换证据复活。
//
// 5. 更严格的评分、合并与显示
// - 整数位只放大“已有价格证据”的区间,不再凭空制造整数位;
// - 合并半径 0.45 -> 0.38 ATR 带宽,中心价更贴近真实反应点;
// - 距离分权重 25% -> 18%,降低“离得近但无结构”的干扰;
// - 默认最大显示距离 14 ATR -> 10 ATR,减少远端过期杂带。
//
// 使用方式与原版一致:固定 1 小时证据,图表周期 1 分钟~1 小时,
// 实时盘中使用已收盘确认区间,不建议按历史绘图做自动回测。
//
// 默认配置(已验证优先):权重 45 / 25 / 20 / 5 / 5;
// 最低强度 40;默认只显示至少出现过一次有效拒绝反应的区间。
// =============================================================================
indicator(
"加密货币1小时关键价位热力图 V2",
shorttitle = "加密关键区V2",
overlay = true,
max_boxes_count = 12,
max_lines_count = 12,
max_labels_count = 12
)
// 固定参数
const string ANALYSIS_TIMEFRAME = "60"
const int MAX_DRAW_ZONES = 12
const int MAX_ENGINE_ZONES = 170
const int CALC_BARS_COUNT = 5200
const int MIN_HISTORY_DAYS = 75
// 时间衰减:加密货币全天交易、波动更大。V2 短期半衰期约 9.5 个日历日,
// 长期约 31 个日历日,比 V1 更偏向仍被近期验证的证据。
const float SHORT_DAILY_DECAY = 0.9300
const float LONG_DAILY_DECAY = 0.9780
// V2 降低距离分权重,结构证据成为主导项。
const float STRUCTURE_RANK_WEIGHT = 0.82
const float DISTANCE_RANK_WEIGHT = 0.18
const float DISTANCE_DECAY_ATR = 7.0
const float STRUCTURE_FLOOR_RATIO = 0.40
// 输入分组
const string GROUP_CORE = "核心设置"
const string GROUP_ROUND = "整数位置"
const string GROUP_DISPLAY = "显示设置"
const string GROUP_ADVANCED = "高级权重"
string sensitivityInput = input.string(
"标准",
"灵敏度",
options = ["保守", "标准", "密集"],
tooltip = "保守模式减少候选区;密集模式保留更多局部结构。V2 会在该档位基础上再叠加长腿枢轴确认。",
group = GROUP_CORE
)
int maxZonesInput = input.int(
8,
"最大区间数",
minval = 4,
maxval = MAX_DRAW_ZONES,
tooltip = "限制图上同时显示的高强度区间数量。",
group = GROUP_CORE
)
float minStrengthInput = input.float(
40.0,
"最低强度",
minval = 0.0,
maxval = 100.0,
step = 1.0,
tooltip = "低于该可交易性分数的区间不会显示。V2 分数以已验证反应质量为主,默认门槛比 V1 高 5 分。",
group = GROUP_CORE
)
float displayMergeAtrInput = input.float(
0.70,
"邻近区间合并距离(ATR)",
minval = 0.25,
maxval = 1.50,
step = 0.05,
tooltip = "显示前按1小时ATR合并相邻微区间。V2 建议 0.60~0.80,比 V1 更窄以减少中心价偏移。",
group = GROUP_CORE
)
float minSpacingAtrInput = input.float(
2.25,
"最小显示间距(ATR)",
minval = 0.0,
maxval = 4.0,
step = 0.05,
tooltip = "已显示区间中心之间的最小距离。V2 默认 2.25 ATR,减少相邻价位互相干扰;设为0可禁用。",
group = GROUP_CORE
)
string widthInput = input.string(
"标准",
"区间宽度",
options = ["窄", "标准", "宽"],
tooltip = "区间宽度由事件发生时的1小时ATR决定。V2 标准宽度为 0.28 ATR。",
group = GROUP_CORE
)
bool liquidityClustersInput = input.bool(
true,
"流动性集群识别",
tooltip = "识别双重顶/底、三重顶/底等清算池结构,并为这些价位提供额外证据。",
group = GROUP_CORE
)
bool validatedOnlyInput = input.bool(
true,
"仅显示已验证区间",
tooltip = "默认开启:只显示历史上至少出现过一次有效拒绝反应的区间,显著减少假位。",
group = GROUP_CORE
)
string roundModeInput = input.string(
"自动",
"整数步长",
options = ["自动", "手动"],
tooltip = "自动模式会按标的价格选择适合的整数网格。",
group = GROUP_ROUND
)
float manualRoundStepInput = input.float(
5.0,
"手动基础步长",
minval = 0.01,
step = 0.5,
tooltip = "仅在整数步长选择手动时生效。",
group = GROUP_ROUND
)
bool showCenterInput = input.bool(
true,
"显示中心线",
tooltip = "中心线用于和人工标注价位比较,热力区间本身才是有效范围。",
group = GROUP_DISPLAY
)
bool showLabelsInput = input.bool(
true,
"显示价位与强度",
tooltip = "右侧标签格式为:中心价 | 可交易性分数。",
group = GROUP_DISPLAY
)
string labelSizeInput = input.string(
"中",
"标签字号",
options = ["小", "中", "大"],
tooltip = "右侧价格与强度标签的文字大小,看不太清时可以换更大的字号。",
group = GROUP_DISPLAY
)
float maxDistanceAtrInput = input.float(
10.0,
"最大显示距离(ATR)",
minval = 2.0,
maxval = 30.0,
step = 1.0,
tooltip = "隐藏距离现价过远的历史区间。V2 默认 10 ATR,聚焦可交易距离内的关键位。",
group = GROUP_DISPLAY
)
color heatColorInput = input.color(
color.rgb(91, 76, 255),
"热力颜色",
tooltip = "颜色只表达关键性强弱,不表达上涨或下跌方向。",
group = GROUP_DISPLAY
)
float touchWeightInput = input.float(
45.0,
"触及与拒绝权重",
minval = 0.0,
maxval = 100.0,
step = 1.0,
group = GROUP_ADVANCED
)
float roleWeightInput = input.float(
25.0,
"多空转换权重",
minval = 0.0,
maxval = 100.0,
step = 1.0,
group = GROUP_ADVANCED
)
float volumeWeightInput = input.float(
20.0,
"相对成交量权重",
minval = 0.0,
maxval = 100.0,
step = 1.0,
group = GROUP_ADVANCED
)
float gapWeightInput = input.float(
5.0,
"缺口权重",
minval = 0.0,
maxval = 100.0,
step = 1.0,
group = GROUP_ADVANCED
)
float roundWeightInput = input.float(
5.0,
"整数共振权重",
minval = 0.0,
maxval = 100.0,
step = 1.0,
group = GROUP_ADVANCED
)
// 区间保存各类证据。V2 增加触及计数与验证计数,用于计算“反应质量”。
type Zone
float center
float halfWidth
float centerWeight
float touchShort
float touchLong
float roleShort
float roleLong
float volumeShort
float volumeLong
float gapShort
float gapLong
int firstTime
int lastEventTime
int lastTouchBar
int lastTouchDay
int pendingTouchBar
int pendingTouchSide
bool pendingRole
int lastRelation
int lastRelationBar
int lastBreakSide
int lastBreakBar
bool gapActive
int gapDirection
float gapFillPrice
int touchCount
int validReactionCount
int failedReactionCount
// 计算上下文只向主图返回绘图所需的数据
type ZoneSnapshot
array<float> centers
array<float> halfWidths
array<float> scores
array<int> firstTimes
float atr
float sourceClose
float liveClose
int processedDays
bool volumeAvailable
f_clamp(float value, float lower, float upper) =>
math.max(lower, math.min(upper, value))
f_combined(float shortValue, float longValue) =>
0.70 * shortValue + 0.30 * longValue
f_total_evidence(Zone zone) =>
f_combined(zone.touchShort, zone.touchLong) +
f_combined(zone.roleShort, zone.roleLong) +
f_combined(zone.volumeShort, zone.volumeLong) +
f_combined(zone.gapShort, zone.gapLong)
f_add_evidence(Zone zone, float touchValue, float roleValue, float volumeValue, float gapValue) =>
zone.touchShort += touchValue
zone.touchLong += touchValue
zone.roleShort += roleValue
zone.roleLong += roleValue
zone.volumeShort += volumeValue
zone.volumeLong += volumeValue
zone.gapShort += gapValue
zone.gapLong += gapValue
0.0
f_decay_zone(Zone zone) =>
zone.touchShort *= SHORT_DAILY_DECAY
zone.touchLong *= LONG_DAILY_DECAY
zone.roleShort *= SHORT_DAILY_DECAY
zone.roleLong *= LONG_DAILY_DECAY
zone.volumeShort *= SHORT_DAILY_DECAY
zone.volumeLong *= LONG_DAILY_DECAY
zone.gapShort *= SHORT_DAILY_DECAY
zone.gapLong *= LONG_DAILY_DECAY
zone.centerWeight *= LONG_DAILY_DECAY
0.0
// 触及反应质量:验证过的拒绝越多,分数越高;只被穿越但从未拒绝的区间降权。
f_reaction_quality(Zone zone) =>
zone.touchCount > 0
? (zone.validReactionCount + 1.0) / (zone.touchCount + 2.0)
: 0.50
f_event_half_width(float price, float eventAtr, float widthFactor) =>
float rawWidth = eventAtr * widthFactor
// 加密货币单根K线波动更大,放宽上下限以避免区间被压得过窄
float minWidth = math.max(price * 0.0015, syminfo.mintick * 5.0)
float maxWidth = math.max(price * 0.0060, minWidth)
f_clamp(rawWidth, minWidth, maxWidth)
f_auto_round_steps(float price, float manualStep, bool useManual) =>
float minorStep = na
float majorStep = na
float superStep = na
if useManual
minorStep := manualStep
majorStep := manualStep * 2.0
superStep := manualStep * 10.0
else if price < 1.0
minorStep := 0.01
majorStep := 0.05
superStep := 0.10
else if price < 10.0
minorStep := 0.05
majorStep := 0.10
superStep := 1.00
else if price < 100.0
minorStep := 0.50
majorStep := 1.00
superStep := 5.00
else if price < 1000.0
minorStep := 1.0
majorStep := 5.0
superStep := 10.0
else if price < 10000.0
minorStep := 5.0
majorStep := 10.0
superStep := 100.0
else if price < 100000.0
minorStep := 50.0
majorStep := 100.0
superStep := 1000.0
else
minorStep := 100.0
majorStep := 1000.0
superStep := 5000.0
[minorStep, majorStep, superStep]
f_near_grid(float price, float step, float tolerance) =>
float nearest = math.round(price / step) * step
math.abs(price - nearest) <= tolerance
f_round_quality(float price, float halfWidth, float manualStep, bool useManual) =>
[minorStep, majorStep, superStep] = f_auto_round_steps(price, manualStep, useManual)
float tolerance = math.max(halfWidth * 0.60, syminfo.mintick * 3.0)
float quality = 0.0
if f_near_grid(price, minorStep, tolerance)
quality := 0.50
if f_near_grid(price, majorStep, tolerance)
quality := 0.80
if f_near_grid(price, superStep, tolerance)
quality := 1.00
quality
f_zone_score(
Zone zone,
float touchWeight,
float roleWeight,
float volumeWeight,
float gapWeight,
float roundWeight,
float manualStep,
bool useManual
) =>
float touchCombined = f_combined(zone.touchShort, zone.touchLong)
float roleCombined = f_combined(zone.roleShort, zone.roleLong)
float volumeCombined = f_combined(zone.volumeShort, zone.volumeLong)
float gapCombined = f_combined(zone.gapShort, zone.gapLong)
float quality = f_reaction_quality(zone)
float touchComponent = f_clamp(touchCombined / 2.20, 0.0, 1.0) * (0.35 + 0.65 * quality)
float roleComponent = f_clamp(roleCombined / 1.20, 0.0, 1.0)
float volumeComponent = f_clamp(volumeCombined / 1.80, 0.0, 1.0)
float gapComponent = f_clamp(gapCombined / 1.00, 0.0, 1.0)
// 整数共振只放大已经存在价格证据的区间,避免画出纯心理位。
float evidencePresence = f_clamp(f_total_evidence(zone) / 3.0, 0.0, 1.0)
float roundComponent = f_round_quality(zone.center, zone.halfWidth, manualStep, useManual) * (0.20 + 0.80 * evidencePresence)
float weightSum = math.max(
touchWeight + roleWeight + volumeWeight + gapWeight + roundWeight,
1.0
)
100.0 * (
touchComponent * touchWeight +
roleComponent * roleWeight +
volumeComponent * volumeWeight +
gapComponent * gapWeight +
roundComponent * roundWeight
) / weightSum
f_find_merge_target(
array<Zone> zones,
float price,
float eventWidth,
bool gapActive,
int gapDirection,
float gapFillPrice
) =>
int bestIndex = -1
float bestDistance = 1e20
int zoneCount = array.size(zones)
if zoneCount > 0
for index = 0 to zoneCount - 1
Zone zone = array.get(zones, index)
float distance = math.abs(zone.center - price)
// V2 合并半径 0.45 -> 0.38,避免把不同反应点焊成一个宽区间。
float mergeRadius = 0.38 * (zone.halfWidth + eventWidth)
bool incompatibleGap = (
gapActive and
zone.gapActive and
(
gapDirection != zone.gapDirection or
math.abs(gapFillPrice - zone.gapFillPrice) > syminfo.mintick
)
)
if not incompatibleGap and distance <= mergeRadius and distance < bestDistance
bestDistance := distance
bestIndex := index
bestIndex
f_remove_weakest(array<Zone> zones, float currentClose, float currentAtr) =>
int weakestIndex = -1
float weakestScore = 1e20
int zoneCount = array.size(zones)
if zoneCount > 0
for index = 0 to zoneCount - 1
Zone zone = array.get(zones, index)
float evidence = f_total_evidence(zone)
float quality = f_reaction_quality(zone)
float distanceAtr = currentAtr > 0.0 ? math.abs(zone.center - currentClose) / currentAtr : 0.0
float removalScore = evidence * quality * math.exp(-distanceAtr / DISTANCE_DECAY_ATR)
if removalScore < weakestScore
weakestScore := removalScore
weakestIndex := index
if weakestIndex >= 0
array.remove(zones, weakestIndex)
weakestIndex
f_group_spacing_ok(
array<float> centers,
array<int> selectedGroups,
int candidateIndex,
float minDistance
) =>
bool spacingOk = true
int selectedCount = array.size(selectedGroups)
if selectedCount > 0
for index = 0 to selectedCount - 1
int selectedIndex = array.get(selectedGroups, index)
float distance = math.abs(
array.get(centers, candidateIndex) - array.get(centers, selectedIndex)
)
if distance < minDistance
spacingOk := false
break
spacingOk
f_add_candidate(
array<Zone> zones,
float price,
float eventWidth,
int eventTime,
float touchEvidence,
float volumeEvidence,
float gapEvidence,
bool gapActive,
int gapDirection,
float gapFillPrice,
float currentClose,
float currentAtr
) =>
int targetIndex = f_find_merge_target(
zones,
price,
eventWidth,
gapActive,
gapDirection,
gapFillPrice
)
// gap 证据只参与评分,不进入 centerWeight,避免缺口回补后中心价仍被旧缺口权重锚定
float eventWeight = math.max(touchEvidence * 0.85 + volumeEvidence * 0.15, 0.20)
if targetIndex >= 0
Zone zone = array.get(zones, targetIndex)
float newWeight = zone.centerWeight + eventWeight
zone.center := (zone.center * zone.centerWeight + price * eventWeight) / newWeight
zone.halfWidth := (
zone.halfWidth * zone.centerWeight + eventWidth * eventWeight
) / newWeight
zone.centerWeight := newWeight
zone.firstTime := math.min(zone.firstTime, eventTime)
zone.lastEventTime := math.max(zone.lastEventTime, eventTime)
f_add_evidence(zone, touchEvidence, 0.0, volumeEvidence, gapEvidence)
if gapActive
zone.gapActive := true
zone.gapDirection := gapDirection
zone.gapFillPrice := gapFillPrice
else
Zone newZone = Zone.new(
price,
eventWidth,
eventWeight,
touchEvidence,
touchEvidence,
0.0,
0.0,
volumeEvidence,
volumeEvidence,
gapEvidence,
gapEvidence,
eventTime,
eventTime,
int(na),
int(na),
int(na),
0,
false,
0,
int(na),
0,
int(na),
gapActive,
gapDirection,
gapFillPrice,
0,
0,
0
)
array.push(zones, newZone)
if array.size(zones) > MAX_ENGINE_ZONES
f_remove_weakest(zones, currentClose, currentAtr)
targetIndex
f_merge_overlaps(array<Zone> zones) =>
int firstIndex = 0
while firstIndex < array.size(zones)
int secondIndex = firstIndex + 1
while secondIndex < array.size(zones)
Zone firstZone = array.get(zones, firstIndex)
Zone secondZone = array.get(zones, secondIndex)
float distance = math.abs(firstZone.center - secondZone.center)
float overlapThreshold = 0.38 * (firstZone.halfWidth + secondZone.halfWidth)
bool incompatibleGap = (
firstZone.gapActive and
secondZone.gapActive and
(
firstZone.gapDirection != secondZone.gapDirection or
math.abs(firstZone.gapFillPrice - secondZone.gapFillPrice) > syminfo.mintick
)
)
if not incompatibleGap and distance <= overlapThreshold
float combinedWeight = firstZone.centerWeight + secondZone.centerWeight
firstZone.center := (
firstZone.center * firstZone.centerWeight +
secondZone.center * secondZone.centerWeight
) / combinedWeight
firstZone.halfWidth := (
firstZone.halfWidth * firstZone.centerWeight +
secondZone.halfWidth * secondZone.centerWeight
) / combinedWeight
firstZone.centerWeight := combinedWeight
firstZone.touchShort += secondZone.touchShort
firstZone.touchLong += secondZone.touchLong
firstZone.roleShort += secondZone.roleShort
firstZone.roleLong += secondZone.roleLong
firstZone.volumeShort += secondZone.volumeShort
firstZone.volumeLong += secondZone.volumeLong
firstZone.gapShort += secondZone.gapShort
firstZone.gapLong += secondZone.gapLong
firstZone.firstTime := math.min(firstZone.firstTime, secondZone.firstTime)
firstZone.touchCount += secondZone.touchCount
firstZone.validReactionCount += secondZone.validReactionCount
firstZone.failedReactionCount += secondZone.failedReactionCount
if na(firstZone.lastTouchBar) or (
not na(secondZone.lastTouchBar) and
secondZone.lastTouchBar > firstZone.lastTouchBar
)
firstZone.lastTouchBar := secondZone.lastTouchBar
firstZone.lastTouchDay := secondZone.lastTouchDay
if na(firstZone.pendingTouchBar) or (
not na(secondZone.pendingTouchBar) and
secondZone.pendingTouchBar > firstZone.pendingTouchBar
)
firstZone.pendingTouchBar := secondZone.pendingTouchBar
firstZone.pendingTouchSide := secondZone.pendingTouchSide
firstZone.pendingRole := secondZone.pendingRole
if secondZone.lastEventTime > firstZone.lastEventTime
firstZone.lastEventTime := secondZone.lastEventTime
if na(firstZone.lastRelationBar) or (
not na(secondZone.lastRelationBar) and
secondZone.lastRelationBar > firstZone.lastRelationBar
)
firstZone.lastRelation := secondZone.lastRelation
firstZone.lastRelationBar := secondZone.lastRelationBar
if na(firstZone.lastBreakBar) or (
not na(secondZone.lastBreakBar) and
secondZone.lastBreakBar > firstZone.lastBreakBar
)
firstZone.lastBreakSide := secondZone.lastBreakSide
firstZone.lastBreakBar := secondZone.lastBreakBar
if secondZone.gapActive
firstZone.gapActive := true
firstZone.gapDirection := secondZone.gapDirection
firstZone.gapFillPrice := secondZone.gapFillPrice
array.remove(zones, secondIndex)
else
secondIndex += 1
firstIndex += 1
0
f_prune_stale(array<Zone> zones) =>
int index = array.size(zones) - 1
while index >= 0
Zone zone = array.get(zones, index)
if f_total_evidence(zone) < 0.10
array.remove(zones, index)
index -= 1
0
f_update_zone(
Zone zone,
float currentAtr,
float relativeVolume,
int cooldownBars,
int reactionBars,
bool newTradingDay,
float reactionHigh,
float reactionLow,
float reactionCloseHigh,
float reactionCloseLow
) =>
if newTradingDay
f_decay_zone(zone)
// 缺口完全回补后只移除缺口证据
if zone.gapActive
bool gapFilled = (
zone.gapDirection == 1 and low <= zone.gapFillPrice
) or (
zone.gapDirection == -1 and high >= zone.gapFillPrice
)
if gapFilled
zone.gapActive := false
zone.gapShort := 0.0
zone.gapLong := 0.0
float zoneTop = zone.center + zone.halfWidth
float zoneBottom = zone.center - zone.halfWidth
// 反应观察窗口结束后,按真实反向位移验证这次触及是否有效。
// side = 1 表示价格从上方触及下方支撑区;side = -1 表示从下方触及上方阻力区。
if not na(zone.pendingTouchBar) and bar_index - zone.pendingTouchBar >= reactionBars
float favorableAtr = 0.0
float adverseAtr = 0.0
if zone.pendingTouchSide != 0 and currentAtr > 0.0 and not na(reactionHigh) and not na(reactionLow) and not na(reactionCloseHigh) and not na(reactionCloseLow)
if zone.pendingTouchSide == 1
favorableAtr := math.max((reactionCloseHigh - zone.center) / currentAtr, 0.0)
adverseAtr := math.max((zone.center - reactionLow) / currentAtr, 0.0)
else if zone.pendingTouchSide == -1
favorableAtr := math.max((zone.center - reactionCloseLow) / currentAtr, 0.0)
adverseAtr := math.max((reactionHigh - zone.center) / currentAtr, 0.0)
if favorableAtr >= 0.35
zone.validReactionCount += 1
// 有效拒绝:反向位移越强、不利穿透越浅,证据越高。
float rejectionEvidence = f_clamp(
0.30 + favorableAtr * 0.90 - adverseAtr * 0.45,
0.20,
1.60
)
f_add_evidence(zone, rejectionEvidence, 0.0, 0.0, 0.0)
if zone.pendingRole
float roleEvidence = f_clamp(0.40 + favorableAtr * 0.60, 0.0, 1.40)
f_add_evidence(zone, 0.0, roleEvidence, 0.0, 0.0)
else if adverseAtr >= 0.65
// 无效穿越:价格继续朝不利方向推进,说明该区间没有防守。
zone.failedReactionCount += 1
zone.touchShort *= 0.88
zone.touchLong *= 0.94
zone.pendingTouchBar := int(na)
zone.pendingTouchSide := 0
zone.pendingRole := false
bool intersects = high >= zoneBottom and low <= zoneTop
bool cooldownReady = na(zone.lastTouchBar) or bar_index - zone.lastTouchBar >= cooldownBars
if intersects and cooldownReady
int approachSide = close[1] > zoneTop ? 1 : close[1] < zoneBottom ? -1 : 0
int currentDay = time("D")
bool distinctDay = na(zone.lastTouchDay) or currentDay != zone.lastTouchDay
// V2 基础触及分略低于 V1,真正的强度交给随后的验证结果。
float baseTouch = 0.40 + (distinctDay ? 0.15 : 0.0)
float touchVolumeEvidence = f_clamp((relativeVolume - 1.0) * 0.50, 0.0, 1.00)
f_add_evidence(zone, baseTouch, 0.0, touchVolumeEvidence, 0.0)
zone.touchCount += 1
zone.lastTouchBar := bar_index
zone.lastTouchDay := currentDay
zone.pendingTouchBar := bar_index
zone.pendingTouchSide := approachSide
zone.pendingRole := (
approachSide != 0 and
approachSide == zone.lastBreakSide and
not na(zone.lastBreakBar) and
bar_index - zone.lastBreakBar <= 60
)
// 收盘有效穿越会削弱旧区间;若以后回踩验证,可通过多空转换重新加分。
float breakBuffer = zone.halfWidth * 0.30
int relation = close > zoneTop + breakBuffer ? 1 : close < zoneBottom - breakBuffer ? -1 : 0
if relation != 0
if zone.lastRelation != 0 and relation != zone.lastRelation
zone.touchShort *= 0.90
zone.touchLong *= 0.96
zone.roleShort *= 0.55
zone.roleLong *= 0.65
zone.lastBreakSide := relation
zone.lastBreakBar := bar_index
zone.lastRelation := relation
zone.lastRelationBar := bar_index
0
f_add_pivot_candidate(
array<Zone> zones,
float price,
int eventTime,
float eventAtr,
float eventRvol,
float touchBase,
float widthFactor,
float currentClose,
float currentAtr,
float volumeCap
) =>
float eventWidth = f_event_half_width(price, eventAtr, widthFactor)
float volumeEvidence = f_clamp((eventRvol - 1.0) * 0.60, 0.0, volumeCap)
f_add_candidate(
zones,
price,
eventWidth,
eventTime,
touchBase,
volumeEvidence,
0.0,
false,
0,
float(na),
currentClose,
currentAtr
)
f_swing_cluster_info(
array<float> levels,
array<int> bars,
float price,
int eventBar,
float tolerance,
int maxAgeBars
) =>
int size = array.size(levels)
int index = size - 1
while index >= 0
if eventBar - array.get(bars, index) > maxAgeBars
array.remove(levels, index)
array.remove(bars, index)
index -= 1
int matches = 0
float sum = price
int newSize = array.size(levels)
if newSize > 0
for matchIndex = 0 to newSize - 1
if math.abs(price - array.get(levels, matchIndex)) <= tolerance
matches += 1
sum += array.get(levels, matchIndex)
array.push(levels, price)
array.push(bars, eventBar)
float clusterCenter = matches > 0 ? sum / (matches + 1.0) : price
[clusterCenter, matches + 1]
f_engine(
string sensitivity,
float widthFactor,
int requestedZones,
float minimumStrength,
float maxDistanceAtr,
float displayMergeAtr,
float minSpacingAtr,
bool useManualRound,
float manualRoundStep,
float touchWeight,
float roleWeight,
float volumeWeight,
float gapWeight,
float roundWeight,
bool useLiquidityClusters,
bool validatedOnly
) =>
var array<Zone> zones = array.new<Zone>()
var array<float> outputCenters = array.new<float>()
var array<float> outputWidths = array.new<float>()
var array<float> outputScores = array.new<float>()
var array<int> outputTimes = array.new<int>()
var array<float> swingHighs = array.new<float>()
var array<int> swingHighBars = array.new<int>()
var array<float> swingLows = array.new<float>()
var array<int> swingLowBars = array.new<int>()
var int processedBars = 0
var int processedDays = 1
var bool observedVolume = false
var int lastProcessedBar = na
float currentAtr = ta.atr(14)
float volumeAverage = ta.sma(volume[1], 20)
float relativeVolume = (
not na(volume) and
not na(volumeAverage) and
volumeAverage > 0.0
) ? volume / volumeAverage : 1.0
// 反应窗口使用固定长度内置函数,避免动态长度在 Pine 中的兼容问题。
float reactionHigh = ta.highest(high, 4)
float reactionLow = ta.lowest(low, 4)
float reactionCloseHigh = ta.highest(close, 4)
float reactionCloseLow = ta.lowest(close, 4)
// 只处理已收盘的1小时K线,且同一根K线只处理一次。
bool processBar = barstate.isconfirmed and (na(lastProcessedBar) or bar_index > lastProcessedBar)
if processBar
lastProcessedBar := bar_index
processedBars += 1
observedVolume := observedVolume or (not na(volume) and volume > 0.0)
bool newTradingDay = ta.change(time("D")) > 0
if newTradingDay
processedDays += 1
int pivotLegs = sensitivity == "保守" ? 5 : sensitivity == "密集" ? 3 : 4
int cooldownBars = sensitivity == "保守" ? 7 : sensitivity == "密集" ? 3 : 5
float gapAtrThreshold = sensitivity == "保守" ? 0.55 : sensitivity == "密集" ? 0.30 : 0.40
float relativeVolumeThreshold = sensitivity == "保守" ? 1.70 : sensitivity == "密集" ? 1.20 : 1.40
float wickThreshold = sensitivity == "保守" ? 0.50 : sensitivity == "密集" ? 0.30 : 0.40
int clusterMaxAge = sensitivity == "保守" ? 140 : sensitivity == "密集" ? 200 : 170
int existingCount = array.size(zones)
if existingCount > 0
for index = 0 to existingCount - 1
Zone zone = array.get(zones, index)
f_update_zone(
zone,
currentAtr,
relativeVolume,
cooldownBars,
4,
newTradingDay,
reactionHigh,
reactionLow,
reactionCloseHigh,
reactionCloseLow
)
bool candidateAdded = false
// V2:短腿枢轴负责灵敏度,长腿枢轴负责主结构;多尺度共振自动获得更高证据。
float pivotHighPrimary = ta.pivothigh(high, pivotLegs, pivotLegs)
float pivotLowPrimary = ta.pivotlow(low, pivotLegs, pivotLegs)
float pivotHighSecondary = ta.pivothigh(high, pivotLegs + 2, pivotLegs + 2)
float pivotLowSecondary = ta.pivotlow(low, pivotLegs + 2, pivotLegs + 2)
int pivotEventTime = time[pivotLegs]
int pivotEventBar = bar_index - pivotLegs
int pivotSecondaryTime = time[pivotLegs + 2]
if not na(pivotHighPrimary) and not na(currentAtr[pivotLegs]) and currentAtr[pivotLegs] > 0.0
float pivotAtr = currentAtr[pivotLegs]
float pivotRvol = relativeVolume[pivotLegs]
f_add_pivot_candidate(
zones,
pivotHighPrimary,
pivotEventTime,
pivotAtr,
pivotRvol,
0.75,
widthFactor,
close,
currentAtr,
1.20
)
candidateAdded := true
if useLiquidityClusters
float clusterTolerance = math.max(currentAtr * 0.30, pivotHighPrimary * 0.0012)
[clusterCenter, clusterCount] = f_swing_cluster_info(
swingHighs,
swingHighBars,
pivotHighPrimary,
pivotEventBar,
clusterTolerance,
clusterMaxAge
)
if clusterCount >= 2
f_add_candidate(
zones,
clusterCenter,
f_event_half_width(clusterCenter, pivotAtr, widthFactor),
pivotEventTime,
0.45 * math.min(clusterCount, 4),
0.0,
0.0,
false,
0,
float(na),
close,
currentAtr
)
candidateAdded := true
if not na(pivotLowPrimary) and not na(currentAtr[pivotLegs]) and currentAtr[pivotLegs] > 0.0
float pivotAtr = currentAtr[pivotLegs]
float pivotRvol = relativeVolume[pivotLegs]
f_add_pivot_candidate(
zones,
pivotLowPrimary,
pivotEventTime,
pivotAtr,
pivotRvol,
0.75,
widthFactor,
close,
currentAtr,
1.20
)
candidateAdded := true
if useLiquidityClusters
float clusterTolerance = math.max(currentAtr * 0.30, pivotLowPrimary * 0.0012)
[clusterCenter, clusterCount] = f_swing_cluster_info(
swingLows,
swingLowBars,
pivotLowPrimary,
pivotEventBar,
clusterTolerance,
clusterMaxAge
)
if clusterCount >= 2
f_add_candidate(
zones,
clusterCenter,
f_event_half_width(clusterCenter, pivotAtr, widthFactor),
pivotEventTime,
0.45 * math.min(clusterCount, 4),
0.0,
0.0,
false,
0,
float(na),
close,
currentAtr
)
candidateAdded := true
if not na(pivotHighSecondary) and not na(currentAtr[pivotLegs + 2]) and currentAtr[pivotLegs + 2] > 0.0
float pivotAtr = currentAtr[pivotLegs + 2]
float pivotRvol = relativeVolume[pivotLegs + 2]
f_add_pivot_candidate(
zones,
pivotHighSecondary,
pivotSecondaryTime,
pivotAtr,
pivotRvol,
0.95,
widthFactor,
close,
currentAtr,
1.00
)
candidateAdded := true
if not na(pivotLowSecondary) and not na(currentAtr[pivotLegs + 2]) and currentAtr[pivotLegs + 2] > 0.0
float pivotAtr = currentAtr[pivotLegs + 2]
float pivotRvol = relativeVolume[pivotLegs + 2]
f_add_pivot_candidate(
zones,
pivotLowSecondary,
pivotSecondaryTime,
pivotAtr,
pivotRvol,
0.95,
widthFactor,
close,
currentAtr,
1.00
)
candidateAdded := true
// 加密货币全天交易没有传统“隔夜缺口”,改为识别相邻1小时K线之间
// 无重叠的显著跳空(常由杠杆清算瀑布、插针或突发消息造成)。
if not na(close[1]) and not na(currentAtr[1]) and currentAtr[1] > 0.0
float previousClose = close[1]
float previousHigh = high[1]
float previousLow = low[1]
float gapAtr = currentAtr[1]
float upGapSize = open - previousHigh
float downGapSize = previousLow - open
float minimumGap = math.max(gapAtr * gapAtrThreshold, previousClose * 0.0030)
int gapDirection = 0
float lowerEdge = na
float upperEdge = na
float gapFillPrice = na
bool filledOnOpeningBar = false
if upGapSize >= minimumGap
gapDirection := 1
lowerEdge := previousHigh
upperEdge := open
gapFillPrice := previousHigh
filledOnOpeningBar := low <= previousHigh
else if downGapSize >= minimumGap
gapDirection := -1
lowerEdge := open
upperEdge := previousLow
gapFillPrice := previousLow
filledOnOpeningBar := high >= previousLow
if gapDirection != 0
float openingGapEvidence = filledOnOpeningBar ? 0.0 : 0.90
float lowerWidth = f_event_half_width(lowerEdge, gapAtr, widthFactor)
float upperWidth = f_event_half_width(upperEdge, gapAtr, widthFactor)
f_add_candidate(
zones,
lowerEdge,
lowerWidth,
time,
0.20,
0.0,
openingGapEvidence,
not filledOnOpeningBar,
gapDirection,
gapFillPrice,
close,
gapAtr
)
f_add_candidate(
zones,
upperEdge,
upperWidth,
time,
0.20,
0.0,
openingGapEvidence,
not filledOnOpeningBar,
gapDirection,
gapFillPrice,
close,
gapAtr
)
candidateAdded := true
// 放量拒绝K线补充没有形成标准枢轴的结构边缘
float candleRange = high - low
if (
candleRange > 0.0 and
not na(currentAtr) and
currentAtr > 0.0 and
relativeVolume >= relativeVolumeThreshold
)
float lowerWickRatio = (math.min(open, close) - low) / candleRange
float upperWickRatio = (high - math.max(open, close)) / candleRange
float volumeEvidence = f_clamp((relativeVolume - 1.0) * 0.90, 0.0, 1.50)
if lowerWickRatio >= wickThreshold
float eventWidth = f_event_half_width(low, currentAtr, widthFactor)
int lowerTarget = f_find_merge_target(zones, low, eventWidth, false, 0, float(na))
bool lowerAlreadyTouched = false
if lowerTarget >= 0
lowerAlreadyTouched := array.get(zones, lowerTarget).lastTouchBar == bar_index
if not lowerAlreadyTouched
f_add_candidate(
zones,
low,
eventWidth,
time,
0.60,
volumeEvidence,
0.0,
false,
0,
float(na),
close,
currentAtr
)
candidateAdded := true
if upperWickRatio >= wickThreshold
float eventWidth = f_event_half_width(high, currentAtr, widthFactor)
int upperTarget = f_find_merge_target(zones, high, eventWidth, false, 0, float(na))
bool upperAlreadyTouched = false
if upperTarget >= 0
upperAlreadyTouched := array.get(zones, upperTarget).lastTouchBar == bar_index
if not upperAlreadyTouched
f_add_candidate(
zones,
high,
eventWidth,
time,
0.60,
volumeEvidence,
0.0,
false,
0,
float(na),
close,
currentAtr
)
candidateAdded := true
if candidateAdded
f_merge_overlaps(zones)
if processedBars % 20 == 0
f_prune_stale(zones)
// 集合只在请求上下文的最后一根K线上返回,避免为每根历史K线复制数组
ZoneSnapshot result = na
if barstate.islast
array.clear(outputCenters)
array.clear(outputWidths)
array.clear(outputScores)
array.clear(outputTimes)
// 显示层使用已确认的 close/ATR,避免未收盘 1H K 线导致分数与选区随 tick 变化。
float displayClose = barstate.isconfirmed ? close : close[1]
float displayAtr = barstate.isconfirmed ? currentAtr : currentAtr[1]
array<float> eligibleScores = array.new<float>()
array<int> eligibleIndices = array.new<int>()
int zoneCount = array.size(zones)
if zoneCount > 0 and not na(displayAtr) and displayAtr > 0.0
for index = 0 to zoneCount - 1
Zone zone = array.get(zones, index)
float structureScore = f_zone_score(
zone,
touchWeight,
roleWeight,
volumeWeight,
gapWeight,
roundWeight,
manualRoundStep,
useManualRound
)
float distanceAtr = math.abs(zone.center - displayClose) / displayAtr
float distanceScore = 100.0 * math.exp(-distanceAtr / DISTANCE_DECAY_ATR)
float tradabilityScore = (
structureScore * STRUCTURE_RANK_WEIGHT +
distanceScore * DISTANCE_RANK_WEIGHT
)
bool withinDistance = distanceAtr <= maxDistanceAtr
bool enoughStructure = structureScore >= minimumStrength * STRUCTURE_FLOOR_RATIO
bool hasValidation = not validatedOnly or zone.validReactionCount > 0
if tradabilityScore >= minimumStrength and enoughStructure and withinDistance and hasValidation
array.push(eligibleScores, tradabilityScore)
array.push(eligibleIndices, index)
// 先以最高分微区间为锚点,再按ATR距离聚合邻近价位
int eligibleCount = array.size(eligibleScores)
array<float> groupAnchors = array.new<float>()
array<float> groupCenters = array.new<float>()
array<float> groupWidths = array.new<float>()
array<float> groupScores = array.new<float>()
array<float> groupWeights = array.new<float>()
array<int> groupTimes = array.new<int>()
if eligibleCount > 0
array<int> sortedCandidatePositions = array.sort_indices(
eligibleScores,
order.descending
)
for orderPosition = 0 to eligibleCount - 1
int candidatePosition = array.get(sortedCandidatePositions, orderPosition)
int zoneIndex = array.get(eligibleIndices, candidatePosition)
Zone candidateZone = array.get(zones, zoneIndex)
float candidateScore = array.get(eligibleScores, candidatePosition)
float mergeDistance = displayAtr * displayMergeAtr
int targetGroup = -1
float nearestDistance = 1e20
int currentGroupCount = array.size(groupCenters)
bool candidateIsUpper = candidateZone.center >= displayClose
if currentGroupCount > 0
for groupIndex = 0 to currentGroupCount - 1
float groupAnchor = array.get(groupAnchors, groupIndex)
bool groupIsUpper = groupAnchor >= displayClose
float distance = math.abs(
candidateZone.center - groupAnchor
)
// 不跨现价合并,避免上下方区间被压成一个无效中点
if (
candidateIsUpper == groupIsUpper and
distance <= mergeDistance and
distance < nearestDistance
)
targetGroup := groupIndex
nearestDistance := distance
float candidateWeight = math.max(candidateScore, 1.0)
if targetGroup >= 0
float oldCenter = array.get(groupCenters, targetGroup)
float oldWeight = array.get(groupWeights, targetGroup)
float newWeight = oldWeight + candidateWeight
float newCenter = (
oldCenter * oldWeight +
candidateZone.center * candidateWeight
) / newWeight
float oldScore = array.get(groupScores, targetGroup)
float combinedScore = f_clamp(
math.max(oldScore, candidateScore) +
math.min(oldScore, candidateScore) * 0.15,
0.0,
100.0
)
array.set(groupCenters, targetGroup, newCenter)
array.set(
groupWidths,
targetGroup,
(
array.get(groupWidths, targetGroup) * oldWeight +
candidateZone.halfWidth * candidateWeight
) / newWeight
)
array.set(groupScores, targetGroup, combinedScore)
array.set(groupWeights, targetGroup, newWeight)
array.set(
groupTimes,
targetGroup,
math.min(array.get(groupTimes, targetGroup), candidateZone.firstTime)
)
else
array.push(groupAnchors, candidateZone.center)
array.push(groupCenters, candidateZone.center)
array.push(groupWidths, candidateZone.halfWidth)
array.push(groupScores, candidateScore)
array.push(groupWeights, candidateWeight)
array.push(groupTimes, candidateZone.firstTime)
// 上下方分别保留一半名额,不足时再由另一侧高分区间补齐
int groupCount = array.size(groupScores)
if groupCount > 0
float spacingDistance = displayAtr * minSpacingAtr
array<int> sortedGroupIndices = array.sort_indices(groupScores, order.descending)
array<int> selectedGroups = array.new<int>()
int selectedLimit = math.min(requestedZones, groupCount)
int upperQuota = int(math.ceil(selectedLimit / 2.0))
int lowerQuota = selectedLimit - upperQuota
int upperCount = 0
int lowerCount = 0
for orderPosition = 0 to groupCount - 1
int groupIndex = array.get(sortedGroupIndices, orderPosition)
bool isUpper = array.get(groupCenters, groupIndex) >= displayClose
bool spacingOk = f_group_spacing_ok(groupCenters, selectedGroups, groupIndex, spacingDistance)
if (
isUpper and
upperCount < upperQuota and
array.size(selectedGroups) < selectedLimit and
spacingOk
)
array.push(selectedGroups, groupIndex)
upperCount += 1
else if (
not isUpper and
lowerCount < lowerQuota and
array.size(selectedGroups) < selectedLimit and
spacingOk
)
array.push(selectedGroups, groupIndex)
lowerCount += 1
// 某一侧候选不足或被间距过滤时,继续按综合分数填满剩余名额
for orderPosition = 0 to groupCount - 1
int groupIndex = array.get(sortedGroupIndices, orderPosition)
bool spacingOk = f_group_spacing_ok(groupCenters, selectedGroups, groupIndex, spacingDistance)
if (
array.size(selectedGroups) < selectedLimit and
not array.includes(selectedGroups, groupIndex) and
spacingOk
)
array.push(selectedGroups, groupIndex)
int selectedCount = array.size(selectedGroups)
if selectedCount > 0
for selectionPosition = 0 to selectedCount - 1
int groupIndex = array.get(selectedGroups, selectionPosition)
float groupCenter = array.get(groupCenters, groupIndex)
// V2 显示半宽更窄,让中心价更贴近真实反应点。
float displayPadding = displayAtr * 0.02
float displayHalfWidth = math.min(
array.get(groupWidths, groupIndex) + displayPadding,
displayAtr * 0.30
)
array.push(outputCenters, groupCenter)
array.push(outputWidths, displayHalfWidth)
array.push(outputScores, array.get(groupScores, groupIndex))
array.push(outputTimes, array.get(groupTimes, groupIndex))
result := ZoneSnapshot.new(
outputCenters,
outputWidths,
outputScores,
outputTimes,
displayAtr,
displayClose,
close,
processedDays,
observedVolume
)
result
// 将输入映射为稳定的内部参数
// V2 标准宽度 0.28 ATR,比 V1 的 0.30 略窄,减少多个关键位互相覆盖。
float widthFactor = widthInput == "窄" ? 0.22 : widthInput == "宽" ? 0.38 : 0.28
bool useManualRound = roundModeInput == "手动"
bool supportedTimeframe = (
timeframe.isintraday and
timeframe.in_seconds() >= 60 and
timeframe.in_seconds() <= 3600
)
// 加密货币全天交易,ticker.new 的会话参数只接受 session.regular / session.extended。
// 使用 session.extended 请求全部数据,避免任何按交易时段的过滤。
string cryptoTicker = ticker.new(syminfo.prefix, syminfo.ticker, session.extended)
ZoneSnapshot snapshot = request.security(
cryptoTicker,
ANALYSIS_TIMEFRAME,
f_engine(
sensitivityInput,
widthFactor,
maxZonesInput,
minStrengthInput,
maxDistanceAtrInput,
displayMergeAtrInput,
minSpacingAtrInput,
useManualRound,
manualRoundStepInput,
touchWeightInput,
roleWeightInput,
volumeWeightInput,
gapWeightInput,
roundWeightInput,
liquidityClustersInput,
validatedOnlyInput
),
gaps = barmerge.gaps_off,
lookahead = barmerge.lookahead_off,
ignore_invalid_symbol = true,
calc_bars_count = CALC_BARS_COUNT
)
// 绘图对象只创建一次,之后使用设置函数复用
var array<box> heatBoxes = array.new<box>()
var array<line> centerLines = array.new<line>()
var array<label> scoreLabels = array.new<label>()
var table statusTable = table.new(position.top_right, 1, 1)
if barstate.isfirst
for index = 0 to MAX_DRAW_ZONES - 1
box heatBox = box.new(
left = na,
top = na,
right = na,
bottom = na,
xloc = xloc.bar_time,
extend = extend.right,
border_color = na,
bgcolor = na
)
line centerLine = line.new(
x1 = na,
y1 = na,
x2 = na,
y2 = na,
xloc = xloc.bar_time,
extend = extend.right,
color = na,
style = line.style_dotted
)
label scoreLabel = label.new(
x = na,
y = na,
text = "",
xloc = xloc.bar_time,
style = label.style_label_left,
color = na,
textcolor = na,
size = labelSizeInput == "小" ? size.tiny : labelSizeInput == "大" ? size.normal : size.small
)
array.push(heatBoxes, heatBox)
array.push(centerLines, centerLine)
array.push(scoreLabels, scoreLabel)
f_hide_drawing(int index) =>
box heatBox = array.get(heatBoxes, index)
line centerLine = array.get(centerLines, index)
label scoreLabel = array.get(scoreLabels, index)
box.set_top(heatBox, na)
box.set_bottom(heatBox, na)
box.set_bgcolor(heatBox, na)
box.set_border_color(heatBox, na)
line.set_y1(centerLine, na)
line.set_y2(centerLine, na)
line.set_color(centerLine, na)
label.set_y(scoreLabel, na)
label.set_text(scoreLabel, "")
label.set_color(scoreLabel, na)
label.set_textcolor(scoreLabel, na)
if barstate.islast
bool snapshotReady = not na(snapshot)
bool volumeReady = false
bool historyReady = false
if snapshotReady
volumeReady := snapshot.volumeAvailable
historyReady := snapshot.processedDays >= MIN_HISTORY_DAYS
bool canDraw = supportedTimeframe and snapshotReady and volumeReady and historyReady
string statusText = ""
color statusColor = color.new(color.black, 100)
if not supportedTimeframe
statusText := "仅支持1分钟至1小时图表"
statusColor := color.new(color.orange, 15)
else if not snapshotReady
statusText := "等待1小时数据"
statusColor := color.new(color.orange, 15)
else if not volumeReady
statusText := "当前标的缺少成交量数据"
statusColor := color.new(color.orange, 15)
else if not historyReady
statusText := "历史数据不足,需要约75天"
statusColor := color.new(color.orange, 15)
table.cell(
statusTable,
0,
0,
statusText,
text_color = color.white,
bgcolor = statusColor,
text_size = size.small
)
int visibleCount = canDraw ? array.size(snapshot.centers) : 0
int rightTime = chart.right_visible_bar_time
for index = 0 to MAX_DRAW_ZONES - 1
if index < visibleCount
float center = array.get(snapshot.centers, index)
float halfWidth = array.get(snapshot.halfWidths, index)
float strength = array.get(snapshot.scores, index)
int firstTime = array.get(snapshot.firstTimes, index)
// 以当前显示门槛为起点拉开明暗,高分区间获得同步视觉强调
float visualRange = math.max(100.0 - minStrengthInput, 1.0)
float visualIntensity = f_clamp(
(strength - minStrengthInput) / visualRange,
0.0,
1.0
)
int fillTransparency = int(math.round(78.0 - visualIntensity * 34.0))
int borderTransparency = int(math.round(64.0 - visualIntensity * 46.0))
int lineTransparency = int(math.round(58.0 - visualIntensity * 48.0))
int labelTransparency = int(math.round(38.0 - visualIntensity * 30.0))
color fillColor = color.new(heatColorInput, fillTransparency)
color borderColor = color.new(heatColorInput, borderTransparency)
bool priceInside = snapshot.liveClose >= center - halfWidth and snapshot.liveClose <= center + halfWidth
bool highScoreEmphasis = visualIntensity >= 0.60
box heatBox = array.get(heatBoxes, index)
box.set_lefttop(heatBox, firstTime, center + halfWidth)
box.set_rightbottom(heatBox, time, center - halfWidth)
box.set_bgcolor(heatBox, fillColor)
box.set_border_color(heatBox, borderColor)
box.set_border_width(heatBox, priceInside or highScoreEmphasis ? 2 : 1)
line centerLine = array.get(centerLines, index)
line.set_xy1(centerLine, firstTime, center)
line.set_xy2(centerLine, time, center)
line.set_color(
centerLine,
showCenterInput ? color.new(heatColorInput, lineTransparency) : na
)
line.set_width(centerLine, priceInside or highScoreEmphasis ? 2 : 1)
label scoreLabel = array.get(scoreLabels, index)
label.set_x(scoreLabel, rightTime)
label.set_y(scoreLabel, center)
label.set_text(
scoreLabel,
showLabelsInput
? str.tostring(center, format.mintick) + " | " + str.tostring(math.round(strength))
: ""
)
label.set_color(
scoreLabel,
showLabelsInput ? color.new(heatColorInput, labelTransparency) : na
)
label.set_textcolor(scoreLabel, showLabelsInput ? color.white : na)
else
f_hide_drawing(index)
Show all 1,504 lines