Skip to content

test validated BNB divergence profile #238

test validated BNB divergence profile

test validated BNB divergence profile #238

Triggered via push July 23, 2026 17:53
Status Failure
Total duration 28s
Artifacts –

quality.yml

on: push
test-and-build
25s
test-and-build
Fit to window
Zoom out
Zoom in

Annotations

9 errors and 1 warning
test-and-build
Process completed with exit code 1.
tests/strategy-divergence-alignment.test.ts > Strategy divergence alignment > does not change indicator output: tests/strategy-divergence-alignment.test.ts#L46
AssertionError: expected '//@Version=6\n// PineForge validated …' to contain '// === Integrated RSI divergence pane…' - Expected + Received - // === Integrated RSI divergence pane === + //@Version=6 + // PineForge validated profile: BNBUSDT, 30-minute chart. + // Development: 2019-2022. Validation: 2023-2024. Final 2025+ holdout was not opened. + // Commission 0.15% per side is used as a TradingView proxy for the research model's 0.10% commission plus 0.05% adverse slippage. + // Review and test this script before using real capital. + indicator("RSI Divergence Reversal · BNBUSDT 30m", overlay=true, max_labels_count=500, max_lines_count=500) + + // === Validated profile constants === + expectedTicker = "BNBUSDT" + expectedTimeframe = "30" + profileMarketOk = syminfo.ticker == expectedTicker + profileTimeframeOk = timeframe.period == expectedTimeframe + profileAllowed = profileMarketOk and profileTimeframeOk + confirmedOnly = input.bool(true, "Confirmed candles only") + rsiLength = input.int(14, "RSI length", minval=2) + pivotLeft = input.int(5, "Divergence pivot left", minval=1) + pivotRight = input.int(5, "Divergence pivot right", minval=1) + pivotRangeMin = input.int(5, "Minimum pivot range", minval=1) + pivotRangeMax = input.int(60, "Maximum pivot range", minval=2) + confirmationExpiry = input.int(30, "EMA confirmation expiry bars", minval=1) + swingLength = input.int(15, "Swing stop lookback", minval=2) + riskReward = input.float(1.8, "Risk/reward target", minval=0.1, step=0.1) + volumeLength = input.int(20, "Volume average length", minval=1) + volumeMultiplier = input.float(0.8, "Minimum volume multiplier", minval=0.1, step=0.05) + + // === Core calculations === + rsiValue = ta.rsi(close, rsiLength) + ema9 = ta.ema(close, 9) + wma45 = ta.wma(close, 45) + ema50 = ta.ema(close, 50) + ema200 = ta.ema(close, 200) + volumeAverage = ta.sma(volume, volumeLength) + confirmationOk = not confirmedOnly or barstate.isconfirmed + volumeOk = volume >= volumeAverage * volumeMultiplier + longTrendOk = ema50 > ema200 and close > ema200 + shortTrendOk = ema50 < ema200 and close < ema200 + + // === Confirmed regular RSI divergence === + pivotLowFound = not na(ta.pivotlow(rsiValue, pivotLeft, pivotRight)) + pivotHighFound = not na(ta.pivothigh(rsiValue, pivotLeft, pivotRight)) + inPivotRange(condition) => + barsSince = ta.barssince(condition) + pivotRangeMin <= barsSince and barsSince <= pivotRangeMax + previousLowInRange = inPivotRange(pivotLowFound[1]) + rsiHigherLow = rsiValue[pivotRight] > ta.valuewhen(pivotLowFound, rsiValue[pivotRight], 1) and previousLowInRange + priceLowerLow = low[pivotRight] < ta.valuewhen(pivotLowFound, low[pivotRight], 1) + regularBullishDivergence = pivotLowFound and rsiHigherLow and priceLowerLow + previousHighInRange = inPivotRange(pivotHighFound[1]) + rsiLowerHigh = rsiValue[pivotRight] < ta.valuewhen(pivotHighFound, rsiValue[pivotRight], 1) and previousHighInRange + priceHigherHigh = high[pivotRight] > ta.valuewhen(pivotHighFound, high[pivotRight], 1) + regularBearishDivergence = pivotHighFound and rsiLowerHigh and priceHigherHigh + + // === Divergence is armed until EMA9/WMA45 confirms === + var int pendingDirection = 0 + var int pendingExpiresAt = na + if regularBullishDivergence + pendingDirection := 1 + pendingExpiresAt := bar_index + confirmationExpiry + if regularBearishDivergence + pendingDirection := -1 + pendingExpiresAt := bar_index + confirmationExpiry + if not na(pendingExpiresAt) and bar_index > pendingExpiresAt + pendingDirection := 0 + pendingExpiresAt := na + longConfirmation = pendingDirection == 1 and ta.crossover(ema9, wma45) + shortConfirmation = pendingDirection == -1 and ta.crossunder(ema9, wma45) + longSignal = profileAllowed and confirmationOk and volumeOk and longTrendOk and longConfirmation + shortSignal = profileAllowed and confirmationOk and volumeOk and shortTrendOk and shortConfirmation + if longSignal or shortSignal + pendingDirection := 0 + pendingExpiresAt := na + + // === Frozen swing risk captured at entry === + var float
tests/strategy-divergence-alignment.test.ts > Strategy divergence alignment > uses the confirmed regular divergence engine when divergence is enabled: tests/strategy-divergence-alignment.test.ts#L16
AssertionError: expected '//@Version=6\n// PineForge validated …' to contain '// Confirmed regular RSI divergence s…' - Expected + Received - // Confirmed regular RSI divergence shared with Indicator mode. + //@Version=6 + // PineForge validated profile: BNBUSDT, 30-minute chart. + // Development: 2019-2022. Validation: 2023-2024. Final 2025+ holdout was not opened. + // Commission 0.15% per side is used as a TradingView proxy for the research model's 0.10% commission plus 0.05% adverse slippage. + // Review and test this script before using real capital. + strategy("RSI Divergence Reversal · BNBUSDT 30m", overlay=true, pyramiding=0, process_orders_on_close=true, initial_capital=10000, commission_type=strategy.commission.percent, commission_value=0.15) + + // === Validated profile constants === + expectedTicker = "BNBUSDT" + expectedTimeframe = "30" + profileMarketOk = syminfo.ticker == expectedTicker + profileTimeframeOk = timeframe.period == expectedTimeframe + profileAllowed = profileMarketOk and profileTimeframeOk + confirmedOnly = input.bool(true, "Confirmed candles only") + rsiLength = input.int(14, "RSI length", minval=2) + pivotLeft = input.int(5, "Divergence pivot left", minval=1) + pivotRight = input.int(5, "Divergence pivot right", minval=1) + pivotRangeMin = input.int(5, "Minimum pivot range", minval=1) + pivotRangeMax = input.int(60, "Maximum pivot range", minval=2) + confirmationExpiry = input.int(30, "EMA confirmation expiry bars", minval=1) + swingLength = input.int(15, "Swing stop lookback", minval=2) + riskReward = input.float(1.8, "Risk/reward target", minval=0.1, step=0.1) + volumeLength = input.int(20, "Volume average length", minval=1) + volumeMultiplier = input.float(0.8, "Minimum volume multiplier", minval=0.1, step=0.05) + + // === Core calculations === + rsiValue = ta.rsi(close, rsiLength) + ema9 = ta.ema(close, 9) + wma45 = ta.wma(close, 45) + ema50 = ta.ema(close, 50) + ema200 = ta.ema(close, 200) + volumeAverage = ta.sma(volume, volumeLength) + confirmationOk = not confirmedOnly or barstate.isconfirmed + volumeOk = volume >= volumeAverage * volumeMultiplier + longTrendOk = ema50 > ema200 and close > ema200 + shortTrendOk = ema50 < ema200 and close < ema200 + + // === Confirmed regular RSI divergence === + pivotLowFound = not na(ta.pivotlow(rsiValue, pivotLeft, pivotRight)) + pivotHighFound = not na(ta.pivothigh(rsiValue, pivotLeft, pivotRight)) + inPivotRange(condition) => + barsSince = ta.barssince(condition) + pivotRangeMin <= barsSince and barsSince <= pivotRangeMax + previousLowInRange = inPivotRange(pivotLowFound[1]) + rsiHigherLow = rsiValue[pivotRight] > ta.valuewhen(pivotLowFound, rsiValue[pivotRight], 1) and previousLowInRange + priceLowerLow = low[pivotRight] < ta.valuewhen(pivotLowFound, low[pivotRight], 1) + regularBullishDivergence = pivotLowFound and rsiHigherLow and priceLowerLow + previousHighInRange = inPivotRange(pivotHighFound[1]) + rsiLowerHigh = rsiValue[pivotRight] < ta.valuewhen(pivotHighFound, rsiValue[pivotRight], 1) and previousHighInRange + priceHigherHigh = high[pivotRight] > ta.valuewhen(pivotHighFound, high[pivotRight], 1) + regularBearishDivergence = pivotHighFound and rsiLowerHigh and priceHigherHigh + + // === Divergence is armed until EMA9/WMA45 confirms === + var int pendingDirection = 0 + var int pendingExpiresAt = na + if regularBullishDivergence + pendingDirection := 1 + pendingExpiresAt := bar_index + confirmationExpiry + if regularBearishDivergence + pendingDirection := -1 + pendingExpiresAt := bar_index + confirmationExpiry + if not na(pendingExpiresAt) and bar_index > pendingExpiresAt + pendingDirection := 0 + pendingExpiresAt := na + longConfirmation = pendingDirection == 1 and ta.crossover(ema9, wma45) + shortConfirmation = pendingDirection == -1 and ta.crossunder(ema9, wma45) + longSignal = profileAllowed and confirmationOk and volumeOk and longTrendOk and longConfirmation + shortSignal = profileAllowed and confirmationOk and volumeOk and shortTrendOk and shortConfirmation + if longSignal or shortSignal +
tests/strategy-atr-entry-freeze.test.ts > strategy ATR entry freeze > keeps the aligned divergence engine and freezes ATR for RSI Divergence Reversal strategy: tests/strategy-atr-entry-freeze.test.ts#L33
AssertionError: expected '//@Version=6\n// PineForge validated …' to contain '// Confirmed regular RSI divergence s…' - Expected + Received - // Confirmed regular RSI divergence shared with Indicator mode. + //@Version=6 + // PineForge validated profile: BNBUSDT, 30-minute chart. + // Development: 2019-2022. Validation: 2023-2024. Final 2025+ holdout was not opened. + // Commission 0.15% per side is used as a TradingView proxy for the research model's 0.10% commission plus 0.05% adverse slippage. + // Review and test this script before using real capital. + strategy("RSI Divergence Reversal · BNBUSDT 30m", overlay=true, pyramiding=0, process_orders_on_close=true, initial_capital=10000, commission_type=strategy.commission.percent, commission_value=0.15) + + // === Validated profile constants === + expectedTicker = "BNBUSDT" + expectedTimeframe = "30" + profileMarketOk = syminfo.ticker == expectedTicker + profileTimeframeOk = timeframe.period == expectedTimeframe + profileAllowed = profileMarketOk and profileTimeframeOk + confirmedOnly = input.bool(true, "Confirmed candles only") + rsiLength = input.int(14, "RSI length", minval=2) + pivotLeft = input.int(5, "Divergence pivot left", minval=1) + pivotRight = input.int(5, "Divergence pivot right", minval=1) + pivotRangeMin = input.int(5, "Minimum pivot range", minval=1) + pivotRangeMax = input.int(60, "Maximum pivot range", minval=2) + confirmationExpiry = input.int(30, "EMA confirmation expiry bars", minval=1) + swingLength = input.int(15, "Swing stop lookback", minval=2) + riskReward = input.float(1.8, "Risk/reward target", minval=0.1, step=0.1) + volumeLength = input.int(20, "Volume average length", minval=1) + volumeMultiplier = input.float(0.8, "Minimum volume multiplier", minval=0.1, step=0.05) + + // === Core calculations === + rsiValue = ta.rsi(close, rsiLength) + ema9 = ta.ema(close, 9) + wma45 = ta.wma(close, 45) + ema50 = ta.ema(close, 50) + ema200 = ta.ema(close, 200) + volumeAverage = ta.sma(volume, volumeLength) + confirmationOk = not confirmedOnly or barstate.isconfirmed + volumeOk = volume >= volumeAverage * volumeMultiplier + longTrendOk = ema50 > ema200 and close > ema200 + shortTrendOk = ema50 < ema200 and close < ema200 + + // === Confirmed regular RSI divergence === + pivotLowFound = not na(ta.pivotlow(rsiValue, pivotLeft, pivotRight)) + pivotHighFound = not na(ta.pivothigh(rsiValue, pivotLeft, pivotRight)) + inPivotRange(condition) => + barsSince = ta.barssince(condition) + pivotRangeMin <= barsSince and barsSince <= pivotRangeMax + previousLowInRange = inPivotRange(pivotLowFound[1]) + rsiHigherLow = rsiValue[pivotRight] > ta.valuewhen(pivotLowFound, rsiValue[pivotRight], 1) and previousLowInRange + priceLowerLow = low[pivotRight] < ta.valuewhen(pivotLowFound, low[pivotRight], 1) + regularBullishDivergence = pivotLowFound and rsiHigherLow and priceLowerLow + previousHighInRange = inPivotRange(pivotHighFound[1]) + rsiLowerHigh = rsiValue[pivotRight] < ta.valuewhen(pivotHighFound, rsiValue[pivotRight], 1) and previousHighInRange + priceHigherHigh = high[pivotRight] > ta.valuewhen(pivotHighFound, high[pivotRight], 1) + regularBearishDivergence = pivotHighFound and rsiLowerHigh and priceHigherHigh + + // === Divergence is armed until EMA9/WMA45 confirms === + var int pendingDirection = 0 + var int pendingExpiresAt = na + if regularBullishDivergence + pendingDirection := 1 + pendingExpiresAt := bar_index + confirmationExpiry + if regularBearishDivergence + pendingDirection := -1 + pendingExpiresAt := bar_index + confirmationExpiry + if not na(pendingExpiresAt) and bar_index > pendingExpiresAt + pendingDirection := 0 + pendingExpiresAt := na + longConfirmation = pendingDirection == 1 and ta.crossover(ema9, wma45) + shortConfirmation = pendingDirection == -1 and ta.crossunder(ema9, wma45) + longSignal = profileAllowed and confirmationOk and volumeOk and longTrendOk and longConfirmation + shortSignal = profileAllowed and confirmationOk and volumeOk and shortTrendOk and shortConfirmation + if longSignal or shortSignal +
tests/rsi-divergence-risk-consistency.test.ts > RSI Divergence Reversal consistency > does not overwrite an active same-direction visual trade: tests/rsi-divergence-risk-consistency.test.ts#L29
AssertionError: expected '//@Version=6\n// PineForge validated …' to contain 'acceptedLongSignal = longSignal and r…' - Expected + Received - acceptedLongSignal = longSignal and riskDirection != 1 + //@Version=6 + // PineForge validated profile: BNBUSDT, 30-minute chart. + // Development: 2019-2022. Validation: 2023-2024. Final 2025+ holdout was not opened. + // Commission 0.15% per side is used as a TradingView proxy for the research model's 0.10% commission plus 0.05% adverse slippage. + // Review and test this script before using real capital. + indicator("RSI Divergence Reversal · BNBUSDT 30m", overlay=true, max_labels_count=500, max_lines_count=500) + + // === Validated profile constants === + expectedTicker = "BNBUSDT" + expectedTimeframe = "30" + profileMarketOk = syminfo.ticker == expectedTicker + profileTimeframeOk = timeframe.period == expectedTimeframe + profileAllowed = profileMarketOk and profileTimeframeOk + confirmedOnly = input.bool(true, "Confirmed candles only") + rsiLength = input.int(14, "RSI length", minval=2) + pivotLeft = input.int(5, "Divergence pivot left", minval=1) + pivotRight = input.int(5, "Divergence pivot right", minval=1) + pivotRangeMin = input.int(5, "Minimum pivot range", minval=1) + pivotRangeMax = input.int(60, "Maximum pivot range", minval=2) + confirmationExpiry = input.int(30, "EMA confirmation expiry bars", minval=1) + swingLength = input.int(15, "Swing stop lookback", minval=2) + riskReward = input.float(1.8, "Risk/reward target", minval=0.1, step=0.1) + volumeLength = input.int(20, "Volume average length", minval=1) + volumeMultiplier = input.float(0.8, "Minimum volume multiplier", minval=0.1, step=0.05) + + // === Core calculations === + rsiValue = ta.rsi(close, rsiLength) + ema9 = ta.ema(close, 9) + wma45 = ta.wma(close, 45) + ema50 = ta.ema(close, 50) + ema200 = ta.ema(close, 200) + volumeAverage = ta.sma(volume, volumeLength) + confirmationOk = not confirmedOnly or barstate.isconfirmed + volumeOk = volume >= volumeAverage * volumeMultiplier + longTrendOk = ema50 > ema200 and close > ema200 + shortTrendOk = ema50 < ema200 and close < ema200 + + // === Confirmed regular RSI divergence === + pivotLowFound = not na(ta.pivotlow(rsiValue, pivotLeft, pivotRight)) + pivotHighFound = not na(ta.pivothigh(rsiValue, pivotLeft, pivotRight)) + inPivotRange(condition) => + barsSince = ta.barssince(condition) + pivotRangeMin <= barsSince and barsSince <= pivotRangeMax + previousLowInRange = inPivotRange(pivotLowFound[1]) + rsiHigherLow = rsiValue[pivotRight] > ta.valuewhen(pivotLowFound, rsiValue[pivotRight], 1) and previousLowInRange + priceLowerLow = low[pivotRight] < ta.valuewhen(pivotLowFound, low[pivotRight], 1) + regularBullishDivergence = pivotLowFound and rsiHigherLow and priceLowerLow + previousHighInRange = inPivotRange(pivotHighFound[1]) + rsiLowerHigh = rsiValue[pivotRight] < ta.valuewhen(pivotHighFound, rsiValue[pivotRight], 1) and previousHighInRange + priceHigherHigh = high[pivotRight] > ta.valuewhen(pivotHighFound, high[pivotRight], 1) + regularBearishDivergence = pivotHighFound and rsiLowerHigh and priceHigherHigh + + // === Divergence is armed until EMA9/WMA45 confirms === + var int pendingDirection = 0 + var int pendingExpiresAt = na + if regularBullishDivergence + pendingDirection := 1 + pendingExpiresAt := bar_index + confirmationExpiry + if regularBearishDivergence + pendingDirection := -1 + pendingExpiresAt := bar_index + confirmationExpiry + if not na(pendingExpiresAt) and bar_index > pendingExpiresAt + pendingDirection := 0 + pendingExpiresAt := na + longConfirmation = pendingDirection == 1 and ta.crossover(ema9, wma45) + shortConfirmation = pendingDirection == -1 and ta.crossunder(ema9, wma45) + longSignal = profileAllowed and confirmationOk and volumeOk and longTrendOk and longConfirmation + shortSignal = profileAllowed and confirmationOk and volumeOk and shortTrendOk and shortConfirmation + if longSignal or shortSignal + pendingDirection := 0 + pendingExpiresAt := na + + // === Frozen swing risk captured at entry ==
tests/rsi-divergence-risk-consistency.test.ts > RSI Divergence Reversal consistency > reuses the main RSI calculation for divergence: tests/rsi-divergence-risk-consistency.test.ts#L16
AssertionError: expected '//@Version=6\n// PineForge validated …' to contain 'divRsi = rsiValue' - Expected + Received - divRsi = rsiValue + //@Version=6 + // PineForge validated profile: BNBUSDT, 30-minute chart. + // Development: 2019-2022. Validation: 2023-2024. Final 2025+ holdout was not opened. + // Commission 0.15% per side is used as a TradingView proxy for the research model's 0.10% commission plus 0.05% adverse slippage. + // Review and test this script before using real capital. + indicator("RSI Divergence Reversal · BNBUSDT 30m", overlay=true, max_labels_count=500, max_lines_count=500) + + // === Validated profile constants === + expectedTicker = "BNBUSDT" + expectedTimeframe = "30" + profileMarketOk = syminfo.ticker == expectedTicker + profileTimeframeOk = timeframe.period == expectedTimeframe + profileAllowed = profileMarketOk and profileTimeframeOk + confirmedOnly = input.bool(true, "Confirmed candles only") + rsiLength = input.int(14, "RSI length", minval=2) + pivotLeft = input.int(5, "Divergence pivot left", minval=1) + pivotRight = input.int(5, "Divergence pivot right", minval=1) + pivotRangeMin = input.int(5, "Minimum pivot range", minval=1) + pivotRangeMax = input.int(60, "Maximum pivot range", minval=2) + confirmationExpiry = input.int(30, "EMA confirmation expiry bars", minval=1) + swingLength = input.int(15, "Swing stop lookback", minval=2) + riskReward = input.float(1.8, "Risk/reward target", minval=0.1, step=0.1) + volumeLength = input.int(20, "Volume average length", minval=1) + volumeMultiplier = input.float(0.8, "Minimum volume multiplier", minval=0.1, step=0.05) + + // === Core calculations === + rsiValue = ta.rsi(close, rsiLength) + ema9 = ta.ema(close, 9) + wma45 = ta.wma(close, 45) + ema50 = ta.ema(close, 50) + ema200 = ta.ema(close, 200) + volumeAverage = ta.sma(volume, volumeLength) + confirmationOk = not confirmedOnly or barstate.isconfirmed + volumeOk = volume >= volumeAverage * volumeMultiplier + longTrendOk = ema50 > ema200 and close > ema200 + shortTrendOk = ema50 < ema200 and close < ema200 + + // === Confirmed regular RSI divergence === + pivotLowFound = not na(ta.pivotlow(rsiValue, pivotLeft, pivotRight)) + pivotHighFound = not na(ta.pivothigh(rsiValue, pivotLeft, pivotRight)) + inPivotRange(condition) => + barsSince = ta.barssince(condition) + pivotRangeMin <= barsSince and barsSince <= pivotRangeMax + previousLowInRange = inPivotRange(pivotLowFound[1]) + rsiHigherLow = rsiValue[pivotRight] > ta.valuewhen(pivotLowFound, rsiValue[pivotRight], 1) and previousLowInRange + priceLowerLow = low[pivotRight] < ta.valuewhen(pivotLowFound, low[pivotRight], 1) + regularBullishDivergence = pivotLowFound and rsiHigherLow and priceLowerLow + previousHighInRange = inPivotRange(pivotHighFound[1]) + rsiLowerHigh = rsiValue[pivotRight] < ta.valuewhen(pivotHighFound, rsiValue[pivotRight], 1) and previousHighInRange + priceHigherHigh = high[pivotRight] > ta.valuewhen(pivotHighFound, high[pivotRight], 1) + regularBearishDivergence = pivotHighFound and rsiLowerHigh and priceHigherHigh + + // === Divergence is armed until EMA9/WMA45 confirms === + var int pendingDirection = 0 + var int pendingExpiresAt = na + if regularBullishDivergence + pendingDirection := 1 + pendingExpiresAt := bar_index + confirmationExpiry + if regularBearishDivergence + pendingDirection := -1 + pendingExpiresAt := bar_index + confirmationExpiry + if not na(pendingExpiresAt) and bar_index > pendingExpiresAt + pendingDirection := 0 + pendingExpiresAt := na + longConfirmation = pendingDirection == 1 and ta.crossover(ema9, wma45) + shortConfirmation = pendingDirection == -1 and ta.crossunder(ema9, wma45) + longSignal = profileAllowed and confirmationOk and volumeOk and longTrendOk and longConfirmation + shortSignal = profileAllowed and confirmationOk and volumeOk and shortTrendOk and shortConfirmation + if longSignal or shortSignal + pendingDirection := 0 + pendingExpiresAt := na + + // === Frozen swing risk captured at entry === + var float activeStop = na + var float activeTarget = na
tests/rsi-divergence-entry-alignment.test.ts > RSI divergence entry alignment > does not change the strategy output path: tests/rsi-divergence-entry-alignment.test.ts#L47
AssertionError: expected '//@Version=6\n// PineForge validated …' to contain 'bullishDivergence =' - Expected + Received - bullishDivergence = + //@Version=6 + // PineForge validated profile: BNBUSDT, 30-minute chart. + // Development: 2019-2022. Validation: 2023-2024. Final 2025+ holdout was not opened. + // Commission 0.15% per side is used as a TradingView proxy for the research model's 0.10% commission plus 0.05% adverse slippage. + // Review and test this script before using real capital. + strategy("RSI Divergence Reversal · BNBUSDT 30m", overlay=true, pyramiding=0, process_orders_on_close=true, initial_capital=10000, commission_type=strategy.commission.percent, commission_value=0.15) + + // === Validated profile constants === + expectedTicker = "BNBUSDT" + expectedTimeframe = "30" + profileMarketOk = syminfo.ticker == expectedTicker + profileTimeframeOk = timeframe.period == expectedTimeframe + profileAllowed = profileMarketOk and profileTimeframeOk + confirmedOnly = input.bool(true, "Confirmed candles only") + rsiLength = input.int(14, "RSI length", minval=2) + pivotLeft = input.int(5, "Divergence pivot left", minval=1) + pivotRight = input.int(5, "Divergence pivot right", minval=1) + pivotRangeMin = input.int(5, "Minimum pivot range", minval=1) + pivotRangeMax = input.int(60, "Maximum pivot range", minval=2) + confirmationExpiry = input.int(30, "EMA confirmation expiry bars", minval=1) + swingLength = input.int(15, "Swing stop lookback", minval=2) + riskReward = input.float(1.8, "Risk/reward target", minval=0.1, step=0.1) + volumeLength = input.int(20, "Volume average length", minval=1) + volumeMultiplier = input.float(0.8, "Minimum volume multiplier", minval=0.1, step=0.05) + + // === Core calculations === + rsiValue = ta.rsi(close, rsiLength) + ema9 = ta.ema(close, 9) + wma45 = ta.wma(close, 45) + ema50 = ta.ema(close, 50) + ema200 = ta.ema(close, 200) + volumeAverage = ta.sma(volume, volumeLength) + confirmationOk = not confirmedOnly or barstate.isconfirmed + volumeOk = volume >= volumeAverage * volumeMultiplier + longTrendOk = ema50 > ema200 and close > ema200 + shortTrendOk = ema50 < ema200 and close < ema200 + + // === Confirmed regular RSI divergence === + pivotLowFound = not na(ta.pivotlow(rsiValue, pivotLeft, pivotRight)) + pivotHighFound = not na(ta.pivothigh(rsiValue, pivotLeft, pivotRight)) + inPivotRange(condition) => + barsSince = ta.barssince(condition) + pivotRangeMin <= barsSince and barsSince <= pivotRangeMax + previousLowInRange = inPivotRange(pivotLowFound[1]) + rsiHigherLow = rsiValue[pivotRight] > ta.valuewhen(pivotLowFound, rsiValue[pivotRight], 1) and previousLowInRange + priceLowerLow = low[pivotRight] < ta.valuewhen(pivotLowFound, low[pivotRight], 1) + regularBullishDivergence = pivotLowFound and rsiHigherLow and priceLowerLow + previousHighInRange = inPivotRange(pivotHighFound[1]) + rsiLowerHigh = rsiValue[pivotRight] < ta.valuewhen(pivotHighFound, rsiValue[pivotRight], 1) and previousHighInRange + priceHigherHigh = high[pivotRight] > ta.valuewhen(pivotHighFound, high[pivotRight], 1) + regularBearishDivergence = pivotHighFound and rsiLowerHigh and priceHigherHigh + + // === Divergence is armed until EMA9/WMA45 confirms === + var int pendingDirection = 0 + var int pendingExpiresAt = na + if regularBullishDivergence + pendingDirection := 1 + pendingExpiresAt := bar_index + confirmationExpiry + if regularBearishDivergence + pendingDirection := -1 + pendingExpiresAt := bar_index + confirmationExpiry + if not na(pendingExpiresAt) and bar_index > pendingExpiresAt + pendingDirection := 0 + pendingExpiresAt := na + longConfirmation = pendingDirection == 1 and ta.crossover(ema9, wma45) + shortConfirmation = pendingDirection == -1 and ta.crossunder(ema9, wma45) + longSignal = profileAllowed and confirmationOk and volumeOk and longTrendOk and longConfirmation + shortSignal = profileAllowed and confirmationOk and volumeOk and shortTrendOk and shortConfirmation + if longSignal or shortSignal + pendingDirection := 0 + pendingExpiresAt := na + + // === Fro
tests/rsi-divergence-entry-alignment.test.ts > RSI divergence entry alignment > uses the same regular divergence events for pane labels and entries: tests/rsi-divergence-entry-alignment.test.ts#L16
AssertionError: expected '//@Version=6\n// PineForge validated …' to contain 'bullishDivergence = divRegularBullAle…' - Expected + Received - bullishDivergence = divRegularBullAlert + //@Version=6 + // PineForge validated profile: BNBUSDT, 30-minute chart. + // Development: 2019-2022. Validation: 2023-2024. Final 2025+ holdout was not opened. + // Commission 0.15% per side is used as a TradingView proxy for the research model's 0.10% commission plus 0.05% adverse slippage. + // Review and test this script before using real capital. + indicator("RSI Divergence Reversal · BNBUSDT 30m", overlay=true, max_labels_count=500, max_lines_count=500) + + // === Validated profile constants === + expectedTicker = "BNBUSDT" + expectedTimeframe = "30" + profileMarketOk = syminfo.ticker == expectedTicker + profileTimeframeOk = timeframe.period == expectedTimeframe + profileAllowed = profileMarketOk and profileTimeframeOk + confirmedOnly = input.bool(true, "Confirmed candles only") + rsiLength = input.int(14, "RSI length", minval=2) + pivotLeft = input.int(5, "Divergence pivot left", minval=1) + pivotRight = input.int(5, "Divergence pivot right", minval=1) + pivotRangeMin = input.int(5, "Minimum pivot range", minval=1) + pivotRangeMax = input.int(60, "Maximum pivot range", minval=2) + confirmationExpiry = input.int(30, "EMA confirmation expiry bars", minval=1) + swingLength = input.int(15, "Swing stop lookback", minval=2) + riskReward = input.float(1.8, "Risk/reward target", minval=0.1, step=0.1) + volumeLength = input.int(20, "Volume average length", minval=1) + volumeMultiplier = input.float(0.8, "Minimum volume multiplier", minval=0.1, step=0.05) + + // === Core calculations === + rsiValue = ta.rsi(close, rsiLength) + ema9 = ta.ema(close, 9) + wma45 = ta.wma(close, 45) + ema50 = ta.ema(close, 50) + ema200 = ta.ema(close, 200) + volumeAverage = ta.sma(volume, volumeLength) + confirmationOk = not confirmedOnly or barstate.isconfirmed + volumeOk = volume >= volumeAverage * volumeMultiplier + longTrendOk = ema50 > ema200 and close > ema200 + shortTrendOk = ema50 < ema200 and close < ema200 + + // === Confirmed regular RSI divergence === + pivotLowFound = not na(ta.pivotlow(rsiValue, pivotLeft, pivotRight)) + pivotHighFound = not na(ta.pivothigh(rsiValue, pivotLeft, pivotRight)) + inPivotRange(condition) => + barsSince = ta.barssince(condition) + pivotRangeMin <= barsSince and barsSince <= pivotRangeMax + previousLowInRange = inPivotRange(pivotLowFound[1]) + rsiHigherLow = rsiValue[pivotRight] > ta.valuewhen(pivotLowFound, rsiValue[pivotRight], 1) and previousLowInRange + priceLowerLow = low[pivotRight] < ta.valuewhen(pivotLowFound, low[pivotRight], 1) + regularBullishDivergence = pivotLowFound and rsiHigherLow and priceLowerLow + previousHighInRange = inPivotRange(pivotHighFound[1]) + rsiLowerHigh = rsiValue[pivotRight] < ta.valuewhen(pivotHighFound, rsiValue[pivotRight], 1) and previousHighInRange + priceHigherHigh = high[pivotRight] > ta.valuewhen(pivotHighFound, high[pivotRight], 1) + regularBearishDivergence = pivotHighFound and rsiLowerHigh and priceHigherHigh + + // === Divergence is armed until EMA9/WMA45 confirms === + var int pendingDirection = 0 + var int pendingExpiresAt = na + if regularBullishDivergence + pendingDirection := 1 + pendingExpiresAt := bar_index + confirmationExpiry + if regularBearishDivergence + pendingDirection := -1 + pendingExpiresAt := bar_index + confirmationExpiry + if not na(pendingExpiresAt) and bar_index > pendingExpiresAt + pendingDirection := 0 + pendingExpiresAt := na + longConfirmation = pendingDirection == 1 and ta.crossover(ema9, wma45) + shortConfirmation = pendingDirection == -1 and ta.crossunder(ema9, wma45) + longSignal = profileAllowed and confirmationOk and volumeOk and longTrendOk and longConfirmation + shortSignal = profileAllowed and confirmationOk and volumeOk and shortTrendOk and shortConfirmation + if longSignal or shortSignal + pendingDirection := 0 + pendingExpiresAt := na + + // === Frozen swing risk captured at entry === + var float a
tests/compiler.test.ts > preset behavior fixtures > RSI Divergence Reversal satisfies validation, Pine and explanation contracts: tests/compiler.test.ts#L26
AssertionError: [ { "level": "error", "code": "filter.ema_trend.long_missing", "message": "Long filter ema_trend is missing." }, { "level": "error", "code": "filter.ema_trend.short_missing", "message": "Short filter ema_trend is missing." }, { "level": "error", "code": "filter.divergence.long_missing", "message": "Long filter divergence is missing." }, { "level": "error", "code": "filter.divergence.short_missing", "message": "Short filter divergence is missing." }, { "level": "error", "code": "indicator.risk_stop_missing", "message": "Indicator risk settings require a visible stop plot." }, { "level": "error", "code": "indicator.risk_target_missing", "message": "Indicator target settings require a visible target plot." } ]: expected [ { level: 'error', …(2) }, …(5) ] to deeply equal [] - Expected + Received - [] + [ + { + "code": "filter.ema_trend.long_missing", + "level": "error", + "message": "Long filter ema_trend is missing.", + }, + { + "code": "filter.ema_trend.short_missing", + "level": "error", + "message": "Short filter ema_trend is missing.", + }, + { + "code": "filter.divergence.long_missing", + "level": "error", + "message": "Long filter divergence is missing.", + }, + { + "code": "filter.divergence.short_missing", + "level": "error", + "message": "Short filter divergence is missing.", + }, + { + "code": "indicator.risk_stop_missing", + "level": "error", + "message": "Indicator risk settings require a visible stop plot.", + }, + { + "code": "indicator.risk_target_missing", + "level": "error", + "message": "Indicator target settings require a visible target plot.", + }, + ] ❯ expectClean tests/compiler.test.ts:26:85 ❯ tests/compiler.test.ts:150:7
test-and-build
Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: actions/checkout@v4, actions/setup-node@v4. For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/