v5: add timeframe profiles with auto/override + default per-timeframe ATR/Multiplier; README updated

This commit is contained in:
root
2025-10-18 14:21:43 +02:00
parent ef6fa0baeb
commit bfda6c2617
5 changed files with 349 additions and 11 deletions

View File

@@ -0,0 +1,115 @@
//@version=5
strategy("Bullmania Money Line v2", overlay=true,
initial_capital=10000, currency=currency.USD, pyramiding=0,
commission_type=strategy.commission.percent, commission_value=0.1, // 0.1% commission
slippage=1, process_orders_on_close=true)
// Inputs
atrPeriod = input.int(10, "ATR Period", minval=1)
multiplier = input.float(3.0, "Multiplier", minval=0.1, step=0.1)
// Backtest toggles
enableLongs = input.bool(true, "Enable Longs")
enableShorts = input.bool(true, "Enable Shorts")
// Date range filter
startYear = input.int(2020, "Start Year", minval=1970, maxval=2100)
startMonth = input.int(1, "Start Month", minval=1, maxval=12)
startDay = input.int(1, "Start Day", minval=1, maxval=31)
endYear = input.int(2100, "End Year", minval=1970, maxval=2100)
endMonth = input.int(12, "End Month", minval=1, maxval=12)
endDay = input.int(31, "End Day", minval=1, maxval=31)
startTime = timestamp(startYear, startMonth, startDay, 0, 0)
endTime = timestamp(endYear, endMonth, endDay, 23, 59)
inDateRange = time >= startTime and time <= endTime
// Position sizing
riskMode = input.string("Percent of equity", "Sizing Mode", options=["Percent of equity", "Fixed contracts"])
posSizePct = input.float(10.0, "Position Size % of Equity", minval=0.0, step=0.1)
fixedQty = input.float(1.0, "Fixed Contracts/Qty", minval=0.0, step=0.1)
allowFractional = input.bool(true, "Allow fractional quantity")
usePct = riskMode == "Percent of equity"
calcQty(price) =>
qtyCalc = usePct ? (strategy.equity * (posSizePct / 100.0)) / price : fixedQty
allowFractional ? qtyCalc : math.floor(qtyCalc)
// Stops/Targets
useMoneyLineStop = input.bool(true, "Use Money Line as Stop")
tpAtrMult = input.float(0.0, "Take Profit ATR Multiplier (0 = Off)", minval=0.0, step=0.1)
// Core calculations (same as v1)
atr = ta.atr(atrPeriod)
src = (high + low) / 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 := close[1] > up1 ? math.max(up, up1) : up
dn1 := close[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 := close < tsl ? -1 : 1
else
tsl := math.min(dn1, tsl)
trend := close > 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)
// Signals
longSignal = trend == 1 and trend[1] == -1
shortSignal = trend == -1 and trend[1] == 1
// Plot buy/sell signals
plotshape(longSignal, title="Buy Signal", location=location.belowbar, color=color.green, style=shape.circle, size=size.small)
plotshape(shortSignal, title="Sell Signal", location=location.abovebar, color=color.red, style=shape.circle, size=size.small)
// 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))
// Alerts
alertcondition(longSignal, title="Bullmania Long", message="Bullmania Money Line: Long signal")
alertcondition(shortSignal, title="Bullmania Short", message="Bullmania Money Line: Short signal")
// Backtest entries
canLong = enableLongs and inDateRange
canShort = enableShorts and inDateRange
// Issue entries on signal
if longSignal and canLong
strategy.entry(id="Long", direction=strategy.long, qty = calcQty(close))
if shortSignal and canShort
strategy.entry(id="Short", direction=strategy.short, qty = calcQty(close))
// Dynamic stops/targets via strategy.exit
longStop = useMoneyLineStop and trend == 1 ? supertrend : na
shortStop = useMoneyLineStop and trend == -1 ? supertrend : na
// Optional ATR-based take profits referenced from average fill price
longTP = tpAtrMult > 0 and strategy.position_size > 0 ? strategy.position_avg_price + atr * tpAtrMult : na
shortTP = tpAtrMult > 0 and strategy.position_size < 0 ? strategy.position_avg_price - atr * tpAtrMult : na
// Update exits every bar (works even when not in position)
strategy.exit("XL", from_entry="Long", stop=longStop, limit=longTP)
strategy.exit("XS", from_entry="Short", stop=shortStop, limit=shortTP)

View File

@@ -54,9 +54,13 @@ showFlipLevelLine = input.bool(true, "Show Flip Level line")
flipLineAutoColor = input.bool(true, "Flip line color by trend")
flipLineCustomCol = input.color(color.new(color.silver, 0), "Flip line custom color")
showFlipLevelValue = input.bool(true, "Show Flip Level value label")
flipLabelPosOpt = input.string("Flip bar", "Flip Level label position", options=["Current bar","Flip bar"], tooltip="Where to place the Flip Level value label. 'Flip bar' anchors the label to the candle where the last flip occurred so it moves with that candle when you pan the chart.")
extendLinesRight = input.bool(false, "Extend lines to right edge", tooltip="If enabled, flip and next-flip lines will extend to the right edge (HUD-style). Disable to keep them glued strictly to candles.")
flipLineWidth = input.int(3, "Flip line width", minval=1, maxval=6)
nextFlipLineWidth = input.int(2, "Next flip line width", minval=1, maxval=6)
labelSizeOpt = input.string("small", "Value label size", options=["tiny","small","normal"])
// Global label size derived from option (used by all labels)
lblSize = labelSizeOpt == "tiny" ? size.tiny : labelSizeOpt == "small" ? size.small : size.normal
// Preview of the next flip thresholds
showNextBullFlip = input.bool(true, "Show Next Bull Flip level")
showNextBearFlip = input.bool(false, "Show Next Bear Flip level")
@@ -359,20 +363,35 @@ downTrend = trend == -1 ? supertrend : na
// Persistent Flip Level (holds value from last flip until next)
var float flipLevel = na
var int flipBarIndex = na
flipLevel := (flipUp or flipDn) ? supertrend : nz(flipLevel[1])
flipBarIndex := (flipUp or flipDn) ? bar_index : nz(flipBarIndex[1])
flipLineColor = flipLineAutoColor ? (trend == 1 ? color.new(color.green, 0) : color.new(color.red, 0)) : flipLineCustomCol
plot(showFlipLevelLine ? flipLevel : na, title="Flip Level", color=flipLineColor, linewidth=flipLineWidth, style=plot.style_linebr, trackprice=true)
plot(showFlipLevelLine ? flipLevel : na, title="Flip Level", color=flipLineColor, linewidth=flipLineWidth, style=plot.style_linebr, trackprice=extendLinesRight)
// Optional always-visible value label at current bar (high-contrast)
var label flipValLbl = na
if showFlipLevelValue and not na(flipLevel)
if not na(flipValLbl)
label.delete(flipValLbl)
// High-contrast: black text on green (bull), white text on maroon (bear)
flipLblBg = flipLineAutoColor ? (trend == 1 ? color.new(color.green, 0) : color.new(color.maroon, 0)) : flipLineCustomCol
flipLblTxt = flipLineAutoColor ? (trend == 1 ? color.black : color.white) : color.white
lblSize = labelSizeOpt == "tiny" ? size.tiny : labelSizeOpt == "small" ? size.small : size.normal
flipValLbl := label.new(bar_index, flipLevel, text="Flip Level: " + str.tostring(flipLevel, format.mintick), style=label.style_label_left, color=flipLblBg, textcolor=flipLblTxt, yloc=yloc.price, size=lblSize)
// Determine x position based on user preference
flipLblX = flipLabelPosOpt == "Flip bar" and not na(flipBarIndex) ? flipBarIndex : bar_index
if na(flipValLbl)
flipValLbl := label.new(flipLblX, flipLevel, text="Flip Level: " + str.tostring(flipLevel, format.mintick), xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_left, color=flipLblBg, textcolor=flipLblTxt, size=lblSize)
else
label.set_x(flipValLbl, flipLblX)
label.set_y(flipValLbl, flipLevel)
label.set_text(flipValLbl, "Flip Level: " + str.tostring(flipLevel, format.mintick))
label.set_color(flipValLbl, flipLblBg)
label.set_textcolor(flipValLbl, flipLblTxt)
label.set_style(flipValLbl, label.style_label_left)
label.set_size(flipValLbl, lblSize)
else
if not na(flipValLbl)
label.delete(flipValLbl)
flipValLbl := na
// Next flip preview levels (dynamic thresholds)
nextBullLevel = tsl + aBufferATR * atr // price needed to start bull flip counting
@@ -384,23 +403,54 @@ nextBearActive = trend == 1
nbCol = color.new(color.lime, 0)
nsCol = color.new(color.red, 0)
plot(showNextBullFlip and nextBullActive ? nextBullLevel : na, title="Next Bull Flip Level", color=nbCol, linewidth=nextFlipLineWidth, style=plot.style_linebr, trackprice=true)
plot(showNextBearFlip and nextBearActive ? nextBearLevel : na, title="Next Bear Flip Level", color=nsCol, linewidth=nextFlipLineWidth, style=plot.style_linebr, trackprice=true)
plot(showNextBullFlip and nextBullActive ? nextBullLevel : na, title="Next Bull Flip Level", color=nbCol, linewidth=nextFlipLineWidth, style=plot.style_linebr, trackprice=extendLinesRight)
plot(showNextBearFlip and nextBearActive ? nextBearLevel : na, title="Next Bear Flip Level", color=nsCol, linewidth=nextFlipLineWidth, style=plot.style_linebr, trackprice=extendLinesRight)
// Optional live value labels for next flip thresholds (high-contrast)
var label nextBullLbl = na
var label nextBearLbl = na
if showNextFlipValue
// Next Bull label lifecycle
if showNextBullFlip and nextBullActive and not na(nextBullLevel)
if na(nextBullLbl)
nextBullLbl := label.new(bar_index, nextBullLevel, text="Next Bull Flip: " + str.tostring(nextBullLevel, format.mintick) + (aConfirmBars > 1 ? " (" + str.tostring(aConfirmBars) + " closes)" : ""), xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_left, color=color.new(color.lime, 0), textcolor=color.black, size=lblSize)
else
label.set_x(nextBullLbl, bar_index)
label.set_y(nextBullLbl, nextBullLevel)
label.set_text(nextBullLbl, "Next Bull Flip: " + str.tostring(nextBullLevel, format.mintick) + (aConfirmBars > 1 ? " (" + str.tostring(aConfirmBars) + " closes)" : ""))
label.set_color(nextBullLbl, color.new(color.lime, 0))
label.set_textcolor(nextBullLbl, color.black)
label.set_style(nextBullLbl, label.style_label_left)
label.set_size(nextBullLbl, lblSize)
else
if not na(nextBullLbl)
label.delete(nextBullLbl)
// Bright lime background with black text for readability
nextBullLbl := label.new(bar_index, nextBullLevel, text="Next Bull Flip: " + str.tostring(nextBullLevel, format.mintick) + (aConfirmBars > 1 ? " (" + str.tostring(aConfirmBars) + " closes)" : ""), style=label.style_label_left, color=color.new(color.lime, 0), textcolor=color.black, yloc=yloc.price, size=lblSize)
nextBullLbl := na
// Next Bear label lifecycle
if showNextBearFlip and nextBearActive and not na(nextBearLevel)
if na(nextBearLbl)
nextBearLbl := label.new(bar_index, nextBearLevel, text="Next Bear Flip: " + str.tostring(nextBearLevel, format.mintick) + (aConfirmBars > 1 ? " (" + str.tostring(aConfirmBars) + " closes)" : ""), xloc=xloc.bar_index, yloc=yloc.price, style=label.style_label_left, color=color.new(color.maroon, 0), textcolor=color.white, size=lblSize)
else
label.set_x(nextBearLbl, bar_index)
label.set_y(nextBearLbl, nextBearLevel)
label.set_text(nextBearLbl, "Next Bear Flip: " + str.tostring(nextBearLevel, format.mintick) + (aConfirmBars > 1 ? " (" + str.tostring(aConfirmBars) + " closes)" : ""))
label.set_color(nextBearLbl, color.new(color.maroon, 0))
label.set_textcolor(nextBearLbl, color.white)
label.set_style(nextBearLbl, label.style_label_left)
label.set_size(nextBearLbl, lblSize)
else
if not na(nextBearLbl)
label.delete(nextBearLbl)
// Darker red background for contrast
nextBearLbl := label.new(bar_index, nextBearLevel, text="Next Bear Flip: " + str.tostring(nextBearLevel, format.mintick) + (aConfirmBars > 1 ? " (" + str.tostring(aConfirmBars) + " closes)" : ""), style=label.style_label_left, color=color.new(color.maroon, 0), textcolor=color.white, yloc=yloc.price, size=lblSize)
nextBearLbl := na
else
if not na(nextBullLbl)
label.delete(nextBullLbl)
nextBullLbl := na
if not na(nextBearLbl)
label.delete(nextBearLbl)
nextBearLbl := 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)

View File

@@ -0,0 +1,125 @@
//@version=5
indicator("Bullmania Money Line v5", overlay=true)
// 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")
// 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 (unchanged from v1)
atr = ta.atr(atrPeriod)
src = (high + low) / 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 := close[1] > up1 ? math.max(up, up1) : up
dn1 := close[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 := close < tsl ? -1 : 1
else
tsl := math.min(dn1, tsl)
trend := close > 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 (once per bar close)
showProfileLabel = input.bool(true, "Show active profile label", group="Profiles")
var label profLbl = na
if barstate.islast and showProfileLabel
if not na(profLbl)
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
plotshape(buyFlip and longOk, title="Buy Signal", location=location.belowbar, color=color.green, style=shape.circle, size=size.small)
plotshape(sellFlip and shortOk, title="Sell Signal", location=location.abovebar, color=color.red, style=shape.circle, size=size.small)
// 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))

View File

@@ -1,5 +1,27 @@
# Bullmania Money Line
## Bullmania Money Line v2 (strategy)
`Bullmania_Money_Line_v2.pine` is a TradingView Strategy version of the original indicator with built-in backtesting.
- Convert the original Money Line into a strategy with entries on trend flips.
- Long/Short toggles and date range filter for backtests.
- Commission/slippage controls via strategy() header.
- Two sizing modes: Percent of equity (qty computed from equity and price) or fixed quantity.
- Optional Money Line-based stop and ATR-multiple take profit.
How to use:
1. Open the file in TradingView Pine Editor and click Add to chart.
2. In the Inputs tab, set your ATR Period and Multiplier just like v1.
3. Set backtest range (Start/End date) and toggles (Enable Longs/Shorts).
4. Choose sizing mode: Percent of equity (qty_percent) or Fixed contracts (qty).
5. Optionally enable Money Line as Stop and ATR-based Take Profit.
Notes:
- Entries occur on trend flips (when the Money Line switches from down to up for longs and vice versa for shorts).
- Exits are managed using `strategy.exit` with the Money Line as trailing stop when enabled. TP is optional.
- Default initial capital is 10,000 and commission is 0.1% with 1 tick slippage; adjust as needed.
ATR-based trend line and flip signals for TradingView (Pine v6). The indicator draws a “Money Line” that trails price and flips direction when price crosses it. Green dots mark bullish flips (trend turns up), red dots mark bearish flips (trend turns down).
This repo contains multiple versions; v4 is the stable baseline and `v4+` adds optional filters, alerts, and per-timeframe profiles while preserving v4 defaults (off by default).
@@ -10,6 +32,7 @@ This repo contains multiple versions; v4 is the stable baseline and `v4+` adds o
- `Bullmania_Money_Line_v3.pine` — Historical variant.
- `Bullmania_Money_Line_v4.pine` — v1 + optional noise reduction (confirmation bars, ATR buffer). Tagged `v4.0.0`.
- `Bullmania_Money_Line_v4_plus.pine` — v4-compatible but with optional filters and quality-of-life features (Pine v6).
- `Bullmania_Money_Line_v5.pine` — v1 + optional MACD confirmation gate and optional timeframe profiles (Single mode or auto profile by chart TF).
## How it works (all versions)
- Money Line is derived from a mid-price `(high + low) / 2` and `ATR(atrPeriod)` scaled by `multiplier`.
@@ -154,10 +177,35 @@ Keep Multiplier roughly stable and fine-tune by small increments; HA candles all
5) Open Inputs to adjust parameters.
## Changelog
- v5 — v1 + optional MACD gate (buy dot requires MACD line > signal; sell dot requires MACD line < signal). Adds optional timeframe profiles: choose "Single" or "Profiles by timeframe". Profiles buckets: Minutes, Hours, Daily, Weekly/Monthly with independent ATR Period and Multiplier. Defaults keep v1 behavior.
- v4+ — Pine v6 variant with optional ADX/MTF/session/cooldown, calc source toggle, EMA trend filter, anti-chop (CHOP, retest, min body), gated markers, and per-timeframe profiles.
- v4.0.0 — Adds Flip confirmation bars and Flip buffer (×ATR). Defaults keep v1 behavior. Tag: `v4.0.0`.
- v1 — Initial baseline.
## v5 timeframe profiles (quick guide)
In `Bullmania_Money_Line_v5.pine` you can switch "Parameter Mode" between:
- Single — one global ATR Period and Multiplier (same as v1 behavior).
- Profiles by timeframe — separate inputs per bucket with auto-selection from chart TF or via "Profile Override":
- Minutes (<= 59m)
- Hours (>= 1h and < 1d)
- Daily (>= 1d and < 1w)
- Weekly/Monthly (>= 1w)
Inputs added in v5 when profiles are enabled:
- ATR Period (Minutes/Hours/Daily/Weekly-Monthly)
- Multiplier (Minutes/Hours/Daily/Weekly-Monthly)
- Profile Override: Auto, or force a specific bucket
- Optional: show active profile label on the chart
Note: Only ATR Period and Multiplier are profiled in v5; filters remain limited to the optional MACD confirmation gate.
### Default values (v5 profiles)
Out of the box defaults meant to be robust across popular markets; adjust per symbol/volatility:
- Minutes: ATR 12, Multiplier 3.3
- Hours: ATR 10, Multiplier 3.0
- Daily: ATR 10, Multiplier 2.8
- Weekly/Monthly: ATR 7, Multiplier 2.5
## Notes
- v4 is designed to reduce whips without changing the core Money Line logic.
- Optional future add-ons (can be toggled if requested):