Changes: - moneyline_v11_2_indicator.pinescript: Alert format now includes SCORE:100 - parse_signal_enhanced.json: Added indicatorScore parsing (SCORE:X regex) - execute/route.ts: Added hasIndicatorScore bypass (score >= 90 bypasses quality check) - Money_Machine.json: Both Execute Trade nodes now pass indicatorScore to API Rationale: v11.2 indicator filters already optimized (2.544 PF, +51.80% return). Bot-side quality scoring was blocking proven profitable signals (e.g., quality 75). Now indicator passes SCORE:100, bot respects it and executes immediately. This completes the signal chain: Indicator (SCORE:100) → n8n parser (indicatorScore) → workflow → bot endpoint (bypass)
336 lines
16 KiB
Plaintext
336 lines
16 KiB
Plaintext
//@version=6
|
|
indicator("Bullmania Money Line v12", shorttitle="ML v12", overlay=true)
|
|
// V12 (Dec 25, 2025)
|
|
// - Default params from full-year sweeps: ADX min 16, flip threshold 0.20%, long pos cap 82%
|
|
// - Two-stage confirmation on next bar close with +0.35% move (configurable)
|
|
// - Keeps v11.2 filters (ATR buffer, RSI, volume, price position) with tightened long cap to avoid top-chasing
|
|
|
|
// Calculation source
|
|
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
|
|
groupTiming = "Signal Timing"
|
|
confirmBars = input.int(1, "Bars to confirm after flip", minval=0, maxval=3, group=groupTiming, tooltip="1 bar confirmation - fast response while avoiding instant whipsaws.")
|
|
flipThreshold = input.float(0.20, "Flip threshold %", minval=0.0, maxval=2.0, step=0.05, group=groupTiming, tooltip="0.20% - tighter trend confirmation from v11.2 testing.")
|
|
|
|
// Two-stage confirmation (next-bar price move)
|
|
groupTwoStage = "Two-Stage Confirmation"
|
|
useTwoStage = input.bool(true, "Enable two-stage confirmation", group=groupTwoStage, tooltip="Require the next bar to move by confirm % before signaling.")
|
|
confirmPct = input.float(0.35, "Confirm move % (next bar)", minval=0.0, maxval=2.0, step=0.05, group=groupTwoStage, tooltip="Default 0.35% from latest full-year sweep (best with ADX 16). Set to 0 to disable.")
|
|
|
|
// Entry filters
|
|
groupFilters = "Entry filters"
|
|
useEntryBuffer = input.bool(true, "Require entry buffer (ATR)", group=groupFilters, tooltip="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="0.10 ATR from exhaustive sweep.")
|
|
useAdx = input.bool(true, "Use ADX trend-strength filter", group=groupFilters, tooltip="Filters weak chop.")
|
|
adxLen = input.int(16, "ADX Length", minval=1, group=groupFilters)
|
|
adxMin = input.int(16, "ADX minimum", minval=0, maxval=100, group=groupFilters, tooltip="Default 16 (full-year sweep sweet spot).")
|
|
|
|
// Quality Filters
|
|
groupV6Filters = "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(82, "Long max position %", minval=0, maxval=100, group=groupV6Filters, tooltip="82% cap from full-year sweep to avoid top-chasing while keeping breakouts.")
|
|
shortPosMin = input.float(5, "Short min position %", minval=0, maxval=100, group=groupV6Filters, tooltip="5% floor to avoid bottom-chasing shorts.")
|
|
|
|
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="0.1 volume floor.")
|
|
volMax = input.float(3.5, "Volume max ratio", minval=0.5, step=0.5, group=groupV6Filters, tooltip="Max 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="56-69 balanced long band.")
|
|
rsiLongMax = input.float(69, "RSI long maximum", minval=0, maxval=100, group=groupV6Filters)
|
|
rsiShortMin = input.float(30, "RSI short minimum", minval=0, maxval=100, group=groupV6Filters)
|
|
rsiShortMax = input.float(70, "RSI short maximum", minval=0, maxval=100, group=groupV6Filters)
|
|
|
|
// v9 MA Gap visualization
|
|
groupV9MA = "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 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
|
|
|
|
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
|
|
|
|
// Source OHLC
|
|
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)
|
|
|
|
// Flip threshold
|
|
thresholdAmount = tsl * (flipThreshold / 100)
|
|
|
|
// Momentum bars
|
|
var int bullMomentumBars = 0
|
|
var int bearMomentumBars = 0
|
|
|
|
if trend == 1
|
|
tsl := math.max(up1, tsl)
|
|
if calcC < (tsl - thresholdAmount)
|
|
bearMomentumBars += 1
|
|
bullMomentumBars := 0
|
|
else
|
|
bearMomentumBars := 0
|
|
trend := bearMomentumBars >= (confirmBars + 1) ? -1 : 1
|
|
else
|
|
tsl := math.min(dn1, tsl)
|
|
if calcC > (tsl + thresholdAmount)
|
|
bullMomentumBars += 1
|
|
bearMomentumBars := 0
|
|
else
|
|
bullMomentumBars := 0
|
|
trend := bullMomentumBars >= (confirmBars + 1) ? 1 : -1
|
|
|
|
supertrend = tsl
|
|
|
|
// Plot 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)
|
|
|
|
// Active profile label
|
|
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
|
|
[macdLine, macdSignal, macdHist] = ta.macd(macdSrc, macdFastLen, macdSlowLen, macdSigLen)
|
|
longOk = not useMacd or (macdLine > macdSignal)
|
|
shortOk = not useMacd or (macdLine < macdSignal)
|
|
|
|
// Flip detection
|
|
buyFlip = trend == 1 and trend[1] == -1
|
|
sellFlip = trend == -1 and trend[1] == 1
|
|
|
|
// ADX
|
|
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 buffers
|
|
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
|
|
atrPercent = (atr / calcC) * 100
|
|
rsi14 = ta.rsi(calcC, 14)
|
|
volMA20 = ta.sma(volume, 20)
|
|
volumeRatio = volume / volMA20
|
|
highest100 = ta.highest(calcH, 100)
|
|
lowest100 = ta.lowest(calcL, 100)
|
|
priceRange = highest100 - lowest100
|
|
pricePosition = priceRange == 0 ? 50.0 : ((calcC - lowest100) / priceRange) * 100
|
|
|
|
// MA gap
|
|
ma50 = ta.sma(close, 50)
|
|
ma200 = ta.sma(close, 200)
|
|
maGap = ma200 == 0 ? 0.0 : ((ma50 - ma200) / ma200) * 100
|
|
|
|
// Filters
|
|
longPositionOk = not usePricePosition or (pricePosition < longPosMax)
|
|
shortPositionOk = not usePricePosition or (pricePosition > shortPosMin)
|
|
volumeOk = not useVolumeFilter or (volumeRatio >= volMin and volumeRatio <= volMax)
|
|
rsiLongOk = not useRsiFilter or (rsi14 >= rsiLongMin and rsi14 <= rsiLongMax)
|
|
rsiShortOk = not useRsiFilter or (rsi14 >= rsiShortMin and rsi14 <= rsiShortMax)
|
|
|
|
// Candidate signals (filters applied)
|
|
candidateLongSignal = buyReady and longOk and adxOk and longBufferOk and rsiLongOk and longPositionOk and volumeOk
|
|
candidateShortSignal = sellReady and shortOk and adxOk and shortBufferOk and rsiShortOk and shortPositionOk and volumeOk
|
|
|
|
// Two-stage confirmation state
|
|
var int pendingDir = 0 // 1 = long, -1 = short, 0 = none
|
|
var float pendingPrice = na
|
|
var int pendingBar = na
|
|
|
|
finalLongSignal = false
|
|
finalShortSignal = false
|
|
|
|
if useTwoStage and confirmPct > 0
|
|
if candidateLongSignal
|
|
pendingDir := 1
|
|
pendingPrice := calcC
|
|
pendingBar := bar_index
|
|
if candidateShortSignal
|
|
pendingDir := -1
|
|
pendingPrice := calcC
|
|
pendingBar := bar_index
|
|
|
|
if pendingDir != 0 and bar_index == pendingBar + 1
|
|
if pendingDir == 1 and calcC >= pendingPrice * (1 + confirmPct / 100)
|
|
finalLongSignal := true
|
|
if pendingDir == -1 and calcC <= pendingPrice * (1 - confirmPct / 100)
|
|
finalShortSignal := true
|
|
pendingDir := 0
|
|
pendingPrice := na
|
|
pendingBar := na
|
|
else
|
|
finalLongSignal := candidateLongSignal
|
|
finalShortSignal := candidateShortSignal
|
|
|
|
// Debug labels for blocked candidates
|
|
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 candidateLongSignal
|
|
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 candidateShortSignal
|
|
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)
|
|
|
|
// Base currency
|
|
baseCurrency = str.replace(syminfo.ticker, "USDT", "")
|
|
baseCurrency := str.replace(baseCurrency, "USD", "")
|
|
baseCurrency := str.replace(baseCurrency, "PERP", "")
|
|
|
|
// Version
|
|
indicatorVer = "v12"
|
|
|
|
// Alerts
|
|
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
|
|
|
|
if finalLongSignal
|
|
alert(longAlertMsg, alert.freq_once_per_bar_close)
|
|
if finalShortSignal
|
|
alert(shortAlertMsg, alert.freq_once_per_bar_close)
|
|
|
|
// Fill area
|
|
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))
|