feat(indicator): add Money Line v4+ (Pine v6) with optional ADX/MTF/session/cooldown, calc source, EMA trend filter, anti-chop (CHOP, retest, min body), gated markers, and per-timeframe profiles
This commit is contained in:
326
Bullmania_Money_Line_v4_plus.pine
Normal file
326
Bullmania_Money_Line_v4_plus.pine
Normal file
@@ -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}}")
|
||||
Reference in New Issue
Block a user