diff --git a/Bullmania_Money_Line_v4_plus.pine b/Bullmania_Money_Line_v4_plus.pine new file mode 100644 index 0000000..1f15b1d --- /dev/null +++ b/Bullmania_Money_Line_v4_plus.pine @@ -0,0 +1,326 @@ +//@version=6 +indicator("Bullmania Money Line v4+ (optional filters)", overlay=true) + +// ============================= +// Base inputs (match v4 defaults) +// ============================= +atrPeriod = input.int(10, "ATR Period", minval=1) +multiplier = input.float(3.0, "Multiplier", minval=0.1, step=0.1) +confirmBars = input.int(1, "Flip confirmation bars", minval=1, maxval=5, tooltip="Require this many consecutive closes beyond the Money Line before flipping trend. 1 = v4 behavior.") +bufferATR = input.float(0.0, "Flip buffer (×ATR)", minval=0.0, step=0.05, tooltip="Extra distance beyond the Money Line required to flip. Example: 0.3 means close must cross by 0.3 × ATR.") + +// Optional smoothing (applied to src only; ATR remains unchanged) +smoothLen = input.int(0, "Smoothing EMA Length (0 = off)", minval=0) + +// Price source selection (independent of chart type) +srcMode = input.string("Chart", "Calculation source", options=["Chart","Heikin Ashi"]) + +// ============================= +// Optional filters (all default off to preserve v4 behavior) +// ============================= +useAdxFilt = input.bool(false, "Use ADX/DMI filter (gates labels/alerts)") +adxLength = input.int(14, "ADX Length", minval=1) +minAdx = input.float(20.0, "Min ADX", minval=1.0, step=0.5) + +useMTFConf = input.bool(false, "Use higher timeframe confirmation") +higherTF = input.timeframe("", "Higher timeframe (e.g. 60, 1D)") + +sessionStr = input.session("0000-2400", "Active session (labels/alerts only)") +cooldownBars= input.int(0, "Cooldown bars after flip", minval=0) +alertsCloseOnly = input.bool(true, "Alerts/labels on bar close only") + +useTrendMA = input.bool(false, "Use EMA trend filter") +trendMALen = input.int(200, "EMA length", minval=1) + +// Anti-chop filters (optional) +useChopFilt = input.bool(false, "Use Choppiness filter (gates)") +chopLength = input.int(14, "Choppiness Length", minval=2) +maxChop = input.float(55.0, "Max CHOP (allow if <=)", minval=0.0, maxval=100.0, step=0.5, tooltip="Lower = stronger trend; typical thresholds 38-61") + +useRetest = input.bool(false, "Require retest after flip") +retestWindow = input.int(3, "Retest window (bars)", minval=1, maxval=20) +retestTolATR = input.float(0.20, "Retest tolerance (×ATR)", minval=0.0, step=0.05) + +useBodyFilter = input.bool(false, "Min candle body fraction") +minBodyFrac = input.float(0.35, "Min body as fraction of range", minval=0.0, maxval=1.0, step=0.05) + +showLabels = input.bool(true, "Show flip labels") +showCircles = input.bool(true, "Show flip circles (v4 style)") +showFill = input.bool(true, "Shade price-to-line area") +gateCircles = input.bool(false, "Gate flip circles with filters") +showGatedMarkers = input.bool(true, "Show gated markers (triangles)") + +// ============================= +// Per-timeframe profiles (optional) +// ============================= +useProfiles = input.bool(false, "Use per-timeframe profiles") + +grpI = "Profile: Intraday (<60m)" +pI_confirmBars = input.int(2, "Confirm bars", minval=1, group=grpI) +pI_bufferATR = input.float(0.15, "Buffer (×ATR)", minval=0.0, step=0.05, group=grpI) +pI_smoothLen = input.int(1, "Smoothing EMA", minval=0, group=grpI) +pI_adxLength = input.int(18, "ADX Length", minval=1, group=grpI) +pI_minAdx = input.float(20.0, "Min ADX", minval=1.0, step=0.5, group=grpI) +pI_chopLength = input.int(14, "CHOP Length", minval=2, group=grpI) +pI_maxChop = input.float(55.0, "Max CHOP", minval=0.0, maxval=100.0, step=0.5, group=grpI) +pI_retestWindow = input.int(3, "Retest window (bars)", minval=1, group=grpI) +pI_retestTolATR = input.float(0.20, "Retest tol (×ATR)", minval=0.0, step=0.05, group=grpI) +pI_minBodyFrac = input.float(0.35, "Min body fraction", minval=0.0, maxval=1.0, step=0.05, group=grpI) +pI_trendMALen = input.int(100, "EMA trend length", minval=1, group=grpI) + +grpH = "Profile: 1–4h" +pH_confirmBars = input.int(2, "Confirm bars", minval=1, group=grpH) +pH_bufferATR = input.float(0.10, "Buffer (×ATR)", minval=0.0, step=0.05, group=grpH) +pH_smoothLen = input.int(1, "Smoothing EMA", minval=0, group=grpH) +pH_adxLength = input.int(20, "ADX Length", minval=1, group=grpH) +pH_minAdx = input.float(19.0, "Min ADX", minval=1.0, step=0.5, group=grpH) +pH_chopLength = input.int(14, "CHOP Length", minval=2, group=grpH) +pH_maxChop = input.float(57.0, "Max CHOP", minval=0.0, maxval=100.0, step=0.5, group=grpH) +pH_retestWindow = input.int(3, "Retest window (bars)", minval=1, group=grpH) +pH_retestTolATR = input.float(0.20, "Retest tol (×ATR)", minval=0.0, step=0.05, group=grpH) +pH_minBodyFrac = input.float(0.30, "Min body fraction", minval=0.0, maxval=1.0, step=0.05, group=grpH) +pH_trendMALen = input.int(200, "EMA trend length", minval=1, group=grpH) + +grpD = "Profile: 1D+" +pD_confirmBars = input.int(1, "Confirm bars", minval=1, group=grpD) +pD_bufferATR = input.float(0.10, "Buffer (×ATR)", minval=0.0, step=0.05, group=grpD) +pD_smoothLen = input.int(0, "Smoothing EMA", minval=0, group=grpD) +pD_adxLength = input.int(14, "ADX Length", minval=1, group=grpD) +pD_minAdx = input.float(18.0, "Min ADX", minval=1.0, step=0.5, group=grpD) +pD_chopLength = input.int(14, "CHOP Length", minval=2, group=grpD) +pD_maxChop = input.float(60.0, "Max CHOP", minval=0.0, maxval=100.0, step=0.5, group=grpD) +pD_retestWindow = input.int(2, "Retest window (bars)", minval=1, group=grpD) +pD_retestTolATR = input.float(0.20, "Retest tol (×ATR)", minval=0.0, step=0.05, group=grpD) +pD_minBodyFrac = input.float(0.30, "Min body fraction", minval=0.0, maxval=1.0, step=0.05, group=grpD) +pD_trendMALen = input.int(200, "EMA trend length", minval=1, group=grpD) + +// Compute active profile based on timeframe +tfSec = timeframe.in_seconds(timeframe.period) +isIntraday = tfSec < 60 * 60 +isHto4h = tfSec >= 60 * 60 and tfSec <= 4 * 60 * 60 +// else daily+ + +aConfirmBars = useProfiles ? (isIntraday ? pI_confirmBars : isHto4h ? pH_confirmBars : pD_confirmBars) : confirmBars +aBufferATR = useProfiles ? (isIntraday ? pI_bufferATR : isHto4h ? pH_bufferATR : pD_bufferATR) : bufferATR +aSmoothLen = useProfiles ? (isIntraday ? pI_smoothLen : isHto4h ? pH_smoothLen : pD_smoothLen) : smoothLen +aAdxLength = useProfiles ? (isIntraday ? pI_adxLength : isHto4h ? pH_adxLength : pD_adxLength) : adxLength +aMinAdx = useProfiles ? (isIntraday ? pI_minAdx : isHto4h ? pH_minAdx : pD_minAdx) : minAdx +aChopLength = useProfiles ? (isIntraday ? pI_chopLength : isHto4h ? pH_chopLength : pD_chopLength) : chopLength +aMaxChop = useProfiles ? (isIntraday ? pI_maxChop : isHto4h ? pH_maxChop : pD_maxChop) : maxChop +aRetestWindow = useProfiles ? (isIntraday ? pI_retestWindow : isHto4h ? pH_retestWindow : pD_retestWindow) : retestWindow +aRetestTolATR = useProfiles ? (isIntraday ? pI_retestTolATR : isHto4h ? pH_retestTolATR : pD_retestTolATR) : retestTolATR +aMinBodyFrac = useProfiles ? (isIntraday ? pI_minBodyFrac : isHto4h ? pH_minBodyFrac : pD_minBodyFrac) : minBodyFrac +aTrendMALen = useProfiles ? (isIntraday ? pI_trendMALen : isHto4h ? pH_trendMALen : pD_trendMALen) : trendMALen + +// ============================= +// Build selected source (Chart vs Heikin Ashi) +// ============================= +haTicker = ticker.heikinashi(syminfo.tickerid) +haH = request.security(haTicker, timeframe.period, high) +haL = request.security(haTicker, timeframe.period, low) +haC = request.security(haTicker, timeframe.period, close) +haO = request.security(haTicker, timeframe.period, open) + +calcH = srcMode == "Heikin Ashi" ? haH : high +calcL = srcMode == "Heikin Ashi" ? haL : low +calcC = srcMode == "Heikin Ashi" ? haC : close +calcO = srcMode == "Heikin Ashi" ? haO : open + +// ============================= +// Core Money Line logic (v4-compatible on selected source) +// ============================= +atr_custom(len) => + tr1 = calcH - calcL + tr2 = math.abs(calcH - nz(calcC[1], calcC)) + tr3 = math.abs(calcL - nz(calcC[1], calcC)) + tr = math.max(tr1, math.max(tr2, tr3)) + ta.rma(tr, len) + +atr = atr_custom(atrPeriod) +srcBase = (calcH + calcL) / 2.0 +src = aSmoothLen > 0 ? ta.ema(srcBase, aSmoothLen) : srcBase + +up = src - (multiplier * atr) +dn = src + (multiplier * atr) + +var float up1 = na +var float dn1 = na +up1 := nz(up1[1], up) +dn1 := nz(dn1[1], dn) +up1 := calcC[1] > up1 ? math.max(up, up1) : up +dn1 := calcC[1] < dn1 ? math.min(dn, dn1) : dn + +var int trend = 1 +var float tsl = na +var int buyCount = 0 +var int sellCount = 0 + +tsl := nz(tsl[1], up1) + +if trend == 1 + tsl := math.max(up1, tsl) + sellCross = calcC < (tsl - aBufferATR * atr) + sellCount := sellCross ? sellCount + 1 : 0 + trend := sellCount >= aConfirmBars ? -1 : 1 + if trend == -1 + buyCount := 0 +else + tsl := math.min(dn1, tsl) + buyCross = calcC > (tsl + aBufferATR * atr) + buyCount := buyCross ? buyCount + 1 : 0 + trend := buyCount >= aConfirmBars ? 1 : -1 + if trend == 1 + sellCount := 0 + +supertrend = tsl + +// ============================= +// Flip signals (v4 style) +// ============================= +flipUp = trend == 1 and nz(trend[1], -1) == -1 +flipDn = trend == -1 and nz(trend[1], 1) == 1 + +// Track bars since last flip (for cooldown) +var int barsSinceFlip = 100000 +barsSinceFlip := (flipUp or flipDn) ? 0 : nz(barsSinceFlip[1], 100000) + 1 +cooldownOk = barsSinceFlip >= cooldownBars + +// Choppiness Index (on selected source) +chop_tr(len) => + t1 = calcH - calcL + t2 = math.abs(calcH - nz(calcC[1], calcC)) + t3 = math.abs(calcL - nz(calcC[1], calcC)) + math.max(t1, math.max(t2, t3)) + +f_chop(len) => + // sum(x, len) equivalent for wide compatibility: sma(x, len) * len + sumTR = ta.sma(chop_tr(len), len) * len + hh = ta.highest(calcH, len) + ll = ta.lowest(calcL, len) + denom = math.max(hh - ll, syminfo.mintick) + 100 * math.log10(sumTR / denom) / math.log10(len) + +chop = f_chop(aChopLength) +chopOk = not useChopFilt or (chop <= aMaxChop) + +// Retest-after-flip gate +var bool retestSatisfied = true +if flipUp or flipDn + retestSatisfied := not useRetest ? true : false + +retestTouchLong = (trend == 1) and (barsSinceFlip > 0) and (barsSinceFlip <= aRetestWindow) and (calcL <= supertrend + aRetestTolATR * atr) +retestTouchShort = (trend == -1) and (barsSinceFlip > 0) and (barsSinceFlip <= aRetestWindow) and (calcH >= supertrend - aRetestTolATR * atr) +if useRetest and not retestSatisfied and (retestTouchLong or retestTouchShort) + retestSatisfied := true + +retestOk = not useRetest or retestSatisfied + +// Minimum body fraction gate +barRange = math.max(calcH - calcL, syminfo.mintick) +barBody = math.abs(calcC - calcO) +bodyOk = not useBodyFilter or (barBody / barRange >= aMinBodyFrac) + +// ============================= +// Optional ADX/DMI filter (self-contained implementation on selected source) +// ============================= +f_adx(len) => + upMove = calcH - nz(calcH[1], calcH) + downMove = nz(calcL[1], calcL) - calcL + plusDM = (upMove > downMove and upMove > 0) ? upMove : 0.0 + minusDM = (downMove > upMove and downMove > 0) ? downMove : 0.0 + tr1 = calcH - calcL + tr2 = math.abs(calcH - nz(calcC[1], calcC)) + tr3 = math.abs(calcL - nz(calcC[1], calcC)) + tr = math.max(tr1, math.max(tr2, tr3)) + trRma = ta.rma(tr, len) + pdmR = ta.rma(plusDM, len) + mdmR = ta.rma(minusDM, len) + pdi = trRma == 0 ? 0.0 : 100.0 * pdmR / trRma + mdi = trRma == 0 ? 0.0 : 100.0 * mdmR / trRma + dx = (pdi + mdi == 0) ? 0.0 : 100.0 * math.abs(pdi - mdi) / (pdi + mdi) + adxVal = ta.rma(dx, len) + [adxVal, pdi, mdi] + +[adx, diPlus, diMinus] = f_adx(aAdxLength) +adxOk = not useAdxFilt or (adx >= aMinAdx and ((trend == 1 and diPlus > diMinus) or (trend == -1 and diMinus > diPlus))) + +// ============================= +// Optional higher timeframe confirmation +// ============================= +f_dir(_atrLen, _mult, _confBars, _bufAtr) => + _atr = ta.atr(_atrLen) + _srcBase = (high + low) / 2.0 + _up = _srcBase - (_mult * _atr) + _dn = _srcBase + (_mult * _atr) + var float _up1 = na + var float _dn1 = na + _up1 := nz(_up1[1], _up) + _dn1 := nz(_dn1[1], _dn) + _up1 := close[1] > _up1 ? math.max(_up, _up1) : _up + _dn1 := close[1] < _dn1 ? math.min(_dn, _dn1) : _dn + var int _trend = 1 + var float _tsl = na + var int _buyCount = 0 + var int _sellCount = 0 + _tsl := nz(_tsl[1], _up1) + if _trend == 1 + _tsl := math.max(_up1, _tsl) + _sellCross = close < (_tsl - _bufAtr * _atr) + _sellCount := _sellCross ? _sellCount + 1 : 0 + _trend := _sellCount >= _confBars ? -1 : 1 + if _trend == -1 + _buyCount := 0 + else + _tsl := math.min(_dn1, _tsl) + _buyCross = close > (_tsl + _bufAtr * _atr) + _buyCount := _buyCross ? _buyCount + 1 : 0 + _trend := _buyCount >= _confBars ? 1 : -1 + if _trend == 1 + _sellCount := 0 + _trend + +htfDir = useMTFConf and (higherTF != "") ? request.security(syminfo.tickerid, higherTF, f_dir(atrPeriod, multiplier, confirmBars, bufferATR), lookahead=barmerge.lookahead_off) : na +mtfOk = not useMTFConf or (trend == htfDir) + +// ============================= +// Session & trend filters and final gating +// ============================= +inSession = not na(time(timeframe.period, sessionStr)) +emaTrend = ta.ema(calcC, aTrendMALen) +trendMAOk = not useTrendMA or ((trend == 1 and calcC >= emaTrend) or (trend == -1 and calcC <= emaTrend)) +barOk = not alertsCloseOnly or barstate.isconfirmed +signalsOk = adxOk and mtfOk and inSession and cooldownOk and trendMAOk and chopOk and retestOk and bodyOk and barOk + +// ============================= +// Plots +// ============================= +upTrend = trend == 1 ? supertrend : na +downTrend = trend == -1 ? supertrend : na + +plot(upTrend, "Up Trend", color=color.new(color.green, 0), style=plot.style_linebr, linewidth=2) +plot(downTrend, "Down Trend", color=color.new(color.red, 0), style=plot.style_linebr, linewidth=2) + +// v4-style flip circles (optionally gated) +rawUpCond = showCircles and (gateCircles ? (flipUp and signalsOk) : flipUp) +rawDownCond = showCircles and (gateCircles ? (flipDn and signalsOk) : flipDn) +plotshape(rawUpCond, title="Buy Signal (circle)", location=location.belowbar, color=color.green, style=shape.circle, size=size.small) +plotshape(rawDownCond, title="Sell Signal (circle)", location=location.abovebar, color=color.red, style=shape.circle, size=size.small) + +// Distinct gated markers for passes (triangles) +plotshape(showGatedMarkers and signalsOk and flipUp, title="Gated Buy", location=location.belowbar, color=color.lime, style=shape.triangleup, size=size.tiny, offset=0) +plotshape(showGatedMarkers and signalsOk and flipDn, title="Gated Sell", location=location.abovebar, color=color.maroon, style=shape.triangledown, size=size.tiny, offset=0) + +// Labels (gated) +if showLabels and signalsOk and flipUp + label.new(bar_index, supertrend, text="bullish", style=label.style_label_up, color=color.new(color.green, 0), textcolor=color.white, yloc=yloc.belowbar) +if showLabels and signalsOk and flipDn + label.new(bar_index, supertrend, text="bearish", style=label.style_label_down, color=color.new(color.red, 0), textcolor=color.white, yloc=yloc.abovebar) + +// Fills +pClose = plot(calcC, title="Close (hidden)", display=display.none) +fill(pClose, plot(upTrend, display=display.none), color=showFill ? color.new(color.green, 90) : color.new(color.green, 100)) +fill(pClose, plot(downTrend, display=display.none), color=showFill ? color.new(color.red, 90) : color.new(color.red, 100)) + +// Alerts (gated) +alertcondition(signalsOk and flipUp, title="Bullmania v4+ Flip Up", message="Bullmania Money Line v4+: Bullish flip on {{ticker}} @ {{close}}") +alertcondition(signalsOk and flipDn, title="Bullmania v4+ Flip Down", message="Bullmania Money Line v4+: Bearish flip on {{ticker}} @ {{close}}")