//@version=6
strategy(
     title="CNPS23 VOLUME ORDER BLOCK RETEST",
     shorttitle="CNPS23 VOB RETEST",
     overlay=true,
     initial_capital=100000000,
     default_qty_type=strategy.fixed,
     default_qty_value=3,
     pyramiding=0,
     margin_long=0,
     margin_short=0,
     slippage=3,
     commission_type=strategy.commission.cash_per_contract,
     commission_value=10000,
     fill_orders_on_standard_ohlc=true,
     process_orders_on_close=true,
     calc_on_order_fills=true,
     max_labels_count=300,
     max_lines_count=300,
     max_bars_back=5000
)


// ============================================================================
// CNPS23 VOLUME ORDER BLOCK RETEST
//
// SOURCE LOGIC:
//
// 1. Detect displacement + volume impulse
// 2. Create an Order Block from the candle BEFORE the impulse
// 3. Keep multiple active zones
// 4. Wait for price to retest an active zone
// 5. Optional:
//      - Directional candle
//      - Rejection wick
//      - Retest volume dry-up
//      - EMA trend filter
// 6. One trade per zone by default
//
// CNPS EXECUTION:
// - Current chart timeframe
// - 100M initial capital
// - 3 contracts
// - Commission: VND 10,000 / contract
// - Slippage: 3 ticks
// - Entry: 09:00 -> 14:30
// - Force close: 14:45
//
// DEFAULT EXIT:
// - Fixed SL = 5 points
// - Fixed TP = 15 points
// ============================================================================


// ============================================================================
// SECTION 1: TRADE DIRECTION
// ============================================================================

tradeDirection = input.string(
     "Both",
     title="Trade Direction",
     options=["Long", "Short", "Both"],
     group="Trade Control"
)

bool allowLong = tradeDirection == "Long" or tradeDirection == "Both"
bool allowShort = tradeDirection == "Short" or tradeDirection == "Both"

maximumTradesPerDay = input.int(
     0,
     title="Maximum Trades Per Day (0 = Unlimited)",
     minval=0,
     maxval=50,
     group="Trade Control"
)

cooldownBars = input.int(
     0,
     title="Cooldown Bars After Entry",
     minval=0,
     maxval=100,
     group="Trade Control"
)


// ============================================================================
// SECTION 2: ORDER BLOCK DETECTION
// ============================================================================

atrLength = input.int(
     14,
     title="ATR Length",
     minval=1,
     group="Order Block Detection"
)

displacementMultiplier = input.float(
     0.8,
     title="Displacement Body >= ATR x",
     minval=0.1,
     step=0.1,
     group="Order Block Detection"
)

volumeLength = input.int(
     20,
     title="Volume SMA Length",
     minval=1,
     group="Order Block Detection"
)

impulseVolumeMultiplier = input.float(
     1.0,
     title="Impulse Volume >= SMA x",
     minval=0.0,
     step=0.1,
     group="Order Block Detection"
)

requireOppositeCandle = input.bool(
     false,
     title="Require Opposite Candle Before Impulse?",
     group="Order Block Detection"
)

requireImpulseBreak = input.bool(
     false,
     title="Require Impulse Close Beyond Previous Range?",
     group="Order Block Detection"
)

useBodyOnlyZone = input.bool(
     false,
     title="Use Previous Candle Body Only For Zone?",
     group="Order Block Detection"
)

maximumActiveZones = input.int(
     15,
     title="Maximum Active Zones",
     minval=1,
     maxval=40,
     group="Order Block Detection"
)

maximumZoneAge = input.int(
     300,
     title="Maximum Zone Age (Bars)",
     minval=10,
     group="Order Block Detection"
)


// ============================================================================
// SECTION 3: RETEST FILTERS
// ============================================================================

firstTouchOnly = input.bool(
     true,
     title="Each Zone Trades Once?",
     group="Retest Filters"
)

requireDirectionCandle = input.bool(
     true,
     title="Require Candle In Trade Direction?",
     group="Retest Filters"
)

useRejectionWick = input.bool(
     false,
     title="Require Rejection Wick?",
     group="Retest Filters"
)

minimumWickPercent = input.float(
     25.0,
     title="Minimum Rejection Wick %",
     minval=5.0,
     maxval=95.0,
     step=1.0,
     group="Retest Filters"
)

useRetestVolumeFilter = input.bool(
     false,
     title="Require Volume Dry-Up On Retest?",
     group="Retest Filters"
)

retestVolumeMaximum = input.float(
     1.0,
     title="Retest Volume <= Impulse Volume x",
     minval=0.1,
     step=0.05,
     group="Retest Filters"
)

useTrendFilter = input.bool(
     false,
     title="Use EMA Trend Filter?",
     group="Retest Filters"
)

trendEMALength = input.int(
     100,
     title="EMA Length",
     minval=2,
     group="Retest Filters"
)


// ============================================================================
// SECTION 4: EXIT MODE
// ============================================================================

exitMode = input.string(
     "Fixed Points",
     title="Exit Mode",
     options=["Fixed Points", "Zone RR"],
     group="Risk Management"
)


// ============================================================================
// SECTION 5: FIXED POINT EXIT
// ============================================================================

stopLossPoints = input.float(
     5.0,
     title="Fixed Stop Loss (Points)",
     minval=0.5,
     step=0.5,
     group="Risk Management"
)

takeProfitPoints = input.float(
     15.0,
     title="Fixed Take Profit (Points)",
     minval=0.5,
     step=0.5,
     group="Risk Management"
)


// ============================================================================
// SECTION 6: SOURCE-STYLE ZONE RR
// ============================================================================

zoneStopATRBuffer = input.float(
     0.2,
     title="Zone Stop Buffer (ATR x)",
     minval=0.0,
     step=0.05,
     group="Zone RR"
)

zoneRewardRisk = input.float(
     1.5,
     title="Zone Take Profit (R)",
     minval=0.2,
     step=0.1,
     group="Zone RR"
)


// ============================================================================
// SECTION 7: OPTIONAL BREAK EVEN
// ============================================================================

useBreakEven = input.bool(
     false,
     title="Use Break Even?",
     group="Break Even"
)

breakEvenTriggerR = input.float(
     1.0,
     title="Break Even Trigger (R)",
     minval=0.2,
     step=0.1,
     group="Break Even"
)

breakEvenOffsetPoints = input.float(
     0.0,
     title="Break Even Offset (Points)",
     minval=0.0,
     step=0.5,
     group="Break Even"
)


// ============================================================================
// SECTION 8: OPTIONAL TIME STOP
// ============================================================================

maximumBarsInTrade = input.int(
     0,
     title="Time Stop Bars (0 = Off)",
     minval=0,
     maxval=500,
     group="Time Stop"
)


// ============================================================================
// SECTION 9: SESSION
// ============================================================================

strategyTimezone = input.string(
     "Asia/Ho_Chi_Minh",
     title="Strategy Timezone",
     group="Session"
)

sessionStartHour = input.int(
     9,
     title="Entry Start Hour",
     minval=0,
     maxval=23,
     group="Session"
)

sessionStartMinute = input.int(
     0,
     title="Entry Start Minute",
     minval=0,
     maxval=59,
     group="Session"
)

lastEntryHour = input.int(
     14,
     title="Last Entry Hour",
     minval=0,
     maxval=23,
     group="Session"
)

lastEntryMinute = input.int(
     30,
     title="Last Entry Minute",
     minval=0,
     maxval=59,
     group="Session"
)

sessionEndHour = input.int(
     14,
     title="Force Close Hour",
     minval=0,
     maxval=23,
     group="Session"
)

sessionEndMinute = input.int(
     45,
     title="Force Close Minute",
     minval=0,
     maxval=59,
     group="Session"
)


// ============================================================================
// SECTION 10: ALERT CONTROL
// ============================================================================

disableSLTPAlerts = input.bool(
     false,
     title="Disable SL / TP Alerts?",
     group="Alerts"
)


// ============================================================================
// SECTION 11: DISPLAY
// ============================================================================

showTrendEMA = input.bool(
     false,
     title="Show Trend EMA?",
     group="Display"
)

showImpulseMarkers = input.bool(
     false,
     title="Show Impulse Markers?",
     group="Display"
)

showRetestMarkers = input.bool(
     false,
     title="Show Retest Signals?",
     group="Display"
)

showSelectedZone = input.bool(
     false,
     title="Show Selected Signal Zone?",
     group="Display"
)

showActiveSLTP = input.bool(
     true,
     title="Show Active SL / TP?",
     group="Display"
)

showDiagnostics = input.bool(
     false,
     title="Show Diagnostics?",
     group="Display"
)


// ============================================================================
// SECTION 12: TIME ENGINE
// ============================================================================

int currentMinute = hour(time, strategyTimezone) * 60 + minute(time, strategyTimezone)

int currentCloseMinute = hour(time_close, strategyTimezone) * 60 + minute(time_close, strategyTimezone)

int sessionStartTotal = sessionStartHour * 60 + sessionStartMinute

int lastEntryTotal = lastEntryHour * 60 + lastEntryMinute

int sessionEndTotal = sessionEndHour * 60 + sessionEndMinute

bool entrySession = currentMinute >= sessionStartTotal and currentMinute <= lastEntryTotal


// ============================================================================
// SECTION 13: DAILY TRADE COUNTER
// ============================================================================

int dayKey = year(time, strategyTimezone) * 10000 + month(time, strategyTimezone) * 100 + dayofmonth(time, strategyTimezone)

bool newTradingDay = bar_index == 0 or dayKey != nz(dayKey[1], dayKey)

var int tradesToday = 0

if newTradingDay
    tradesToday := 0


// ============================================================================
// SECTION 14: POSITION STATE
// ============================================================================

bool flat = strategy.position_size == 0

bool inLong = strategy.position_size > 0

bool inShort = strategy.position_size < 0

bool newLongPosition = inLong and strategy.position_size[1] <= 0

bool newShortPosition = inShort and strategy.position_size[1] >= 0

bool positionJustClosed = flat and strategy.position_size[1] != 0


// ============================================================================
// SECTION 15: COOLDOWN / TRADE LIMIT
// ============================================================================

var int lastEntryBar = na

bool cooldownOK = na(lastEntryBar) or bar_index - lastEntryBar > cooldownBars

bool tradeLimitOK = maximumTradesPerDay == 0 or tradesToday < maximumTradesPerDay

bool commonEntryApproval = (
     flat and
     entrySession and
     currentCloseMinute < sessionEndTotal and
     cooldownOK and
     tradeLimitOK and
     barstate.isconfirmed
)


// ============================================================================
// SECTION 16: CORE CALCULATIONS
// ============================================================================

float atrValue = ta.atr(
     atrLength
)

float volumeSMA = ta.sma(
     volume,
     volumeLength
)

float trendEMA = ta.ema(
     close,
     trendEMALength
)

float candleBody = math.abs(close - open)

float candleRange = high - low

float upperWick = high - math.max(open, close)

float lowerWick = math.min(open, close) - low


// ============================================================================
// SECTION 17: IMPULSE DETECTION
// ============================================================================

float previousATR = nz(
     atrValue[1]
)

float previousVolumeSMA = nz(
     volumeSMA[1]
)

bool largeBody = candleBody >= previousATR * displacementMultiplier

bool largeVolume = volume >= previousVolumeSMA * impulseVolumeMultiplier


bool bullishImpulse = (
     close > open and
     largeBody and
     largeVolume and
     (
         not requireImpulseBreak or
         close > high[1]
     ) and
     (
         not requireOppositeCandle or
         close[1] < open[1]
     )
)


bool bearishImpulse = (
     close < open and
     largeBody and
     largeVolume and
     (
         not requireImpulseBreak or
         close < low[1]
     ) and
     (
         not requireOppositeCandle or
         close[1] > open[1]
     )
)


// ============================================================================
// SECTION 18: ORDER BLOCK ARRAYS
// ============================================================================
//
// Every array index represents one active Order Block.
//
// zoneTop
// zoneBottom
// zoneImpulseVolume
// zoneBornBar
// zoneBull
// zoneUsed
//
// ============================================================================

var array<float> zoneTop = array.new<float>()

var array<float> zoneBottom = array.new<float>()

var array<float> zoneImpulseVolume = array.new<float>()

var array<int> zoneBornBar = array.new<int>()

var array<bool> zoneBull = array.new<bool>()

var array<bool> zoneUsed = array.new<bool>()


// ============================================================================
// SECTION 19: SIGNAL STATE FOR CURRENT BAR
// ============================================================================

bool longSignal = false

bool shortSignal = false

float signalZoneTop = na

float signalZoneBottom = na

float signalImpulseVolume = na

int signalZoneIndex = na


// ============================================================================
// SECTION 20: MANAGE EXISTING ZONES
// ============================================================================
//
// Source logic:
//
// Bull zone dies:
//     Close < Zone Bottom
//
// Bear zone dies:
//     Close > Zone Top
//
// Any zone dies:
//     Age > Maximum Zone Age
//
// Touch:
//     High >= Bottom
//     Low <= Top
//
// Search newest zone first.
//
// ============================================================================

int zoneIndex = array.size(zoneTop) - 1

while zoneIndex >= 0

    float zTop = array.get(
         zoneTop,
         zoneIndex
    )

    float zBottom = array.get(
         zoneBottom,
         zoneIndex
    )

    float zImpulseVolume = array.get(
         zoneImpulseVolume,
         zoneIndex
    )

    int zBornBar = array.get(
         zoneBornBar,
         zoneIndex
    )

    bool zBull = array.get(
         zoneBull,
         zoneIndex
    )

    bool zUsed = array.get(
         zoneUsed,
         zoneIndex
    )


    bool zoneDead = (
         (
             zBull and
             close < zBottom
         ) or
         (
             not zBull and
             close > zTop
         ) or
         (
             bar_index - zBornBar >
             maximumZoneAge
         )
    )


    if zoneDead

        array.remove(
             zoneTop,
             zoneIndex
        )

        array.remove(
             zoneBottom,
             zoneIndex
        )

        array.remove(
             zoneImpulseVolume,
             zoneIndex
        )

        array.remove(
             zoneBornBar,
             zoneIndex
        )

        array.remove(
             zoneBull,
             zoneIndex
        )

        array.remove(
             zoneUsed,
             zoneIndex
        )


    else

        bool zoneTouched = (
             high >= zBottom and
             low <= zTop
        )


        bool zoneAvailable = (
             not firstTouchOnly or
             not zUsed
        )


        if (
             zoneTouched and
             commonEntryApproval and
             zoneAvailable and
             not longSignal and
             not shortSignal
        )

            bool retestVolumeOK = (
                 not useRetestVolumeFilter or
                 volume <=
                 zImpulseVolume *
                 retestVolumeMaximum
            )


            if zBull and allowLong

                bool rejectionOK = (
                     not useRejectionWick or
                     (
                         candleRange >
                         0 and
                         lowerWick /
                         candleRange *
                         100.0 >=
                         minimumWickPercent
                     )
                )


                bool trendOK = (
                     not useTrendFilter or
                     close >
                     trendEMA
                )


                bool directionCandleOK = (
                     not requireDirectionCandle or
                     close >
                     open
                )


                float candidateStop = zBottom - atrValue * zoneStopATRBuffer

                float candidateRisk = close - candidateStop


                bool zoneRiskOK = (
                     exitMode !=
                     "Zone RR" or
                     candidateRisk >
                     syminfo.mintick
                )


                bool longConditions = (
                     directionCandleOK and
                     close >
                     zBottom and
                     rejectionOK and
                     retestVolumeOK and
                     trendOK and
                     zoneRiskOK
                )


                if longConditions

                    longSignal := true

                    signalZoneTop := zTop

                    signalZoneBottom := zBottom

                    signalImpulseVolume := zImpulseVolume

                    signalZoneIndex := zoneIndex


            else if (
                 not zBull and
                 allowShort
            )

                bool rejectionOK = (
                     not useRejectionWick or
                     (
                         candleRange >
                         0 and
                         upperWick /
                         candleRange *
                         100.0 >=
                         minimumWickPercent
                     )
                )


                bool trendOK = (
                     not useTrendFilter or
                     close <
                     trendEMA
                )


                bool directionCandleOK = (
                     not requireDirectionCandle or
                     close <
                     open
                )


                float candidateStop = zTop + atrValue * zoneStopATRBuffer

                float candidateRisk = candidateStop - close


                bool zoneRiskOK = (
                     exitMode !=
                     "Zone RR" or
                     candidateRisk >
                     syminfo.mintick
                )


                bool shortConditions = (
                     directionCandleOK and
                     close <
                     zTop and
                     rejectionOK and
                     retestVolumeOK and
                     trendOK and
                     zoneRiskOK
                )


                if shortConditions

                    shortSignal := true

                    signalZoneTop := zTop

                    signalZoneBottom := zBottom

                    signalImpulseVolume := zImpulseVolume

                    signalZoneIndex := zoneIndex


    zoneIndex -= 1


// ============================================================================
// SECTION 21: CREATE BULLISH ORDER BLOCK
// ============================================================================
//
// A bullish impulse creates a bullish OB using the PREVIOUS candle.
//
// Full-zone default:
//     Top    = High[1]
//     Bottom = Low[1]
//
// Body-only:
//     Top    = max(Open[1], Close[1])
//     Bottom = min(Open[1], Close[1])
//
// ============================================================================

if (
     bar_index >
     math.max(
         atrLength,
         volumeLength
     ) +
     1 and
     bullishImpulse and
     allowLong
)

    float newBullTop = (
         useBodyOnlyZone
         ? math.max(
             open[1],
             close[1]
         )
         : high[1]
    )


    float newBullBottom = (
         useBodyOnlyZone
         ? math.min(
             open[1],
             close[1]
         )
         : low[1]
    )


    bool overlapsExistingBull = false

    int bullCheckIndex = 0

    while bullCheckIndex < array.size(zoneTop)

        bool existingBull = array.get(
             zoneBull,
             bullCheckIndex
        )

        float existingTop = array.get(
             zoneTop,
             bullCheckIndex
        )

        float existingBottom = array.get(
             zoneBottom,
             bullCheckIndex
        )


        if (
             existingBull and
             newBullTop >=
             existingBottom and
             newBullBottom <=
             existingTop
        )

            overlapsExistingBull := true


        bullCheckIndex += 1


    if not overlapsExistingBull

        array.push(
             zoneTop,
             newBullTop
        )

        array.push(
             zoneBottom,
             newBullBottom
        )

        array.push(
             zoneImpulseVolume,
             volume
        )

        array.push(
             zoneBornBar,
             bar_index
        )

        array.push(
             zoneBull,
             true
        )

        array.push(
             zoneUsed,
             false
        )


// ============================================================================
// SECTION 22: CREATE BEARISH ORDER BLOCK
// ============================================================================

if (
     bar_index >
     math.max(
         atrLength,
         volumeLength
     ) +
     1 and
     bearishImpulse and
     allowShort
)

    float newBearTop = (
         useBodyOnlyZone
         ? math.max(
             open[1],
             close[1]
         )
         : high[1]
    )


    float newBearBottom = (
         useBodyOnlyZone
         ? math.min(
             open[1],
             close[1]
         )
         : low[1]
    )


    bool overlapsExistingBear = false

    int bearCheckIndex = 0

    while bearCheckIndex < array.size(zoneTop)

        bool existingBull = array.get(
             zoneBull,
             bearCheckIndex
        )

        float existingTop = array.get(
             zoneTop,
             bearCheckIndex
        )

        float existingBottom = array.get(
             zoneBottom,
             bearCheckIndex
        )


        if (
             not existingBull and
             newBearTop >=
             existingBottom and
             newBearBottom <=
             existingTop
        )

            overlapsExistingBear := true


        bearCheckIndex += 1


    if not overlapsExistingBear

        array.push(
             zoneTop,
             newBearTop
        )

        array.push(
             zoneBottom,
             newBearBottom
        )

        array.push(
             zoneImpulseVolume,
             volume
        )

        array.push(
             zoneBornBar,
             bar_index
        )

        array.push(
             zoneBull,
             false
        )

        array.push(
             zoneUsed,
             false
        )


// ============================================================================
// SECTION 23: LIMIT NUMBER OF ACTIVE ZONES
// ============================================================================

while array.size(zoneTop) > maximumActiveZones

    array.shift(
         zoneTop
    )

    array.shift(
         zoneBottom
    )

    array.shift(
         zoneImpulseVolume
    )

    array.shift(
         zoneBornBar
    )

    array.shift(
         zoneBull
    )

    array.shift(
         zoneUsed
    )


// ============================================================================
// SECTION 24: FINAL SIGNALS
// ============================================================================
//
// Only one signal can be selected because zones were scanned newest-first.
//
// ============================================================================

bool finalLongSignal = longSignal and not shortSignal

bool finalShortSignal = shortSignal and not longSignal


// ============================================================================
// SECTION 25: PENDING SOURCE-STYLE STOPS
// ============================================================================

var float pendingLongZoneStop = na

var float pendingShortZoneStop = na


// ============================================================================
// SECTION 26: LONG ENTRY
// ============================================================================

if finalLongSignal

    if exitMode == "Zone RR"

        pendingLongZoneStop := signalZoneBottom - atrValue * zoneStopATRBuffer

    else

        pendingLongZoneStop := na


    strategy.entry(
         "Long",
         strategy.long,
         qty=3,
         comment="VOB Retest Long",
         alert_message="ENTRY_LONG_CNPS23"
    )


    if (
         firstTouchOnly and
         not na(signalZoneIndex) and
         signalZoneIndex >=
         0 and
         signalZoneIndex <
         array.size(zoneUsed)
    )

        array.set(
             zoneUsed,
             signalZoneIndex,
             true
        )


    lastEntryBar := bar_index

    tradesToday += 1


// ============================================================================
// SECTION 27: SHORT ENTRY
// ============================================================================

if finalShortSignal

    if exitMode == "Zone RR"

        pendingShortZoneStop := signalZoneTop + atrValue * zoneStopATRBuffer

    else

        pendingShortZoneStop := na


    strategy.entry(
         "Short",
         strategy.short,
         qty=3,
         comment="VOB Retest Short",
         alert_message="ENTRY_SHORT_CNPS23"
    )


    if (
         firstTouchOnly and
         not na(signalZoneIndex) and
         signalZoneIndex >=
         0 and
         signalZoneIndex <
         array.size(zoneUsed)
    )

        array.set(
             zoneUsed,
             signalZoneIndex,
             true
        )


    lastEntryBar := bar_index

    tradesToday += 1


// ============================================================================
// SECTION 28: ACTIVE TRADE STATE
// ============================================================================

var float activeEntryPrice = na

var float activeInitialRisk = na

var float activeLongSL = na

var float activeLongTP = na

var float activeShortSL = na

var float activeShortTP = na

var bool breakEvenActivated = false

var int activeEntryBar = na


// ============================================================================
// SECTION 29: INITIALIZE LONG EXIT
// ============================================================================

if newLongPosition

    activeEntryPrice := strategy.position_avg_price


    if exitMode == "Zone RR"

        float sourceLongRisk = activeEntryPrice - pendingLongZoneStop

        if (
             na(sourceLongRisk) or
             sourceLongRisk <=
             syminfo.mintick
        )

            sourceLongRisk := stopLossPoints


        activeInitialRisk := sourceLongRisk

        activeLongSL := activeEntryPrice - activeInitialRisk

        activeLongTP := activeEntryPrice + activeInitialRisk * zoneRewardRisk


    else

        activeInitialRisk := stopLossPoints

        activeLongSL := activeEntryPrice - stopLossPoints

        activeLongTP := activeEntryPrice + takeProfitPoints


    activeShortSL := na

    activeShortTP := na

    breakEvenActivated := false

    activeEntryBar := bar_index

    pendingLongZoneStop := na


// ============================================================================
// SECTION 30: INITIALIZE SHORT EXIT
// ============================================================================

if newShortPosition

    activeEntryPrice := strategy.position_avg_price


    if exitMode == "Zone RR"

        float sourceShortRisk = pendingShortZoneStop - activeEntryPrice

        if (
             na(sourceShortRisk) or
             sourceShortRisk <=
             syminfo.mintick
        )

            sourceShortRisk := stopLossPoints


        activeInitialRisk := sourceShortRisk

        activeShortSL := activeEntryPrice + activeInitialRisk

        activeShortTP := activeEntryPrice - activeInitialRisk * zoneRewardRisk


    else

        activeInitialRisk := stopLossPoints

        activeShortSL := activeEntryPrice + stopLossPoints

        activeShortTP := activeEntryPrice - takeProfitPoints


    activeLongSL := na

    activeLongTP := na

    breakEvenActivated := false

    activeEntryBar := bar_index

    pendingShortZoneStop := na


// ============================================================================
// SECTION 31: LONG BREAK EVEN
// ============================================================================

if (
     inLong and
     useBreakEven and
     not breakEvenActivated and
     not na(activeEntryPrice) and
     not na(activeInitialRisk) and
     activeInitialRisk >
     0
)

    float longBreakEvenTrigger = activeEntryPrice + activeInitialRisk * breakEvenTriggerR


    if high >= longBreakEvenTrigger

        float newLongStop = activeEntryPrice + breakEvenOffsetPoints


        if (
             na(activeLongSL) or
             newLongStop >
             activeLongSL
        )

            activeLongSL := newLongStop


        breakEvenActivated := true


// ============================================================================
// SECTION 32: SHORT BREAK EVEN
// ============================================================================

if (
     inShort and
     useBreakEven and
     not breakEvenActivated and
     not na(activeEntryPrice) and
     not na(activeInitialRisk) and
     activeInitialRisk >
     0
)

    float shortBreakEvenTrigger = activeEntryPrice - activeInitialRisk * breakEvenTriggerR


    if low <= shortBreakEvenTrigger

        float newShortStop = activeEntryPrice - breakEvenOffsetPoints


        if (
             na(activeShortSL) or
             newShortStop <
             activeShortSL
        )

            activeShortSL := newShortStop


        breakEvenActivated := true


// ============================================================================
// SECTION 33: LONG EXIT
// ============================================================================

if (
     inLong and
     not na(activeLongSL) and
     not na(activeLongTP)
)

    strategy.exit(
         "Long Exit",
         from_entry="Long",
         stop=activeLongSL,
         limit=activeLongTP,
         alert_profit="EXIT_LONG_TP_CNPS23",
         alert_loss="EXIT_LONG_SL_CNPS23",
         disable_alert=disableSLTPAlerts
    )


// ============================================================================
// SECTION 34: SHORT EXIT
// ============================================================================

if (
     inShort and
     not na(activeShortSL) and
     not na(activeShortTP)
)

    strategy.exit(
         "Short Exit",
         from_entry="Short",
         stop=activeShortSL,
         limit=activeShortTP,
         alert_profit="EXIT_SHORT_TP_CNPS23",
         alert_loss="EXIT_SHORT_SL_CNPS23",
         disable_alert=disableSLTPAlerts
    )


// ============================================================================
// SECTION 35: OPTIONAL TIME STOP
// ============================================================================

int barsInTrade = 0

if (
     not flat and
     not na(activeEntryBar)
)

    barsInTrade := bar_index - activeEntryBar


bool longTimeStop = (
     inLong and
     maximumBarsInTrade >
     0 and
     barsInTrade >=
     maximumBarsInTrade
)

bool shortTimeStop = (
     inShort and
     maximumBarsInTrade >
     0 and
     barsInTrade >=
     maximumBarsInTrade
)


if longTimeStop

    strategy.close(
         "Long",
         comment="Time Stop Long",
         alert_message="TIME_EXIT_LONG_CNPS23"
    )


if shortTimeStop

    strategy.close(
         "Short",
         comment="Time Stop Short",
         alert_message="TIME_EXIT_SHORT_CNPS23"
    )


// ============================================================================
// SECTION 36: POSITION CLEANUP
// ============================================================================

if positionJustClosed

    activeEntryPrice := na

    activeInitialRisk := na

    activeLongSL := na

    activeLongTP := na

    activeShortSL := na

    activeShortTP := na

    breakEvenActivated := false

    activeEntryBar := na


// ============================================================================
// SECTION 37: SESSION END
// ============================================================================

bool finalSessionBar = currentMinute < sessionEndTotal and currentCloseMinute >= sessionEndTotal


if finalSessionBar

    if strategy.position_size != 0

        strategy.close_all(
             immediately=true,
             comment="Session End",
             alert_message="SESSION_END_CNPS23"
        )


// ============================================================================
// SECTION 38: TREND EMA DISPLAY
// ============================================================================

plot(
     showTrendEMA and useTrendFilter
     ? trendEMA
     : na,
     title="Trend EMA",
     color=color.gray,
     linewidth=1
)


// ============================================================================
// SECTION 39: IMPULSE MARKERS
// ============================================================================

plotshape(
     showImpulseMarkers and bullishImpulse,
     title="Bullish Impulse",
     style=shape.triangleup,
     location=location.belowbar,
     color=color.teal,
     size=size.tiny,
     text="I"
)

plotshape(
     showImpulseMarkers and bearishImpulse,
     title="Bearish Impulse",
     style=shape.triangledown,
     location=location.abovebar,
     color=color.red,
     size=size.tiny,
     text="I"
)


// ============================================================================
// SECTION 40: RETEST SIGNAL MARKERS
// ============================================================================

plotshape(
     showRetestMarkers and finalLongSignal,
     title="Long Retest",
     style=shape.triangleup,
     location=location.belowbar,
     color=color.green,
     size=size.small,
     text="L",
     textcolor=color.white
)

plotshape(
     showRetestMarkers and finalShortSignal,
     title="Short Retest",
     style=shape.triangledown,
     location=location.abovebar,
     color=color.red,
     size=size.small,
     text="S",
     textcolor=color.white
)


// ============================================================================
// SECTION 41: SELECTED SIGNAL ZONE
// ============================================================================

plot(
     showSelectedZone
     ? signalZoneTop
     : na,
     title="Signal Zone Top",
     color=color.new(color.orange, 30),
     linewidth=1,
     style=plot.style_linebr
)

plot(
     showSelectedZone
     ? signalZoneBottom
     : na,
     title="Signal Zone Bottom",
     color=color.new(color.orange, 30),
     linewidth=1,
     style=plot.style_linebr
)


// ============================================================================
// SECTION 42: ACTIVE SL / TP
// ============================================================================

plot(
     showActiveSLTP and inLong
     ? activeLongSL
     : na,
     title="Long SL",
     color=color.red,
     linewidth=2,
     style=plot.style_linebr
)

plot(
     showActiveSLTP and inLong
     ? activeLongTP
     : na,
     title="Long TP",
     color=color.green,
     linewidth=2,
     style=plot.style_linebr
)

plot(
     showActiveSLTP and inShort
     ? activeShortSL
     : na,
     title="Short SL",
     color=color.red,
     linewidth=2,
     style=plot.style_linebr
)

plot(
     showActiveSLTP and inShort
     ? activeShortTP
     : na,
     title="Short TP",
     color=color.green,
     linewidth=2,
     style=plot.style_linebr
)


// ============================================================================
// SECTION 43: DIAGNOSTIC COUNTERS
// ============================================================================

var int bullishImpulseCount = 0

var int bearishImpulseCount = 0

var int longSignalCount = 0

var int shortSignalCount = 0

var int longEntryCount = 0

var int shortEntryCount = 0


if bullishImpulse
    bullishImpulseCount += 1


if bearishImpulse
    bearishImpulseCount += 1


if longSignal
    longSignalCount += 1


if shortSignal
    shortSignalCount += 1


if finalLongSignal
    longEntryCount += 1


if finalShortSignal
    shortEntryCount += 1


// ============================================================================
// SECTION 44: DIAGNOSTICS
// ============================================================================

var table diagnostics = table.new(
     position.top_right,
     2,
     16,
     border_width=1
)


if showDiagnostics and barstate.islast

    string positionText = "FLAT"

    if inLong
        positionText := "LONG"

    else if inShort
        positionText := "SHORT"


    table.cell(
         diagnostics,
         0,
         0,
         "CNPS23"
    )

    table.cell(
         diagnostics,
         1,
         0,
         timeframe.period
    )


    table.cell(
         diagnostics,
         0,
         1,
         "Active Zones"
    )

    table.cell(
         diagnostics,
         1,
         1,
         str.tostring(
             array.size(zoneTop)
         )
    )


    table.cell(
         diagnostics,
         0,
         2,
         "Bull Impulse"
    )

    table.cell(
         diagnostics,
         1,
         2,
         str.tostring(
             bullishImpulseCount
         )
    )


    table.cell(
         diagnostics,
         0,
         3,
         "Bear Impulse"
    )

    table.cell(
         diagnostics,
         1,
         3,
         str.tostring(
             bearishImpulseCount
         )
    )


    table.cell(
         diagnostics,
         0,
         4,
         "Long Retests"
    )

    table.cell(
         diagnostics,
         1,
         4,
         str.tostring(
             longSignalCount
         )
    )


    table.cell(
         diagnostics,
         0,
         5,
         "Short Retests"
    )

    table.cell(
         diagnostics,
         1,
         5,
         str.tostring(
             shortSignalCount
         )
    )


    table.cell(
         diagnostics,
         0,
         6,
         "Long Entries"
    )

    table.cell(
         diagnostics,
         1,
         6,
         str.tostring(
             longEntryCount
         )
    )


    table.cell(
         diagnostics,
         0,
         7,
         "Short Entries"
    )

    table.cell(
         diagnostics,
         1,
         7,
         str.tostring(
             shortEntryCount
         )
    )


    table.cell(
         diagnostics,
         0,
         8,
         "ATR"
    )

    table.cell(
         diagnostics,
         1,
         8,
         str.tostring(
             atrValue,
             "0.00"
         )
    )


    table.cell(
         diagnostics,
         0,
         9,
         "Volume / SMA"
    )

    float volumeRatio = 0.0

    if volumeSMA > 0
        volumeRatio := volume / volumeSMA


    table.cell(
         diagnostics,
         1,
         9,
         str.tostring(
             volumeRatio,
             "0.00"
         )
    )


    table.cell(
         diagnostics,
         0,
         10,
         "Bull Impulse Now"
    )

    table.cell(
         diagnostics,
         1,
         10,
         bullishImpulse
         ? "YES"
         : "NO"
    )


    table.cell(
         diagnostics,
         0,
         11,
         "Bear Impulse Now"
    )

    table.cell(
         diagnostics,
         1,
         11,
         bearishImpulse
         ? "YES"
         : "NO"
    )


    table.cell(
         diagnostics,
         0,
         12,
         "Exit Mode"
    )

    table.cell(
         diagnostics,
         1,
         12,
         exitMode
    )


    table.cell(
         diagnostics,
         0,
         13,
         "Trades Today"
    )

    table.cell(
         diagnostics,
         1,
         13,
         str.tostring(
             tradesToday
         )
    )


    table.cell(
         diagnostics,
         0,
         14,
         "Bars In Trade"
    )

    table.cell(
         diagnostics,
         1,
         14,
         str.tostring(
             barsInTrade
         )
    )


    table.cell(
         diagnostics,
         0,
         15,
         "Position"
    )

    table.cell(
         diagnostics,
         1,
         15,
         positionText
    )