482 lines
27 KiB
Plaintext
482 lines
27 KiB
Plaintext
//@version=6
|
||
indicator("Bullmania Money Line v4+ multi time frame profiles (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)")
|
||
// Flip level line (persistent) visuals
|
||
showFlipLevelLine = input.bool(true, "Show Flip Level line")
|
||
flipLineAutoColor = input.bool(true, "Flip line color by trend")
|
||
flipLineCustomCol = input.color(color.new(color.silver, 0), "Flip line custom color")
|
||
showFlipLevelValue = input.bool(true, "Show Flip Level value label")
|
||
flipLabelPosOpt = input.string("Flip bar", "Flip Level label position", options=["Current bar","Flip bar"], tooltip="Where to place the Flip Level value label. 'Flip bar' anchors the label to the candle where the last flip occurred so it moves with that candle when you pan the chart.")
|
||
extendLinesRight = input.bool(false, "Extend lines to right edge", tooltip="If enabled, flip and next-flip lines will extend to the right edge (HUD-style). Disable to keep them glued strictly to candles.")
|
||
flipLineWidth = input.int(3, "Flip line width", minval=1, maxval=6)
|
||
nextFlipLineWidth = input.int(2, "Next flip line width", minval=1, maxval=6)
|
||
labelSizeOpt = input.string("small", "Value label size", options=["tiny","small","normal"])
|
||
// Global label size derived from option (used by all labels)
|
||
lblSize = labelSizeOpt == "tiny" ? size.tiny : labelSizeOpt == "small" ? size.small : size.normal
|
||
// Preview of the next flip thresholds
|
||
showNextBullFlip = input.bool(true, "Show Next Bull Flip level")
|
||
showNextBearFlip = input.bool(false, "Show Next Bear Flip level")
|
||
showNextFlipValue = input.bool(true, "Show Next Flip value label")
|
||
|
||
// =============================
|
||
// Per-timeframe profiles (optional)
|
||
// =============================
|
||
useProfiles = input.bool(false, "Use per-timeframe profiles")
|
||
|
||
grpI = "Profile: Intraday (<60m)"
|
||
pI_atrPeriod = input.int(10, "ATR Period", minval=1, group=grpI)
|
||
pI_multiplier = input.float(3.0, "Multiplier", minval=0.1, step=0.1, group=grpI)
|
||
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_useAdxFilt = input.bool(false, "Use ADX/DMI filter", 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_useMTFConf = input.bool(false, "Use higher timeframe confirmation", group=grpI)
|
||
pI_higherTF = input.timeframe("", "Higher timeframe (e.g. 60, 1D)", group=grpI)
|
||
pI_sessionStr = input.session("0000-2400", "Active session", group=grpI)
|
||
pI_cooldownBars = input.int(0, "Cooldown bars after flip", minval=0, group=grpI)
|
||
pI_alertsClose = input.bool(true, "Alerts/labels on bar close only", group=grpI)
|
||
pI_useTrendMA = input.bool(false, "Use EMA trend filter", 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_useChopFilt = input.bool(false, "Use Choppiness filter", group=grpI)
|
||
pI_useRetest = input.bool(false, "Require retest after flip", 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_useBodyFilter = input.bool(false, "Min candle body fraction", 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_atrPeriod = input.int(10, "ATR Period", minval=1, group=grpH)
|
||
pH_multiplier = input.float(3.0, "Multiplier", minval=0.1, step=0.1, group=grpH)
|
||
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_useAdxFilt = input.bool(false, "Use ADX/DMI filter", 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_useMTFConf = input.bool(false, "Use higher timeframe confirmation", group=grpH)
|
||
pH_higherTF = input.timeframe("", "Higher timeframe (e.g. 60, 1D)", group=grpH)
|
||
pH_sessionStr = input.session("0000-2400", "Active session", group=grpH)
|
||
pH_cooldownBars = input.int(0, "Cooldown bars after flip", minval=0, group=grpH)
|
||
pH_alertsClose = input.bool(true, "Alerts/labels on bar close only", group=grpH)
|
||
pH_useTrendMA = input.bool(false, "Use EMA trend filter", 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_useChopFilt = input.bool(false, "Use Choppiness filter", group=grpH)
|
||
pH_useRetest = input.bool(false, "Require retest after flip", 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_useBodyFilter = input.bool(false, "Min candle body fraction", 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_atrPeriod = input.int(10, "ATR Period", minval=1, group=grpD)
|
||
pD_multiplier = input.float(3.0, "Multiplier", minval=0.1, step=0.1, group=grpD)
|
||
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_useAdxFilt = input.bool(false, "Use ADX/DMI filter", 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_useMTFConf = input.bool(false, "Use higher timeframe confirmation", group=grpD)
|
||
pD_higherTF = input.timeframe("", "Higher timeframe (e.g. 60, 1D)", group=grpD)
|
||
pD_sessionStr = input.session("0000-2400", "Active session", group=grpD)
|
||
pD_cooldownBars = input.int(0, "Cooldown bars after flip", minval=0, group=grpD)
|
||
pD_alertsClose = input.bool(true, "Alerts/labels on bar close only", group=grpD)
|
||
pD_useTrendMA = input.bool(false, "Use EMA trend filter", 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_useChopFilt = input.bool(false, "Use Choppiness filter", group=grpD)
|
||
pD_useRetest = input.bool(false, "Require retest after flip", 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_useBodyFilter = input.bool(false, "Min candle body fraction", 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
|
||
aAtrPeriod = useProfiles ? (isIntraday ? pI_atrPeriod : isHto4h ? pH_atrPeriod : pD_atrPeriod) : atrPeriod
|
||
aMultiplier = useProfiles ? (isIntraday ? pI_multiplier : isHto4h ? pH_multiplier : pD_multiplier) : multiplier
|
||
aAdxLength = useProfiles ? (isIntraday ? pI_adxLength : isHto4h ? pH_adxLength : pD_adxLength) : adxLength
|
||
aMinAdx = useProfiles ? (isIntraday ? pI_minAdx : isHto4h ? pH_minAdx : pD_minAdx) : minAdx
|
||
aUseAdxFilt = useProfiles ? (isIntraday ? pI_useAdxFilt : isHto4h ? pH_useAdxFilt : pD_useAdxFilt) : useAdxFilt
|
||
aUseMTFConf = useProfiles ? (isIntraday ? pI_useMTFConf : isHto4h ? pH_useMTFConf : pD_useMTFConf) : useMTFConf
|
||
aHigherTF = useProfiles ? (isIntraday ? pI_higherTF : isHto4h ? pH_higherTF : pD_higherTF) : higherTF
|
||
aSessionStr = useProfiles ? (isIntraday ? pI_sessionStr : isHto4h ? pH_sessionStr : pD_sessionStr) : sessionStr
|
||
aCooldownBars = useProfiles ? (isIntraday ? pI_cooldownBars : isHto4h ? pH_cooldownBars : pD_cooldownBars) : cooldownBars
|
||
aAlertsClose = useProfiles ? (isIntraday ? pI_alertsClose : isHto4h ? pH_alertsClose : pD_alertsClose) : alertsCloseOnly
|
||
aUseTrendMA = useProfiles ? (isIntraday ? pI_useTrendMA : isHto4h ? pH_useTrendMA : pD_useTrendMA) : useTrendMA
|
||
aChopLength = useProfiles ? (isIntraday ? pI_chopLength : isHto4h ? pH_chopLength : pD_chopLength) : chopLength
|
||
aMaxChop = useProfiles ? (isIntraday ? pI_maxChop : isHto4h ? pH_maxChop : pD_maxChop) : maxChop
|
||
aUseChopFilt = useProfiles ? (isIntraday ? pI_useChopFilt : isHto4h ? pH_useChopFilt : pD_useChopFilt) : useChopFilt
|
||
aUseRetest = useProfiles ? (isIntraday ? pI_useRetest : isHto4h ? pH_useRetest : pD_useRetest) : useRetest
|
||
aRetestWindow = useProfiles ? (isIntraday ? pI_retestWindow : isHto4h ? pH_retestWindow : pD_retestWindow) : retestWindow
|
||
aRetestTolATR = useProfiles ? (isIntraday ? pI_retestTolATR : isHto4h ? pH_retestTolATR : pD_retestTolATR) : retestTolATR
|
||
aUseBodyFilter= useProfiles ? (isIntraday ? pI_useBodyFilter: isHto4h ? pH_useBodyFilter: pD_useBodyFilter) : useBodyFilter
|
||
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(aAtrPeriod)
|
||
srcBase = (calcH + calcL) / 2.0
|
||
src = aSmoothLen > 0 ? ta.ema(srcBase, aSmoothLen) : srcBase
|
||
|
||
up = src - (aMultiplier * atr)
|
||
dn = src + (aMultiplier * 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 >= aCooldownBars
|
||
|
||
// 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 aUseChopFilt or (chop <= aMaxChop)
|
||
|
||
// Retest-after-flip gate
|
||
var bool retestSatisfied = true
|
||
if flipUp or flipDn
|
||
retestSatisfied := not aUseRetest ? 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 aUseRetest and not retestSatisfied and (retestTouchLong or retestTouchShort)
|
||
retestSatisfied := true
|
||
|
||
retestOk = not aUseRetest or retestSatisfied
|
||
|
||
// Minimum body fraction gate
|
||
barRange = math.max(calcH - calcL, syminfo.mintick)
|
||
barBody = math.abs(calcC - calcO)
|
||
bodyOk = not aUseBodyFilter 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 aUseAdxFilt 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 = aUseMTFConf and (aHigherTF != "") ? request.security(syminfo.tickerid, aHigherTF, f_dir(aAtrPeriod, aMultiplier, aConfirmBars, aBufferATR), lookahead=barmerge.lookahead_off) : na
|
||
mtfOk = not aUseMTFConf or (trend == htfDir)
|
||
|
||
// =============================
|
||
// Session & trend filters and final gating (per-profile when enabled)
|
||
// =============================
|
||
inSession = not na(time(timeframe.period, aSessionStr))
|
||
emaTrend = ta.ema(calcC, aTrendMALen)
|
||
trendMAOk = not aUseTrendMA or ((trend == 1 and calcC >= emaTrend) or (trend == -1 and calcC <= emaTrend))
|
||
barOk = not aAlertsClose 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
|
||
|
||
// Persistent Flip Level (holds value from last flip until next)
|
||
var float flipLevel = na
|
||
var int flipBarIndex = na
|
||
flipLevel := (flipUp or flipDn) ? supertrend : nz(flipLevel[1])
|
||
flipBarIndex := (flipUp or flipDn) ? bar_index : nz(flipBarIndex[1])
|
||
flipLineColor = flipLineAutoColor ? (trend == 1 ? color.new(color.green, 0) : color.new(color.red, 0)) : flipLineCustomCol
|
||
plot(showFlipLevelLine ? flipLevel : na, title="Flip Level", color=flipLineColor, linewidth=flipLineWidth, style=plot.style_linebr, trackprice=extendLinesRight)
|
||
|
||
// Optional always-visible value label at current bar (high-contrast)
|
||
var label flipValLbl = na
|
||
if showFlipLevelValue and not na(flipLevel)
|
||
// High-contrast: black text on green (bull), white text on maroon (bear)
|
||
flipLblBg = flipLineAutoColor ? (trend == 1 ? color.new(color.green, 0) : color.new(color.maroon, 0)) : flipLineCustomCol
|
||
flipLblTxt = flipLineAutoColor ? (trend == 1 ? color.black : color.white) : color.white
|
||
// Determine x position based on user preference
|
||
flipLblX = flipLabelPosOpt == "Flip bar" and not na(flipBarIndex) ? flipBarIndex : bar_index
|
||
if na(flipValLbl)
|
||
flipValLbl := label.new(flipLblX, flipLevel, text="Flip Level: " + str.tostring(flipLevel, format.mintick), xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_left, color=flipLblBg, textcolor=flipLblTxt, size=lblSize)
|
||
else
|
||
label.set_x(flipValLbl, flipLblX)
|
||
label.set_y(flipValLbl, flipLevel)
|
||
label.set_text(flipValLbl, "Flip Level: " + str.tostring(flipLevel, format.mintick))
|
||
label.set_color(flipValLbl, flipLblBg)
|
||
label.set_textcolor(flipValLbl, flipLblTxt)
|
||
label.set_style(flipValLbl, label.style_label_left)
|
||
label.set_size(flipValLbl, lblSize)
|
||
|
||
else
|
||
if not na(flipValLbl)
|
||
label.delete(flipValLbl)
|
||
flipValLbl := na
|
||
|
||
// Next flip preview levels (dynamic thresholds)
|
||
nextBullLevel = tsl + aBufferATR * atr // price needed to start bull flip counting
|
||
nextBearLevel = tsl - aBufferATR * atr // price needed to start bear flip counting
|
||
|
||
nextBullActive = trend == -1
|
||
nextBearActive = trend == 1
|
||
|
||
nbCol = color.new(color.lime, 0)
|
||
nsCol = color.new(color.red, 0)
|
||
|
||
plot(showNextBullFlip and nextBullActive ? nextBullLevel : na, title="Next Bull Flip Level", color=nbCol, linewidth=nextFlipLineWidth, style=plot.style_linebr, trackprice=extendLinesRight)
|
||
plot(showNextBearFlip and nextBearActive ? nextBearLevel : na, title="Next Bear Flip Level", color=nsCol, linewidth=nextFlipLineWidth, style=plot.style_linebr, trackprice=extendLinesRight)
|
||
|
||
// Optional live value labels for next flip thresholds (high-contrast)
|
||
var label nextBullLbl = na
|
||
var label nextBearLbl = na
|
||
if showNextFlipValue
|
||
// Next Bull label lifecycle
|
||
if showNextBullFlip and nextBullActive and not na(nextBullLevel)
|
||
if na(nextBullLbl)
|
||
nextBullLbl := label.new(bar_index, nextBullLevel, text="Next Bull Flip: " + str.tostring(nextBullLevel, format.mintick) + (aConfirmBars > 1 ? " (" + str.tostring(aConfirmBars) + " closes)" : ""), xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_left, color=color.new(color.lime, 0), textcolor=color.black, size=lblSize)
|
||
else
|
||
label.set_x(nextBullLbl, bar_index)
|
||
label.set_y(nextBullLbl, nextBullLevel)
|
||
label.set_text(nextBullLbl, "Next Bull Flip: " + str.tostring(nextBullLevel, format.mintick) + (aConfirmBars > 1 ? " (" + str.tostring(aConfirmBars) + " closes)" : ""))
|
||
label.set_color(nextBullLbl, color.new(color.lime, 0))
|
||
label.set_textcolor(nextBullLbl, color.black)
|
||
label.set_style(nextBullLbl, label.style_label_left)
|
||
label.set_size(nextBullLbl, lblSize)
|
||
|
||
else
|
||
if not na(nextBullLbl)
|
||
label.delete(nextBullLbl)
|
||
nextBullLbl := na
|
||
// Next Bear label lifecycle
|
||
if showNextBearFlip and nextBearActive and not na(nextBearLevel)
|
||
if na(nextBearLbl)
|
||
nextBearLbl := label.new(bar_index, nextBearLevel, text="Next Bear Flip: " + str.tostring(nextBearLevel, format.mintick) + (aConfirmBars > 1 ? " (" + str.tostring(aConfirmBars) + " closes)" : ""), xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_left, color=color.new(color.maroon, 0), textcolor=color.white, size=lblSize)
|
||
else
|
||
label.set_x(nextBearLbl, bar_index)
|
||
label.set_y(nextBearLbl, nextBearLevel)
|
||
label.set_text(nextBearLbl, "Next Bear Flip: " + str.tostring(nextBearLevel, format.mintick) + (aConfirmBars > 1 ? " (" + str.tostring(aConfirmBars) + " closes)" : ""))
|
||
label.set_color(nextBearLbl, color.new(color.maroon, 0))
|
||
label.set_textcolor(nextBearLbl, color.white)
|
||
label.set_style(nextBearLbl, label.style_label_left)
|
||
label.set_size(nextBearLbl, lblSize)
|
||
|
||
else
|
||
if not na(nextBearLbl)
|
||
label.delete(nextBearLbl)
|
||
nextBearLbl := na
|
||
else
|
||
if not na(nextBullLbl)
|
||
label.delete(nextBullLbl)
|
||
nextBullLbl := na
|
||
if not na(nextBearLbl)
|
||
label.delete(nextBearLbl)
|
||
nextBearLbl := 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}}")
|