- Use syminfo.ticker to dynamically get symbol name - Strip USD/USDT/PERP suffixes to get base currency - Works for ETH, SOL, BTC, and any other symbol - Alerts now correctly show 'ETH buy' for Ethereum, 'BTC buy' for Bitcoin, etc. This fixes the bug where ETH triggers sent 'SOL buy' alerts.
210 lines
9.9 KiB
Plaintext
210 lines
9.9 KiB
Plaintext
//@version=5
|
||
indicator("Bullmania Money Line v5 Optimzed Final", overlay=true)
|
||
|
||
// Calculation source (Chart vs Heikin Ashi)
|
||
srcMode = input.string("Chart", "Calculation source", options=["Chart","Heikin Ashi"], tooltip="Use regular chart candles or Heikin Ashi for the line calculation.")
|
||
|
||
// Parameter Mode
|
||
paramMode = input.string("Profiles by timeframe", "Parameter Mode", options=["Single", "Profiles by timeframe"], tooltip="Choose whether to use one global set of parameters or timeframe-specific profiles.")
|
||
|
||
// Single (global) parameters
|
||
atrPeriodSingle = input.int(10, "ATR Period (Single mode)", minval=1, group="Single Mode")
|
||
multiplierSingle = input.float(3.0, "Multiplier (Single mode)", minval=0.1, step=0.1, group="Single Mode")
|
||
|
||
// Profile override when using profiles
|
||
profileOverride = input.string("Auto", "Profile Override", options=["Auto", "Minutes", "Hours", "Daily", "Weekly/Monthly"], tooltip="When in 'Profiles by timeframe' mode, choose a fixed profile or let it auto-detect from the chart timeframe.", group="Profiles")
|
||
|
||
// Timeframe profile parameters
|
||
// Minutes (<= 59m)
|
||
atr_m = input.int(12, "ATR Period (Minutes)", minval=1, group="Profiles — Minutes")
|
||
mult_m = input.float(3.3, "Multiplier (Minutes)", minval=0.1, step=0.1, group="Profiles — Minutes")
|
||
|
||
// Hours (>=1h and <1d)
|
||
atr_h = input.int(10, "ATR Period (Hours)", minval=1, group="Profiles — Hours")
|
||
mult_h = input.float(3.0, "Multiplier (Hours)", minval=0.1, step=0.1, group="Profiles — Hours")
|
||
|
||
// Daily (>=1d and <1w)
|
||
atr_d = input.int(10, "ATR Period (Daily)", minval=1, group="Profiles — Daily")
|
||
mult_d = input.float(2.8, "Multiplier (Daily)", minval=0.1, step=0.1, group="Profiles — Daily")
|
||
|
||
// Weekly/Monthly (>=1w)
|
||
atr_w = input.int(7, "ATR Period (Weekly/Monthly)", minval=1, group="Profiles — Weekly/Monthly")
|
||
mult_w = input.float(2.5, "Multiplier (Weekly/Monthly)", minval=0.1, step=0.1, group="Profiles — Weekly/Monthly")
|
||
|
||
// Optional MACD confirmation
|
||
useMacd = input.bool(false, "Use MACD confirmation", inline="macd")
|
||
macdSrc = input.source(close, "MACD Source", inline="macd")
|
||
macdFastLen = input.int(12, "Fast", minval=1, inline="macdLens")
|
||
macdSlowLen = input.int(26, "Slow", minval=1, inline="macdLens")
|
||
macdSigLen = input.int(9, "Signal", minval=1, inline="macdLens")
|
||
|
||
// Entry filters (optional)
|
||
groupFilters = "Entry filters"
|
||
useEntryBuffer = input.bool(false, "Require entry buffer (ATR)", group=groupFilters, tooltip="If enabled, the close must be beyond the Money Line by the buffer amount to avoid wick flips.")
|
||
entryBufferATR = input.float(0.15, "Buffer size (in ATR)", minval=0.0, step=0.05, group=groupFilters, tooltip="0.10–0.20 works well on 1h.")
|
||
confirmBars = input.int(0, "Bars to confirm after flip", minval=0, maxval=2, group=groupFilters, tooltip="0 = signal on flip bar. 1 = wait one bar.")
|
||
useAdx = input.bool(false, "Use ADX trend-strength filter", group=groupFilters, tooltip="If enabled, require ADX to be above a threshold to reduce chop.")
|
||
adxLen = input.int(14, "ADX Length", minval=1, group=groupFilters)
|
||
adxMin = input.int(20, "ADX minimum", minval=0, maxval=100, group=groupFilters)
|
||
|
||
// Determine effective parameters based on selected mode/profile
|
||
var string activeProfile = ""
|
||
resSec = timeframe.in_seconds(timeframe.period)
|
||
isMinutes = resSec < 3600
|
||
isHours = resSec >= 3600 and resSec < 86400
|
||
isDaily = resSec >= 86400 and resSec < 604800
|
||
isWeeklyOrMore = resSec >= 604800
|
||
|
||
// Resolve profile bucket
|
||
string profileBucket = "Single"
|
||
if paramMode == "Single"
|
||
profileBucket := "Single"
|
||
else
|
||
if profileOverride == "Minutes"
|
||
profileBucket := "Minutes"
|
||
else if profileOverride == "Hours"
|
||
profileBucket := "Hours"
|
||
else if profileOverride == "Daily"
|
||
profileBucket := "Daily"
|
||
else if profileOverride == "Weekly/Monthly"
|
||
profileBucket := "Weekly/Monthly"
|
||
else
|
||
profileBucket := isMinutes ? "Minutes" : isHours ? "Hours" : isDaily ? "Daily" : "Weekly/Monthly"
|
||
|
||
atrPeriod = profileBucket == "Single" ? atrPeriodSingle : profileBucket == "Minutes" ? atr_m : profileBucket == "Hours" ? atr_h : profileBucket == "Daily" ? atr_d : atr_w
|
||
multiplier = profileBucket == "Single" ? multiplierSingle : profileBucket == "Minutes" ? mult_m : profileBucket == "Hours" ? mult_h : profileBucket == "Daily" ? mult_d : mult_w
|
||
activeProfile := profileBucket
|
||
|
||
// Core Money Line logic (with selectable source)
|
||
// Build selected source OHLC
|
||
// Optimized: Calculate Heikin Ashi directly instead of using request.security()
|
||
haC = srcMode == "Heikin Ashi" ? (open + high + low + close) / 4 : close
|
||
haO = srcMode == "Heikin Ashi" ? (nz(haC[1]) + nz(open[1])) / 2 : open
|
||
haH = srcMode == "Heikin Ashi" ? math.max(high, math.max(haO, haC)) : high
|
||
haL = srcMode == "Heikin Ashi" ? math.min(low, math.min(haO, haC)) : low
|
||
calcH = haH
|
||
calcL = haL
|
||
calcC = haC
|
||
|
||
// ATR on selected source
|
||
tr = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1])))
|
||
atr = ta.rma(tr, atrPeriod)
|
||
src = (calcH + calcL) / 2
|
||
|
||
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
|
||
|
||
tsl := nz(tsl[1], up1)
|
||
|
||
if trend == 1
|
||
tsl := math.max(up1, tsl)
|
||
trend := calcC < tsl ? -1 : 1
|
||
else
|
||
tsl := math.min(dn1, tsl)
|
||
trend := calcC > tsl ? 1 : -1
|
||
|
||
supertrend = tsl
|
||
|
||
// Plot the Money Line
|
||
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)
|
||
|
||
// Show active profile on chart as a label (optimized - only on confirmed bar)
|
||
showProfileLabel = input.bool(true, "Show active profile label", group="Profiles")
|
||
var label profLbl = na
|
||
if barstate.islast and barstate.isconfirmed and showProfileLabel
|
||
label.delete(profLbl)
|
||
profLbl := label.new(bar_index, close, text="Profile: " + activeProfile + " | ATR=" + str.tostring(atrPeriod) + " Mult=" + str.tostring(multiplier), yloc=yloc.price, style=label.style_label_upper_left, textcolor=color.white, color=color.new(color.blue, 20))
|
||
|
||
// MACD confirmation logic
|
||
[macdLine, macdSignal, macdHist] = ta.macd(macdSrc, macdFastLen, macdSlowLen, macdSigLen)
|
||
longOk = not useMacd or (macdLine > macdSignal)
|
||
shortOk = not useMacd or (macdLine < macdSignal)
|
||
|
||
// Plot buy/sell signals (gated by optional MACD)
|
||
buyFlip = trend == 1 and trend[1] == -1
|
||
sellFlip = trend == -1 and trend[1] == 1
|
||
|
||
// ADX computation (always calculate for context, but only filter if enabled)
|
||
upMove = calcH - calcH[1]
|
||
downMove = calcL[1] - calcL
|
||
plusDM = (upMove > downMove and upMove > 0) ? upMove : 0.0
|
||
minusDM = (downMove > upMove and downMove > 0) ? downMove : 0.0
|
||
trADX = math.max(calcH - calcL, math.max(math.abs(calcH - calcC[1]), math.abs(calcL - calcC[1])))
|
||
atrADX = ta.rma(trADX, adxLen)
|
||
plusDMSmooth = ta.rma(plusDM, adxLen)
|
||
minusDMSmooth = ta.rma(minusDM, adxLen)
|
||
plusDI = atrADX == 0.0 ? 0.0 : 100.0 * plusDMSmooth / atrADX
|
||
minusDI = atrADX == 0.0 ? 0.0 : 100.0 * minusDMSmooth / atrADX
|
||
dx = (plusDI + minusDI == 0.0) ? 0.0 : 100.0 * math.abs(plusDI - minusDI) / (plusDI + minusDI)
|
||
adxVal = ta.rma(dx, adxLen)
|
||
adxOk = not useAdx or (adxVal > adxMin)
|
||
|
||
// Entry buffer gates relative to current Money Line
|
||
longBufferOk = not useEntryBuffer or (calcC > supertrend + entryBufferATR * atr)
|
||
shortBufferOk = not useEntryBuffer or (calcC < supertrend - entryBufferATR * atr)
|
||
|
||
// Confirmation bars after flip
|
||
buyReady = ta.barssince(buyFlip) == confirmBars
|
||
sellReady = ta.barssince(sellFlip) == confirmBars
|
||
|
||
// Final gated signals
|
||
finalLongSignal = buyReady and longOk and adxOk and longBufferOk
|
||
finalShortSignal = sellReady and shortOk and adxOk and shortBufferOk
|
||
|
||
plotshape(finalLongSignal, title="Buy Signal", location=location.belowbar, color=color.green, style=shape.circle, size=size.small)
|
||
plotshape(finalShortSignal, title="Sell Signal", location=location.abovebar, color=color.red, style=shape.circle, size=size.small)
|
||
|
||
// === CONTEXT METRICS FOR SIGNAL QUALITY ===
|
||
// Calculate ATR as percentage of price
|
||
atrPercent = (atr / calcC) * 100
|
||
|
||
// Calculate RSI
|
||
rsi14 = ta.rsi(calcC, 14)
|
||
|
||
// Volume ratio (current volume vs 20-bar MA)
|
||
volMA20 = ta.sma(volume, 20)
|
||
volumeRatio = volume / volMA20
|
||
|
||
// Price position in recent 20-bar range (0-100%)
|
||
highest20 = ta.highest(calcH, 20)
|
||
lowest20 = ta.lowest(calcL, 20)
|
||
priceRange = highest20 - lowest20
|
||
pricePosition = priceRange == 0 ? 50.0 : ((calcC - lowest20) / priceRange) * 100
|
||
|
||
// Extract base currency from ticker (e.g., "ETHUSD" -> "ETH", "SOLUSD" -> "SOL")
|
||
baseCurrency = str.replace(syminfo.ticker, "USD", "")
|
||
baseCurrency := str.replace(baseCurrency, "USDT", "")
|
||
baseCurrency := str.replace(baseCurrency, "PERP", "")
|
||
|
||
// Build enhanced alert messages with context
|
||
longAlertMsg = baseCurrency + " buy .P " + str.tostring(timeframe.period) + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#")
|
||
|
||
shortAlertMsg = baseCurrency + " sell .P " + str.tostring(timeframe.period) + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#")
|
||
|
||
// Fire alerts with dynamic messages (use alert() not alertcondition() for dynamic content)
|
||
if finalLongSignal
|
||
alert(longAlertMsg, alert.freq_once_per_bar_close)
|
||
|
||
if finalShortSignal
|
||
alert(shortAlertMsg, alert.freq_once_per_bar_close)
|
||
|
||
// Fill area between price and Money Line
|
||
fill(plot(close, display=display.none), plot(upTrend, display=display.none), color=color.new(color.green, 90))
|
||
fill(plot(close, display=display.none), plot(downTrend, display=display.none), color=color.new(color.red, 90))
|