Files
trading_bot_v4/workflows/trading/moneyline_v7_line_flips.pinescript
mindesbunister cc0d754b9a feat: Add Money Line v7 with pure line flip signals
- Created moneyline_v7_line_flips.pinescript
- Signals fire on EVERY line color change (red↔green)
- All filters removed - line flip IS the signal
- confirmBars = 0 for immediate flip bar signals
- Risk management via ATR-based TP/SL (bot-side)

- Restored v6 to original filtered mode
- v6: Conservative with all filters (ADX, volume, RSI, etc)
- v7: Aggressive pure flips for maximum accuracy

Rationale: Chart analysis shows ~100% accuracy on line flips
Every green section = price up, every red section = price down
ATR-based stops handle risk, no need for entry filters
2025-11-18 10:57:37 +01:00

246 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//@version=5
indicator("Bullmania Money Line v7 Line Flips", shorttitle="Money Line v7", 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")
// Signal timing (ALWAYS applies to all signals)
groupTiming = "Signal Timing"
confirmBars = input.int(0, "Bars to confirm after flip", minval=0, maxval=2, group=groupTiming, tooltip="V7 MODE: Always 0 for line flip signals. 1-2 adds delay (not recommended for v7).")
// 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.100.20 works well on 1h.")
useAdx = input.bool(true, "Use ADX trend-strength filter", group=groupFilters, tooltip="If enabled, require ADX to be above a threshold to reduce chop.")
adxLen = input.int(16, "ADX Length", minval=1, group=groupFilters)
adxMin = input.int(14, "ADX minimum", minval=0, maxval=100, group=groupFilters)
// 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="Don't buy if price is above this % of 100-bar range (prevents chasing highs).")
shortPosMin = input.float(15, "Short min position %", minval=0, maxval=100, group=groupV6Filters, tooltip="Don't sell if price is below this % of 100-bar range (prevents chasing lows).")
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="Minimum volume relative to 20-bar MA.")
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)
rsiShortMax = input.float(70, "RSI short maximum", minval=0, maxval=100, group=groupV6Filters)
// 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
// === 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)
// V7: PURE LINE FLIP SIGNALS - Maximum accuracy mode
// Signal fires on EVERY line color change (red↔green)
// Risk management: ATR-based TP/SL + trailing stops (bot-side)
// Filters removed: Line flip IS the signal
finalLongSignal = buyReady // 🟢 Signal on every red → green flip
finalShortSignal = sellReady // 🔴 Signal on every green → red flip
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 = "v6"
// Build enhanced alert messages with context (timeframe.period is dynamic)
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, "#.#") + " | 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, "#.#") + " | 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(col