//@version=6 indicator("多周期EMA52_Pro", overlay=true, max_labels_count=500, max_lines_count=500) // ======================== 配置输入 ======================== // 主控 enableCTF = input.bool(true, "启用本级别", group="显示") enableHTF = input.bool(true, "启用长级别", group="显示") enableLTF = input.bool(true, "启用次级别", group="显示") maxHTFInput = input.int(10, "最多显示长级别数量", minval=1, maxval=12, group="显示") maxLTFInput = input.int(3, "最多显示次级别数量", minval=1, maxval=4, group="显示") protectAutoscale = input.bool(true, "防拉伸: 仅绘制可视区间内", group="显示") lookbackBars = input.int(300, "可视区间近多少根K线", minval=50, maxval=5000, group="显示") padPercent = input.float(2.0, "可视边界留白(%)", minval=0.0, maxval=20.0, step=0.1, group="显示") // 固定 12 周期开关(本级别也会跟随对应周期开关) showRes1 = input.bool(true, "显示 1m", group="级别开关") showRes2 = input.bool(true, "显示 3m", group="级别开关") showRes3 = input.bool(true, "显示 5m", group="级别开关") showRes4 = input.bool(true, "显示 15m", group="级别开关") showRes5 = input.bool(true, "显示 30m", group="级别开关") showRes6 = input.bool(true, "显示 45m", group="级别开关") showRes7 = input.bool(true, "显示 1h", group="级别开关") showRes8 = input.bool(true, "显示 2h", group="级别开关") showRes9 = input.bool(true, "显示 4h", group="级别开关") showRes10 = input.bool(true, "显示 6h", group="级别开关") showRes11 = input.bool(true, "显示 8h", group="级别开关") showRes12 = input.bool(true, "显示 12h", group="级别开关") // 视觉(虚线效果) dashLenHTF = input.int(6, "长级别虚线段长度", minval=1, maxval=50, group="线型") dashGapHTF = input.int(4, "长级别虚线间隔", minval=1, maxval=50, group="线型") dashLenLTF = input.int(2, "次级别虚线段长度", minval=1, maxval=50, group="线型") dashGapLTF = input.int(3, "次级别虚线间隔", minval=1, maxval=50, group="线型") lineWidthHTF = input.int(1, "长级别线宽", minval=1, maxval=4, group="线型") lineWidthLTF = input.int(1, "次级别线宽", minval=1, maxval=4, group="线型") lineWidthCTF = input.int(2, "本级别线宽", minval=1, maxval=4, group="线型") enableSmooth = input.bool(true, "平滑显示(EMA)", group="线型") smoothLenHTF = input.int(3, "长级别平滑长度", minval=1, maxval=50, group="线型") smoothLenLTF = input.int(3, "次级别平滑长度", minval=1, maxval=50, group="线型") smoothLenCTF = input.int(3, "本级别平滑长度", minval=1, maxval=50, group="线型") smoothMethod = input.string("ALMA", "平滑方法", options=["EMA","DEMA","TEMA","WMA","HMA","ALMA","LinReg"], group="线型") almaOffset = input.float(0.85, "ALMA偏移(0-1)", minval=0.0, maxval=1.0, step=0.05, group="线型") almaSigma = input.float(6.0, "ALMA Sigma", minval=0.5, maxval=10.0, step=0.5, group="线型") labelBgAlpha = input.int(85, "标签背景透明(0-100)", minval=0, maxval=100, group="标注") labelSize = input.string("normal", "标签字号", options=["tiny","small","normal","large","huge"], group="标注") labelOffsetBars = input.int(2, "标签右移K线的距离(根数)", minval=0, maxval=50, group="标注") // 常量上限(编译期固定,用于生成固定数量的 plot 位) const int MAX_HTF = 12 const int MAX_LTF = 4 // ======================== 工具函数 ======================== f_dash(series float v, int phase, int dashLen, int dashGap) => // 通过丢弃部分柱来营造虚线效果 isShow = (bar_index + phase) % (dashLen + dashGap) < dashLen isShow ? v : na // 自定义 DEMA / TEMA(Pine 无内置 dema/tema) f_dema(series float v, int len) => ema1 = ta.ema(v, len) ema2 = ta.ema(ema1, len) 2.0 * ema1 - ema2 f_tema(series float v, int len) => ema1 = ta.ema(v, len) ema2 = ta.ema(ema1, len) ema3 = ta.ema(ema2, len) 3.0 * (ema1 - ema2) + ema3 // 通用平滑(仅用于显示) f_smooth(series float v, int len) => not enableSmooth or len <= 1 ? v : smoothMethod == "EMA" ? ta.ema(v, len) : smoothMethod == "DEMA" ? f_dema(v, len) : smoothMethod == "TEMA" ? f_tema(v, len) : smoothMethod == "WMA" ? ta.wma(v, len) : smoothMethod == "HMA" ? ta.hma(v, len) : smoothMethod == "ALMA" ? ta.alma(v, len, almaOffset, almaSigma) : ta.linreg(v, len, 0) f_strToSize(string s) => switch s "tiny" => size.tiny "small" => size.small "normal" => size.normal "large" => size.large => size.huge f_showRes(string tf) => switch tf "1" => showRes1 "3" => showRes2 "5" => showRes3 "15" => showRes4 "30" => showRes5 "45" => showRes6 "60" => showRes7 "120" => showRes8 "240" => showRes9 "360" => showRes10 "480" => showRes11 "720" => showRes12 => true // 颜色调色板(循环使用) var color[] PALETTE = array.new_color() if barstate.isfirst and array.size(PALETTE) == 0 array.push(PALETTE, color.new(color.teal, 0)) array.push(PALETTE, color.new(color.orange, 0)) array.push(PALETTE, color.new(color.fuchsia,0)) array.push(PALETTE, color.new(color.aqua, 0)) array.push(PALETTE, color.new(color.yellow, 0)) array.push(PALETTE, color.new(color.purple, 0)) array.push(PALETTE, color.new(color.lime, 0)) array.push(PALETTE, color.new(color.red, 0)) array.push(PALETTE, color.new(color.blue, 0)) array.push(PALETTE, color.new(color.navy, 0)) array.push(PALETTE, color.new(color.maroon, 0)) array.push(PALETTE, color.new(color.silver, 0)) f_paletteColor(int idx) => sz = math.max(1, array.size(PALETTE)) array.get(PALETTE, idx % sz) // 计算可视范围边界(近 N 根 K 线 + 百分比留白) rangeHi = ta.highest(high, lookbackBars) rangeLo = ta.lowest(low, lookbackBars) rangeSpan = rangeHi - rangeLo pad = rangeSpan * padPercent * 0.01 minY = rangeLo - pad maxY = rangeHi + pad // 仅在可视区间内绘制(超出返回 na,以避免影响自动缩放)。 f_gate(series float v) => protectAutoscale ? (v >= minY and v <= maxY ? v : na) : v // 检查是否在可视区间内 f_inRange(series float v) => protectAutoscale ? (v >= minY and v <= maxY) : true // 周期字符串美化(如 1 -> 1m, 60 -> 1h, D -> 1D, W -> 1W, 3M -> 3M) f_prettyTf(string tf) => s = timeframe.in_seconds(tf) string out = tf if not na(s) if s < 3600 mins = math.max(1, math.round(s / 60)) out := str.tostring(mins) + 'm' else if s % 3600 == 0 and s < 86400 hrs = math.round(s / 3600) out := str.tostring(hrs) + 'h' else if s % 86400 == 0 and s < 7 * 86400 days = math.round(s / 86400) out := str.tostring(days) + 'D' else if s % (7 * 86400) == 0 and s < 30 * 86400 weeks = math.round(s / (7 * 86400)) out := str.tostring(weeks) + 'W' else out := tf else // 月等返回 na 的分辨率保持原样(M/3M) out := tf out // ======================== 标准时间周期表 ======================== // 备注:TradingView 分辨率字符串 —— 分钟: "1","3","5"...;小时: "60","120"...; // 仅保留分钟与小时级别。 var string[] ALL_RES = array.new_string() if barstate.isfirst and array.size(ALL_RES) == 0 // 由低到高,便于筛选 // 分钟 array.push(ALL_RES, "1") array.push(ALL_RES, "3") array.push(ALL_RES, "5") array.push(ALL_RES, "15") array.push(ALL_RES, "30") array.push(ALL_RES, "45") // 小时(以分钟表示) array.push(ALL_RES, "60") array.push(ALL_RES, "120") array.push(ALL_RES, "240") array.push(ALL_RES, "360") array.push(ALL_RES, "480") array.push(ALL_RES, "720") // ======================== 周期间关系与选择 ======================== curResStr = timeframe.period curResSec = timeframe.in_seconds(curResStr) // 构建长/次级别列表 var string[] htfRes = array.new_string() var string[] ltfRes = array.new_string() array.clear(htfRes) array.clear(ltfRes) for i = 0 to array.size(ALL_RES) - 1 res = array.get(ALL_RES, i) rsec = timeframe.in_seconds(res) // timeframe.in_seconds 不支持时返回 na,需跳过 if na(rsec) continue if rsec > curResSec array.push(htfRes, res) else if rsec < curResSec array.push(ltfRes, res) // 选取靠近当前周期的若干次级别(更有参考意义):取 ltfRes 的末尾(更接近当前) var string[] ltfSel = array.new_string() array.clear(ltfSel) ltfsz = array.size(ltfRes) if ltfsz > 0 take = math.min(maxLTFInput, MAX_LTF, ltfsz) for k = 0 to take - 1 array.push(ltfSel, array.get(ltfRes, ltfsz - 1 - k)) // 限制长级别数量 htfCountSelected = math.min(array.size(htfRes), maxHTFInput, MAX_HTF) ltfCountSelected = math.min(array.size(ltfSel), MAX_LTF) // ======================== 计算各周期 EMA52 ======================== // 预分配数值数组(保持固定最大长度) var float[] htfVals = array.new_float() var float[] ltfVals = array.new_float() var float[] htfLabelVals = array.new_float() var float[] ltfLabelVals = array.new_float() float ctfVal = na float ctfLabelVal = na if barstate.isfirst for _i = 0 to MAX_HTF - 1 array.push(htfVals, na) array.push(htfLabelVals, na) for _j = 0 to MAX_LTF - 1 array.push(ltfVals, na) array.push(ltfLabelVals, na) // 填充长级别数值 for i = 0 to MAX_HTF - 1 float val = na float labelVal = na if enableHTF and i < htfCountSelected tfStr = array.get(htfRes, i) if f_showRes(tfStr) // 计算该长级别的 EMA52 val := request.security(syminfo.tickerid, tfStr, ta.ema(close, 52), barmerge.gaps_off, barmerge.lookahead_off) // 标签不跟随虚线断续,仅跟随级别开关与可视区间 labelVal := f_gate(f_smooth(val, smoothLenHTF)) array.set(htfVals, i, val) array.set(htfLabelVals, i, labelVal) // 填充次级别数值 for i = 0 to MAX_LTF - 1 float val = na float labelVal = na if enableLTF and i < ltfCountSelected tfStr = array.get(ltfSel, i) if f_showRes(tfStr) // 计算该次级别的 EMA52(低级别向上合成,注意其更频繁更新) val := request.security(syminfo.tickerid, tfStr, ta.ema(close, 52), barmerge.gaps_off, barmerge.lookahead_off) // 标签不跟随虚线断续,仅跟随级别开关与可视区间 labelVal := f_gate(f_smooth(val, smoothLenLTF)) array.set(ltfVals, i, val) array.set(ltfLabelVals, i, labelVal) // 本级别 EMA52 if enableCTF and f_showRes(curResStr) ctfVal := ta.ema(close, 52) ctfLabelVal := (enableCTF and f_showRes(curResStr)) ? f_gate(f_smooth(ctfVal, smoothLenCTF)) : na // ======================== 绘制(虚线 + 标签) ======================== // 顶层固定 plot(避免在局部/循环中调用 plot) plot((enableCTF and f_showRes(curResStr)) ? f_gate(f_smooth(ctfVal, smoothLenCTF)) : na, title='CTF EMA52', color=color.new(color.white, 0), linewidth=lineWidthCTF, style=plot.style_line) plot((enableHTF and 0 < htfCountSelected and not na(array.get(htfVals, 0))) ? f_gate(f_dash(f_smooth(array.get(htfVals, 0), smoothLenHTF), 0 * 2, dashLenHTF, dashGapHTF)) : na, title='HTF EMA52 1', color=f_paletteColor(0), linewidth=lineWidthHTF, style=plot.style_line) plot((enableHTF and 1 < htfCountSelected and not na(array.get(htfVals, 1))) ? f_gate(f_dash(f_smooth(array.get(htfVals, 1), smoothLenHTF), 1 * 2, dashLenHTF, dashGapHTF)) : na, title='HTF EMA52 2', color=f_paletteColor(1), linewidth=lineWidthHTF, style=plot.style_line) plot((enableHTF and 2 < htfCountSelected and not na(array.get(htfVals, 2))) ? f_gate(f_dash(f_smooth(array.get(htfVals, 2), smoothLenHTF), 2 * 2, dashLenHTF, dashGapHTF)) : na, title='HTF EMA52 3', color=f_paletteColor(2), linewidth=lineWidthHTF, style=plot.style_line) plot((enableHTF and 3 < htfCountSelected and not na(array.get(htfVals, 3))) ? f_gate(f_dash(f_smooth(array.get(htfVals, 3), smoothLenHTF), 3 * 2, dashLenHTF, dashGapHTF)) : na, title='HTF EMA52 4', color=f_paletteColor(3), linewidth=lineWidthHTF, style=plot.style_line) plot((enableHTF and 4 < htfCountSelected and not na(array.get(htfVals, 4))) ? f_gate(f_dash(f_smooth(array.get(htfVals, 4), smoothLenHTF), 4 * 2, dashLenHTF, dashGapHTF)) : na, title='HTF EMA52 5', color=f_paletteColor(4), linewidth=lineWidthHTF, style=plot.style_line) plot((enableHTF and 5 < htfCountSelected and not na(array.get(htfVals, 5))) ? f_gate(f_dash(f_smooth(array.get(htfVals, 5), smoothLenHTF), 5 * 2, dashLenHTF, dashGapHTF)) : na, title='HTF EMA52 6', color=f_paletteColor(5), linewidth=lineWidthHTF, style=plot.style_line) plot((enableHTF and 6 < htfCountSelected and not na(array.get(htfVals, 6))) ? f_gate(f_dash(f_smooth(array.get(htfVals, 6), smoothLenHTF), 6 * 2, dashLenHTF, dashGapHTF)) : na, title='HTF EMA52 7', color=f_paletteColor(6), linewidth=lineWidthHTF, style=plot.style_line) plot((enableHTF and 7 < htfCountSelected and not na(array.get(htfVals, 7))) ? f_gate(f_dash(f_smooth(array.get(htfVals, 7), smoothLenHTF), 7 * 2, dashLenHTF, dashGapHTF)) : na, title='HTF EMA52 8', color=f_paletteColor(7), linewidth=lineWidthHTF, style=plot.style_line) plot((enableHTF and 8 < htfCountSelected and not na(array.get(htfVals, 8))) ? f_gate(f_dash(f_smooth(array.get(htfVals, 8), smoothLenHTF), 8 * 2, dashLenHTF, dashGapHTF)) : na, title='HTF EMA52 9', color=f_paletteColor(8), linewidth=lineWidthHTF, style=plot.style_line) plot((enableHTF and 9 < htfCountSelected and not na(array.get(htfVals, 9))) ? f_gate(f_dash(f_smooth(array.get(htfVals, 9), smoothLenHTF), 9 * 2, dashLenHTF, dashGapHTF)) : na, title='HTF EMA52 10', color=f_paletteColor(9), linewidth=lineWidthHTF, style=plot.style_line) plot((enableHTF and 10 < htfCountSelected and not na(array.get(htfVals, 10))) ? f_gate(f_dash(f_smooth(array.get(htfVals, 10), smoothLenHTF), 10 * 2, dashLenHTF, dashGapHTF)) : na, title='HTF EMA52 11', color=f_paletteColor(10), linewidth=lineWidthHTF, style=plot.style_line) plot((enableHTF and 11 < htfCountSelected and not na(array.get(htfVals, 11))) ? f_gate(f_dash(f_smooth(array.get(htfVals, 11), smoothLenHTF), 11 * 2, dashLenHTF, dashGapHTF)) : na, title='HTF EMA52 12', color=f_paletteColor(11), linewidth=lineWidthHTF, style=plot.style_line) plot((enableLTF and 0 < ltfCountSelected and not na(array.get(ltfVals, 0))) ? f_gate(f_dash(f_smooth(array.get(ltfVals, 0), smoothLenLTF), 0 * 3 + 1, dashLenLTF, dashGapLTF)) : na, title='LTF EMA52 1', color=color.new(f_paletteColor(0), 30), linewidth=lineWidthLTF, style=plot.style_line) plot((enableLTF and 1 < ltfCountSelected and not na(array.get(ltfVals, 1))) ? f_gate(f_dash(f_smooth(array.get(ltfVals, 1), smoothLenLTF), 1 * 3 + 1, dashLenLTF, dashGapLTF)) : na, title='LTF EMA52 2', color=color.new(f_paletteColor(1), 30), linewidth=lineWidthLTF, style=plot.style_line) plot((enableLTF and 2 < ltfCountSelected and not na(array.get(ltfVals, 2))) ? f_gate(f_dash(f_smooth(array.get(ltfVals, 2), smoothLenLTF), 2 * 3 + 1, dashLenLTF, dashGapLTF)) : na, title='LTF EMA52 3', color=color.new(f_paletteColor(2), 30), linewidth=lineWidthLTF, style=plot.style_line) plot((enableLTF and 3 < ltfCountSelected and not na(array.get(ltfVals, 3))) ? f_gate(f_dash(f_smooth(array.get(ltfVals, 3), smoothLenLTF), 3 * 3 + 1, dashLenLTF, dashGapLTF)) : na, title='LTF EMA52 4', color=color.new(f_paletteColor(3), 30), linewidth=lineWidthLTF, style=plot.style_line) // 右侧标签(仅在最后一根柱更新,避免重复创建) var label[] htfLabels = array.new_label() var label[] ltfLabels = array.new_label() var label ctfLabel = na var string lastTicker = na var string lastTf = na if barstate.isfirst for _i = 0 to MAX_HTF - 1 array.push(htfLabels, na) for _j = 0 to MAX_LTF - 1 array.push(ltfLabels, na) lastTicker := syminfo.tickerid lastTf := timeframe.period // 更新/清理标签 // 如果品种或周期变化,清理旧标签 symbolChanged = lastTicker != syminfo.tickerid tfChanged = lastTf != timeframe.period if symbolChanged or tfChanged for i = 0 to MAX_HTF - 1 lab = array.get(htfLabels, i) if not na(lab) label.delete(lab) array.set(htfLabels, i, na) for i = 0 to MAX_LTF - 1 lab = array.get(ltfLabels, i) if not na(lab) label.delete(lab) array.set(ltfLabels, i, na) if not na(ctfLabel) label.delete(ctfLabel) ctfLabel := na lastTicker := syminfo.tickerid lastTf := timeframe.period if barstate.islast // 长级别 for i = 0 to MAX_HTF - 1 lab = array.get(htfLabels, i) if enableHTF and i < htfCountSelected tfStr = array.get(htfRes, i) v = array.get(htfLabelVals, i) col = f_paletteColor(i) txt = f_prettyTf(tfStr) + ' ' + str.tostring(v, format.mintick) // 与曲线显示条件同步:平滑后且可视区间内才显示标签 if na(v) if not na(lab) label.delete(lab) array.set(htfLabels, i, na) else if na(lab) lab := label.new(bar_index + labelOffsetBars, v, txt, xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_left, textcolor=col, color=color.new(col, labelBgAlpha), size=f_strToSize(labelSize)) array.set(htfLabels, i, lab) else label.set_xy(lab, bar_index + labelOffsetBars, v) label.set_text(lab, txt) label.set_textcolor(lab, col) label.set_color(lab, color.new(col, labelBgAlpha)) label.set_style(lab, label.style_label_left) label.set_size(lab, f_strToSize(labelSize)) else if not na(lab) label.delete(lab) array.set(htfLabels, i, na) // 次级别 for i = 0 to MAX_LTF - 1 lab = array.get(ltfLabels, i) if enableLTF and i < ltfCountSelected tfStr = array.get(ltfSel, i) v = array.get(ltfLabelVals, i) base = f_paletteColor(i) col = color.new(base, 0) txt = f_prettyTf(tfStr) + ' ' + str.tostring(v, format.mintick) if na(v) if not na(lab) label.delete(lab) array.set(ltfLabels, i, na) else if na(lab) lab := label.new(bar_index + labelOffsetBars, v, txt, xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_left, textcolor=col, color=color.new(col, labelBgAlpha + 10 > 100 ? 100 : labelBgAlpha + 10), size=f_strToSize(labelSize)) array.set(ltfLabels, i, lab) else label.set_xy(lab, bar_index + labelOffsetBars, v) label.set_text(lab, txt) label.set_textcolor(lab, col) label.set_color(lab, color.new(col, labelBgAlpha + 10 > 100 ? 100 : labelBgAlpha + 10)) label.set_style(lab, label.style_label_left) label.set_size(lab, f_strToSize(labelSize)) else if not na(lab) label.delete(lab) array.set(ltfLabels, i, na) // 本级别 if enableCTF and f_showRes(curResStr) v = ctfLabelVal col = color.new(color.white, 0) txt = f_prettyTf(curResStr) + ' ' + str.tostring(v, format.mintick) if na(v) if not na(ctfLabel) label.delete(ctfLabel) ctfLabel := na else if na(ctfLabel) ctfLabel := label.new(bar_index + labelOffsetBars, v, txt, xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_left, textcolor=col, color=color.new(col, labelBgAlpha), size=f_strToSize(labelSize)) else label.set_xy(ctfLabel, bar_index + labelOffsetBars, v) label.set_text(ctfLabel, txt) label.set_textcolor(ctfLabel, col) label.set_color(ctfLabel, color.new(col, labelBgAlpha)) label.set_style(ctfLabel, label.style_label_left) label.set_size(ctfLabel, f_strToSize(labelSize)) else if not na(ctfLabel) label.delete(ctfLabel) ctfLabel := na // ======================== 备注 ======================== // 1) 当前周期通过 timeframe.period 自动识别,并用 timeframe.in_seconds( // ) 转换成秒以进行相对大小比较。 // 2) 长级别/次级别集合从标准分辨率表中过滤得出,绘图数量受 MAX_* 与输入上限控制。 // 3) 虚线效果通过在 plot 中周期性输出 na 实现,性能优于逐柱绘制 line 对象。 // 4) 标签在最后一根柱更新,避免过量对象;颜色与曲线一致。 // 5) 次级别(低于当前)EMA52 也计算并显示,更新频率更高,默认更淡。