//@version=6
// ════════════════════════════════════════════════════════════════════════════════
//  Smart Zone Retest — Rejection & CHoCH Confirmation  –  v2
//  Versi Bahasa Indonesia
//  Implementasi orisinal. Membangun zona berbasis struktur dan mencari
//  retest terkonfirmasi (candle rejection dan/atau CHoCH) sebelum sinyal entry.
//
//  Alur
//  ────
//  1. Struktur market dari swing pivot terkonfirmasi (Pivot Period Zone).
//  2. Break struktur + spike impulsif (min. candle searah berurutan + momentum vs ATR)
//     membuat zona: distal = Origin Pivot, proximal = tepi Break Candle.
//  3. Sinyal = harga KEMBALI ke zona, plus konfirmasi yang diaktifkan:
//        • Candle rejection (Pin Bar / Engulfing) pada bar sentuh atau setelahnya
//        • CHoCH searah trade pada timeframe pilihan user, setelah sentuhan
//  4. SL = distal ± threshold.  Hingga 3 tahap take-profit dengan partial close dan
//     opsi break-even setelah TP1.
//  5. Setiap level menampilkan harga, % jarak, R-multiple dan P/L hipotetis berdasarkan
//     input balance akun dan risk %.
//
//  NON-REPAINTING:
//   • Semua logika sinyal/zona berjalan hanya pada bar chart TERKONFIRMASI.
//   • CHoCH pada TF lebih rendah dibaca dari intrabar yang sudah tertutup penuh (security_lower_tf).
//   • CHoCH pada TF lebih tinggi menggunakan bar HTF SEBELUMNYA yang SUDAH SELESAI (lookahead + [1]).
// ════════════════════════════════════════════════════════════════════════════════
indicator("GoodSmart Retest", shorttitle = "GoodSmart", overlay = true, max_boxes_count = 500, max_lines_count = 500, max_labels_count = 500, max_bars_back = 500)

// ═══════════════════════════════════ INPUT ══════════════════════════════════════
const string G0  = "PRESET INSTRUMEN"
const string G1  = "FILTER SPIKE | PERGERAKAN"
const string G2  = "MANAJEMEN POSISI"
const string GT  = "TAKE PROFIT | MULTI-TAHAP"
const string GR  = "RISIKO & BALANCE (P/L hipotetis)"
const string GRJ = "KONFIRMASI ENTRY | REJECTION"
const string GCH = "KONFIRMASI ENTRY | CHOCH"
const string G3  = "PENGATURAN TAMPILAN"
const string G4  = "ALERT"
const string G5  = "STYLE"
const string G6  = "LANJUTAN (extra – tidak ada di UI asli)"

// ── Preset instrumen
string presetIn = input.string("Manual (gunakan input di bawah)", "Preset Instrumen", options = ["Manual (gunakan input di bawah)", "Gold (XAUUSD)", "Forex Majors", "JPY Pairs", "Indices", "Crypto (BTC/ETH majors)"], group = G0, tooltip = "Mengisi otomatis Minimum Spike Bars, Movement Power, Pivot Period Zone, Stop-Loss Threshold (dipaksa ke ATR Multiple), Risk-Reward/TP1-3, Wide Zone Filter, Zone Max Age dan Max Impulse Length untuk kategori di bawah, disesuaikan dengan timeframe chart saat ini (bucket: <=5m, <=15m, <=1h, <=4h, >4h).\nMulti-Stage Take Profit Close % dibagi dan setiap input di RISIKO & BALANCE, KONFIRMASI ENTRY dan TAMPILAN tetap seperti yang Anda atur.\nPilih \"Manual\" untuk mengontrol semua input sendiri.")

// ── Filter spike | Pergerakan
int   minSpike  = input.int(4, "Minimum Candle Spike", minval = 1, maxval = 30, group = G1, tooltip = "Jumlah minimum candle searah BERURUTAN di dalam leg impulsif (origin pivot → break).")
bool  useMove   = input.bool(true, "Kekuatan Pergerakan", inline = "mp", group = G1)
float movePower = input.float(4.5, "", minval = 0.1, step = 0.1, inline = "mp", group = G1, tooltip = "Filter momentum. Range impulsif (origin extreme → leg extreme) harus >= nilai ini x ATR.")
int   pivotLen  = input.int(5, "Periode Pivot Zona", minval = 1, maxval = 50, group = G1, tooltip = "Bar kiri/kanan yang digunakan untuk konfirmasi Swing High / Swing Low (struktur market).")

// ── Manajemen posisi
bool  useSL      = input.bool(true, "Threshold Stop-Loss", inline = "sl", group = G2)
float slThr      = input.float(0.2, "", minval = 0.0, step = 0.05, inline = "sl", group = G2, tooltip = "Buffer ditambahkan di luar tepi distal. Unit dipilih di grup LANJUTAN. Jika dimatikan, SL = tepi distal persis.")
float rr         = input.float(2.0, "Risk-Reward Ratio (TP tunggal)", minval = 0.1, step = 0.1, group = G2, tooltip = "Hanya digunakan saat Multi-Tahap Take Profit OFF.")
bool  useWide    = input.bool(true, "Filter Zona Lebar", inline = "wz", group = G2)
float maxZoneAtr = input.float(2.5, "", minval = 0.1, step = 0.1, inline = "wz", group = G2, tooltip = "Zona yang lebih tinggi dari sekian ATR akan diabaikan.")

// ── Take profit | multi-tahap
bool  useMulti   = input.bool(true, "Multi-Tahap Take Profit", group = GT, tooltip = "ON: tiga target dengan partial close. OFF: target tunggal pada Risk-Reward Ratio di atas.\nTahap dengan Close % = 0 dinonaktifkan.")
float tp1R       = input.float(1.0, "TP1 Reward (R)", minval = 0.1, step = 0.1, inline = "t1", group = GT)
float tp1P       = input.float(40.0, "Close %", minval = 0.0, maxval = 100.0, step = 5.0, inline = "t1", group = GT)
float tp2R       = input.float(2.0, "TP2 Reward (R)", minval = 0.1, step = 0.1, inline = "t2", group = GT)
float tp2P       = input.float(30.0, "Close %", minval = 0.0, maxval = 100.0, step = 5.0, inline = "t2", group = GT)
float tp3R       = input.float(3.0, "TP3 Reward (R)", minval = 0.1, step = 0.1, inline = "t3", group = GT)
float tp3P       = input.float(30.0, "Close %", minval = 0.0, maxval = 100.0, step = 5.0, inline = "t3", group = GT, tooltip = "Nilai Close % dinormalisasi, jadi tidak harus berjumlah 100.")
bool  beAfterTP1 = input.bool(false, "Pindahkan SL ke Break-even setelah TP1", group = GT, tooltip = "OFF (default): stop tetap di level aslinya selama trade.\nON: setelah TP1 stop pindah ke harga entry; garis SL ASLI tetap di tempatnya dan garis SL→BE putus-putus baru dimulai dari bar tersebut.")

// ── Risiko & balance
float acctBal = input.float(10000.0, "Balance Akun", minval = 1.0, step = 100.0, group = GR)
float riskPct = input.float(1.0, "Risk % per Trade", minval = 0.01, maxval = 100.0, step = 0.1, group = GR, tooltip = "P/L hipotetis pada setiap level = Balance x Risk% x R-multiple x porsi posisi yang ditutup.")

// ── Konfirmasi entry | Rejection
bool  useReject = input.bool(true, "Konfirmasi Rejection", group = GRJ, tooltip = "Candle sinyal harus menyentuh zona, close di luar tepi proximal DAN merupakan candle rejection (tipe di bawah).")
bool  usePin    = input.bool(true, "Pin Bar", inline = "rj", group = GRJ)
bool  useEngulf = input.bool(true, "Engulfing", inline = "rj", group = GRJ, tooltip = "Jika kedua pola dimatikan, candle apapun diterima.")
float pinRatio  = input.float(2.0, "Pin Bar: wick >= N x body", minval = 0.5, step = 0.25, group = GRJ, tooltip = "Wick rejection harus minimal N kali body dan minimal 50% dari range candle.")

// ── Konfirmasi entry | CHoCH
bool   useChoch = input.bool(false, "Konfirmasi CHoCH", group = GCH, tooltip = "Setelah harga masuk zona, CHoCH searah trade harus terjadi pada timeframe di bawah sebelum sinyal diizinkan.")
string chochTF  = input.timeframe("15", "Timeframe CHoCH", group = GCH, tooltip = "TF lebih rendah (mis. 1/5/15) = konfirmasi LTF klasik. TF sama atau lebih tinggi juga didukung.")
int    chLen    = input.int(3, "Periode Pivot CHoCH", minval = 1, maxval = 20, group = GCH, tooltip = "Bar kiri/kanan untuk deteksi swing pada timeframe CHoCH.")
bool   autoChoch = input.bool(true, "Auto Timeframe CHoCH (dari preset)", group = GCH, tooltip = "ON (default): saat preset di atas aktif, Timeframe CHoCH dipilih otomatis dari preset (kira-kira 1/4-1/5 dari timeframe chart).\nOFF: selalu gunakan field Timeframe CHoCH secara manual, meski preset dipilih.")

// ── Tampilan
string dispMode  = input.string("Signal", "Mode Tampilan", options = ["Signal", "Setup"], group = G3, tooltip = "Signal: zona + sinyal Long/Short setelah retest-dan-exit.\nSetup: zona + rencana Entry/SL/TP (limit order di tepi proximal) begitu zona dibuat.")
bool   showEntry = input.bool(true, "Tampilkan Level Entry", group = G3)
bool   onlyLast  = input.bool(false, "Hanya Tampilkan Posisi Terakhir", group = G3)
int    drawW     = input.int(2, "Lebar Gambar Setup", minval = 1, maxval = 5, group = G3, tooltip = "Lebar garis untuk border zona dan level Entry / SL / TP.")
string lblSizeIn = input.string("Large", "Ukuran Label Level", options = ["Small", "Normal", "Large", "Huge"], group = G3, tooltip = "Ukuran teks label info Entry / SL / TP dan label hasil trade.")
string lblFontIn = input.string("Monospace", "Font Label Level", options = ["Monospace", "Default"], group = G3, tooltip = "Monospace menjaga harga dan persentase tetap sejajar dan lebih mudah dibaca.")
bool   lblBold   = input.bool(true, "Label Level Tebal", group = G3)

// ── Alert
bool useAlert = input.bool(true, "Alert", group = G4, tooltip = "Kirim pesan alert() pada sinyal baru (dan setup saat Mode Tampilan = Setup). Buat alert dengan kondisi: TRH → Any alert() function call.")

// ── Style
color bullClr = input.color(color.new(#4caf50, 78), "Long / Zona Demand", group = G5)
color bearClr = input.color(color.new(#f23645, 78), "Short / Zona Supply", group = G5)
color sigBull = input.color(#1b8a2f, "Sinyal Long", group = G5)
color sigBear = input.color(#a3151a, "Sinyal Short", group = G5)

// ── Lanjutan (extra)
int    atrLen    = input.int(14, "Panjang ATR", minval = 1, group = G6)
int    maxImp    = input.int(15, "Max Panjang Impuls (bar)", minval = 3, maxval = 100, group = G6, tooltip = "Jumlah maksimum bar antara origin pivot dan akhir spike.")
int    maxAge    = input.int(300, "Max Umur Zona (bar)", minval = 10, group = G6, tooltip = "Zona yang tidak pernah di-retest akan dibuang setelah sekian bar.")
int    maxZones  = input.int(30, "Max Zona Aktif", minval = 1, maxval = 100, group = G6)
string slUnit    = input.string("% of Price", "Unit Threshold Stop-Loss", options = ["% of Price", "ATR Multiple", "Zone Height Multiple"], group = G6)
bool   showStats = input.bool(false, "Tampilkan Tabel Statistik", group = G6)

color cEntry = color.new(#787b86, 0)
color cSL    = color.new(#f23645, 0)
color cTP    = color.new(#089981, 0)

// Bucket timeframe chart untuk memilih baris dari tabel preset.
f_bucket() =>
    int sec = timeframe.in_seconds()
    string b = sec <= 300 ? "M5" : sec <= 900 ? "M15" : sec <= 3600 ? "H1" : sec <= 14400 ? "H4" : "D1"
    b

// Tabel lookup preset: [Minimum Candle Spike, Kekuatan Pergerakan, Periode Pivot Zona, Threshold SL (x ATR),
// Risk-Reward/TP1 (R), Filter Zona Lebar (x ATR), Max Umur Zona, Max Panjang Impuls, Timeframe CHoCH]
f_preset(string cat, string b) =>
    int    pMinSpike = minSpike
    float  pPower    = movePower
    int    pPivot    = pivotLen
    float  pSlAtr    = 0.3
    float  pRR       = rr
    float  pWide     = maxZoneAtr
    int    pAge      = maxAge
    int    pImp      = maxImp
    string pChoch    = chochTF
    if cat == "Gold (XAUUSD)"
        switch b
            "M5" =>
                pMinSpike := 3
                pPower    := 3.5
                pPivot    := 10
                pSlAtr    := 0.3
                pRR       := 1.3
                pWide     := 1.8
                pAge      := 60
                pImp      := 8
                pChoch    := "1"
            "M15" =>
                pMinSpike := 4
                pPower    := 4.0
                pPivot    := 7
                pSlAtr    := 0.3
                pRR       := 1.5
                pWide     := 2.0
                pAge      := 100
                pImp      := 10
                pChoch    := "5"
            "H1" =>
                pMinSpike := 4
                pPower    := 4.5
                pPivot    := 5
                pSlAtr    := 0.3
                pRR       := 1.0
                pWide     := 2.2
                pAge      := 150
                pImp      := 12
                pChoch    := "15"
            "H4" =>
                pMinSpike := 5
                pPower    := 5.5
                pPivot    := 4
                pSlAtr    := 0.35
                pRR       := 1.0
                pWide     := 2.5
                pAge      := 200
                pImp      := 15
                pChoch    := "60"
            =>
                pMinSpike := 5
                pPower    := 6.0
                pPivot    := 3
                pSlAtr    := 0.4
                pRR       := 1.0
                pWide     := 3.0
                pAge      := 250
                pImp      := 18
                pChoch    := "240"
    else if cat == "Forex Majors"
        switch b
            "M5" =>
                pMinSpike := 4
                pPower    := 3.0
                pPivot    := 10
                pSlAtr    := 0.25
                pRR       := 1.5
                pWide     := 1.5
                pAge      := 80
                pImp      := 10
                pChoch    := "1"
            "M15" =>
                pMinSpike := 4
                pPower    := 3.5
                pPivot    := 8
                pSlAtr    := 0.25
                pRR       := 1.5
                pWide     := 1.8
                pAge      := 120
                pImp      := 12
                pChoch    := "5"
            "H1" =>
                pMinSpike := 5
                pPower    := 4.0
                pPivot    := 6
                pSlAtr    := 0.3
                pRR       := 1.5
                pWide     := 2.0
                pAge      := 180
                pImp      := 15
                pChoch    := "15"
            "H4" =>
                pMinSpike := 5
                pPower    := 4.5
                pPivot    := 4
                pSlAtr    := 0.3
                pRR       := 1.0
                pWide     := 2.2
                pAge      := 220
                pImp      := 18
                pChoch    := "60"
            =>
                pMinSpike := 5
                pPower    := 5.0
                pPivot    := 3
                pSlAtr    := 0.35
                pRR       := 1.0
                pWide     := 2.5
                pAge      := 260
                pImp      := 20
                pChoch    := "240"
    else if cat == "JPY Pairs"
        switch b
            "M5" =>
                pMinSpike := 4
                pPower    := 3.5
                pPivot    := 9
                pSlAtr    := 0.25
                pRR       := 1.3
                pWide     := 1.8
                pAge      := 70
                pImp      := 9
                pChoch    := "1"
            "M15" =>
                pMinSpike := 4
                pPower    := 4.0
                pPivot    := 7
                pSlAtr    := 0.3
                pRR       := 1.5
                pWide     := 2.0
                pAge      := 110
                pImp      := 11
                pChoch    := "5"
            "H1" =>
                pMinSpike := 4
                pPower    := 4.5
                pPivot    := 5
                pSlAtr    := 0.3
                pRR       := 1.5
                pWide     := 2.2
                pAge      := 160
                pImp      := 14
                pChoch    := "15"
            "H4" =>
                pMinSpike := 5
                pPower    := 5.0
                pPivot    := 4
                pSlAtr    := 0.35
                pRR       := 1.0
                pWide     := 2.5
                pAge      := 200
                pImp      := 16
                pChoch    := "60"
            =>
                pMinSpike := 5
                pPower    := 5.5
                pPivot    := 3
                pSlAtr    := 0.4
                pRR       := 1.0
                pWide     := 2.8
                pAge      := 250
                pImp      := 18
                pChoch    := "240"
    else if cat == "Indices"
        switch b
            "M5" =>
                pMinSpike := 3
                pPower    := 4.0
                pPivot    := 10
                pSlAtr    := 0.3
                pRR       := 1.3
                pWide     := 2.0
                pAge      := 50
                pImp      := 8
                pChoch    := "1"
            "M15" =>
                pMinSpike := 4
                pPower    := 4.5
                pPivot    := 7
                pSlAtr    := 0.35
                pRR       := 1.5
                pWide     := 2.2
                pAge      := 80
                pImp      := 10
                pChoch    := "5"
            "H1" =>
                pMinSpike := 4
                pPower    := 5.0
                pPivot    := 5
                pSlAtr    := 0.35
                pRR       := 1.5
                pWide     := 2.5
                pAge      := 130
                pImp      := 12
                pChoch    := "15"
            "H4" =>
                pMinSpike := 5
                pPower    := 5.5
                pPivot    := 4
                pSlAtr    := 0.4
                pRR       := 1.0
                pWide     := 3.0
                pAge      := 180
                pImp      := 15
                pChoch    := "60"
            =>
                pMinSpike := 5
                pPower    := 6.0
                pPivot    := 3
                pSlAtr    := 0.45
                pRR       := 1.0
                pWide     := 3.5
                pAge      := 220
                pImp      := 18
                pChoch    := "240"
    else if cat == "Crypto (BTC/ETH majors)"
        switch b
            "M5" =>
                pMinSpike := 3
                pPower    := 4.5
                pPivot    := 10
                pSlAtr    := 0.4
                pRR       := 1.3
                pWide     := 2.2
                pAge      := 50
                pImp      := 8
                pChoch    := "1"
            "M15" =>
                pMinSpike := 4
                pPower    := 5.0
                pPivot    := 7
                pSlAtr    := 0.4
                pRR       := 1.5
                pWide     := 2.5
                pAge      := 90
                pImp      := 10
                pChoch    := "5"
            "H1" =>
                pMinSpike := 4
                pPower    := 5.5
                pPivot    := 5
                pSlAtr    := 0.45
                pRR       := 1.5
                pWide     := 2.8
                pAge      := 140
                pImp      := 12
                pChoch    := "15"
            "H4" =>
                pMinSpike := 5
                pPower    := 6.0
                pPivot    := 4
                pSlAtr    := 0.5
                pRR       := 1.0
                pWide     := 3.2
                pAge      := 190
                pImp      := 15
                pChoch    := "60"
            =>
                pMinSpike := 5
                pPower    := 6.5
                pPivot    := 3
                pSlAtr    := 0.55
                pRR       := 1.0
                pWide     := 3.8
                pAge      := 240
                pImp      := 18
                pChoch    := "240"
    [pMinSpike, pPower, pPivot, pSlAtr, pRR, pWide, pAge, pImp, pChoch]

// ── Parameter efektif (sadar-preset) ───────────────────────────────────────────
bool usePreset = presetIn != "Manual (gunakan input di bawah)"
string bucket  = f_bucket()
[pMinSpike, pPower, pPivot, pSlAtr, pRR, pWide, pAge, pImp, pChoch] = f_preset(presetIn, bucket)

int    effMinSpike  = usePreset ? pMinSpike : minSpike
float  effMovePower = usePreset ? pPower : movePower
int    effPivotLen  = usePreset ? pPivot : pivotLen
float  effSlThr      = usePreset ? pSlAtr : slThr
string effSlUnit      = usePreset ? "ATR Multiple" : slUnit
float  effRR         = usePreset ? pRR : rr
float  effTp1R       = usePreset ? pRR : tp1R
float  effTp2R       = usePreset ? pRR + 1.0 : tp2R
float  effTp3R       = usePreset ? pRR + 2.0 : tp3R
float  effMaxZoneAtr = usePreset ? pWide : maxZoneAtr
int    effMaxAge     = usePreset ? pAge : maxAge
int    effMaxImp     = usePreset ? pImp : maxImp
string effChochTF     = (usePreset and autoChoch) ? pChoch : chochTF

// ── Konfigurasi take-profit turunan (bobot dinormalisasi)
float pSum = tp1P + tp2P + tp3P
float w1   = useMulti ? (pSum > 0 ? tp1P / pSum : 1.0) : 1.0
float w2   = useMulti ? (pSum > 0 ? tp2P / pSum : 0.0) : 0.0
float w3   = useMulti ? (pSum > 0 ? tp3P / pSum : 0.0) : 0.0
float r1   = useMulti ? effTp1R : effRR
float r2   = effTp2R
float r3   = effTp3R
float riskAmt = acctBal * riskPct / 100.0

// ── Tampilan label
lblSize = lblSizeIn == "Small" ? size.small : (lblSizeIn == "Normal" ? size.normal : (lblSizeIn == "Large" ? size.large : size.huge))
lblFont = lblFontIn == "Monospace" ? font.family_monospace : font.family_default

// ═════════════════════════════════════ TIPE ═════════════════════════════════════
type Cand
    int   orig
    float distal
    float oOpp
    float brkEdge
    bool  brkFound
    int   run
    int   maxRun
    float ext

type Zone
    int          dir
    float        distal
    float        prox
    float        slp
    int          x0
    int          born
    bool         entered
    bool         choch
    bool         hidden
    box          bx
    array<line>  lns
    array<label> lbs

type Trade
    int          dir
    float        entry
    float        sl
    float        slCur
    float        risk
    float        t1
    float        t2
    float        t3
    bool         h1
    bool         h2
    bool         h3
    float        rem
    float        rR
    int          start
    bool         active
    bool         hidden
    array<line>  lns
    array<label> lbs

// ═════════════════════════════════════ STATE ════════════════════════════════════
var array<Zone>  zones  = array.new<Zone>()
var array<Zone>  fin    = array.new<Zone>()
var array<Trade> trades = array.new<Trade>()
var Cand cBull = na
var Cand cBear = na

var float swH     = na
var int   swHIdx  = na
var bool  swHUsed = true
var float swL     = na
var int   swLIdx  = na
var bool  swLUsed = true

var int   nSig  = 0
var int   nWin  = 0
var int   nLoss = 0
var int   nBE   = 0
var float netR  = 0.0

// ════════════════════════════════════ FUNGSI ════════════════════════════════════
f_buffer(float distal, float height, float atrV) =>
    float b = 0.0
    if useSL
        if effSlUnit == "% of Price"
            b := distal * effSlThr / 100.0
        else if effSlUnit == "ATR Multiple"
            b := atrV * effSlThr
        else
            b := height * effSlThr
    b

f_sgn(float v) =>
    (v >= 0 ? "+" : "") + str.tostring(v, "#.##")

f_money(float v) =>
    f_sgn(v) + " " + syminfo.currency

f_px(float v) =>
    str.tostring(v, format.mintick)

// Satu level = garis + label ditempatkan DI ATAS garis (body label duduk di atas harga)
f_addLvl(array<line> lns, array<label> lbs, int i, float y, string txt, color c, bool dashed, bool setup, bool above) =>
    int x = bar_index
    array.set(lns, i, line.new(x, y, x + 1, y, extend = setup ? extend.right : extend.none, color = c, style = dashed ? line.style_dashed : line.style_solid, width = drawW))
    array.set(lbs, i, label.new(x + 1, y, txt, style = above ? label.style_label_lower_left : label.style_label_upper_left, color = color.new(color.white, 100), textcolor = c, size = lblSize, text_font_family = lblFont, text_formatting = lblBold ? text.format_bold : text.format_none))
    true

f_tpTxt(string name, int dir, float entry, float price, float r, float w) =>
    float pct = dir * (price - entry) / entry * 100.0
    name + " " + f_px(price) + " | " + f_sgn(pct) + "% | " + str.tostring(r, "#.##") + "R | " + f_money(w * r * riskAmt) + (useMulti ? " (" + str.tostring(w * 100.0, "#") + "%)" : "")

// Membangun level Entry / SL / TP1-3. Slot: 0 entry, 1 SL, 2 TP1, 3 TP2, 4 TP3
f_mkLevels(int dir, float entry, float sl, bool setup) =>
    float rk = math.abs(entry - sl)
    array<line>  lns = array.new<line>(5, na)
    array<label> lbs = array.new<label>(5, na)
    f_addLvl(lns, lbs, 0, entry, "Entry " + f_px(entry) + " | Risiko " + str.tostring(riskAmt, "#.##") + " " + syminfo.currency + " (" + str.tostring(riskPct, "#.##") + "%)", cEntry, true, setup, dir == 1)
    float slPct = dir * (sl - entry) / entry * 100.0
    f_addLvl(lns, lbs, 1, sl, "SL " + f_px(sl) + " | " + f_sgn(slPct) + "% | " + f_money(-riskAmt), cSL, false, setup, dir == -1)
    if w1 > 0
        float p1 = dir == 1 ? entry + r1 * rk : entry - r1 * rk
        f_addLvl(lns, lbs, 2, p1, f_tpTxt(useMulti ? "TP1" : "TP", dir, entry, p1, r1, w1), cTP, false, setup, dir == 1)
    if w2 > 0
        float p2 = dir == 1 ? entry + r2 * rk : entry - r2 * rk
        f_addLvl(lns, lbs, 3, p2, f_tpTxt("TP2", dir, entry, p2, r2, w2), cTP, false, setup, dir == 1)
    if w3 > 0
        float p3 = dir == 1 ? entry + r3 * rk : entry - r3 * rk
        f_addLvl(lns, lbs, 4, p3, f_tpTxt("TP3", dir, entry, p3, r3, w3), cTP, false, setup, dir == 1)
    [lns, lbs]

f_delArrays(array<line> lns, array<label> lbs) =>
    if not na(lns)
        for l in lns
            if not na(l)
                line.delete(l)
    if not na(lbs)
        for b in lbs
            if not na(b)
                label.delete(b)
    true

f_dropLevels(Zone z) =>
    f_delArrays(z.lns, z.lbs)
    z.lns := na
    z.lbs := na
    true

f_delZone(Zone z) =>
    if not z.hidden
        if not na(z.bx)
            box.delete(z.bx)
        f_dropLevels(z)
        z.hidden := true
    true

f_delTrade(Trade t) =>
    if not t.hidden
        f_delArrays(t.lns, t.lbs)
        t.lns := na
        t.lbs := na
        t.hidden := true
    true

f_clearFinished() =>
    for z in fin
        f_delZone(z)
    array.clear(fin)
    for t in trades
        f_delTrade(t)
    true

f_addZone(int dir, float dist, float prox, int x0, float atrV) =>
    float h    = math.abs(prox - dist)
    float buf  = f_buffer(dist, h, atrV)
    float sl   = dir == 1 ? dist - buf : dist + buf
    color base = dir == 1 ? bullClr : bearClr
    Zone z = Zone.new(dir = dir, distal = dist, prox = prox, slp = sl, x0 = x0, born = bar_index, entered = false, choch = false, hidden = false)
    z.bx := box.new(x0, math.max(dist, prox), bar_index, math.min(dist, prox), border_color = color.new(base, 70), border_width = drawW, bgcolor = base)
    if dispMode == "Setup" and showEntry
        [ln, lb] = f_mkLevels(dir, prox, sl, true)
        z.lns := ln
        z.lbs := lb
    z

f_addTrade(int dir, float entry, float sl) =>
    float rk = math.abs(entry - sl)
    Trade t = Trade.new(dir = dir, entry = entry, sl = sl, slCur = sl, risk = rk, t1 = dir == 1 ? entry + r1 * rk : entry - r1 * rk, t2 = dir == 1 ? entry + r2 * rk : entry - r2 * rk, t3 = dir == 1 ? entry + r3 * rk : entry - r3 * rk, h1 = false, h2 = false, h3 = false, rem = 1.0, rR = 0.0, start = bar_index, active = true, hidden = false)
    if showEntry
        [ln, lb] = f_mkLevels(dir, entry, sl, false)
        t.lns := ln
        t.lbs := lb
    t

// Pindahkan garis/label SL trade ke break-even
f_moveSLtoBE(Trade t) =>
    t.slCur := t.entry
    if not t.hidden and not na(t.lns)
        line oldL = array.get(t.lns, 1)
        label oldB = array.get(t.lbs, 1)
        if not na(oldL)
            // biarkan garis/label stop asli tidak tersentuh (dipindah ke slot "arsip" 5+),
            // lalu mulai garis stop break-even putus-putus baru dari bar ini
            array.push(t.lns, oldL)
            array.push(t.lbs, oldB)
            array.set(t.lns, 1, line.new(bar_index, t.entry, bar_index + 1, t.entry, color = cSL, style = line.style_dotted, width = drawW))
            array.set(t.lbs, 1, label.new(bar_index + 1, t.entry, "SL → BE " + f_px(t.entry) + " | 0% | " + f_money(0.0), style = t.dir == -1 ? label.style_label_lower_left : label.style_label_upper_left, color = color.new(color.white, 100), textcolor = cSL, size = lblSize, text_font_family = lblFont, text_formatting = lblBold ? text.format_bold : text.format_none))
    true

// Setiap level (SL=slot1, TP1=slot2, TP2=slot3, TP3=slot4) punya tepat SATU
// label persisten, dibuat sekali saat trade dibuka. Saat hit, kita perbarui
// label YANG SAMA teks/warnanya di tempat alih-alih membuat label kedua
// untuk garis yang sama — ini yang sebelumnya menyebabkan label duplikat.
f_updateLevelLabel(Trade t, int i, string txt, bool done) =>
    if showEntry and not t.hidden and not na(t.lbs)
        label lb = array.get(t.lbs, i)
        if not na(lb)
            label.set_text(lb, txt)
            if done
                label.set_textcolor(lb, t.rR > 0.05 ? cTP : (t.rR < -0.05 ? cSL : cEntry))
    true

// Masukkan satu candle ke kandidat spike. Mengembalikan true selama spike masih berjalan.
f_step(Cand c, int dir, float o, float h, float l, float cl) =>
    bool dirCandle = dir * (cl - o) > 0
    c.run := dirCandle ? c.run + 1 : 0
    c.maxRun := math.max(c.maxRun, c.run)
    c.ext := dir == 1 ? math.max(c.ext, h) : math.min(c.ext, l)
    if not c.brkFound and dir * (cl - c.oOpp) > 0
        c.brkFound := true
        c.brkEdge := dir == 1 ? l : h
    dirCandle

// Validasi spike yang sudah selesai dan buat zona TRH jika semua filter lolos.
f_tryZone(Cand c, int dir, float atrV) =>
    bool ok = false
    if c.maxRun >= effMinSpike
        float edge = c.brkFound ? c.brkEdge : c.oOpp
        float prox = dir == 1 ? math.max(c.oOpp, edge) : math.min(c.oOpp, edge)
        float zh   = math.abs(prox - c.distal)
        bool powerOK = not useMove or math.abs(c.ext - c.distal) >= effMovePower * atrV
        bool wideOK  = not useWide or zh <= effMaxZoneAtr * atrV
        if powerOK and wideOK and zh > syminfo.mintick
            if onlyLast and dispMode == "Setup"
                for z in zones
                    f_delZone(z)
                f_clearFinished()
            if array.size(zones) >= maxZones
                f_delZone(array.shift(zones))
            array.push(zones, f_addZone(dir, c.distal, prox, c.orig, atrV))
            ok := true
    ok

// Detektor CHoCH pada timeframe apapun saat dievaluasi. Mengembalikan [bullish, bearish] sebagai 1.0 / 0.0.
// Bullish CHoCH = close menembus swing high terakhir saat tren sedang bearish (dan sebaliknya).
f_choch(int len) =>
    float ph = ta.pivothigh(high, len, len)
    float pl = ta.pivotlow(low, len, len)
    var float sh = na
    var float sl = na
    var bool shUsed = true
    var bool slUsed = true
    var int trend = 0
    if not na(ph)
        sh := ph
        shUsed := false
    if not na(pl)
        sl := pl
        slUsed := false
    float bull = 0.0
    float bear = 0.0
    if not na(sh) and not shUsed and close > sh
        shUsed := true
        if trend == -1
            bull := 1.0
        trend := 1
    if not na(sl) and not slUsed and close < sl
        slUsed := true
        if trend == 1
            bear := 1.0
        trend := -1
    [bull, bear]

// Sama, tapi tertunda satu bar → digunakan dengan lookahead_on pada timeframe LEBIH TINGGI (tanpa repaint)
f_chochLag(int len) =>
    [b, s] = f_choch(len)
    [b[1], s[1]]

// ══════════════════════════════════ KALKULASI ═══════════════════════════════════
float atrV = ta.atr(atrLen)
float ph   = ta.pivothigh(high, effPivotLen, effPivotLen)
float pl   = ta.pivotlow(low, effPivotLen, effPivotLen)

// ── Pola candle rejection (dievaluasi pada candle tertutup)
float rng    = high - low
float body   = math.abs(close - open)
float loWick = math.min(open, close) - low
float upWick = high - math.max(open, close)
bool bullPin = rng > 0 and loWick >= pinRatio * math.max(body, syminfo.mintick) and loWick >= 0.5 * rng
bool bearPin = rng > 0 and upWick >= pinRatio * math.max(body, syminfo.mintick) and upWick >= 0.5 * rng
bool bullEng = close[1] < open[1] and close > open and close >= open[1] and open <= close[1]
bool bearEng = close[1] > open[1] and close < open and close <= open[1] and open >= close[1]
bool anyPat  = not usePin and not useEngulf
bool rejBull = not useReject or anyPat or (usePin and bullPin) or (useEngulf and bullEng)
bool rejBear = not useReject or anyPat or (usePin and bearPin) or (useEngulf and bearEng)

// ── CHoCH pada timeframe terpilih
int  curSec = timeframe.in_seconds()
int  chSec  = timeframe.in_seconds(effChochTF)
bool chLow  = chSec < curSec
bool chHigh = chSec > curSec
bool htfNew = timeframe.change(effChochTF)

[bChart, sChart] = f_choch(chLen)
float bullChoch = 0.0
float bearChoch = 0.0
if useChoch
    if chLow
        [bArr, sArr] = request.security_lower_tf(syminfo.tickerid, effChochTF, f_choch(chLen))
        bullChoch := array.size(bArr) > 0 ? array.max(bArr) : 0.0
        bearChoch := array.size(sArr) > 0 ? array.max(sArr) : 0.0
    else if chHigh
        [bH, sH] = request.security(syminfo.tickerid, effChochTF, f_chochLag(chLen), lookahead = barmerge.lookahead_on)
        bullChoch := htfNew ? nz(bH) : 0.0
        bearChoch := htfNew ? nz(sH) : 0.0
    else
        bullChoch := bChart
        bearChoch := sChart

// ── Output
bool   longSig   = false
bool   shortSig  = false
bool   bullSetup = false
bool   bearSetup = false
float  sigEntry  = na
float  sigSL     = na
float  sigTP1    = na
float  sigTP2    = na
float  sigTP3    = na
string sigConf   = ""

if barstate.isconfirmed
    // ── 1) Struktur market: swing pivot terkonfirmasi ──────────────────────────
    if not na(ph)
        swH     := ph
        swHIdx  := bar_index - effPivotLen
        swHUsed := false
    if not na(pl)
        swL     := pl
        swLIdx  := bar_index - effPivotLen
        swLUsed := false

    // ── 2) Kelola trade terbuka: SL / tahap TP / break-even / statistik ────────
    for t in trades
        if t.active and bar_index > t.start
            bool isL = t.dir == 1
            bool closedNow = false

            if not t.hidden and not na(t.lns)
                for i = 0 to 4
                    line lx = array.get(t.lns, i)
                    if not na(lx)
                        line.set_x2(lx, bar_index)

            bool stopHit = isL ? low <= t.slCur : high >= t.slCur
            if stopHit
                // konservatif: jika SL dan TP keduanya di dalam bar yang sama, SL diasumsikan lebih dulu
                float rs  = (isL ? t.slCur - t.entry : t.entry - t.slCur) / t.risk
                float leg = t.rem * rs
                t.rR := t.rR + leg
                t.rem := 0.0
                closedNow := true
                string txt1 = (rs < -0.01 ? "SL ✘ " : "BE ➜ ") + f_money(leg * riskAmt) + " | Total " + f_sgn(t.rR) + "R " + f_money(t.rR * riskAmt)
                f_updateLevelLabel(t, 1, txt1, true)
            else
                if w1 > 0 and not t.h1 and (isL ? high >= t.t1 : low <= t.t1)
                    t.h1 := true
                    t.rem := t.rem - w1
                    t.rR  := t.rR + w1 * r1
                    bool done1 = t.rem <= 0.0001
                    if done1
                        closedNow := true
                    string txt2 = (useMulti ? "TP1 ✔ " : "TP ✔ ") + f_money(w1 * r1 * riskAmt) + (done1 ? " | Total " + f_sgn(t.rR) + "R " + f_money(t.rR * riskAmt) : "")
                    f_updateLevelLabel(t, 2, txt2, done1)
                    if useMulti and beAfterTP1
                        f_moveSLtoBE(t)
                if w2 > 0 and not t.h2 and (isL ? high >= t.t2 : low <= t.t2)
                    t.h2 := true
                    t.rem := t.rem - w2
                    t.rR  := t.rR + w2 * r2
                    bool done2 = t.rem <= 0.0001
                    if done2
                        closedNow := true
                    string txt3 = "TP2 ✔ " + f_money(w2 * r2 * riskAmt) + (done2 ? " | Total " + f_sgn(t.rR) + "R " + f_money(t.rR * riskAmt) : "")
                    f_updateLevelLabel(t, 3, txt3, done2)
                if w3 > 0 and not t.h3 and (isL ? high >= t.t3 : low <= t.t3)
                    t.h3 := true
                    t.rem := t.rem - w3
                    t.rR  := t.rR + w3 * r3
                    bool done3 = t.rem <= 0.0001
                    if done3
                        closedNow := true
                    string txt4 = "TP3 ✔ " + f_money(w3 * r3 * riskAmt) + (done3 ? " | Total " + f_sgn(t.rR) + "R " + f_money(t.rR * riskAmt) : "")
                    f_updateLevelLabel(t, 4, txt4, done3)

            if closedNow
                t.active := false
                netR += t.rR
                if t.rR > 0.05
                    nWin += 1
                else if t.rR < -0.05
                    nLoss += 1
                else
                    nBE += 1

    // ── 3) Kelola zona pending: retest → konfirmasi → sinyal ───────────────────
    int zi = array.size(zones) - 1
    while zi >= 0
        Zone z = array.get(zones, zi)
        bool isLong = z.dir == 1
        int res = 0                                   // 0 pertahankan | 1 trigger | 2 matikan
        if (isLong and close < z.slp) or (not isLong and close > z.slp)
            res := 2                                  // tidak valid (close melewati level SL)
        else if bar_index - z.born > effMaxAge
            res := 2                                  // kedaluwarsa
        else
            bool touchNow = isLong ? low <= z.prox : high >= z.prox
            if touchNow
                z.entered := true                     // harga kembali ke zona
            if z.entered and (isLong ? bullChoch > 0 : bearChoch > 0)
                z.choch := true                       // CHoCH terlihat setelah sentuhan
            bool exitOK = isLong ? close > z.prox : close < z.prox
            bool rejOK  = isLong ? rejBull : rejBear
            bool chOK   = not useChoch or z.choch
            bool touchOK = not useReject or touchNow
            if z.entered and exitOK and rejOK and chOK and touchOK
                res := 1                              // exit + konfirmasi → sinyal

        if res == 0
            if not z.hidden
                box.set_right(z.bx, bar_index)
        else if res == 2
            f_delZone(z)
            array.remove(zones, zi)
        else
            if onlyLast
                f_clearFinished()
            float entry = close
            if not z.hidden
                box.set_right(z.bx, bar_index)
                f_dropLevels(z)
            Trade tr = f_addTrade(z.dir, entry, z.slp)
            array.push(trades, tr)
            array.push(fin, z)
            array.remove(zones, zi)
            nSig += 1
            string cf = ""
            if useReject
                if isLong
                    cf := cf + (bullPin ? "PinBar " : "") + (bullEng ? "Engulfing " : "")
                else
                    cf := cf + (bearPin ? "PinBar " : "") + (bearEng ? "Engulfing " : "")
            if useChoch
                cf := cf + "CHoCH(" + effChochTF + ")"
            if isLong
                longSig := true
            else
                shortSig := true
            sigEntry := entry
            sigSL    := z.slp
            sigTP1   := tr.t1
            sigTP2   := tr.t2
            sigTP3   := tr.t3
            sigConf  := cf
        zi -= 1

    if array.size(fin) > 200
        array.shift(fin)
    if array.size(trades) > 200
        array.shift(trades)

    // ── 4) Lanjutkan spike berjalan; finalisasi saat impuls berakhir ───────────
    if not na(cBull)
        bool cont = f_step(cBull, 1, open, high, low, close)
        if not cont or bar_index - cBull.orig >= effMaxImp
            if f_tryZone(cBull, 1, atrV)
                bullSetup := true
            cBull := na
    if not na(cBear)
        bool cont = f_step(cBear, -1, open, high, low, close)
        if not cont or bar_index - cBear.orig >= effMaxImp
            if f_tryZone(cBear, -1, atrV)
                bearSetup := true
            cBear := na

    // ── 5) Break struktur bullish → mulai kandidat spike (demand) ──────────────
    if not na(swH) and not swHUsed and close > swH
        swHUsed := true
        if na(cBull) and close > open
            int n = math.min(bar_index - swHIdx - 1, 490)
            float oLow = low
            int oOff = 0
            if n >= 1
                for k = 1 to n
                    if low[k] < oLow
                        oLow := low[k]
                        oOff := k
            if oOff >= 1 and oOff <= effMaxImp
                Cand c = Cand.new(orig = bar_index - oOff, distal = oLow, oOpp = high[oOff], brkEdge = na, brkFound = false, run = 0, maxRun = 0, ext = high[oOff])
                for k = oOff - 1 to 0
                    f_step(c, 1, open[k], high[k], low[k], close[k])
                cBull := c

    // ── 6) Break struktur bearish → mulai kandidat spike (supply) ──────────────
    if not na(swL) and not swLUsed and close < swL
        swLUsed := true
        if na(cBear) and close < open
            int n = math.min(bar_index - swLIdx - 1, 490)
            float oHigh = high
            int oOff = 0
            if n >= 1
                for k = 1 to n
                    if high[k] > oHigh
                        oHigh := high[k]
                        oOff := k
            if oOff >= 1 and oOff <= effMaxImp
                Cand c = Cand.new(orig = bar_index - oOff, distal = oHigh, oOpp = low[oOff], brkEdge = na, brkFound = false, run = 0, maxRun = 0, ext = low[oOff])
                for k = oOff - 1 to 0
                    f_step(c, -1, open[k], high[k], low[k], close[k])
                cBear := c

// ═════════════════════════════════════ PLOT ═════════════════════════════════════
bool showSig = dispMode == "Signal"
plotshape(showSig and longSig, "Sinyal Long", shape.triangleup, location.belowbar, sigBull, text = "Sinyal Long", textcolor = sigBull, size = size.tiny)
plotshape(showSig and shortSig, "Sinyal Short", shape.triangledown, location.abovebar, sigBear, text = "Sinyal Short", textcolor = sigBear, size = size.tiny)

// ═════════════════════════════════════ ALERT ════════════════════════════════════
alertcondition(longSig, "TRH Sinyal Long", "TRH Sinyal Long di {{ticker}} ({{interval}})")
alertcondition(shortSig, "TRH Sinyal Short", "TRH Sinyal Short di {{ticker}} ({{interval}})")
alertcondition(bullSetup, "TRH Setup Long", "TRH Setup Long (zona demand) di {{ticker}} ({{interval}})")
alertcondition(bearSetup, "TRH Setup Short", "TRH Setup Short (zona supply) di {{ticker}} ({{interval}})")

if useAlert and barstate.isconfirmed
    string lv = " | Entry " + str.tostring(sigEntry, format.mintick) + " | SL " + str.tostring(sigSL, format.mintick) + " | " + (useMulti ? "TP1 " : "TP ") + str.tostring(sigTP1, format.mintick) + (w2 > 0 ? " | TP2 " + str.tostring(sigTP2, format.mintick) : "") + (w3 > 0 ? " | TP3 " + str.tostring(sigTP3, format.mintick) : "") + (sigConf != "" ? " | " + sigConf : "")
    if longSig
        alert("TRH SINYAL LONG | " + syminfo.ticker + " | " + timeframe.period + lv, alert.freq_once_per_bar_close)
    if shortSig
        alert("TRH SINYAL SHORT | " + syminfo.ticker + " | " + timeframe.period + lv, alert.freq_once_per_bar_close)
    if dispMode == "Setup"
        if bullSetup
            alert("TRH SETUP LONG (zona demand) | " + syminfo.ticker + " | " + timeframe.period, alert.freq_once_per_bar_close)
        if bearSetup
            alert("TRH SETUP SHORT (zona supply) | " + syminfo.ticker + " | " + timeframe.period, alert.freq_once_per_bar_close)

// ═════════════════════════════════════ STATISTIK ════════════════════════════════
if showStats and barstate.islast
    var table tb = table.new(position.top_right, 2, 8, bgcolor = color.new(chart.fg_color, 90), frame_color = color.gray, frame_width = 1, border_width = 1)
    int   closed = nWin + nLoss + nBE
    float wr     = closed > 0 ? nWin * 100.0 / closed : 0.0
    table.cell(tb, 0, 0, "Statistik TRH", text_color = chart.fg_color, text_size = size.small)
    table.cell(tb, 1, 0, "", text_color = chart.fg_color)
    table.cell(tb, 0, 1, "Sinyal", text_color = chart.fg_color, text_size = size.small)
    table.cell(tb, 1, 1, str.tostring(nSig), text_color = chart.fg_color, text_size = size.small)
    table.cell(tb, 0, 2, "Menang", text_color = chart.fg_color, text_size = size.small)
    table.cell(tb, 1, 2, str.tostring(nWin), text_color = cTP, text_size = size.small)
    table.cell(tb, 0, 3, "Break-even", text_color = chart.fg_color, text_size = size.small)
    table.cell(tb, 1, 3, str.tostring(nBE), text_color = chart.fg_color, text_size = size.small)
    table.cell(tb, 0, 4, "Kalah", text_color = chart.fg_color, text_size = size.small)
    table.cell(tb, 1, 4, str.tostring(nLoss), text_color = cSL, text_size = size.small)
    table.cell(tb, 0, 5, "Win rate", text_color = chart.fg_color, text_size = size.small)
    table.cell(tb, 1, 5, str.tostring(wr, "#.##") + "%", text_color = chart.fg_color, text_size = size.small)
    table.cell(tb, 0, 6, "Net R", text_color = chart.fg_color, text_size = size.small)
    table.cell(tb, 1, 6, f_sgn(netR), text_color = netR >= 0 ? cTP : cSL, text_size = size.small)
    table.cell(tb, 0, 7, "Net P/L", text_color = chart.fg_color, text_size = size.small)
    table.cell(tb, 1, 7, f_money(netR * riskAmt), text_color = netR >= 0 ? cTP : cSL, text_size = size.small)

Change History

Date Author Description of Change
20-07-2026 Admin Release Full Script