From 017b5b5ff2683298880383b3b599dd90b4fd0a53 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Fri, 5 Dec 2025 10:15:39 +0100 Subject: [PATCH] feat: Create v10 indicator - hybrid of v8 patience + v9 MA gap V10 CONCEPT: Best of both worlds - v8's proven patience (2-bar confirm, 0.8% flip threshold) that delivered 63.6% WR - v9's MA gap convergence quality bonus (without the broken 0-bar confirm) KEY RESTORATIONS FROM v8 (what made it work): - confirmBars: 2 (not v9's 0) - Wait after flip to confirm trend - flipThreshold: 0.8% (not v9's 0.5%) - Real breakouts, not noise - adxMin: 18 (not v9's 21) - Strong trend without over-filtering - longPosMax: 85% (not v9's 75%) - Can catch top breakouts - shortPosMin: 15% (not v9's 20%) - Can catch bottom breakouts - volMin: 0.7 (not v9's 1.0) - Accepts moderate volume - RSI short filter: 30-70 RESTORED (v9 removed it incorrectly) NEW FROM v9 (quality enhancement): - MA50 and MA200 visualization on chart - MA gap quality bonus (optional filter) - MAGAP field in alert message for tracking PERFORMANCE BASELINE: - v8: 11 trades, 63.6% WR, +$325.76 (Nov 19-24) - v9: 9 trades, 33.3% WR, -$373.88 (Nov 26-Dec 5) - v10: TBD - expect v8-level performance with MA quality edge WHY THIS SHOULD WORK: - v8's patience filters flip-flops (2-bar confirm + 0.8% threshold) - v9 failed because it removed patience (0-bar confirm, 0.5% threshold) - v10 restores patience + adds MA quality gate - MA gap convergence = trending setup confirmation File: workflows/trading/moneyline_v10_patience_with_ma_gap.pinescript Alert format: Includes MAGAP field for quality tracking in database --- ...eyline_v10_patience_with_ma_gap.pinescript | 299 ++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 workflows/trading/moneyline_v10_patience_with_ma_gap.pinescript diff --git a/workflows/trading/moneyline_v10_patience_with_ma_gap.pinescript b/workflows/trading/moneyline_v10_patience_with_ma_gap.pinescript new file mode 100644 index 0000000..416e940 --- /dev/null +++ b/workflows/trading/moneyline_v10_patience_with_ma_gap.pinescript @@ -0,0 +1,299 @@ +//@version=6 +indicator("Bullmania Money Line v10 Patience with MA Gap", shorttitle="ML v10", overlay=true) + +// V10 CONCEPT: Hybrid of v8's proven patience (2-bar confirm, 0.8% flip) + v9's MA gap quality bonus +// v8 had 63.6% WR and +$325.76 in 5 days +// v9 had 33.3% WR and -$373.88 in 2 weeks (due to removing confirmation patience) +// v10 restores v8's core logic + adds MA convergence quality scoring + +// 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(2, "Bars to confirm after flip", minval=0, maxval=3, group=groupTiming, tooltip="V10: Restored to 2 bars (v8 setting). Wait after flip to confirm trend change - filters rapid flip-flops.") +flipThreshold = input.float(0.8, "Flip threshold %", minval=0.0, maxval=2.0, step=0.1, group=groupTiming, tooltip="V10: Restored to 0.8% (v8 setting). Require price to move this % beyond line before flip - filters small bounces.") + +// Entry filters (optional) +groupFilters = "Entry filters" +useEntryBuffer = input.bool(true, "Require entry buffer (ATR)", group=groupFilters, tooltip="V8: Enabled by default. Close must be beyond the Money Line by buffer amount to avoid wick flips.") +entryBufferATR = input.float(0.20, "Buffer size (in ATR)", minval=0.0, step=0.05, group=groupFilters, tooltip="V8: Increased to 0.20 ATR (from 0.15) to reduce flip-flops.") +useAdx = input.bool(true, "Use ADX trend-strength filter", group=groupFilters, tooltip="V8: Enabled by default to reduce choppy trades.") +adxLen = input.int(16, "ADX Length", minval=1, group=groupFilters) +adxMin = input.int(18, "ADX minimum", minval=0, maxval=100, group=groupFilters, tooltip="V10: Restored to 18 (v8 setting). Strong trend requirement without being too strict.") + +// 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(85, "Long max position %", minval=0, maxval=100, group=groupV6Filters, tooltip="V10: Restored to 85% (v8 setting). Can buy near tops if trend confirms.") +shortPosMin = input.float(15, "Short min position %", minval=0, maxval=100, group=groupV6Filters, tooltip="V10: Restored to 15% (v8 setting). Can short near bottoms if trend confirms.") + +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.7, "Volume min ratio", minval=0.1, step=0.1, group=groupV6Filters, tooltip="V10: Restored to 0.7 (v8 setting). Accepts moderate volume signals.") +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(35, "RSI long minimum", minval=0, maxval=100, group=groupV6Filters) +rsiLongMax = input.float(70, "RSI long maximum", minval=0, maxval=100, group=groupV6Filters) +rsiShortMin = input.float(30, "RSI short minimum", minval=0, maxval=100, group=groupV6Filters, tooltip="V10: Restored RSI short filter (v8 had it, v9 removed it incorrectly)") +rsiShortMax = input.float(70, "RSI short maximum", minval=0, maxval=100, group=groupV6Filters) + +// V10 NEW: MA GAP VISUALIZATION OPTIONS (from v9) +groupV10MA = "v10 MA Gap Options" +showMAs = input.bool(true, "Show 50 and 200 MAs on chart", group=groupV10MA, tooltip="Display the moving averages for visual reference.") +ma50Color = input.color(color.new(color.yellow, 0), "MA 50 Color", group=groupV10MA) +ma200Color = input.color(color.new(color.orange, 0), "MA 200 Color", group=groupV10MA) +useMAGapBonus = input.bool(true, "Apply MA Gap quality bonus", group=groupV10MA, tooltip="Add quality points when MA50 and MA200 converge (trending setup)") +maGapThreshold = input.float(0.35, "MA Gap threshold %", minval=0.1, maxval=2.0, step=0.05, group=groupV10MA, tooltip="Consider MAs converged when gap < this % of price") + +// 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) + +// V10: Calculate and plot MA50 and MA200 +ma50 = ta.sma(calcC, 50) +ma200 = ta.sma(calcC, 200) + +plot(showMAs ? ma50 : na, "MA 50", color=ma50Color, linewidth=1) +plot(showMAs ? ma200 : na, "MA 200", color=ma200Color, linewidth=1) + +// V10: Calculate MA Gap for quality bonus +maGap = math.abs(ma50 - ma200) +maGapPercent = (maGap / calcC) * 100 +isMAConverged = maGapPercent < maGapThreshold + +// 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) + maStatus = isMAConverged ? " | MA GAP: ✓" : " | MA GAP: ✗" + profLbl := label.new(bar_index, close, text="Profile: " + activeProfile + " | ATR=" + str.tostring(atrPeriod) + " Mult=" + str.tostring(multiplier) + maStatus, 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 + +// 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) + +// V10: MA Gap filter - optional quality gate +maGapOk = not useMAGapBonus or isMAConverged + +// V10: PATIENCE WITH MA GAP - v8's proven patience + v9's MA gap quality +// Signal fires on line color changes ONLY when price breaches threshold +// Protection: 2-bar confirm + 0.8% flip threshold + 0.20 ATR buffer + ADX 18+ + stickier multipliers +// Enhancement: MA gap convergence adds quality confirmation +// Result: Clean trend signals with MA quality confirmation +finalLongSignal = buyReady and longOk and adxOk and longBufferOk and longPositionOk and volumeOk and rsiLongOk and maGapOk +finalShortSignal = sellReady and shortOk and adxOk and shortBufferOk and shortPositionOk and volumeOk and rsiShortOk and maGapOk + +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") +baseCurrency = str.replace(syminfo.ticker, "USD", "") +baseCurrency := str.replace(baseCurrency, "USDT", "") +baseCurrency := str.replace(baseCurrency, "PERP", "") + +// Indicator version for tracking in database +indicatorVer = "v10" + +// Build enhanced alert messages with context (timeframe.period is dynamic) +// V10: Include MA gap in alert message for quality tracking +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(maGapPercent, "#.##") + " | 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(maGapPercent, "#.##") + " | 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))