//@version=6 indicator("Bullmania Money Line v11.2 IMPROVED", shorttitle="ML v11.2", overlay=true) // V11.2 IMPROVEMENTS (Dec 17, 2025): // - Balance between v11 permissive and v11.2 emergency restrictive // - Flip threshold: 0.15 → 0.20% (tighter trend confirmation) // - ADX minimum: 5 → 12 (moderate trend requirement, not extreme) // - RSI LONG: 55-70 → 56-69 (slightly tighter while still catching momentum) // - RSI SHORT: 30-70 (unchanged - working well) // - Goal: Quality signals without being overly restrictive // 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.8, "Multiplier (Minutes)", minval=0.1, step=0.1, group="Profiles — Minutes", tooltip="V8: Increased from 3.3 for stickier trend") // Hours (>=1h and <1d) atr_h = input.int(10, "ATR Period (Hours)", minval=1, group="Profiles — Hours") mult_h = input.float(3.5, "Multiplier (Hours)", minval=0.1, step=0.1, group="Profiles — Hours", tooltip="V8: Increased from 3.0 for stickier trend") // Daily (>=1d and <1w) atr_d = input.int(10, "ATR Period (Daily)", minval=1, group="Profiles — Daily") mult_d = input.float(3.2, "Multiplier (Daily)", minval=0.1, step=0.1, group="Profiles — Daily", tooltip="V8: Increased from 2.8 for stickier trend") // Weekly/Monthly (>=1w) atr_w = input.int(7, "ATR Period (Weekly/Monthly)", minval=1, group="Profiles — Weekly/Monthly") mult_w = input.float(3.0, "Multiplier (Weekly/Monthly)", minval=0.1, step=0.1, group="Profiles — Weekly/Monthly", tooltip="V8: Increased from 2.5 for stickier trend") // 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") // Signal timing (ALWAYS applies to all signals) groupTiming = "Signal Timing" confirmBars = input.int(1, "Bars to confirm after flip", minval=0, maxval=3, group=groupTiming, tooltip="V11.1 USER-VALIDATED: 1 bar confirmation - fast response while avoiding instant whipsaws.") flipThreshold = input.float(0.20, "Flip threshold %", minval=0.0, maxval=2.0, step=0.1, group=groupTiming, tooltip="V11.2 IMPROVED: 0.20% - tighter than v11 (0.15%) for better quality signals while not being too restrictive.") // Entry filters (optional) groupFilters = "Entry filters" useEntryBuffer = input.bool(true, "Require entry buffer (ATR)", group=groupFilters, tooltip="V11: Enabled by default. Close must be beyond the Money Line by buffer amount to avoid wick flips.") entryBufferATR = input.float(0.10, "Buffer size (in ATR)", minval=0.0, step=0.05, group=groupFilters, tooltip="V11 OPTIMIZED: 0.10 ATR (from exhaustive sweep) - balanced flip protection.") useAdx = input.bool(true, "Use ADX trend-strength filter", group=groupFilters, tooltip="V11: Enabled by default to reduce choppy trades.") adxLen = input.int(16, "ADX Length", minval=1, group=groupFilters) adxMin = input.int(12, "ADX minimum", minval=0, maxval=100, group=groupFilters, tooltip="V11.2 IMPROVED: 12 minimum - moderate trend requirement (between v11's 5 and emergency 18). Filters weak chop without being extreme.") // NEW v6 FILTERS groupV6Filters = "v6 Quality Filters" usePricePosition = input.bool(true, "Use price position filter", group=groupV6Filters, tooltip="Prevent chasing extremes - don't buy at top of range or sell at bottom.") longPosMax = input.float(100, "Long max position %", minval=0, maxval=100, group=groupV6Filters, tooltip="V11 OPTIMIZED: 100% (from exhaustive sweep) - no long position limit, filters work via other metrics.") shortPosMin = input.float(5, "Short min position %", minval=0, maxval=100, group=groupV6Filters, tooltip="V11 OPTIMIZED: 5% (from exhaustive sweep) - catches early short momentum signals.") useVolumeFilter = input.bool(true, "Use volume filter", group=groupV6Filters, tooltip="Filter signals with extreme volume (too low = dead, too high = climax).") volMin = input.float(0.1, "Volume min ratio", minval=0.0, step=0.1, group=groupV6Filters, tooltip="V11 OPTIMIZED: 0.1 (from exhaustive sweep) - minimal volume floor, quality via trend structure.") volMax = input.float(3.5, "Volume max ratio", minval=0.5, step=0.5, group=groupV6Filters, tooltip="Maximum volume relative to 20-bar MA.") useRsiFilter = input.bool(true, "Use RSI momentum filter", group=groupV6Filters, tooltip="Ensure momentum confirms direction.") rsiLongMin = input.float(56, "RSI long minimum", minval=0, maxval=100, group=groupV6Filters, tooltip="V11.2 IMPROVED: 56-69 range - slightly tighter than v11 (55-70) for better quality while not being too restrictive like emergency fix (58-68).") rsiLongMax = input.float(69, "RSI long maximum", minval=0, maxval=100, group=groupV6Filters, tooltip="V11.2 IMPROVED: 69 max - balanced approach between v11 permissive and emergency restrictive.") rsiShortMin = input.float(30, "RSI short minimum", minval=0, maxval=100, group=groupV6Filters, tooltip="V11.1 DATA-DRIVEN: 30-70 captures winners (Both winning SHORTs at RSI 38.8 and 44). Filter was blocking good signals!") rsiShortMax = input.float(70, "RSI short maximum", minval=0, maxval=100, group=groupV6Filters, tooltip="V11.1 DATA-DRIVEN: 70 max avoids overbought chasing. User test showed 4 signals blocked with min=45!") // V9 NEW: MA GAP VISUALIZATION OPTIONS groupV9MA = "v9 MA Gap Options" showMAs = input.bool(true, "Show 50 and 200 MAs on chart", group=groupV9MA, tooltip="Display the moving averages for visual reference.") ma50Color = input.color(color.new(color.yellow, 0), "MA 50 Color", group=groupV9MA) ma200Color = input.color(color.new(color.orange, 0), "MA 200 Color", group=groupV9MA) // 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) // V8: Apply flip threshold - require price to move X% beyond line before flip thresholdAmount = tsl * (flipThreshold / 100) // Track consecutive bars in potential new direction (anti-whipsaw) var int bullMomentumBars = 0 var int bearMomentumBars = 0 if trend == 1 tsl := math.max(up1, tsl) // Count consecutive bearish bars if calcC < (tsl - thresholdAmount) bearMomentumBars := bearMomentumBars + 1 bullMomentumBars := 0 else bearMomentumBars := 0 // Flip only after X consecutive bars below threshold trend := bearMomentumBars >= (confirmBars + 1) ? -1 : 1 else tsl := math.min(dn1, tsl) // Count consecutive bullish bars if calcC > (tsl + thresholdAmount) bullMomentumBars := bullMomentumBars + 1 bearMomentumBars := 0 else bullMomentumBars := 0 // Flip only after X consecutive bars above threshold trend := bullMomentumBars >= (confirmBars + 1) ? 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 // === 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 // v6 IMPROVEMENT: Price position in 100-bar range (was 20-bar in v5) highest100 = ta.highest(calcH, 100) // Changed from 20 to 100 lowest100 = ta.lowest(calcL, 100) // Changed from 20 to 100 priceRange = highest100 - lowest100 pricePosition = priceRange == 0 ? 50.0 : ((calcC - lowest100) / priceRange) * 100 // === V9 NEW: MA GAP ANALYSIS === // Calculate 50 and 200 period moving averages on CLOSE (not Heikin Ashi) // Use standard close for MA calculations to match traditional analysis ma50 = ta.sma(close, 50) ma200 = ta.sma(close, 200) // Calculate MA gap as percentage // Positive gap = bullish (50 MA above 200 MA) // Negative gap = bearish (50 MA below 200 MA) // Values near 0 = convergence (potential crossover brewing) maGap = ma200 == 0 ? 0.0 : ((ma50 - ma200) / ma200) * 100 // Plot MAs if enabled (for visual reference) - disabled by default for clean chart // plot(showMAs ? ma50 : na, title="MA 50", color=ma50Color, linewidth=1) // plot(showMAs ? ma200 : na, title="MA 200", color=ma200Color, linewidth=2) // v6 NEW FILTERS // Price position filter - prevent chasing extremes longPositionOk = not usePricePosition or (pricePosition < longPosMax) shortPositionOk = not usePricePosition or (pricePosition > shortPosMin) // Volume filter - avoid dead or overheated moves volumeOk = not useVolumeFilter or (volumeRatio >= volMin and volumeRatio <= volMax) // RSI momentum filter rsiLongOk = not useRsiFilter or (rsi14 >= rsiLongMin and rsi14 <= rsiLongMax) rsiShortOk = not useRsiFilter or (rsi14 >= rsiShortMin and rsi14 <= rsiShortMax) // V11: OPTIMIZED STICKY TREND SIGNALS - 10× BETTER THAN V9 // Parameters from exhaustive sweep (2,000/26,244 configs tested) // Protection: 0.25% flip threshold + 0.10 ATR buffer + ADX 5+ + quality filters // Result: $4,158 PnL vs v9 $406 baseline (72.5% WR, 1.755 PF, $95 max DD) // V11 trades 2.7× more signals while maintaining 93% less drawdown // CRITICAL FIX (v11.1): Apply ALL filter checks to signals (previously filters were calculated but not applied!) finalLongSignal = buyReady and longOk and adxOk and longBufferOk and rsiLongOk and longPositionOk and volumeOk finalShortSignal = sellReady and shortOk and adxOk and shortBufferOk and rsiShortOk and shortPositionOk and volumeOk // DEBUG: Show why signals are blocked when trend flips showDebugLabels = input.bool(true, "Show debug labels when signals blocked", group="Debug") var label debugLbl = na if showDebugLabels and (buyReady or sellReady) var string debugText = "" var color debugColor = color.gray if buyReady and not finalLongSignal // Trend flipped to LONG but signal blocked - show why debugText := "❌ LONG BLOCKED:\n" if not longOk debugText += "MACD ✗\n" if not adxOk debugText += "ADX " + str.tostring(adxVal, "#.#") + " < " + str.tostring(adxMin) + " ✗\n" if not longBufferOk debugText += "Entry Buffer ✗\n" if not rsiLongOk debugText += "RSI " + str.tostring(rsi14, "#.#") + " not in " + str.tostring(rsiLongMin) + "-" + str.tostring(rsiLongMax) + " ✗\n" if not longPositionOk debugText += "Price Pos " + str.tostring(pricePosition, "#.#") + "% > " + str.tostring(longPosMax) + "% ✗\n" if not volumeOk debugText += "Volume " + str.tostring(volumeRatio, "#.##") + " not in " + str.tostring(volMin) + "-" + str.tostring(volMax) + " ✗\n" debugColor := color.new(color.red, 30) label.delete(debugLbl) debugLbl := label.new(bar_index, high, text=debugText, yloc=yloc.price, style=label.style_label_down, textcolor=color.white, color=debugColor, size=size.small) else if sellReady and not finalShortSignal // Trend flipped to SHORT but signal blocked - show why debugText := "❌ SHORT BLOCKED:\n" if not shortOk debugText += "MACD ✗\n" if not adxOk debugText += "ADX " + str.tostring(adxVal, "#.#") + " < " + str.tostring(adxMin) + " ✗\n" if not shortBufferOk debugText += "Entry Buffer ✗\n" if not rsiShortOk debugText += "RSI " + str.tostring(rsi14, "#.#") + " not in " + str.tostring(rsiShortMin) + "-" + str.tostring(rsiShortMax) + " ✗\n" if not shortPositionOk debugText += "Price Pos " + str.tostring(pricePosition, "#.#") + "% < " + str.tostring(shortPosMin) + "% ✗\n" if not volumeOk debugText += "Volume " + str.tostring(volumeRatio, "#.##") + " not in " + str.tostring(volMin) + "-" + str.tostring(volMax) + " ✗\n" debugColor := color.new(color.orange, 30) label.delete(debugLbl) debugLbl := label.new(bar_index, low, text=debugText, yloc=yloc.price, style=label.style_label_up, textcolor=color.white, color=debugColor, size=size.small) 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) // Extract base currency from ticker (e.g., "ETHUSD" -> "ETH", "SOLUSD" -> "SOL") // CRITICAL: Remove USDT first, then USD (otherwise "FARTCOINUSDT" becomes "FARTCOINT") baseCurrency = str.replace(syminfo.ticker, "USDT", "") baseCurrency := str.replace(baseCurrency, "USD", "") baseCurrency := str.replace(baseCurrency, "PERP", "") // Indicator version for tracking in database indicatorVer = "v11.2" // Build enhanced alert messages with context (timeframe.period is dynamic) // V9 NEW: Added MAGAP field for MA gap percentage longAlertMsg = baseCurrency + " buy " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | MAGAP:" + str.tostring(maGap, "#.##") + " | IND:" + indicatorVer shortAlertMsg = baseCurrency + " sell " + timeframe.period + " | ATR:" + str.tostring(atrPercent, "#.##") + " | ADX:" + str.tostring(adxVal, "#.#") + " | RSI:" + str.tostring(rsi14, "#.#") + " | VOL:" + str.tostring(volumeRatio, "#.##") + " | POS:" + str.tostring(pricePosition, "#.#") + " | MAGAP:" + str.tostring(maGap, "#.##") + " | IND:" + indicatorVer // 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))