Indicator · Pine Script v6 · Own pane

Saham: Papan Gerak By. Akhmfz

by akhmfz

This technical analysis dashboard aggregates market data across four dimensions: trend, momentum, volatility, and volume.

View on GitHub

GitHub stars1on the repository
Pine versionv6current
Lines of code1,484
Settings40inputs
LicenceMIT
Last update3 months ago8 Jul 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

1,484 lines of Pine Script v6 from src/PapanGerak.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

PapanGerak.pine
// @version=6
// 01-base.pine — Papan Gerak: Header, Inputs, Tema, Utilities
// ==========================================================
// Papan Gerak — Technical Analysis Dashboard
// Author : Muhammad Akhmal (AKHMFZ)
// Contact: akhmfz.analytics@gmail.com
// License: MIT
// Built   : Pine Script v6 for TradingView
// ==========================================================

// --- Indicator Definition ---
indicator(
    "Saham: Papan Gerak By. Akhmfz",
    shorttitle = "P.Gerak",
    overlay = false,
    format = format.price,
    precision = 0
)

// ==========================================================
// 1. SETTINGS — General
// ==========================================================
// --- Display ---
tablePositionInput = input.string(
    "Top Right",
    "Table Position",
    options = ["Top Left", "Top Right", "Bottom Left", "Bottom Right"],
    display = display.data_window
)
fontSizeInput = input.int(1, "Font Size", minval = 1, maxval = 3, display = display.data_window)
modeInput = input.string("Standard", "Mode", options = ["Compact", "Standard", "Detailed"], display = display.data_window)
bahasaInput = input.string("Indonesia", "Bahasa", options = ["Indonesia", "English"], display = display.data_window)
colorThemeInput = input.string("Gelap", "Color Theme", options = ["Gelap", "Terang", "Bursa Hijau", "Biru Nusantara", "Emas Premium"], display = display.data_window)

// ==========================================================
// 2. SETTINGS — Trend Parameters
// ==========================================================
emaFastPeriod = input.int(20, "EMA Fast Period", group = "Trend", display = display.data_window)
emaMidPeriod = input.int(50, "EMA Mid Period", group = "Trend", display = display.data_window)
emaSlowPeriod = input.int(200, "EMA Slow Period", group = "Trend", display = display.data_window)
superTrendFactor = input.float(3.0, "SuperTrend Factor", step = 0.1, group = "Trend", display = display.data_window)
superTrendPeriod = input.int(10, "SuperTrend ATR Period", group = "Trend", display = display.data_window)
adxLength = input.int(14, "ADX Length", group = "Trend", display = display.data_window)
adxThreshold = input.int(25, "ADX Threshold", group = "Trend", display = display.data_window)

// ==========================================================
// 3. SETTINGS — Momentum Parameters
// ==========================================================
rsiLength = input.int(14, "RSI Length", group = "Momentum", display = display.data_window)
rsiOb = input.int(70, "RSI Overbought", group = "Momentum", display = display.data_window)
rsiOs = input.int(30, "RSI Oversold", group = "Momentum", display = display.data_window)
macdFast = input.int(12, "MACD Fast", group = "Momentum", display = display.data_window)
macdSlow = input.int(26, "MACD Slow", group = "Momentum", display = display.data_window)
macdSignal = input.int(9, "MACD Signal", group = "Momentum", display = display.data_window)
stochK = input.int(14, "Stochastic K", group = "Momentum", display = display.data_window)
stochD = input.int(3, "Stochastic D", group = "Momentum", display = display.data_window)
stochOb = input.int(80, "Stoch Overbought", group = "Momentum", display = display.data_window)
stochOs = input.int(20, "Stoch Oversold", group = "Momentum", display = display.data_window)

// ==========================================================
// 4. SETTINGS — Volatility Parameters
// ==========================================================
atrLength = input.int(14, "ATR Length", group = "Volatility", display = display.data_window)
bbLength = input.int(20, "Bollinger Length", group = "Volatility", display = display.data_window)
bbMult = input.float(2.0, "Bollinger Multiplier", step = 0.1, group = "Volatility", display = display.data_window)
chopLength = input.int(14, "Choppiness Length", group = "Volatility", display = display.data_window)

// ==========================================================
// 5. SETTINGS — Volume Parameters
// ==========================================================
volMaLength = input.int(20, "Volume MA Length", group = "Volume", display = display.data_window)
volSpikeMult = input.float(2.0, "Volume Spike Multiplier", step = 0.1, group = "Volume", display = display.data_window)

// ==========================================================
// 6. SETTINGS — Scoring Weights
// ==========================================================
weightTrend = input.int(30, "Trend Weight (%)", minval = 0, maxval = 100, step = 5, group = "Weights", display = display.data_window)
weightMomentum = input.int(30, "Momentum Weight (%)", minval = 0, maxval = 100, step = 5, group = "Weights", display = display.data_window)
weightVolatility = input.int(20, "Volatility Weight (%)", minval = 0, maxval = 100, step = 5, group = "Weights", display = display.data_window)
weightVolume = input.int(20, "Volume Weight (%)", minval = 0, maxval = 100, step = 5, group = "Weights", display = display.data_window)

// ==========================================================
// 7. SETTINGS — Smart Money (Optional)
// ==========================================================
enableSmartMoney = input.bool(false, "Enable Smart Money Flow", group = "Smart Money", display = display.data_window)
weightSmartMoney = input.int(20, "SM Weight (%)", minval = 0, maxval = 100, step = 5, group = "Smart Money", display = display.data_window)
smVolumeThreshold = input.float(2.5, "SM Volume Z-Threshold", step = 0.1, group = "Smart Money", display = display.data_window)
smPeriod = input.int(20, "SM Lookback Period", group = "Smart Money", display = display.data_window)

// ==========================================================
// 8. SETTINGS — Signal Parameters
// ==========================================================
minSignalBars = input.int(5, "Min Bars Between Signals", minval = 1, maxval = 50, group = "Signal", display = display.data_window, tooltip = "Jarak minimal antar sinyal (dalam bar). Mencegah sinyal beruntun terlalu rapat.")
signalFilterMode = input.string("Disabled", "Signal Filter", options = ["Disabled", "Only Trending (Chop<38)", "Only Ranging (Chop>62)"], group = "Signal", display = display.data_window, tooltip = "Hanya izinkan sinyal saat market trending (Chop<38) atau ranging (Chop>62). Nonaktifkan untuk semua kondisi.")
confirmOnBarClose = input.bool(true, "Confirm on Bar Close", group = "Signal", display = display.data_window, tooltip = "Hanya kirim alert saat candle benar-benar tutup. Mencegah sinyal/alert palsu di tengah bar berjalan (repaint). Nonaktifkan untuk respons real-time lebih cepat.")

// ==========================================================
// 9. SETTINGS — Risk Management
// ==========================================================
showRiskLevels = input.bool(true, "Show Risk Levels", group = "Risk Mgmt", display = display.data_window, tooltip = "Tampilkan baris Level Referensi (SL/Target) di tabel. Nonaktifkan untuk mode scan cepat.")
slTypeInput = input.string("ATR", "SL Type", options = ["ATR", "Swing Low/High", "Both (Wider)"], group = "Risk Mgmt", display = display.data_window, tooltip = "Metode Stop Loss: ATR-based, Swing Low/High, atau kombinasi (ambil yang lebih jauh).")
slAtrMult = input.float(1.5, "SL ATR Multiplier", minval = 0.5, maxval = 5.0, step = 0.1, group = "Risk Mgmt", display = display.data_window, tooltip = "Pengali ATR untuk SL. Semakin besar semakin longgar. Contoh: 1.5 × ATR(14).")
slSwingPeriod = input.int(10, "SL Swing Lookback", minval = 5, maxval = 50, group = "Risk Mgmt", display = display.data_window, tooltip = "Jumlah bar untuk mencari swing low/high sebagai referensi SL. Makin panjang makin jauh dari entry.")
targetR = input.float(2.0, "Target R-Multiple", minval = 0.5, maxval = 10.0, step = 0.5, group = "Risk Mgmt", display = display.data_window, tooltip = "Target take-profit sebagai kelipatan risiko. 2R = profit 2× dari jarak SL.")

// ==========================================================
// 10. SETTINGS — Position Sizing
// ==========================================================
showPositionSizing = input.bool(false, "Show Position Sizing", group = "Position Size", display = display.data_window, tooltip = "Tampilkan kalkulator ukuran posisi (dalam lot) berdasarkan balance dan risk %.")
accountBalanceInput = input.int(50000000, "Account Balance (Rp)", minval = 1000000, step = 1000000, group = "Position Size", display = display.data_window, tooltip = "Total modal trading dalam Rupiah. Digunakan untuk menghitung jumlah lot per sinyal.")
riskPctInput = input.float(1.0, "Risk Per Trade (%)", minval = 0.1, maxval = 5.0, step = 0.1, group = "Position Size", display = display.data_window, tooltip = "Persentase modal yang siap dirisikokan per trade. Contoh: 1% dari Rp50jt = Rp500rb.")

// ==========================================================
// 11. SETTINGS — Multi-Timeframe Alignment
// ==========================================================
enableMTF = input.bool(true, "Enable MTF Alignment", group = "MTF", display = display.data_window, tooltip = "Aktifkan Multi-Timeframe Alignment Score. Menghitung alignment 3 timeframe untuk konfirmasi sinyal.")
mtfPrimaryTF = input.string("D", "Primary TF", options = ["D", "W", "M"], group = "MTF", display = display.data_window, tooltip = "Primary timeframe — entry timing. Default: Daily.")
mtfSecondaryTF = input.string("W", "Secondary TF", options = ["D", "W", "M"], group = "MTF", display = display.data_window, tooltip = "Secondary timeframe — trend konfirmasi. Default: Weekly.")
mtfTertiaryTF = input.string("M", "Tertiary TF", options = ["D", "W", "M"], group = "MTF", display = display.data_window, tooltip = "Tertiary timeframe — konteks makro. Default: Monthly.")
mtfPrimaryWeight = input.float(0.50, "Primary Weight", minval = 0.0, maxval = 1.0, step = 0.05, group = "MTF", display = display.data_window, tooltip = "Bobot primary timeframe dalam skor akhir. Default: 0.50 (50%).")
mtfSecondaryWeight = input.float(0.35, "Secondary Weight", minval = 0.0, maxval = 1.0, step = 0.05, group = "MTF", display = display.data_window, tooltip = "Bobot secondary timeframe. Default: 0.35 (35%).")
mtfTertiaryWeight = input.float(0.15, "Tertiary Weight", minval = 0.0, maxval = 1.0, step = 0.05, group = "MTF", display = display.data_window, tooltip = "Bobot tertiary timeframe. Default: 0.15 (15%).")
mtfAlpha = input.float(0.70, "Directional Alpha", minval = 0.0, maxval = 1.0, step = 0.05, group = "MTF", display = display.data_window, tooltip = "Bobot directional components (Trend+Momentum) vs context (Volatility+Volume). Default: 0.70.")
mtfMaxPenalty = input.float(0.25, "Max Conflict Penalty", minval = 0.0, maxval = 0.50, step = 0.05, group = "MTF", display = display.data_window, tooltip = "Penalti maksimum saat timeframe saling bertentangan. 0 = tanpa penalti, 0.25 = 25% maks.")
mtfDirectionHysteresis = input.float(10.0, "Direction Hysteresis", minval = 0.0, maxval = 30.0, step = 1.0, group = "MTF", display = display.data_window, tooltip = "Ambang hysteresis untuk direction flag. Lebih tinggi = lebih jarang flip direction. Default: 10.")
mtfDebug = input.bool(false, "Debug Mode (detail semua komponen)", group = "MTF", display = display.data_window, tooltip = "Jika aktif, tampilkan semua komponen MTF di Data Window untuk debugging.")

// Legacy MTF filter (retained for backward compat — trend row & narrative)
enableMtfFilter = input.bool(false, "Enable MTF Trend Filter (legacy)", group = "MTF", display = display.data_window, tooltip = "Legacy: filter tren Multi-Timeframe sederhana. Sinyal harian dibandingkan dengan tren higher TF.")
mtfTimeframe = input.string("W", "MTF Timeframe (legacy)", options = ["W", "M"], group = "MTF", display = display.data_window, tooltip = "Timeframe legacy MTF. W = Weekly, M = Monthly.")

// ==========================================================
// 12. SETTINGS — Entry Trigger
// ==========================================================
entryTriggerMode = input.string("Composite Score", "Entry Trigger", options = ["Composite Score", "Pullback", "Breakout"], group = "Entry Trigger", display = display.data_window, tooltip = "Mode sinyal: Composite Score (zona 40/70), Pullback (entry saat retrace), atau Breakout (saat harga tembus level support/resistance).")

// ==========================================================
// 13. SETTINGS — Webhook / Alert Format
// ==========================================================
webhookFormat = input.string("Simple", "Alert Format", options = ["Simple", "JSON"], group = "Webhook", display = display.data_window, tooltip = "Format alert: Simple (teks biasa) atau JSON (PG|key=value untuk integrasi TradersPost/Pine Connector).")

// ==========================================================
// 14. COLOR THEMES
// ==========================================================
var color bgColor = na
var color headerColor = na
var color labelColor = na
var color scoreColor = na
var color tableBgColor = na
var color bullColor = na
var color bearColor = na
var color neutralColor = na
var color signalLineColor = na
var color dimTrend = na
var color dimMomentum = na
var color dimVolatility = na
var color dimVolume = na

if colorThemeInput == "Gelap"
    bgColor := color.rgb(0, 0, 0, 85)
    headerColor := color.rgb(38, 166, 154)
    labelColor := color.white
    scoreColor := color.rgb(38, 166, 154)
    tableBgColor := color.rgb(13, 22, 30, 90)
    bullColor := color.rgb(38, 166, 154)
    bearColor := color.rgb(239, 83, 80)
    neutralColor := color.rgb(255, 202, 40)
    signalLineColor := color.rgb(100, 100, 100)
    dimTrend := color.rgb(66, 165, 245)
    dimMomentum := color.rgb(255, 202, 40)
    dimVolatility := color.rgb(239, 83, 80)
    dimVolume := color.rgb(126, 87, 194)
else
    if colorThemeInput == "Terang"
        bgColor := color.rgb(255, 255, 255, 85)
        headerColor := color.rgb(0, 150, 136)
        labelColor := color.rgb(33, 33, 33)
        scoreColor := color.rgb(0, 150, 136)
        tableBgColor := color.rgb(245, 245, 245, 90)
        bullColor := color.rgb(0, 150, 136)
        bearColor := color.rgb(211, 47, 47)
        neutralColor := color.rgb(255, 160, 0)
        signalLineColor := color.rgb(158, 158, 158)
        dimTrend := color.rgb(33, 150, 243)
        dimMomentum := color.rgb(255, 193, 7)
        dimVolatility := color.rgb(244, 67, 54)
        dimVolume := color.rgb(156, 39, 176)
    else
        if colorThemeInput == "Bursa Hijau"
            bgColor := color.rgb(0, 30, 0, 85)
            headerColor := color.rgb(76, 175, 80)
            labelColor := color.white
            scoreColor := color.rgb(76, 175, 80)
            tableBgColor := color.rgb(0, 20, 0, 90)
            bullColor := color.rgb(76, 175, 80)
            bearColor := color.rgb(244, 67, 54)
            neutralColor := color.rgb(255, 235, 59)
            signalLineColor := color.rgb(100, 100, 100)
            dimTrend := color.rgb(129, 199, 132)
            dimMomentum := color.rgb(255, 235, 59)
            dimVolatility := color.rgb(239, 83, 80)
            dimVolume := color.rgb(186, 104, 200)
        else
            if colorThemeInput == "Biru Nusantara"
                bgColor := color.rgb(0, 20, 50, 85)
                headerColor := color.rgb(41, 98, 255)
                labelColor := color.white
                scoreColor := color.rgb(41, 98, 255)
                tableBgColor := color.rgb(0, 10, 30, 90)
                bullColor := color.rgb(41, 98, 255)
                bearColor := color.rgb(255, 82, 82)
                neutralColor := color.rgb(255, 202, 40)
                signalLineColor := color.rgb(100, 100, 100)
                dimTrend := color.rgb(100, 181, 246)
                dimMomentum := color.rgb(255, 202, 40)
                dimVolatility := color.rgb(239, 83, 80)
                dimVolume := color.rgb(179, 136, 255)
            else
                bgColor := color.rgb(30, 20, 0, 85)
                headerColor := color.rgb(255, 193, 7)
                labelColor := color.rgb(33, 33, 33)
                scoreColor := color.rgb(255, 193, 7)
                tableBgColor := color.rgb(20, 15, 0, 90)
                bullColor := color.rgb(76, 175, 80)
                bearColor := color.rgb(244, 67, 54)
                neutralColor := color.rgb(255, 193, 7)
                signalLineColor := color.rgb(158, 158, 158)
                dimTrend := color.rgb(255, 183, 77)
                dimMomentum := color.rgb(255, 213, 79)
                dimVolatility := color.rgb(239, 83, 80)
                dimVolume := color.rgb(206, 147, 216)

// ==========================================================
// 10. UTILITY FUNCTIONS
// ==========================================================
f_secure(input, def) => na(input) ? def : input

f_scoreHigher(float val, float min, float max) =>
    na(val) ? na : math.max(0, math.min(100, (val - min) / (max - min) * 100))

f_scoreLower(float val, float min, float max) =>
    na(val) ? na : math.max(0, math.min(100, (max - val) / (max - min) * 100))

f_scoreRange(float val, float min, float max, float idealLow, float idealHigh) =>
    if na(val)
        na
    else
        if val >= idealLow and val <= idealHigh
            100.0
        else
            if val < idealLow
                if val <= min
                    0.0
                else
                    (val - min) / (idealLow - min) * 100
            else
                if val >= max
                    0.0
                else
                    (max - val) / (max - idealHigh) * 100


f_colorScore(float score) =>
    if na(score)
        color.gray
    else
        if score >= 70
            bullColor
        else
            if score >= 40
                neutralColor
            else
                bearColor

f_signalText(float score, string lang) =>
    if na(score)
        lang == "Indonesia" ? "N/A" : "N/A"
    else
        if score >= 70
            lang == "Indonesia" ? "BULLISH" : "BULLISH"
        else
            if score >= 40
                lang == "Indonesia" ? "NETRAL" : "NEUTRAL"
            else
                lang == "Indonesia" ? "BEARISH" : "BEARISH"

// @version=6
// 02-data.pine — Papan Gerak: Market Engine & TA Calculations
// ==========================================================

// ==========================================================
// 1. TREND INDICATORS
// ==========================================================
// --- EMA ---
emaFast = ta.ema(close, emaFastPeriod)
emaMid = ta.ema(close, emaMidPeriod)
emaSlow = ta.ema(close, emaSlowPeriod)

// EMA Position Score component
emaPosition = close > emaFast ? (close > emaMid ? (close > emaSlow ? 2 : 1) : 0) : -1
emaTrend = close > emaSlow ? 1 : -1

// --- SuperTrend ---
[superTrend, superTrendDir] = ta.supertrend(superTrendFactor, superTrendPeriod)
stBullish = superTrendDir == 1

// --- ADX / DMI ---
[adxUp, adxDn, adxValue] = ta.dmi(adxLength, adxLength)

// ADX strength
adxStrong = adxValue >= adxThreshold
adxTrend = adxUp > adxDn ? 1 : -1
// Trend alignment
trendAligned = (adxTrend == 1 and stBullish and emaTrend == 1) or (adxTrend == -1 and not stBullish and emaTrend == -1)

// ==========================================================
// 2. MOMENTUM INDICATORS
// ==========================================================
// --- RSI ---
rsiValue = ta.rsi(close, rsiLength)
rsiObHit = rsiValue >= rsiOb
rsiOsHit = rsiValue <= rsiOs
rsiDirection = ta.rising(rsiValue, 1) ? 1 : -1

// --- MACD ---
[macdLine, macdSignalLine, macdHist] = ta.macd(close, macdFast, macdSlow, macdSignal)
macdBullish = macdLine > macdSignalLine
macdHistRising = macdHist > macdHist[1]
macdZero = macdLine > 0

// --- Stochastic ---
stochKVal = ta.stoch(close, high, low, stochK)
stochDVal = ta.sma(stochKVal, stochD)
stochBullish = stochKVal > stochDVal
stochObHit = stochKVal >= stochOb
stochOsHit = stochKVal <= stochOs

// ==========================================================
// 3. VOLATILITY INDICATORS
// ==========================================================
// --- ATR% (normalized) ---
atrValue = ta.atr(atrLength)
atrPercent = atrValue / close * 100

// --- Bollinger Bands Width ---
bbMiddle = ta.sma(close, bbLength)
bbStdDev = ta.stdev(close, bbLength)
bbUpper = bbMiddle + bbMult * bbStdDev
bbLower = bbMiddle - bbMult * bbStdDev
bbWidth = bbMiddle != 0 ? (bbUpper - bbLower) / bbMiddle * 100 : 0
bbWidthAvg = ta.sma(bbWidth, bbLength)

// Price position within bands (0-100)
bbPosition = bbUpper != bbLower ? (close - bbLower) / (bbUpper - bbLower) * 100 : 50

// --- Choppiness Index ---
f_choppiness(int length) =>
    highestHigh = ta.highest(high, length)
    lowestLow = ta.lowest(low, length)
    atrSum = math.sum(ta.atr(1), length)
    range_ = highestHigh - lowestLow
    if range_ == 0
        100.0
    else
        math.log(atrSum / range_) / math.log(length) * 100

chopValue = f_choppiness(chopLength)
chopTrending = chopValue <= 38
chopRanging = chopValue >= 62

// ==========================================================
// 4. VOLUME INDICATORS
// ==========================================================
// --- Volume MA ---
volMA = ta.sma(volume, volMaLength)
volRatio = volMA > 0 ? volume / volMA : na
volSpike = volRatio >= volSpikeMult

// --- OBV ---
obvValue = ta.obv
obvMA = ta.sma(obvValue, volMaLength)
obvTrend = obvValue > obvMA ? 1 : -1
obvAgree = not na(close[1]) and ((obvTrend == 1 and close > close[1]) or (obvTrend == -1 and close < close[1]))

// --- Net Volume (proxy) ---
f_netVolume() =>
    mfMultiplier = (close - low) - (high - close)
    upVol = volume * mfMultiplier / (high - low + 1e-10)
    na(upVol) ? 0 : upVol

netVol = f_netVolume()
netVolSmooth = ta.sma(netVol, 5)

// ==========================================================
// 5. SMART MONEY FLOW (Optional)
// ==========================================================
// Institutional flow detection via volume Z-score
f_smartMoneyDetect() =>
    if not enableSmartMoney
        false
    else
        volSMA = ta.sma(volume, smPeriod)
        volStd = ta.stdev(volume, smPeriod)
        if volStd == 0
            false
        else
            zScore = (volume - volSMA) / volStd
            zScore >= smVolumeThreshold and close > open

// Volume inside body (institutional) vs at wick (retail)
f_isBodyVolume() =>
    bodyRange = math.abs(close - open)
    totalRange = high - low
    if totalRange == 0
        false
    else
        bodyRange / totalRange >= 0.6 and volSpike

smDetected = f_smartMoneyDetect()
smBodyVol = f_isBodyVolume()

// ==========================================================
// 6. MULTI-TIMEFRAME DATA FETCHERS
// ==========================================================
// Self-contained HTF scoring — semua computed dari built-in vars (close/high/low/volume)
// sehingga aman dipanggil dari dalam request.security(). Tidak mereferensi global vars
// yang sudah di-binding ke primary timeframe.
f_mtfTfScores() =>
    emaF = ta.ema(close, emaFastPeriod)
    emaM = ta.ema(close, emaMidPeriod)
    emaS = ta.ema(close, emaSlowPeriod)
    [st, stD] = ta.supertrend(superTrendFactor, superTrendPeriod)
    trendScore = 50.0
    if close > emaF and close > emaM and close > emaS
        trendScore := 80.0
    else if close < emaF and close < emaM and close < emaS
        trendScore := 20.0
    else if close > emaF
        trendScore := 60.0
    else
        trendScore := 40.0
    if not na(stD)
        trendScore := stD == 1 ? math.min(100, trendScore + 10) : math.max(0, trendScore - 10)

    rsi = ta.rsi(close, rsiLength)
    [mL, mS, mH] = ta.macd(close, macdFast, macdSlow, macdSignal)
    momScore = f_scoreHigher(rsi, 30, 70)
    if not na(mL) and not na(mS)
        momScore := mL > mS ? math.min(100, momScore + 10) : math.max(0, momScore - 10)

    atr = ta.atr(atrLength)
    atrPct = atr / close * 100
    volScore = f_scoreRange(atrPct, 0.5, 10.0, 1.0, 3.0)

    vMA = ta.sma(volume, volMaLength)
    vR = vMA > 0 ? volume / vMA : na
    volmScore = f_scoreRange(vR, 0.2, 5.0, 0.8, 2.0)

    [trendScore, momScore, volScore, volmScore]

mtfTf1Trend = na
mtfTf1Mom = na
mtfTf1Vol = na
mtfTf1Volm = na
mtfTf2Trend = na
mtfTf2Mom = na
mtfTf2Vol = na
mtfTf2Volm = na
mtfTf3Trend = na
mtfTf3Mom = na
mtfTf3Vol = na
mtfTf3Volm = na

if enableMTF
    [t1tr, t1mo, t1vo, t1vl] = request.security(syminfo.tickerid, mtfPrimaryTF, f_mtfTfScores(), barmerge.gaps_off, barmerge.lookahead_off)
    mtfTf1Trend := t1tr
    mtfTf1Mom := t1mo
    mtfTf1Vol := t1vo
    mtfTf1Volm := t1vl

    [t2tr, t2mo, t2vo, t2vl] = request.security(syminfo.tickerid, mtfSecondaryTF, f_mtfTfScores(), barmerge.gaps_off, barmerge.lookahead_off)
    mtfTf2Trend := t2tr
    mtfTf2Mom := t2mo
    mtfTf2Vol := t2vo
    mtfTf2Volm := t2vl

    [t3tr, t3mo, t3vo, t3vl] = request.security(syminfo.tickerid, mtfTertiaryTF, f_mtfTfScores(), barmerge.gaps_off, barmerge.lookahead_off)
    mtfTf3Trend := t3tr
    mtfTf3Mom := t3mo
    mtfTf3Vol := t3vo
    mtfTf3Volm := t3vl

// Legacy MTF trend filter (retained for backward compat)
mtfTrendScore = if enableMtfFilter
    request.security(syminfo.tickerid, mtfTimeframe,
        (close > ta.ema(close, emaSlowPeriod) ? 50 : 0) +
        (close > ta.ema(close, emaMidPeriod) ? 30 : 0) +
        (close > ta.ema(close, emaFastPeriod) ? 20 : 0),
        barmerge.gaps_off, barmerge.lookahead_off)
else
    na

// @version=6
// 03-scoring.pine — Papan Gerak: Scoring Engine + Render
// ==========================================================

// ==========================================================
// 1. TREND SCORE (0-100)
// ==========================================================
f_trendScore() =>
    scoreSum = 0.0
    count = 0.0

    // EMA Position (weight: 3)
    pos = emaPosition
    if not na(pos)
        scoreSum += (pos + 1) / 3 * 100 * 3
        count += 3

    // SuperTrend Direction (weight: 2)
    if not na(superTrendDir)
        scoreSum += (superTrendDir == 1 ? 100 : 0) * 2
        count += 2

    // ADX Strength (weight: 2)
    if not na(adxValue)
        adxScore = adxStrong ? (adxTrend == 1 ? 80 : 20) : 50
        scoreSum += adxScore * 2
        count += 2

    // Trend Alignment (weight: 1 — sinyal echo, bukan indepen)
    if not na(adxValue)
        alignScore = trendAligned ? (adxTrend == 1 ? 100 : 0) : 50
        scoreSum += alignScore * 1
        count += 1

    // EMA Slope — rate of change (weight: 1)
    // [1] ratio dipilih alih-alih [5] offset agar konsisten di semua timeframe.
    // Pada monthly chart, [5] = 5 bulan (~150 hari) tidak relevan untuk slope jangka pendek.
    slopeFast = emaFast / emaFast[1] - 1
    if not na(slopeFast)
        slopeScore = f_scoreHigher(slopeFast, -0.02, 0.02)
        scoreSum += slopeScore * 1
        count += 1

    count > 0 ? scoreSum / count : na

// ==========================================================
// 2. MOMENTUM SCORE (0-100)
// ==========================================================
f_momentumScore() =>
    scoreSum = 0.0
    count = 0.0

    // RSI (weight: 3)
    if not na(rsiValue)
        rsiScore = f_scoreHigher(rsiValue, 30, 70)
        scoreSum += rsiScore * 3
        count += 3

    // MACD (weight: 3)
    if not na(macdHist)
        macdScore = 50.0
        if macdBullish and macdHistRising
            macdScore := macdZero ? 90 : 70
        else
            if macdBullish
                macdScore := macdZero ? 70 : 55
            else
                if macdHistRising
                    macdScore := 45
                else
                    macdScore := macdZero ? 35 : 10
        scoreSum += macdScore * 3
        count += 3

    // Stochastic (weight: 2)
    if not na(stochKVal)
        stochScore = f_scoreHigher(stochKVal, 20, 80)
        scoreSum += stochScore * 2
        count += 2

    // RSI Direction (weight: 1)
    if not na(rsiDirection)
        dirScore = rsiDirection == 1 ? 70 : 30
        scoreSum += dirScore * 1
        count += 1

    // MACD Histogram Slope (weight: 1)
    if not na(macdHist) and not na(macdHist[2])
        slopeScore = macdHist > macdHist[2] ? 70 : 30
        scoreSum += slopeScore * 1
        count += 1

    count > 0 ? scoreSum / count : na

// ==========================================================
// 3. VOLATILITY SCORE (0-100)
// ==========================================================
f_volatilityScore() =>
    scoreSum = 0.0
    count = 0.0

    // Bollinger Width vs Average (weight: 3)
    if not na(bbWidth) and not na(bbWidthAvg)
        // Squeeze = low volatility, Expansion = high volatility
        widthRatio = bbWidth / bbWidthAvg
        // Ideal: moderate volatility (0.8-1.2x average)
        volScore = f_scoreRange(widthRatio, 0.3, 3.0, 0.8, 1.2)
        scoreSum += volScore * 3
        count += 3

    // Choppiness Index (weight: 3)
    if not na(chopValue)
        // Low chop = trending (good), High chop = ranging (bad)
        chopScore = 100 - math.min(100, chopValue)
        scoreSum += chopScore * 3
        count += 3

    // ATR% (weight: 2)
    if not na(atrPercent)
        // Moderate ATR is ideal
        atrScore = f_scoreRange(atrPercent, 0.5, 10.0, 1.0, 3.0)
        scoreSum += atrScore * 2
        count += 2

    // Bollinger Position (weight: 1)
    if not na(bbPosition)
        // Price near middle = moderate, near bands = extreme
        posScore = 100 - math.abs(bbPosition - 50) * 2
        scoreSum += posScore * 1
        count += 1

    count > 0 ? scoreSum / count : na

// ==========================================================
// 4. VOLUME SCORE (0-100)
// ==========================================================
f_volumeScore() =>
    scoreSum = 0.0
    count = 0.0

    // Volume vs MA (weight: 3)
    if not na(volRatio)
        volScore = f_scoreRange(volRatio, 0.2, 5.0, 0.8, 2.0)
        scoreSum += volScore * 3
        count += 3

    // OBV Agreement (weight: 3)
    if not na(obvValue)
        obvScore = obvAgree ? 80 : 30
        scoreSum += obvScore * 3
        count += 3

    // Net Volume Direction (weight: 2)
    if not na(netVolSmooth)
        netScore = netVolSmooth > 0 ? 70 : 30
        scoreSum += netScore * 2
        count += 2

    // Volume Spike Quality (weight: 1)
    if not na(volRatio)
        spikeScore = 50.0
        if volSpike
            closeInUpperHalf = close >= (high + low) / 2
            netPositive = not na(netVolSmooth) and netVolSmooth > 0
            spikeScore := closeInUpperHalf and netPositive ? 90 : closeInUpperHalf or netPositive ? 70 : 30
        scoreSum += spikeScore * 1
        count += 1

    count > 0 ? scoreSum / count : na

// ==========================================================
// 5. SMART MONEY SCORE (Optional, 0-100)
// ==========================================================
f_smartMoneyScore() =>
    if not enableSmartMoney
        na
    else
        scoreSum = 0.0
        count = 0.0

        // SM Detection (weight: 3)
        if not na(volume)
            scoreSum += (smDetected ? 90 : 50) * 3
            count += 3

        // Body Volume (weight: 2)
        if not na(volume)
            scoreSum += (smBodyVol ? 80 : 50) * 2
            count += 2

        // OBV Alignment (weight: 2)
        if not na(obvValue)
            scoreSum += (obvAgree ? 75 : 40) * 2
            count += 2

        // Net Volume (weight: 1)
        if not na(netVolSmooth)
            scoreSum += (netVolSmooth > 0 ? 70 : 40) * 1
            count += 1

        count > 0 ? scoreSum / count : na

// ==========================================================
// 6. MULTI-TIMEFRAME ALIGNMENT SCORE
// ==========================================================
// Menghitung alignment 3 timeframe:
//   - Directional (Trend+Momentum) untuk conflict detection
//   - Context (Volatility+Volume) sebagai modifier non-directional
//   - Continuous penalty berdasarkan divergence directional antar TF
//
// Output: [finalScore (0-100), direction (1/-1/0), debugString, tf1Dir, tf2Dir, tf3Dir, penalty]
f_mtfAlignmentScore() =>
    if not enableMTF
        [na, 0, "", na, na, na, na]

    // --- Directional components (Trend + Momentum) ---
    tf1Dir = mtfTf1Trend * 0.55 + mtfTf1Mom * 0.45
    tf2Dir = mtfTf2Trend * 0.55 + mtfTf2Mom * 0.45
    tf3Dir = mtfTf3Trend * 0.55 + mtfTf3Mom * 0.45

    // --- Context components (Volatility + Volume) ---
    tf1Ctx = mtfTf1Vol * 0.55 + mtfTf1Volm * 0.45
    tf2Ctx = mtfTf2Vol * 0.55 + mtfTf2Volm * 0.45
    tf3Ctx = mtfTf3Vol * 0.55 + mtfTf3Volm * 0.45

    // Guard: jika data tidak tersedia, skip
    if na(tf1Dir) or na(tf2Dir)
        [na, 0, "", na, na, na, na]

    // --- Weighted combination ---
    totalW = mtfPrimaryWeight + mtfSecondaryWeight + mtfTertiaryWeight
    normW1 = totalW > 0 ? mtfPrimaryWeight / totalW : 0.50
    normW2 = totalW > 0 ? mtfSecondaryWeight / totalW : 0.35
    normW3 = totalW > 0 ? mtfTertiaryWeight / totalW : 0.15

    mtfDirRaw = tf1Dir * normW1 + tf2Dir * normW2 + tf3Dir * normW3
    mtfCtxRaw = tf1Ctx * normW1 + tf2Ctx * normW2 + tf3Ctx * normW3

    // --- Fuzzy threshold untuk direction detection (symmetric bull/bear) ---
    f_bullConf(float score) => math.max(math.min((score - 50) / 20, 1), 0)
    f_bearConf(float score) => math.max(math.min((50 - score) / 20, 1), 0)

    tf1Bull = f_bullConf(tf1Dir)
    tf2Bull = f_bullConf(tf2Dir)
    tf3Bull = f_bullConf(tf3Dir)
    tf1Bear = f_bearConf(tf1Dir)
    tf2Bear = f_bearConf(tf2Dir)
    tf3Bear = f_bearConf(tf3Dir)

    // Weighted majority vote
    weightedBull = tf1Bull * normW1 + tf2Bull * normW2 + tf3Bull * normW3
    weightedBear = tf1Bear * normW1 + tf2Bear * normW2 + tf3Bear * normW3

    hysteresis = mtfDirectionHysteresis / 100
    direction = if weightedBull > weightedBear + hysteresis
        1
    else if weightedBear > weightedBull + hysteresis
        -1
    else
        0

    // --- Continuous penalty (directional divergence only) ---
    maxDiv = math.max(math.abs(tf1Dir - tf2Dir), math.abs(tf1Dir - tf3Dir), math.abs(tf2Dir - tf3Dir))
    penalty = (maxDiv / 100) * mtfMaxPenalty

    conflictScore = mtfDirRaw * (1 - penalty)
    finalScore = conflictScore * mtfAlpha + mtfCtxRaw * (1 - mtfAlpha)

    // --- Debug string (untuk mtfDebug mode) ---
    debugStr = if mtfDebug
        "MTF DEBUG\n" +
        "  Primary (" + mtfPrimaryTF + "): T" + str.tostring(math.round(mtfTf1Trend)) +
        " M" + str.tostring(math.round(mtfTf1Mom)) +
        " V" + str.tostring(math.round(mtfTf1Vol)) +
        " Vo" + str.tostring(math.round(mtfTf1Volm)) + "\n" +
        "  Secondary (" + mtfSecondaryTF + "): T" + str.tostring(math.round(mtfTf2Trend)) +
        " M" + str.tostring(math.round(mtfTf2Mom)) +
        " V" + str.tostring(math.round(mtfTf2Vol)) +
        " Vo" + str.tostring(math.round(mtfTf2Volm)) + "\n" +
        "  Tertiary (" + mtfTertiaryTF + "): T" + str.tostring(math.round(mtfTf3Trend)) +
        " M" + str.tostring(math.round(mtfTf3Mom)) +
        " V" + str.tostring(math.round(mtfTf3Vol)) +
        " Vo" + str.tostring(math.round(mtfTf3Volm)) + "\n" +
        "  Dir: " + (direction == 1 ? "Bullish" : direction == -1 ? "Bearish" : "Mixed") +
        " | Pen: " + str.tostring(math.round(penalty * 100)) + "%" +
        " | DirRaw: " + str.tostring(math.round(mtfDirRaw)) +
        " | CtxRaw: " + str.tostring(math.round(mtfCtxRaw)) +
        " | Final: " + str.tostring(math.round(finalScore))
    else
        ""

    [finalScore, direction, debugStr, tf1Dir, tf2Dir, tf3Dir, penalty]

[mtfScore, mtfDirection, mtfDebugStr, mtfTf1Dir, mtfTf2Dir, mtfTf3Dir, mtfPenalty] = f_mtfAlignmentScore()

// ==========================================================
// 7. CALCULATE ALL SCORES
// ==========================================================
trendScore = f_trendScore()
momentumScore = f_momentumScore()
volatilityScore = f_volatilityScore()
volumeScore = f_volumeScore()
smartMoneyScore = f_smartMoneyScore()

// Normalize weights
totalWeight = weightTrend + weightMomentum + weightVolatility + weightVolume
normTrend = totalWeight > 0 ? weightTrend / totalWeight * 100 : 25
normMomentum = totalWeight > 0 ? weightMomentum / totalWeight * 100 : 25
normVolatility = totalWeight > 0 ? weightVolatility / totalWeight * 100 : 25
normVolume = totalWeight > 0 ? weightVolume / totalWeight * 100 : 25

float overallScore = na
if not na(trendScore) or not na(momentumScore) or not na(volatilityScore) or not na(volumeScore)
    sum = 0.0
    wSum = 0.0
    if not na(trendScore)
        sum += trendScore * normTrend
        wSum += normTrend
    if not na(momentumScore)
        sum += momentumScore * normMomentum
        wSum += normMomentum
    if not na(volatilityScore)
        sum += volatilityScore * normVolatility
        wSum += normVolatility
    if not na(volumeScore)
        sum += volumeScore * normVolume
        wSum += normVolume
    if not na(smartMoneyScore) and enableSmartMoney
        sum += smartMoneyScore * weightSmartMoney
        wSum += weightSmartMoney
    overallScore := wSum > 0 ? sum / wSum : na

// ==========================================================
// 7. SIGNAL GENERATION
// ==========================================================
signalText = f_signalText(overallScore, bahasaInput)
signalColor = f_colorScore(overallScore)

// ==========================================================
// 8. CONFLUENCE COUNTERS (berapa sub-indikator setuju per dimensi)
// ==========================================================
// Trend
trendBull = 0
trendTotal = 0
if not na(emaPosition)
    trendBull += emaPosition >= 1 ? 1 : 0
    trendTotal += 1
if not na(superTrendDir)
    trendBull += superTrendDir == 1 ? 1 : 0
    trendTotal += 1
if not na(adxValue)
    trendBull += (adxStrong and adxTrend == 1) ? 1 : 0
    trendTotal += 1
if not na(adxValue)
    trendBull += trendAligned ? 1 : 0
    trendTotal += 1

// Momentum
momBull = 0
momTotal = 0
if not na(rsiValue)
    momBull += rsiValue > 50 ? 1 : 0
    momTotal += 1
if not na(macdLine)
    momBull += macdBullish ? 1 : 0
    momTotal += 1
if not na(stochKVal)
    momBull += stochBullish ? 1 : 0
    momTotal += 1
if not na(rsiDirection)
    momBull += rsiDirection == 1 ? 1 : 0
    momTotal += 1
if not na(macdHist)
    momBull += macdHistRising ? 1 : 0
    momTotal += 1

// Volatility ("ideal" = trending/tenang, bukan ranging/liar)
volBull = 0
volTotal = 0
if not na(bbWidth) and not na(bbWidthAvg)
    volBull += (bbWidth / bbWidthAvg >= 0.8 and bbWidth / bbWidthAvg <= 1.2) ? 1 : 0
    volTotal += 1
if not na(chopValue)
    volBull += chopValue <= 38 ? 1 : 0
    volTotal += 1
if not na(atrPercent)
    volBull += (atrPercent >= 1.0 and atrPercent <= 3.0) ? 1 : 0
    volTotal += 1
if not na(bbPosition)
    volBull += (bbPosition >= 30 and bbPosition <= 70) ? 1 : 0
    volTotal += 1

// Volume
volmBull = 0
volmTotal = 0
if not na(volRatio)
    volmBull += volRatio >= 0.8 ? 1 : 0
    volmTotal += 1
if not na(obvValue)
    volmBull += obvAgree ? 1 : 0
    volmTotal += 1
if not na(netVolSmooth)
    volmBull += netVolSmooth > 0 ? 1 : 0
    volmTotal += 1
if not na(volRatio)
    volmBull += volSpike ? 1 : 0
    volmTotal += 1

// ==========================================================
// 9. TOOLTIP STRINGS (detail per dimensi untuk hover)
// ==========================================================
// Trend tooltip
trendDetail = "—"
if not na(emaPosition) and not na(trendScore)
    emaLabel = emaPosition >= 1 ? "Price > EMA50 ✓" : emaPosition >= 0 ? "Price > EMA20" : "Price < EMAs ✗"
    stLabel = not na(superTrendDir) ? (superTrendDir == 1 ? "SuperTrend Up ✓" : "SuperTrend Down ✗") : ""
    adxLabel = not na(adxValue) ? (adxStrong ? (adxTrend == 1 ? "ADX " + str.tostring(math.round(adxValue)) + " (strong) ✓" : "ADX " + str.tostring(math.round(adxValue)) + " (strong, down)") : "ADX " + str.tostring(math.round(adxValue)) + " (weak)") : ""
    alignLabel = not na(adxValue) ? (trendAligned ? "Aligned ✓" : "Mixed") : ""
    trendDetail := emaLabel + " | " + stLabel + " | " + adxLabel + " | " + alignLabel

// Momentum tooltip
momDetail = "—"
if not na(rsiValue) and not na(momentumScore)
    rsiLbl = rsiValue >= rsiOb ? "RSI " + str.tostring(math.round(rsiValue)) + " Overbought" : rsiValue <= rsiOs ? "RSI " + str.tostring(math.round(rsiValue)) + " Oversold" : "RSI " + str.tostring(math.round(rsiValue)) + " Netral"
    macdLbl = macdBullish ? (macdHistRising ? "MACD Bullish + hist ↑" : "MACD Bullish + hist ↓") : (macdHistRising ? "MACD Bearish + hist ↑" : "MACD Bearish + hist ↓")
    stochLbl = not na(stochKVal) ? (stochKVal >= stochOb ? "Stoch Overbought" : stochKVal <= stochOs ? "Stoch Oversold" : "Stoch " + str.tostring(math.round(stochKVal)) + " Netral") : ""
    momDetail := rsiLbl + " | " + macdLbl + " | " + stochLbl

// Volatility tooltip
volDetail = "—"
if not na(bbWidth) and not na(atrPercent) and not na(volatilityScore)
    bbRatio = bbWidth / bbWidthAvg
    bbLbl = bbRatio >= 0.8 and bbRatio <= 1.2 ? "BB Moderate ✓" : bbRatio < 0.8 ? "BB Squeeze" : "BB Expand"
    chopLbl = not na(chopValue) ? (chopValue <= 38 ? "Chop " + str.tostring(math.round(chopValue)) + " Trending ✓" : chopValue >= 62 ? "Chop " + str.tostring(math.round(chopValue)) + " Ranging" : "Chop " + str.tostring(math.round(chopValue)) + " Netral") : ""
    atrLbl = "ATR " + str.tostring(math.round(atrPercent * 10) / 10) + "%"
    bbPosLbl = not na(bbPosition) ? (bbPosition >= 30 and bbPosition <= 70 ? "BB Pos " + str.tostring(math.round(bbPosition)) + " Mid ✓" : "BB Pos " + str.tostring(math.round(bbPosition)) + " Edge") : ""
    volDetail := bbLbl + " | " + chopLbl + " | " + atrLbl + " | " + bbPosLbl

// Volume tooltip
volmDetail = "—"
if not na(volRatio) and not na(volumeScore)
    vrLbl = volRatio >= volSpikeMult ? "Vol " + str.tostring(math.round(volRatio * 10) / 10) + "x (spike!)" : volRatio >= 0.8 ? "Vol " + str.tostring(math.round(volRatio * 10) / 10) + "x (healthy) ✓" : "Vol " + str.tostring(math.round(volRatio * 10) / 10) + "x (low)"
    obvLbl = not na(obvValue) ? (obvAgree ? "OBV Agree ✓" : "OBV Diverge ✗") : ""
    netLbl = not na(netVolSmooth) ? (netVolSmooth > 0 ? "Net Vol + Bullish" : "Net Vol - Bearish") : ""
    spikeLbl = volSpike ? "Spike ✓" : "Spike ✗"
    volmDetail := vrLbl + " | " + obvLbl + " | " + netLbl + " | " + spikeLbl

// ==========================================================
// 10. MULTI-TIMEFRAME CONFLICT DETECTION
// ==========================================================
// Legacy conflict (simple binary, for backward compat)
mtfConflict = enableMtfFilter and not na(mtfTrendScore) and not na(overallScore) and (
    (mtfTrendScore >= 50 and overallScore < 40) or
    (mtfTrendScore < 50 and overallScore >= 70)
)

mtfWarning = if enableMtfFilter and not na(mtfTrendScore)
    if mtfTrendScore >= 50
        (bahasaInput == "Indonesia" ? "MTF ▲ " + str.tostring(math.round(mtfTrendScore)) : "MTF ▲ " + str.tostring(math.round(mtfTrendScore)))
    else
        (bahasaInput == "Indonesia" ? "MTF ▼ " + str.tostring(math.round(mtfTrendScore)) : "MTF ▼ " + str.tostring(math.round(mtfTrendScore)))
else
    ""

// New MTF alignment summary (for UI — Sprint C) — disiapkan sekarang
mtfAlignmentDirection = if not na(mtfScore)
    if mtfDirection == 1
        "Bullish"
    else if mtfDirection == -1
        "Bearish"
    else
        "Mixed"
else
    ""

// ==========================================================
// 11. NARRATIVE (1 baris insight untuk trader)
// ==========================================================
f_narrative() =>
    lang = bahasaInput

    if not na(momentumScore) and not na(rsiValue) and not na(trendScore) and not na(overallScore)
        // 1. Momentum kuat tapi RSI overbought — prioritas tertinggi (konflik)
        if momentumScore >= 65 and rsiValue >= rsiOb
            lang == "Indonesia" ? "⚠️  Momentum kuat tapi RSI overbought — waspada koreksi" : "⚠️  Strong momentum but RSI overbought — watch for pullback"

        // 2. RSI oversold + volume spike = potensi bounce
        else if rsiValue <= rsiOs and not na(volRatio) and volSpike
            lang == "Indonesia" ? "📈 RSI oversold + volume spike — potensi bounce" : "📈 RSI oversold + volume spike — potential bounce"

        // 3. Tren lemah & volatil
        else if not na(volatilityScore) and trendScore < 40 and volatilityScore < 40
            lang == "Indonesia" ? "⚠️  Tren lemah & volatil — hindari entry baru" : "⚠️  Weak trend & volatile — avoid new entries"

        // 4. Volume konfirmasi tren
        else if trendScore >= 65 and not na(volmBull) and not na(volmTotal) and volmBull >= 3 and volmTotal >= 3
            lang == "Indonesia" ? "✅ Volume konfirmasi tren — bias searah" : "✅ Volume confirms trend — trade with trend"

        // 5. Semua align bullish
        else if overallScore >= 70 and trendScore >= 65 and not na(volmBull) and volmBull >= 3
            lang == "Indonesia" ? "✅ Semua dimensi align — bias bullish" : "✅ All dimensions aligned — bullish bias"

        // 6. Semua align bearish
        else if overallScore <= 30 and not na(trendScore) and trendScore <= 35 and not na(volmBull) and volmBull <= 1
            lang == "Indonesia" ? "✅ Semua dimensi align — bias bearish" : "✅ All dimensions aligned — bearish bias"

        // 7. OBV divergen
        else if not na(obvValue) and close > close[10] and not obvAgree
            lang == "Indonesia" ? "⚠️  Harga naik tapi OBV divergen — konfirmasi dulu" : "⚠️  Price up but OBV diverging — confirm first"

        // 8. MTF conflict
        else if enableMtfFilter and not na(mtfTrendScore) and mtfConflict and not na(overallScore)
            isBullishDaily = overallScore >= 70
            if lang == "Indonesia"
                if isBullishDaily
                    "⚠️  Tren " + mtfTimeframe + " bearish — sinyal harian perlu konfirmasi"
                else
                    "⚠️  Tren " + mtfTimeframe + " bullish — jangan short saat tren besar naik"
            else
                if isBullishDaily
                    "⚠️  " + mtfTimeframe + " trend bearish — confirm daily signal"
                else
                    "⚠️  " + mtfTimeframe + " trend bullish — don't short against higher TF"

        // 9. Ranging market
        else if not na(chopValue) and chopRanging
            lang == "Indonesia" ? "⏳ Pasar ranging — tunggu breakout" : "⏳ Ranging market — wait for breakout"

        // 10. Fallback: mixed
        else
            lang == "Indonesia" ? "💡 Kondisi campuran — cek detail per dimensi" : "💡 Mixed conditions — check per-dimension details"
    else
        lang == "Indonesia" ? "⏳ Menunggu data..." : "⏳ Waiting for data..."

narrative = f_narrative()

// ==========================================================
// 12. ENTRY TRIGGER ENGINE (3 modes)
// ==========================================================
// Zone tracking (untuk Composite Score mode)
currentZone = na(overallScore) ? -1 : (overallScore >= 70 ? 2 : overallScore >= 40 ? 1 : 0)
var int prevZone = -1
if prevZone < 0 and currentZone >= 0
    prevZone := currentZone
var int lastSignalBar = -1
var float signalEntryPrice = na
var int signalEntryBar = na
var int signalDirection = 0  // 1 = long, -1 = short
var string signalLabel = ""

zoneChanged = prevZone >= 0 and currentZone >= 0 and currentZone != prevZone
minBarsOk = lastSignalBar < 0 or bar_index - lastSignalBar >= minSignalBars
chopFilterOk = signalFilterMode == "Disabled" or (not na(chopValue) and (
    signalFilterMode == "Only Trending (Chop<38)" ? chopValue < 38 :
    signalFilterMode == "Only Ranging (Chop>62)" ? chopValue >= 62 : true
))

// Mode A: Composite Score (zone-based)
signalTriggered = zoneChanged and minBarsOk and chopFilterOk

// Mode B: Pullback (trend pullback ke EMA20 + RSI rebound)
pullbackTrigger = entryTriggerMode == "Pullback" and not na(trendScore) and trendScore >= 65 and not na(emaFast) and not na(atrValue) and math.abs(close - emaFast) <= atrValue * 1.5 and not na(rsiValue) and rsiValue <= 50 and rsiDirection == 1 and minBarsOk and chopFilterOk

// Mode C: Breakout (price break recent high/low + volume confirmation)
recentHigh = ta.highest(high, 20)
recentLow = ta.lowest(low, 20)
breakoutTrigger = entryTriggerMode == "Breakout" and not na(volumeScore) and volumeScore >= 60 and (close >= recentHigh or close <= recentLow) and minBarsOk and chopFilterOk

// Unified entry trigger
entryTriggered = if entryTriggerMode == "Composite Score"
    signalTriggered
else if entryTriggerMode == "Pullback"
    pullbackTrigger
else
    breakoutTrigger

// Signal direction & label
if entryTriggered
    signalDir = if entryTriggerMode == "Composite Score"
        (currentZone > prevZone ? 1 : -1)
    else if entryTriggerMode == "Pullback"
        1
    else
        (close >= recentHigh ? 1 : -1)

    signalLbl = if entryTriggerMode == "Composite Score"
        if signalDir > 0
            if prevZone == 0
                (bahasaInput == "Indonesia" ? "Bearish→Netral (Bullish)" : "Bearish→Neutral (Bullish)")
            else
                (bahasaInput == "Indonesia" ? "Netral→Bullish" : "Neutral→Bullish")
        else
            if prevZone == 2
                (bahasaInput == "Indonesia" ? "Bullish→Netral (Bearish)" : "Bullish→Neutral (Bearish)")
            else
                (bahasaInput == "Indonesia" ? "Netral→Bearish" : "Neutral→Bearish")
    else if entryTriggerMode == "Pullback"
        (bahasaInput == "Indonesia" ? "Pullback Entry" : "Pullback Entry")
    else
        if signalDir > 0
            (bahasaInput == "Indonesia" ? "Breakout Long" : "Breakout Long")
        else
            (bahasaInput == "Indonesia" ? "Breakout Short" : "Breakout Short")

    signalEntryPrice := close
    signalEntryBar := bar_index
    signalDirection := signalDir
    signalLabel := signalLbl
    lastSignalBar := bar_index

if zoneChanged
    prevZone := currentZone

// Forward return
fwdReturn = na(signalEntryPrice) or signalEntryPrice == 0 ? na : (close / signalEntryPrice - 1) * signalDirection * 100

signalStatus = na(fwdReturn) ? "" : fwdReturn >= 5 ? "✅ +" + str.tostring(math.round(fwdReturn)) + "%" : fwdReturn >= 0 ? "➖ +" + str.tostring(math.round(fwdReturn)) + "%" : fwdReturn >= -5 ? "➖ " + str.tostring(math.round(fwdReturn)) + "%" : "❌ " + str.tostring(math.round(fwdReturn)) + "%"

fwdBars = na(signalEntryBar) ? na : bar_index - signalEntryBar

// ==========================================================
// 13. HISTORI SINYAL (circular buffer, evaluasi setiap 10 bar)
// ==========================================================
LOOKBACK_BARS = 10
BT_SIZE = 20

var int[] btEntryBars = array.new_int(BT_SIZE, -1)
var float[] btEntryPrices = array.new_float(BT_SIZE, na)
var int[] btDirs = array.new_int(BT_SIZE, 0)
var int btHead = 0
var int btTotalEval = 0
var int btWins = 0
var float btTotalReturn = 0.0

// Record new signal
if entryTriggered
    array.set(btEntryBars, btHead, bar_index)
    array.set(btEntryPrices, btHead, close)
    array.set(btDirs, btHead, signalDirection)
    btHead := (btHead + 1) % BT_SIZE

// Evaluate resolved signals
btNewWins = 0
btNewLosses = 0
for i = 0 to BT_SIZE - 1
    eb = array.get(btEntryBars, i)
    if eb >= 0 and bar_index - eb >= LOOKBACK_BARS
        ep = array.get(btEntryPrices, i)
        ed = array.get(btDirs, i)
        ret = (close / ep - 1) * ed * 100
        btTotalReturn += ret
        if ret > 0
            btNewWins += 1
        else
            btNewLosses += 1
        array.set(btEntryBars, i, -1)

btTotalEval += btNewWins + btNewLosses
btWins += btNewWins

btWinRate = btTotalEval > 0 ? btWins / btTotalEval * 100 : na

// ==========================================================
// 14. LEVEL REFERENSI RISIKO (SL & Target)
// ==========================================================
var float riskEntryPrice = na
var float riskAtrCapture = na
var float riskSwingLow = na
var float riskSwingHigh = na
var int riskDirection = 0

// ta.lowest/ta.highest must be called unconditionally on every bar (Pine v6 requirement)
riskSwingLowUncond = ta.lowest(low, slSwingPeriod)
riskSwingHighUncond = ta.highest(high, slSwingPeriod)

if entryTriggered
    riskEntryPrice := signalEntryPrice
    riskAtrCapture := atrValue
    riskSwingLow := riskSwingLowUncond
    riskSwingHigh := riskSwingHighUncond
    riskDirection := signalDirection

hasRisk = riskDirection != 0 and not na(riskEntryPrice)

// ATR-based SL
atrSl = hasRisk ? riskEntryPrice - riskAtrCapture * slAtrMult * riskDirection : na

// Swing-based SL
swingSl = hasRisk ? (riskDirection == 1 ? riskSwingLow : riskSwingHigh) : na

// Final SL price
float slPrice = na
if hasRisk
    if slTypeInput == "ATR"
        slPrice := atrSl
    else if slTypeInput == "Swing Low/High"
        slPrice := swingSl
    else
        slPrice := riskDirection == 1 ? math.min(atrSl, swingSl) : math.max(atrSl, swingSl)

// Target (R-multiple: entry + Risk * targetR)
riskAmount = hasRisk ? math.abs(riskEntryPrice - slPrice) : na
targetPrice = hasRisk ? riskEntryPrice + riskAmount * targetR * riskDirection : na

// Format for display
slPct = hasRisk and riskEntryPrice != 0 ? riskAmount / riskEntryPrice * 100 : na
targetPct = hasRisk and riskEntryPrice != 0 ? math.abs(targetPrice - riskEntryPrice) / riskEntryPrice * 100 : na

riskBaseLabel = if hasRisk
    "SL: " + str.tostring(math.round(slPrice)) + " (" + (riskDirection == 1 ? "-" : "+") + str.tostring(math.round(slPct)) + "%)  |  Tgt: " + str.tostring(math.round(targetPrice)) + " (" + (riskDirection == 1 ? "+" : "-") + str.tostring(math.round(targetPct)) + "%)  [" + str.tostring(targetR) + "R]"
else
    "—"

// ==========================================================
// 15. POSITION SIZING
// ==========================================================
riskRp = showPositionSizing ? accountBalanceInput * riskPctInput / 100 : na
suggestedLots = showPositionSizing and not na(riskAmount) and riskAmount > 0 ? math.floor((riskRp / riskAmount) / 100) : na
riskRpK = not na(riskRp) and not na(suggestedLots) ? math.round(riskRp / 1000) : na
riskRpDisplay = not na(riskRpK) ? (riskRpK >= 1000 ? "Rp" + str.tostring(math.round(riskRpK / 1000)) + "jt" : "Rp" + str.tostring(riskRpK) + "rb") : ""
lotLabel = bahasaInput == "Indonesia" ? "Lot" : "Lots"

suggestedLotStr = if hasRisk and showPositionSizing and not na(suggestedLots)
    "  |  " + lotLabel + ": " + str.tostring(math.max(1, suggestedLots)) + (suggestedLots < 1 ? " (min. 1 lot)" : "") + "  |  " + riskRpDisplay + " (" + str.tostring(riskPctInput) + "%)"
else
    ""

riskLevelLabel = riskBaseLabel + suggestedLotStr

// @version=6
// 04-ui.pine — Papan Gerak: Table & Signal Rendering
// ==========================================================

// ==========================================================
// 1. SIGNAL LINE (Overall Score as Oscillator)
// ==========================================================
hline(70, "OB Line", color = color.rgb(239, 83, 80, 60), linestyle = hline.style_dashed)
hline(40, "OS Line", color = color.rgb(38, 166, 154, 60), linestyle = hline.style_dashed)
hline(50, "Midline", color = color.rgb(100, 100, 100, 40), linestyle = hline.style_dotted)

plot(overallScore, "Overall Score", signalColor, 2, plot.style_line)
fill(plot(70), plot(40), color = color.rgb(100, 100, 100, 10))

oscBg = na(overallScore) ? color.new(color.gray, 95) : overallScore >= 70 ? color.new(bullColor, 88) : overallScore >= 40 ? color.new(neutralColor, 88) : color.new(bearColor, 88)
bgcolor(oscBg, title = "Score Zone")

// ==========================================================
// 1b. MTF PLOTS & DEBUG
// ==========================================================
plot(mtfScore, "MTF Score", color.new(color.blue, 60), 1, plot.style_line, display = display.data_window)
plot(mtfDirection * 50 + 50, "MTF Direction", color.new(color.gray, 70), 1, plot.style_cross, display = display.data_window)

if mtfDebug and barstate.islast and not na(mtfScore)
    label.new(bar_index, 3, mtfDebugStr, style = label.style_label_center,
     size = size.small, text_size = size.small, text_color = color.white,
     color = color.new(color.blue, 60))

// ==========================================================
// 2. BUILD TABLE
// ==========================================================
f_getPosition() =>
    if tablePositionInput == "Top Left"
        position.top_left
    else
        if tablePositionInput == "Bottom Left"
            position.bottom_left
        else
            if tablePositionInput == "Bottom Right"
                position.bottom_right
            else
                position.top_right

f_getFontSize() =>
    if fontSizeInput == 1
        size.small
    else
        if fontSizeInput == 2
            size.normal
        else
            size.large

f_confStr(n, total) =>
    str.tostring(n) + "/" + str.tostring(total)

tablePos = f_getPosition()
fontSz = f_getFontSize()

// Render on last bar only
if barstate.islast
    // --- Compact Mode ---
    if modeInput == "Compact"
        tblCompact = table.new(tablePos, 2, 1, bgcolor = tableBgColor, border_width = 1, border_color = signalColor)
        overallDisplay = str.tostring(math.round(overallScore))
        mtfCompact = not na(mtfScore) ? " | MTF: " + str.tostring(math.round(mtfScore)) + (mtfDirection == 1 ? "▲" : mtfDirection == -1 ? "▼" : "—") : ""
        table.cell(tblCompact, 0, 0, "PAPAN GERAK  " + signalText + "  " + overallDisplay + mtfCompact,
         text_color = signalColor, text_size = fontSz, text_halign = text.align_center, width = 10)

    else
        // --- Standard & Detailed Mode ---
        isDetailed = modeInput == "Detailed"
        rowCount = 12  // +1 untuk MTF Alignment row
        if enableSmartMoney
            rowCount := rowCount + 1
        if isDetailed
            rowCount := rowCount + 5  // +1 sub-row per dimensi (+1 untuk MTF detail)
        tblFull = table.new(tablePos, 2, rowCount, bgcolor = tableBgColor, border_width = 1, border_color = signalColor,
         frame_color = signalColor, frame_width = 1)

        // Narrative row (full width - column 0 only)
        table.cell(tblFull, 0, 0, narrative, text_color = headerColor, text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)

        // Header
        table.cell(tblFull, 0, 1, "PAPAN GERAK (last bar)", text_color = headerColor, text_size = fontSz, text_halign = text.align_left, bgcolor = bgColor)
        overallDisplay = str.tostring(math.round(overallScore))
        overallTip = not na(overallScore) ? (overallScore >= 70 ? "Bullish: overall >70" : overallScore >= 40 ? "Neutral: 40-70" : "Bearish: <40") : ""
        table.cell(tblFull, 1, 1, overallDisplay, text_color = signalColor, text_size = fontSz, text_halign = text.align_right, bgcolor = bgColor, tooltip = overallTip)

        // Signal
        table.cell(tblFull, 0, 2, bahasaInput == "Indonesia" ? "Sinyal" : "Signal", text_color = labelColor, text_size = fontSz, text_halign = text.align_left, bgcolor = bgColor)
        table.cell(tblFull, 1, 2, signalText, text_color = signalColor, text_size = fontSz, text_halign = text.align_right, bgcolor = bgColor)

        // Forward Return (sejak sinyal terakhir)
        valLabel = bahasaInput == "Indonesia" ? "Return Sejak Sinyal" : "Forward Return"
        valDisplay = not na(signalStatus) and signalStatus != "" ? signalStatus : "—"
        valTooltip = not na(fwdBars) and not na(signalLabel) ? signalLabel + " | Bars: " + str.tostring(fwdBars) + " | Trigger: " + entryTriggerMode + " | MinBars: " + str.tostring(minSignalBars) : ""
        table.cell(tblFull, 0, 3, "  📊 " + valLabel, text_color = labelColor, text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)
        table.cell(tblFull, 1, 3, valDisplay, text_color = labelColor, text_size = size.small, text_halign = text.align_right, bgcolor = bgColor, tooltip = valTooltip)

        // Histori Sinyal row
        btLabel = bahasaInput == "Indonesia" ? "Histori Sinyal (kumulatif)" : "Signal History (cumulative)"
        btDisplay = not na(btWinRate) ? str.tostring(math.round(btWinRate)) + "% win (" + str.tostring(btWins) + "/" + str.tostring(btTotalEval) + ")" : "—"
        btTooltip = not na(btTotalEval) and btTotalEval > 0 ? "Total return: " + str.tostring(math.round(btTotalReturn * 10) / 10) + "% | Lookback: 10 bar | Filter: " + signalFilterMode : ""
        table.cell(tblFull, 0, 4, "  📊 " + btLabel, text_color = labelColor, text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)
        table.cell(tblFull, 1, 4, btDisplay, text_color = btWinRate >= 60 ? bullColor : btWinRate >= 40 ? neutralColor : bearColor, text_size = size.small, text_halign = text.align_right, bgcolor = bgColor, tooltip = btTooltip)

        // MTF Alignment
        mtfArrow = not na(mtfScore) ? (mtfScore >= 70 ? "↑" : mtfScore >= 40 ? "→" : "↓") : "—"
        mtfDirLabel = not na(mtfDirection) ? (mtfDirection == 1 ? " (Bullish)" : mtfDirection == -1 ? " (Bearish)" : " (Mixed)") : ""
        mtfDisplay = not na(mtfScore) ? str.tostring(math.round(mtfScore)) + mtfDirLabel : "—"
        mtfDetailTF = not na(mtfScore) ? str.format("{0}:{1}/{2}/{3}",
         mtfPrimaryTF, str.tostring(math.round(mtfTf1Dir)), str.tostring(math.round(mtfTf2Dir)), str.tostring(math.round(mtfTf3Dir))) : ""
        table.cell(tblFull, 0, 5, "  " + mtfArrow + " " + (bahasaInput == "Indonesia" ? "MTF Alignment" : "MTF Alignment"), text_color = color.new(color.blue, 70), text_size = fontSz, text_halign = text.align_left, bgcolor = bgColor)
        table.cell(tblFull, 1, 5, mtfDisplay, text_color = f_colorScore(mtfScore), text_size = fontSz, text_halign = text.align_right, bgcolor = bgColor, tooltip = mtfDebug ? mtfDebugStr : mtfDetailTF)
        if isDetailed
            table.cell(tblFull, 0, 6, "", text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)
            table.cell(tblFull, 1, 6, mtfDebugStr, text_color = labelColor, text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)

        // Trend (geser 1 row karena MTF row)
        trendDisplay = str.tostring(math.round(trendScore))
        trendArrow = not na(trendScore) ? (trendScore >= 70 ? "↑" : trendScore >= 40 ? "→" : "↓") : "—"
        trendConf = not na(trendBull) ? " (" + f_confStr(trendBull, trendTotal) + ")" : ""
        trendMtf = enableMtfFilter and not na(mtfTrendScore) ? "  " + mtfWarning : ""
        table.cell(tblFull, 0, 7, "  " + trendArrow + " " + (bahasaInput == "Indonesia" ? "Tren" : "Trend"), text_color = dimTrend, text_size = fontSz, text_halign = text.align_left, bgcolor = bgColor)
        table.cell(tblFull, 1, 7, trendDisplay + trendConf + trendMtf, text_color = f_colorScore(trendScore), text_size = fontSz, text_halign = text.align_right, bgcolor = bgColor, tooltip = trendDetail)
        if isDetailed
            table.cell(tblFull, 0, 8, "", text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)
            table.cell(tblFull, 1, 8, trendDetail, text_color = labelColor, text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)

        // Momentum (geser 1 row)
        momDisplay = str.tostring(math.round(momentumScore))
        momArrow = not na(momentumScore) ? (momentumScore >= 70 ? "↑" : momentumScore >= 40 ? "→" : "↓") : "—"
        momConf = not na(momBull) ? " (" + f_confStr(momBull, momTotal) + ")" : ""
        momBase = 8
        if isDetailed
            momBase := 9
        table.cell(tblFull, 0, momBase, "  " + momArrow + " " + (bahasaInput == "Indonesia" ? "Momentum" : "Momentum"), text_color = dimMomentum, text_size = fontSz, text_halign = text.align_left, bgcolor = bgColor)
        table.cell(tblFull, 1, momBase, momDisplay + momConf, text_color = f_colorScore(momentumScore), text_size = fontSz, text_halign = text.align_right, bgcolor = bgColor, tooltip = momDetail)
        if isDetailed
            table.cell(tblFull, 0, momBase + 1, "", text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)
            table.cell(tblFull, 1, momBase + 1, momDetail, text_color = labelColor, text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)

        // Volatility
        volDisplay = str.tostring(math.round(volatilityScore))
        volArrow = not na(volatilityScore) ? (volatilityScore >= 70 ? "≈" : volatilityScore >= 40 ? "→" : "⚡") : "—"
        volConf = not na(volBull) ? " (" + f_confStr(volBull, volTotal) + ")" : ""
        volBase = momBase + (isDetailed ? 2 : 1)
        table.cell(tblFull, 0, volBase, "  " + volArrow + " " + (bahasaInput == "Indonesia" ? "Volatilitas" : "Volatility"), text_color = dimVolatility, text_size = fontSz, text_halign = text.align_left, bgcolor = bgColor)
        table.cell(tblFull, 1, volBase, volDisplay + volConf, text_color = f_colorScore(volatilityScore), text_size = fontSz, text_halign = text.align_right, bgcolor = bgColor, tooltip = volDetail)
        if isDetailed
            table.cell(tblFull, 0, volBase + 1, "", text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)
            table.cell(tblFull, 1, volBase + 1, volDetail, text_color = labelColor, text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)

        // Volume
        volmDisplay = str.tostring(math.round(volumeScore))
        volmArrow = not na(volumeScore) ? (volumeScore >= 70 ? "↑" : volumeScore >= 40 ? "→" : "↓") : "—"
        volmConf = not na(volmBull) ? " (" + f_confStr(volmBull, volmTotal) + ")" : ""
        volmBase = volBase + (isDetailed ? 2 : 1)
        table.cell(tblFull, 0, volmBase, "  " + volmArrow + " " + (bahasaInput == "Indonesia" ? "Volume" : "Volume"), text_color = dimVolume, text_size = fontSz, text_halign = text.align_left, bgcolor = bgColor)
        table.cell(tblFull, 1, volmBase, volmDisplay + volmConf, text_color = f_colorScore(volumeScore), text_size = fontSz, text_halign = text.align_right, bgcolor = bgColor, tooltip = volmDetail)
        if isDetailed
            table.cell(tblFull, 0, volmBase + 1, "", text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)
            table.cell(tblFull, 1, volmBase + 1, volmDetail, text_color = labelColor, text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)

        // Smart Money (if enabled)
        if enableSmartMoney
            smBase = volmBase + (isDetailed ? 2 : 1)
            smDisplay = str.tostring(math.round(smartMoneyScore))
            smTooltip = not na(smartMoneyScore) ? (smDetected ? "SM flow: detected" : "SM flow: none") : ""
            table.cell(tblFull, 0, smBase, "  ◆ " + (bahasaInput == "Indonesia" ? "Smart Money" : "Smart Money"), text_color = color.rgb(255, 64, 129), text_size = fontSz, text_halign = text.align_left, bgcolor = bgColor)
            table.cell(tblFull, 1, smBase, smDisplay, text_color = f_colorScore(smartMoneyScore), text_size = fontSz, text_halign = text.align_right, bgcolor = bgColor, tooltip = smTooltip)

        // Risk Levels (SL & Target)
        riskBase = volmBase + (isDetailed ? 2 : 1)
        if enableSmartMoney
            riskBase := riskBase + 1
        if showRiskLevels
            riskTooltip = bahasaInput == "Indonesia" ? "Level referensi — bukan rekomendasi. Sesuaikan dgn toleransi risiko Anda." : "Reference levels — not a recommendation. Adjust to your risk tolerance."
            table.cell(tblFull, 0, riskBase, "  ⚠️ " + (bahasaInput == "Indonesia" ? "Level Referensi" : "Risk Levels"), text_color = color.rgb(255, 64, 129), text_size = size.small, text_halign = text.align_left, bgcolor = bgColor, tooltip = riskTooltip)
            table.cell(tblFull, 1, riskBase, riskLevelLabel, text_color = labelColor, text_size = size.small, text_halign = text.align_right, bgcolor = bgColor, tooltip = riskTooltip)

        // Legend (last row)
        legendBase = showRiskLevels ? riskBase + 1 : riskBase
        legendText = bahasaInput == "Indonesia" ? "🔴 <40 Bearish | 🟡 40-70 Netral | 🟢 >70 Bullish" : "🔴 <40 Bearish | 🟡 40-70 Neutral | 🟢 >70 Bullish"
        table.cell(tblFull, 0, legendBase, legendText, text_color = labelColor, text_size = size.small, text_halign = text.align_left, bgcolor = bgColor)

// ==========================================================
// 3. ALERTS
// ==========================================================
f_webhookMsg(eventType, params) =>
    if webhookFormat == "JSON"
        str.format("PG|event={0}|time={1}|ticker={2}|{3}",
         eventType, str.tostring(time), syminfo.tickerid, params)
    else
        params

// Alert: Entry trigger (mode-aware) — guard: bar-close confirmation
if entryTriggered and (not confirmOnBarClose or barstate.isconfirmed)
    msg = if webhookFormat == "JSON"
        f_webhookMsg("entry", str.format("mode={0}|action={1}|price={2}|score={3}|signal={4}",
            entryTriggerMode, signalDirection > 0 ? "buy" : "sell", str.tostring(close),
            str.tostring(math.round(overallScore)), signalLabel))
    else
        str.format("PAPAN GERAK ({0}): {1} → {2}", entryTriggerMode, signalLabel, signalText)
    alert(msg)

// Alert: Trend Extreme
if (trendScore >= 80 or trendScore <= 20) and (not confirmOnBarClose or barstate.isconfirmed)
    msg = if webhookFormat == "JSON"
        f_webhookMsg("trend_extreme", "trendScore=" + str.tostring(math.round(trendScore)))
    else
        str.format("PAPAN GERAK: Trend ekstrem ({0})", str.tostring(math.round(trendScore)))
    alert(msg)

// Alert: Momentum Oversold
if rsiOsHit and (not confirmOnBarClose or barstate.isconfirmed)
    msg = if webhookFormat == "JSON"
        f_webhookMsg("rsi_oversold", "rsi=" + str.tostring(math.round(rsiValue)))
    else
        str.format("PAPAN GERAK: RSI oversold ({0})", str.tostring(math.round(rsiValue)))
    alert(msg)

// Alert: Momentum Overbought
if rsiObHit and (not confirmOnBarClose or barstate.isconfirmed)
    msg = if webhookFormat == "JSON"
        f_webhookMsg("rsi_overbought", "rsi=" + str.tostring(math.round(rsiValue)))
    else
        str.format("PAPAN GERAK: RSI overbought ({0})", str.tostring(math.round(rsiValue)))
    alert(msg)

// Alert: Volume Spike
if volSpike and (not confirmOnBarClose or barstate.isconfirmed)
    msg = if webhookFormat == "JSON"
        f_webhookMsg("volume_spike", "ratio=" + str.tostring(math.round(volRatio * 100)))
    else
        str.format("PAPAN GERAK: Volume spike ({0}% of MA)", str.tostring(math.round(volRatio * 100)))
    alert(msg)

// Alert: Smart Money Detected
if smDetected and (not confirmOnBarClose or barstate.isconfirmed)
    msg = if webhookFormat == "JSON"
        f_webhookMsg("smart_money", "detected=true")
    else
        "PAPAN GERAK: Institutional flow terdeteksi"
    alert(msg)

// Alert: Choppiness (ranging market)
if chopRanging and (not confirmOnBarClose or barstate.isconfirmed)
    msg = if webhookFormat == "JSON"
        f_webhookMsg("choppiness", "chop=" + str.tostring(math.round(chopValue)))
    else
        str.format("PAPAN GERAK: Pasar ranging (Choppiness: {0})", str.tostring(math.round(chopValue)))
    alert(msg)

Overview

Saham: Papan Gerak By. Akhmfz evaluates multiple technical indicators including exponential moving averages, SuperTrend, ADX, RSI, MACD, Stochastic, Bollinger Bands, and Choppiness Index to generate composite technical scores. The indicator renders its metrics in a customizable on-screen dashboard table with selectable visual themes, language options (Indonesian or English), and display modes. Traders can configure individual indicator lengths, overbought and oversold thresholds, and scoring weight distributions across dimensions directly from the input settings.

Details

Published by

akhmfz on GitHub

Type

Indicator, Pine Script v6

Draws

Separate pane below the chart

Category

Screeners & dashboards, Trend, Oscillators & momentum, Volatility & bands

Markets

Stocks

Timeframes

Any timeframe

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Table PositionstringTop RighttablePositionInput
Font Sizeint1fontSizeInput
ModestringStandardmodeInput
BahasastringIndonesiabahasaInput
Color ThemestringGelapcolorThemeInput
EMA Fast Periodint20emaFastPeriod
EMA Mid Periodint50emaMidPeriod
EMA Slow Periodint200emaSlowPeriod
SuperTrend Factorfloat3.0superTrendFactor
SuperTrend ATR Periodint10superTrendPeriod
ADX Lengthint14adxLength
ADX Thresholdint25adxThreshold
Show all 40 settings
SettingTypeDefaultVariable
RSI Lengthint14rsiLength
RSI Overboughtint70rsiOb
RSI Oversoldint30rsiOs
MACD Fastint12macdFast
MACD Slowint26macdSlow
MACD Signalint9macdSignal
Stochastic Kint14stochK
Stochastic Dint3stochD
Stoch Overboughtint80stochOb
Stoch Oversoldint20stochOs
ATR Lengthint14atrLength
Bollinger Lengthint20bbLength
Bollinger Multiplierfloat2.0bbMult
Choppiness Lengthint14chopLength
Volume MA Lengthint20volMaLength
Volume Spike Multiplierfloat2.0volSpikeMult
Trend Weight (%)int30weightTrend
Momentum Weight (%)int30weightMomentum
Volatility Weight (%)int20weightVolatility
Volume Weight (%)int20weightVolume
Enable Smart Money FlowboolfalseenableSmartMoney
SM Weight (%)int20weightSmartMoney
SM Volume Z-Thresholdfloat2.5smVolumeThreshold
SM Lookback Periodint20smPeriod
Min Bars Between Signalsint5minSignalBars
Signal FilterstringDisabledsignalFilterMode
Confirm on Bar ClosebooltrueconfirmOnBarClose
Show Risk LevelsbooltrueshowRiskLevels
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

The Adaptive Cycle Detector estimates the dominant market cycle from WaveTrend oscillator zero-crossing frequency and applies smoothing to recommend adaptive indicator lengths. Fast markets with frequent crossings receive shorter lengths while slow swing marke

Indicator v6 Oscillators & momentumVolatility & bands
Updated yesterday · MIT GitHub repository of Adaptive Cycle Detector [WavesUnchained]

Manias, Panics and Crashes

by MeridianAlgo
9 stars

Manias, Panics and Crashes is a separate-pane market regime indicator that quantifies market sentiment and systemic risk. It processes technical metrics including RSI, moving averages, and volume alongside optional intermarket data such as VIX volatility and c

Indicator v6 Oscillators & momentumVolatility & bands
Updated 4 months ago · MIT GitHub repository of Manias, Panics and Crashes

Elton's Screener

by eltonaguiar
4 stars

Elton's Screener is an indicator that runs in a separate pane and scores one of twenty selectable strategies, or auto-selects a route from the symbol type. It filters signals by a minimum confidence level and a higher-timeframe EMA trend alignment. ATR multipl

Indicator v6 Screeners & dashboardsOscillators & momentum Repaints
Updated today · None stated GitHub repository of Elton's Screener

DEPV2 C6 Gölge Kasa

by YuceSezar89
1 stars

This indicator evaluates signal performance and multi-timeframe market dynamics through a shadow bankroll tracking framework. It measures market conditions across trend, momentum ROC Z-scores, ATR volatility expansion, and volume flow, alongside multi-timefram

Indicator v6 TrendOscillators & momentum
Updated 11 days ago · None stated GitHub repository of DEPV2 C6 Gölge Kasa

Market Regime Dashboard

by vantradelabs
2 stars

The Market Regime Dashboard is a multi-timeframe dashboard indicator that evaluates market conditions across three user-defined timeframes. It measures four core dimensions: trend strength using ADX and DI direction, momentum using RSI z-scores, volatility via

Indicator v6 Screeners & dashboardsTrend
Updated 2 months ago · Custom licence GitHub repository of Market Regime Dashboard

This indicator evaluates market conditions across four user-defined timeframes by combining RSI, MACD crossovers, triple EMA trend filters, Bollinger Band boundaries, and volume spikes. It computes aggregate bullish and bearish scores, plotting buy and sell si

Indicator v5 Screeners & dashboardsOscillators & momentum
Updated 8 months ago · None stated GitHub repository of Multi-Timeframe Trade Signal Indicator

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers