//@version=5 // ===================================================================== // 춘삼 트리거 캔들 지표 v1 // 볼린저밴드 위치 + 장악형/강한장악형/확장장악형 + 모순캔들(①②③/역) // 이평(5/20/60) 장악 단계 / CCI 다이버전스(O·준·W) / 스퀴즈(일반·슈퍼) // 스퀴즈 노이즈 필터 + 스퀴즈 돌파 핵심 트리거(★) // ===================================================================== indicator("춘삼 트리거 캔들 v1", shorttitle="TriggerCandle", overlay=true, max_labels_count=500, max_bars_back=500) // ───────── 입력 ───────── G1 = "볼린저밴드 / 이평" bbLen = input.int(20, "BB 기간", group=G1) bbMult = input.float(2.0, "BB 배수", step=0.1, group=G1) showBB = input.bool(true, "밴드 표시", group=G1) showMA = input.bool(true, "이평 5/20/60/200/365 표시", group=G1) G2 = "제1조건: 위치" bandTol = input.float(0.10, "밴드 근접 허용(밴드폭 대비)", step=0.01, group=G2) midTol = input.float(0.06, "중단선 근접 허용(밴드폭 대비, 일/주봉)", step=0.01, group=G2) allowMa5 = input.bool(true, "밴드와 멀어도 5이평 접촉 시 인정", group=G2) zoneLb = input.int(1, "직전 n봉까지 위치 확인", minval=0, maxval=3, group=G2) h4BandOnly = input.bool(true, "일봉 미만(4시간봉 등)은 밴드 상·하단만", group=G2) G3 = "제2-1조건: 장악형" engRatio = input.float(1.0, "장악 비율 (현재몸통 / 직전몸통 ≥)", step=0.05, group=G3) strongRatio = input.float(2.0, "강한 장악형 비율 (≥)", step=0.1, group=G3) dojiAtr = input.float(0.15, "도지 기준 (몸통 ≤ ATR×)", step=0.01, group=G3) dojiEngAtr = input.float(0.7, "도지 다음 장악 최소 몸통 (ATR×)", step=0.05, group=G3) strongFloorAtr = input.float(0.5, "강한장악 비교 하한 (ATR×)", step=0.05, group=G3) useExt = input.bool(true, "확장 장악형", group=G3) useTrend = input.bool(true, "추세지속 장악형(밴드 밖 마감)", group=G3) G4 = "제2-2조건: 모순캔들" waveLb = input.int(30, "파동 탐색 범위(봉)", minval=5, maxval=100, group=G4) minWave = input.int(3, "파동 최소 길이", minval=2, group=G4) wavePiv = input.int(3, "파동 시작 피벗 강도", minval=1, maxval=5, group=G4) minRefAtr = input.float(0.6, "기준캔들 최소 몸통 (ATR×)", step=0.05, group=G4) anoBodyMax = input.float(0.5, "몸통 축소 기준 (현재/기준 ≤)", step=0.05, group=G4) ano1Vol = input.float(0.9, "① 완전모순: 거래량비 ≥", step=0.05, group=G4) ano2Vol = input.float(0.5, "② 불균형: 거래량비 ≥", step=0.05, group=G4) ano2Disp = input.float(2.5, "② 불균형도 (거래량비/몸통비 ≥)", step=0.1, group=G4) useAno3 = input.bool(true, "③ 거래량 과잉 (몸통↓ 거래량↑)", group=G4) ano3Vol = input.float(1.3, "③ 거래량비 ≥", step=0.05, group=G4) ano3Disp = input.float(1.6, "③ 불균형도 ≥", step=0.1, group=G4) useRev = input.bool(true, "역모순 (몸통↑ 거래량↓)", group=G4) revBody = input.float(1.3, "역모순 몸통비 ≥", step=0.05, group=G4) revVol = input.float(0.6, "역모순 거래량비 ≤", step=0.05, group=G4) cmpPrev = input.bool(true, "직전 장대봉과도 비교", group=G4) G5 = "다이버전스 (CCI·RSI, 종가 기준)" divSrc = input.string("CCI", "다이버전스 지표", options=["CCI", "RSI", "둘 다"], group=G5) cciLen = input.int(20, "CCI 기간", group=G5) rsiLen = input.int(14, "RSI 기간", group=G5) rsiLvl = input.float(40, "이전 저점 RSI 기준 (≤, 고점은 100-값 ≥)", group=G5) rsiTol = input.float(2, "RSI 준 다이버전스 허용차", step=0.5, group=G5) divMinSep = input.int(4, "이전 저점과 최소 간격", group=G5) divMaxBack = input.int(60, "최대 탐색 범위", maxval=200, group=G5) divPiv = input.int(2, "저점/고점 피벗 강도", minval=1, maxval=5, group=G5) divTol = input.float(10, "CCI 준 다이버전스 허용차", group=G5) divLvl = input.float(-50, "이전 저점 CCI 기준 (≤, 고점은 반대)", group=G5) retestTol = input.float(0.03, "저점 미훼손(W) 재시험 허용", step=0.005, group=G5) G6 = "스퀴즈 / 노이즈 필터 (일·주봉)" flatBars = input.int(5, "기울기 측정 봉수", group=G6) flat20 = input.float(0.02, "20선 수평 허용", step=0.005, group=G6) flat60 = input.float(0.015, "60선 수평 허용", step=0.005, group=G6) flat200 = input.float(0.012, "200선 수평 허용", step=0.002, group=G6) bbwLen = input.int(120, "밴드폭 백분위 기간", group=G6) bbwMax = input.float(40, "밴드폭 백분위 ≤ (%)", group=G6) spreadMajor = input.float(0.025, "5/20/60 이격 (BTC·ETH·SOL·XRP)", step=0.001, group=G6) spreadAlt = input.float(0.032, "5/20/60 이격 (알트)", step=0.001, group=G6) sqRecentN = input.int(5, "최근 n봉 스퀴즈면 돌파로 판정", group=G6) noiseFilter = input.bool(true, "스퀴즈/이평밀집 노이즈 필터", group=G6) showNoise = input.bool(false, "노이즈 신호도 회색으로 표시", group=G6) showSqBg = input.bool(true, "스퀴즈 배경 표시", group=G6) G7 = "표시" showEng = input.bool(true, "장악형 표시", group=G7) showAno = input.bool(true, "모순캔들 표시", group=G7) showDivOnly= input.bool(false, "다이버전스 단독 표시", group=G7) stopPct = input.float(0.05, "손절 기준 (꼬리끝 %)", step=0.005, group=G7) // ───────── 기본 계산 ───────── mid = ta.sma(close, bbLen) dev = ta.stdev(close, bbLen) up = mid + bbMult * dev lo = mid - bbMult * dev ma5 = ta.sma(close, 5) ma20 = ta.sma(close, 20) ma60 = ta.sma(close, 60) ma200 = ta.sma(close, 200) ma365 = ta.sma(close, 365) atr = ta.atr(14) cci = ta.cci(hlc3, cciLen) rsiV = ta.rsi(close, rsiLen) body = math.abs(close - open) isHTF = timeframe.isdwm base = syminfo.basecurrency isMajor = base == "BTC" or base == "ETH" or base == "SOL" or base == "XRP" spreadThr = isMajor ? spreadMajor : spreadAlt // ───────── 스퀴즈 ───────── f_slope(src) => (src - src[flatBars]) / src f_in(x) => x >= lo and x <= up mx = math.max(ma5, math.max(ma20, ma60)) mn = math.min(ma5, math.min(ma20, ma60)) spread = (mx - mn) / close converged = spread <= spreadThr bbw = (up - lo) / mid narrow = ta.percentrank(bbw, bbwLen) <= bbwMax flatOK = math.abs(f_slope(ma20)) <= flat20 and math.abs(f_slope(ma60)) <= flat60 sq = isHTF and f_in(ma5) and f_in(ma20) and f_in(ma60) and flatOK and narrow superSq = sq and f_in(ma200) and math.abs(f_slope(ma200)) <= flat200 sqRecent = ta.highest(sq ? 1 : 0, sqRecentN + 1) > 0 // 모순캔들 파동 시작(최고/최저점) 오프셋 hb = ta.highestbars(high[1], waveLb) lb = ta.lowestbars(low[1], waveLb) sLongH = 1 - hb sLongL = 1 - lb // ───────── 헬퍼 ───────── f_isDir(int j, int dir) => dir > 0 ? close[j] > open[j] : close[j] < open[j] f_isOpp(int j, int dir) => dir > 0 ? close[j] < open[j] : close[j] > open[j] // 위치: 3=밴드, 2=중단, 1=5이평, 0=없음 f_zone(int j, int dir) => bw = up[j] - lo[j] int z = 0 if dir > 0 and low[j] <= lo[j] + bandTol * bw z := 3 else if dir < 0 and high[j] >= up[j] - bandTol * bw z := 3 else if isHTF and low[j] <= mid[j] + midTol * bw and high[j] >= mid[j] - midTol * bw z := 2 else if allowMa5 if dir > 0 and math.min(open[j], close[j]) < mid[j] and ma5[j] < mid[j] and high[j] >= ma5[j] z := 1 else if dir < 0 and math.max(open[j], close[j]) > mid[j] and ma5[j] > mid[j] and low[j] <= ma5[j] z := 1 z f_patternZone(int first, int dir) => int best = 0 for j = 0 to first + zoneLb best := math.max(best, f_zone(j, dir)) best // 장악형: 0 없음, 1 장악, 2 강한, 3 확장 f_engulf(int dir) => int k = 0 int nb = 0 a = atr doji1 = body[1] <= dojiAtr * a doji2 = body[2] <= dojiAtr * a if (f_isOpp(1, dir) or doji1) and f_isDir(0, dir) and body >= engRatio * body[1] and (not doji1 or body >= dojiEngAtr * a) ratio = body / math.max(body[1], strongFloorAtr * a) k := ratio >= strongRatio ? 2 : 1 nb := 2 else if useExt and f_isOpp(2, dir) and f_isDir(1, dir) and f_isDir(0, dir) and not doji2 and body[1] < engRatio * body[2] and body[1] + body >= engRatio * body[2] crossNow = dir > 0 ? close > ma5 : close < ma5 crossBefore = dir > 0 ? close[1] > ma5[1] : close[1] < ma5[1] if crossNow and not crossBefore k := 3 nb := 3 [k, nb] // 이평 장악 단계 f_stage(int nb, int dir) => float ext = dir > 0 ? math.min(open, close) : math.max(open, close) for j = 0 to nb - 1 ext := dir > 0 ? math.min(ext, math.min(open[j], close[j])) : math.max(ext, math.max(open[j], close[j])) x5 = dir > 0 ? (close > ma5 and ext < ma5) : (close < ma5 and ext > ma5) x20 = dir > 0 ? (close > ma20 and ext < ma20) : (close < ma20 and ext > ma20) x60 = dir > 0 ? (close > ma60 and ext < ma60) : (close < ma60 and ext > ma60) st = x5 ? (x20 ? (x60 ? 3 : 2) : 1) : 0 beyond = dir > 0 ? (close > ma5 and close > ma20 and close > ma60) : (close < ma5 and close < ma20 and close < ma60) wasBeyond = dir > 0 ? (close[nb] > ma5[nb] and close[nb] > ma20[nb] and close[nb] > ma60[nb]) : (close[nb] < ma5[nb] and close[nb] < ma20[nb] and close[nb] < ma60[nb]) [st, beyond, wasBeyond, x5 or x20 or x60] // 모순 판정: 4=①, 3=②, 2=③, 1=역 f_anoKind(int r, bool allowRev) => rb = body / body[r] rv = volume / volume[r] disp = rv / math.max(rb, 1e-9) int kd = 0 if rb <= anoBodyMax and rv >= ano1Vol kd := 4 else if rb <= anoBodyMax and rv >= ano2Vol and disp >= ano2Disp kd := 3 else if useAno3 and rb < 1 and rv >= ano3Vol and disp >= ano3Disp kd := 2 else if useRev and allowRev and rb >= revBody and rv <= revVol kd := 1 [kd, rb, rv] f_anoFromStart(int s, int dir) => int best = 0 float bRb = na float bRv = na if s >= minWave int ext = s for k = s to 0 if dir > 0 ? low[k] <= low[ext] : high[k] >= high[ext] ext := k if ext <= 2 int kv = -1 for k = 1 to s if f_isOpp(k, dir) and body[k] >= minRefAtr * atr[k] and body[k] > 0 if kv < 0 kv := k else if volume[k] > volume[kv] kv := k if kv > 0 [kd, rb, rv] = f_anoKind(kv, true) if kd > best best := kd bRb := rb bRv := rv if cmpPrev and f_isOpp(1, dir) and body[1] >= atr[1] and kv != 1 and body[1] > 0 [kd2, rb2, rv2] = f_anoKind(1, false) if kd2 > best best := kd2 bRb := rb2 bRv := rv2 [best, bRb, bRv] f_anomaly(int dir) => // (1) 직전 스윙 피벗에서 시작한 파동 int sPiv = -1 for k = math.max(minWave, wavePiv) to waveLb if sPiv < 0 bool piv = true for q = 1 to wavePiv if dir > 0 ? (high[k] < high[k + q] or high[k] < high[k - q]) : (low[k] > low[k + q] or low[k] > low[k - q]) piv := false break if piv sPiv := k [a1, rb1, rv1] = f_anoFromStart(sPiv, dir) // (2) 탐색 범위 내 최고/최저점에서 시작한 파동 sL = dir > 0 ? sLongH : sLongL [a2, rb2, rv2] = f_anoFromStart(sL, dir) int ra = a1 float rrb = rb1 float rrv = rv1 if a2 > a1 ra := a2 rrb := rb2 rrv := rv2 [ra, rrb, rrv] // 다이버전스: 3=O, 2=준, 1=W(저점 미훼손), 0=X (osc = CCI 또는 RSI) f_divOne(int dir, float osc, float lvlLow, float lvlHigh, float tol) => int cur = 0 for k = 0 to 2 if dir > 0 ? close[k] < close[cur] : close[k] > close[cur] cur := k int res = 0 bool done = false for j = cur + divMinSep to cur + divMaxBack if not done bool piv = true for q = 1 to divPiv if dir > 0 ? (close[j] > close[j + q] or close[j] > close[j - q]) : (close[j] < close[j + q] or close[j] < close[j - q]) piv := false break lvlOK = dir > 0 ? osc[j] <= lvlLow : osc[j] >= lvlHigh if piv and lvlOK and not na(osc[j]) done := true newExt = dir > 0 ? close[cur] < close[j] : close[cur] > close[j] if newExt dd = dir > 0 ? osc[cur] - osc[j] : osc[j] - osc[cur] res := dd > tol ? 3 : dd >= -tol ? 2 : 0 else bool rt = false for k = 0 to 2 if dir > 0 ? (low[k] <= low[j] * (1 + retestTol) and close[k] >= low[j]) : (high[k] >= high[j] * (1 - retestTol) and close[k] <= high[j]) rt := true res := rt ? 1 : 0 [res, cur] // 반환: [최종 등급, 현재 극단 오프셋, CCI 등급, RSI 등급] f_div(int dir) => [dC, curC] = f_divOne(dir, cci, divLvl, -divLvl, divTol) [dR, curR] = f_divOne(dir, rsiV, rsiLvl, 100 - rsiLvl, rsiTol) int c = divSrc == "RSI" ? 0 : dC int r = divSrc == "CCI" ? 0 : dR [math.max(c, r), curC, c, r] // ───────── 신호 평가 ───────── f_eval(int dir) => [ek, nb] = f_engulf(dir) [an, rb, rv] = f_anomaly(dir) int kindE = 0 int zoneE = 0 int st = 0 bool brk = false bool beyondAll = false if ek > 0 and showEng [s0, bey, wasB, anyX] = f_stage(nb, dir) z = f_patternZone(nb - 1, dir) brk0 = isHTF and (sqRecent or converged) and bey and not wasB and anyX if z == 0 and brk0 z := 4 bool trendK = false if z == 0 and useTrend and ek != 3 if dir > 0 ? close > up : close < lo z := 5 trendK := true if z > 0 and (isHTF or not h4BandOnly or z == 3) kindE := trendK ? 4 : ek zoneE := z st := s0 brk := brk0 beyondAll := bey int kindA = 0 int zoneA = 0 if an > 0 and showAno zA = math.max(f_zone(0, dir), zoneLb > 0 ? f_zone(1, dir) : 0) if zA > 0 and (isHTF or not h4BandOnly or zA == 3) kindA := an zoneA := zA if kindE == 0 and kindA > 0 [s1, bey1, wasB1, anyX1] = f_stage(1, dir) st := s1 beyondAll := bey1 [dv, dcur, dvC, dvR] = f_div(dir) bool divOnly = kindE == 0 and kindA == 0 and showDivOnly and dcur == 0 and dv >= 2 and (isHTF or not h4BandOnly) noise = noiseFilter and isHTF and (sq or converged) and not beyondAll and not divOnly int dsrc = (dvC >= 2 ? 1 : 0) + (dvR >= 2 ? 2 : 0) [kindE, zoneE, st, brk, kindA, zoneA, dv, noise, divOnly, dsrc] [lkE, lzE, lSt, lBrk, lkA, lzA, lDv, lNoise, lDivOnly, lSrc] = f_eval(1) [skE, szE, sSt, sBrk, skA, szA, sDv, sNoise, sDivOnly, sSrc] = f_eval(-1) longSig = lkE > 0 or lkA > 0 or lDivOnly shortSig = skE > 0 or skA > 0 or sDivOnly longCore = lkE > 0 and not lNoise and (lBrk or lSt == 3) shortCore = skE > 0 and not sNoise and (sBrk or sSt == 3) // ───────── 라벨 텍스트 ───────── f_kindE(int k) => k == 1 ? "장악" : k == 2 ? "강한장악" : k == 3 ? "확장장악" : k == 4 ? "추세장악" : "" f_kindA(int k) => k == 4 ? "모순①" : k == 3 ? "모순②" : k == 2 ? "모순③" : k == 1 ? "역모순" : "" f_stageT(int s) => s == 1 ? "5" : s == 2 ? "5·20" : s == 3 ? "5·20·60" : "" f_divT(int d, int src) => (d == 3 ? "다이버O" : d == 2 ? "다이버준" : d == 1 ? "저점지지" : "") + (d >= 2 ? (src == 3 ? "(CCI·RSI)" : src == 1 ? "(CCI)" : src == 2 ? "(RSI)" : "") : "") f_zoneT(int z, int dir) => z == 3 ? (dir > 0 ? "하단" : "상단") : z == 2 ? "중단" : z == 1 ? "5이평" : z == 4 ? "스퀴즈" : z == 5 ? "밴드밖" : "" f_text(int kE, int zE, int st, bool brk, int kA, int zA, int dv, bool noise, bool divOnly, int dir, int src) => string t = "" if brk t := "★스퀴즈돌파\n" if kE > 0 t += f_kindE(kE) + (st > 0 ? " " + f_stageT(st) : "") if kA > 0 t += (kE > 0 ? "\n" : "") + f_kindA(kA) if divOnly t += "다이버전스" z = kE > 0 ? zE : zA t += "\n" + f_zoneT(z, dir) if dv > 0 t += " · " + f_divT(dv, src) if noise t += "\n(노이즈)" t if longSig and (not lNoise or showNoise) col = lNoise ? color.new(color.gray, 20) : longCore ? color.new(#f5c518, 0) : lkE > 0 ? color.new(#0fb37d, 0) : color.new(#2b8cff, 0) label.new(bar_index, low, f_text(lkE, lzE, lSt, lBrk, lkA, lzA, lDv, lNoise, lDivOnly, 1, lSrc), xloc.bar_index, yloc.belowbar, col, label.style_label_up, color.white, size.small) if shortSig and (not sNoise or showNoise) col = sNoise ? color.new(color.gray, 20) : shortCore ? color.new(#f5c518, 0) : skE > 0 ? color.new(#e5484d, 0) : color.new(#a35bff, 0) label.new(bar_index, high, f_text(skE, szE, sSt, sBrk, skA, szA, sDv, sNoise, sDivOnly, -1, sSrc), xloc.bar_index, yloc.abovebar, col, label.style_label_down, color.white, size.small) // ───────── 플롯 ───────── pU = plot(showBB ? up : na, "BB 상단", color.new(color.gray, 40)) pL = plot(showBB ? lo : na, "BB 하단", color.new(color.gray, 40)) fill(pU, pL, color.new(color.gray, 88)) plot(showBB ? mid : na, "BB 중단(20)", color.new(#ffd400, 0)) plot(showMA ? ma5 : na, "MA5", color.new(#ff3b30, 0)) plot(showMA ? ma60 : na, "MA60", color.new(#14a37f, 0), 2) plot(showMA ? ma200 : na, "MA200", color.new(#2f6bff, 0), 2) plot(showMA ? ma365 : na, "MA365", color.new(#000000, 0), 1) bgcolor(showSqBg and superSq ? color.new(#ff9800, 82) : showSqBg and sq ? color.new(#ffd400, 88) : na, title="스퀴즈") // ───────── 알람 ───────── alertcondition(longSig and not lNoise, "롱 트리거", "{{ticker}} {{interval}} 롱 트리거 캔들") alertcondition(shortSig and not sNoise, "숏 트리거", "{{ticker}} {{interval}} 숏 트리거 캔들") alertcondition(longCore or shortCore, "★ 핵심 트리거(스퀴즈돌파/5·20·60 장악)", "{{ticker}} {{interval}} 핵심 트리거") alertcondition((longSig and not lNoise and lDv >= 2) or (shortSig and not sNoise and sDv >= 2), "다이버전스 동반 트리거", "{{ticker}} {{interval}} 다이버전스 동반 트리거") if barstate.isconfirmed if longSig and not lNoise alert(syminfo.ticker + " " + timeframe.period + " 롱: " + f_text(lkE, lzE, lSt, lBrk, lkA, lzA, lDv, lNoise, lDivOnly, 1, lSrc), alert.freq_once_per_bar_close) if shortSig and not sNoise alert(syminfo.ticker + " " + timeframe.period + " 숏: " + f_text(skE, szE, sSt, sBrk, skA, szA, sDv, sNoise, sDivOnly, -1, sSrc), alert.freq_once_per_bar_close)