Files
Tradingview/Bullmania_Money_Line_v4_plus.pine

327 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//@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: 14h"
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}}")