//@version=6

strategy(
     title="CNPS40 V1.2 VP VALUE AREA FIXED CORE",
     shorttitle="CNPS40 V1.2 VPVA",
     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=false,
     calc_on_order_fills=true,
     calc_on_every_tick=false,
     max_boxes_count=50,
     max_lines_count=50,
     max_labels_count=100,
     max_bars_back=5000
)


// ============================================================================
// CNPS40 V1.2 VP VALUE AREA FIXED CORE
//
// IMPORTANT FIXES
//
// 1. max_bars_back = 5000
//
// 2. FIX VALUE AREA ORIENTATION
//
// Price rows increase with price.
//
// During Value Area expansion:
// - lowerCursor moves toward LOWER price rows
// - upperCursor moves toward HIGHER price rows
//
// Therefore:
// - VAL must come from the lower-price cursor
// - VAH must come from the higher-price cursor
//
// 3. Runtime-friendly VN30 M1 defaults:
// - Daily profile
// - 8 rolling sessions
// - 40 rows
//
// SOURCE TRADE LOGIC PRESERVED:
//
// FADE LONG:
// Previous low < VAL
// Current close > VAL
// Current close > previous close
// RSI <= 40
// Target = POC
//
// FADE SHORT:
// Previous high > VAH
// Current close < VAH
// Current close < previous close
// RSI >= 60
// Target = POC
//
// BREAKOUT LONG:
// Close crosses above VAH
// Volume > SMA20 x 1.5
//
// BREAKOUT SHORT:
// Close crosses below VAL
// Volume > SMA20 x 1.5
//
// BREAKOUT TARGET:
// nearest HVN / naked POC
// otherwise fallback 1.8R
//
// CNPS:
// - 100M
// - 3 contracts
// - Commission 10k / contract
// - Slippage 3
// - Entry 09:00 -> 14:30
// - Force close 14:45
// ============================================================================


// ============================================================================
// SECTION 1: PROFILE
// ============================================================================

profileTF = input.timeframe(
     "D",
     title="Profile Period",
     options=["D", "W"],
     group="Profile"
)

lookbackPeriods = input.int(
     8,
     title="Rolling Lookback Periods",
     minval=3,
     maxval=30,
     group="Profile"
)

rowsNb = input.int(
     40,
     title="Rows Per Profile",
     minval=20,
     maxval=100,
     group="Profile"
)

vaPct = input.float(
     70.0,
     title="Value Area %",
     minval=10.0,
     maxval=100.0,
     step=1.0,
     group="Profile"
)


// ============================================================================
// SECTION 2: HVN
// ============================================================================

peakPct = input.float(
     30.0,
     title="Peak Threshold %",
     minval=1.0,
     maxval=100.0,
     group="HVN"
)

peakWin = input.int(
     3,
     title="Peak Window",
     minval=1,
     maxval=10,
     group="HVN"
)

minProm = input.float(
     15.0,
     title="Minimum Prominence %",
     minval=0.0,
     maxval=100.0,
     group="HVN"
)

zonePct = input.float(
     50.0,
     title="Zone Width %",
     minval=10.0,
     maxval=100.0,
     group="HVN"
)

maxZones = input.int(
     4,
     title="Maximum HVN Zones",
     minval=1,
     maxval=10,
     group="HVN"
)


// ============================================================================
// SECTION 3: SIGNAL MODE
// ============================================================================

tradeMode = input.string(
     "Fade only",
     title="Trade Mode",
     options=[
         "Fade only",
         "Breakout only",
         "Both"
     ],
     group="Signals"
)

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

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

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


// ============================================================================
// SECTION 4: BREAKOUT
// ============================================================================

volMult = input.float(
     1.5,
     title="Breakout Volume x Average",
     minval=1.0,
     step=0.1,
     group="Signals"
)

volAvgLen = input.int(
     20,
     title="Volume Average Length",
     minval=5,
     group="Signals"
)


// ============================================================================
// SECTION 5: FADE
// ============================================================================

rsiLen = input.int(
     14,
     title="RSI Length",
     minval=2,
     group="Signals"
)

rsiOB = input.int(
     60,
     title="Fade Short RSI",
     minval=50,
     maxval=90,
     group="Signals"
)

rsiOS = input.int(
     40,
     title="Fade Long RSI",
     minval=10,
     maxval=50,
     group="Signals"
)


// ============================================================================
// SECTION 6: SOURCE RISK
// ============================================================================

pivotLen = input.int(
     5,
     title="Swing Pivot Length",
     minval=2,
     maxval=30,
     group="Risk Management"
)

atrLen = input.int(
     14,
     title="ATR Length",
     minval=2,
     group="Risk Management"
)

atrStopMult = input.float(
     1.2,
     title="ATR Stop Multiplier",
     minval=0.2,
     step=0.1,
     group="Risk Management"
)

fallbackR = input.float(
     1.8,
     title="Breakout Fallback R",
     minval=0.5,
     maxval=5.0,
     step=0.1,
     group="Risk Management"
)

useNakedPOC = input.bool(
     true,
     title="Use Naked POC Target?",
     group="Risk Management"
)


// ============================================================================
// SECTION 7: OPTIONAL RISK BAND
// ============================================================================

minimumRiskPoints = input.float(
     0.0,
     title="Minimum Risk Points (0 = Off)",
     minval=0.0,
     step=0.5,
     group="Risk Management"
)

maximumRiskPoints = input.float(
     0.0,
     title="Maximum Risk Points (0 = Off)",
     minval=0.0,
     step=0.5,
     group="Risk Management"
)


// ============================================================================
// SECTION 8: TRADE CONTROL
// ============================================================================

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

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


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

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

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

entryStartMinute = 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"
)

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

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


// ============================================================================
// SECTION 10: ALERTS
// ============================================================================

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


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

showProfileLevels = input.bool(
     true,
     title="Show VAH / POC / VAL?",
     group="Display"
)

showSignals = input.bool(
     false,
     title="Show Signal Markers?",
     group="Display"
)

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

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


// ============================================================================
// SECTION 12: GLOBAL STORAGE
// ============================================================================

var float[] barHi = array.new_float()

var float[] barLo = array.new_float()

var float[] barVol = array.new_float()

var int[] sessStartIdx = array.new_int()

var float profPOC = na

var float profVAH = na

var float profVAL = na

var float[] zoneLo = array.new_float()

var float[] zoneHi = array.new_float()

var float[] zoneVol = array.new_float()

var float[] nakedPOCs = array.new_float()


// ============================================================================
// SECTION 13: SESSION POC FUNCTION
// ============================================================================

f_pocOnly(int offA, int offB) =>

    float profileLow = na

    float profileHigh = na


    for i = offA to offB

        float currentLow = array.get(barLo, i)

        float currentHigh = array.get(barHi, i)

        profileLow := na(profileLow) ? currentLow : math.min(profileLow, currentLow)

        profileHigh := na(profileHigh) ? currentHigh : math.max(profileHigh, currentHigh)


    float stepSize = (profileHigh - profileLow) / rowsNb


    if stepSize <= 0

        stepSize := syminfo.mintick


    float[] rowVolume = array.new_float(
         rowsNb,
         0.0
    )


    for i = offA to offB

        float currentHigh = array.get(barHi, i)

        float currentLow = array.get(barLo, i)

        float currentVolume = array.get(barVol, i)

        int firstRow = int(math.max(0, math.min(rowsNb - 1, math.floor((currentLow - profileLow) / stepSize))))

        int lastRow = int(math.max(0, math.min(rowsNb - 1, math.floor((currentHigh - profileLow) / stepSize))))

        float sharedVolume = currentVolume / (lastRow - firstRow + 1)


        for row = firstRow to lastRow

            array.set(
                 rowVolume,
                 row,
                 array.get(rowVolume, row) + sharedVolume
            )


    int pocRow = 0

    float pocVolume = 0.0


    for row = 0 to rowsNb - 1

        float rowValue = array.get(rowVolume, row)


        if rowValue > pocVolume

            pocVolume := rowValue

            pocRow := row


    float result = profileLow + (pocRow + 0.5) * stepSize

    result


// ============================================================================
// SECTION 14: BUILD PROFILE
// ============================================================================

f_buildProfile(int offA, int offB) =>

    float profileLow = na

    float profileHigh = na


    for i = offA to offB

        float currentLow = array.get(barLo, i)

        float currentHigh = array.get(barHi, i)

        profileLow := na(profileLow) ? currentLow : math.min(profileLow, currentLow)

        profileHigh := na(profileHigh) ? currentHigh : math.max(profileHigh, currentHigh)


    float stepSize = (profileHigh - profileLow) / rowsNb


    if stepSize <= 0

        stepSize := syminfo.mintick


    float[] rowVolume = array.new_float(
         rowsNb,
         0.0
    )


    // ------------------------------------------------------------------------
    // DISTRIBUTE BAR VOLUME
    // ------------------------------------------------------------------------

    for i = offA to offB

        float currentHigh = array.get(barHi, i)

        float currentLow = array.get(barLo, i)

        float currentVolume = array.get(barVol, i)

        int firstRow = int(math.max(0, math.min(rowsNb - 1, math.floor((currentLow - profileLow) / stepSize))))

        int lastRow = int(math.max(0, math.min(rowsNb - 1, math.floor((currentHigh - profileLow) / stepSize))))

        float sharedVolume = currentVolume / (lastRow - firstRow + 1)


        for row = firstRow to lastRow

            array.set(
                 rowVolume,
                 row,
                 array.get(rowVolume, row) + sharedVolume
            )


    float totalVolume = array.sum(rowVolume)

    float maximumVolume = array.max(rowVolume)


    // ------------------------------------------------------------------------
    // POC
    // ------------------------------------------------------------------------

    int pocRow = 0

    float pocVolume = 0.0


    for row = 0 to rowsNb - 1

        float rowValue = array.get(rowVolume, row)


        if rowValue > pocVolume

            pocVolume := rowValue

            pocRow := row


    // ------------------------------------------------------------------------
    // VALUE AREA
    //
    // LOWER INDEX = LOWER PRICE.
    // HIGHER INDEX = HIGHER PRICE.
    //
    // THIS SECTION FIXES THE ORIGINAL VAH / VAL ORIENTATION.
    // ------------------------------------------------------------------------

    float valueAreaTarget = totalVolume * vaPct / 100.0

    float valueAreaVolume = pocVolume

    int lowerCursor = pocRow - 1

    int upperCursor = pocRow + 1


    while valueAreaVolume < valueAreaTarget and (lowerCursor >= 0 or upperCursor < rowsNb)

        float lowerVolume = lowerCursor >= 0 ? array.get(rowVolume, lowerCursor) : -1.0

        float upperVolume = upperCursor < rowsNb ? array.get(rowVolume, upperCursor) : -1.0


        if lowerVolume < 0 and upperVolume < 0

            break


        if lowerVolume >= upperVolume

            valueAreaVolume += lowerVolume

            lowerCursor -= 1


        else

            valueAreaVolume += upperVolume

            upperCursor += 1


    int valRow = math.max(lowerCursor + 1, 0)

    int vahRow = math.min(upperCursor - 1, rowsNb - 1)

    float outputPOC = profileLow + (pocRow + 0.5) * stepSize

    float outputVAL = profileLow + valRow * stepSize

    float outputVAH = profileLow + (vahRow + 1) * stepSize


    // ------------------------------------------------------------------------
    // HVN ZONES
    // ------------------------------------------------------------------------

    array.clear(zoneLo)

    array.clear(zoneHi)

    array.clear(zoneVol)


    if maximumVolume > 0

        int[] peaks = array.new_int()


        for row = 0 to rowsNb - 1

            float currentVolume = array.get(rowVolume, row)


            if currentVolume >= maximumVolume * peakPct / 100.0

                bool isPeak = true

                int searchLow = math.max(0, row - peakWin)

                int searchHigh = math.min(rowsNb - 1, row + peakWin)


                for comparisonRow = searchLow to searchHigh

                    if comparisonRow != row

                        float comparisonVolume = array.get(rowVolume, comparisonRow)


                        if comparisonVolume > currentVolume or (comparisonVolume == currentVolume and comparisonRow < row)

                            isPeak := false


                if isPeak

                    array.push(peaks, row)


        if array.size(peaks) > 0

            float[] candidateLow = array.new_float()

            float[] candidateHigh = array.new_float()

            float[] candidateVolume = array.new_float()


            for peakIndex = 0 to array.size(peaks) - 1

                int peakRow = array.get(peaks, peakIndex)

                float peakVolume = array.get(rowVolume, peakRow)

                float minimumLeft = peakVolume

                int cursor = peakRow - 1


                while cursor >= 0

                    float cursorVolume = array.get(rowVolume, cursor)


                    if cursorVolume > peakVolume

                        break


                    minimumLeft := math.min(minimumLeft, cursorVolume)

                    cursor -= 1


                float minimumRight = peakVolume

                cursor := peakRow + 1


                while cursor < rowsNb

                    float cursorVolume = array.get(rowVolume, cursor)


                    if cursorVolume > peakVolume

                        break


                    minimumRight := math.min(minimumRight, cursorVolume)

                    cursor += 1


                float baseVolume = math.max(minimumLeft, minimumRight)

                float prominence = peakVolume - baseVolume


                if prominence >= peakVolume * minProm / 100.0

                    float volumeFloor = peakVolume - prominence * zonePct / 100.0

                    int zoneLowRow = peakRow


                    while zoneLowRow - 1 >= 0 and array.get(rowVolume, zoneLowRow - 1) >= volumeFloor

                        zoneLowRow -= 1


                    int zoneHighRow = peakRow


                    while zoneHighRow + 1 < rowsNb and array.get(rowVolume, zoneHighRow + 1) >= volumeFloor

                        zoneHighRow += 1


                    array.push(
                         candidateLow,
                         profileLow + zoneLowRow * stepSize
                    )

                    array.push(
                         candidateHigh,
                         profileLow + (zoneHighRow + 1) * stepSize
                    )

                    array.push(
                         candidateVolume,
                         peakVolume
                    )


            int candidateCount = array.size(candidateVolume)


            if candidateCount > 0

                int[] order = array.new_int()


                for index = 0 to candidateCount - 1

                    array.push(order, index)


                if array.size(order) > 1

                    for index = 1 to array.size(order) - 1

                        int comparisonIndex = index


                        while comparisonIndex > 0 and array.get(candidateVolume, array.get(order, comparisonIndex - 1)) < array.get(candidateVolume, array.get(order, comparisonIndex))

                            int temporaryIndex = array.get(order, comparisonIndex - 1)

                            array.set(
                                 order,
                                 comparisonIndex - 1,
                                 array.get(order, comparisonIndex)
                            )

                            array.set(
                                 order,
                                 comparisonIndex,
                                 temporaryIndex
                            )

                            comparisonIndex -= 1


                int keepCount = math.min(maxZones, candidateCount)


                for rankIndex = 0 to keepCount - 1

                    int sourceIndex = array.get(order, rankIndex)

                    array.push(
                         zoneLo,
                         array.get(candidateLow, sourceIndex)
                    )

                    array.push(
                         zoneHi,
                         array.get(candidateHigh, sourceIndex)
                    )

                    array.push(
                         zoneVol,
                         array.get(candidateVolume, sourceIndex)
                    )


    [
         outputPOC,
         outputVAH,
         outputVAL
    ]


// ============================================================================
// SECTION 15: TARGET HELPERS
// ============================================================================

f_nearestZoneAbove(float referencePrice) =>

    float best = na


    if array.size(zoneLo) > 0

        for index = 0 to array.size(zoneLo) - 1

            float midpoint = (array.get(zoneLo, index) + array.get(zoneHi, index)) / 2.0


            if midpoint > referencePrice and (na(best) or midpoint < best)

                best := midpoint


    best


f_nearestZoneBelow(float referencePrice) =>

    float best = na


    if array.size(zoneLo) > 0

        for index = 0 to array.size(zoneLo) - 1

            float midpoint = (array.get(zoneLo, index) + array.get(zoneHi, index)) / 2.0


            if midpoint < referencePrice and (na(best) or midpoint > best)

                best := midpoint


    best


f_nearestNakedAbove(float referencePrice) =>

    float best = na


    if array.size(nakedPOCs) > 0

        for index = 0 to array.size(nakedPOCs) - 1

            float level = array.get(nakedPOCs, index)


            if level > referencePrice and (na(best) or level < best)

                best := level


    best


f_nearestNakedBelow(float referencePrice) =>

    float best = na


    if array.size(nakedPOCs) > 0

        for index = 0 to array.size(nakedPOCs) - 1

            float level = array.get(nakedPOCs, index)


            if level < referencePrice and (na(best) or level > best)

                best := level


    best


f_pickNearer(float firstTarget, float secondTarget, float referencePrice) =>

    float result = na


    if na(firstTarget) and na(secondTarget)

        result := na


    else if na(firstTarget)

        result := secondTarget


    else if na(secondTarget)

        result := firstTarget


    else

        float firstDistance = math.abs(firstTarget - referencePrice)

        float secondDistance = math.abs(secondTarget - referencePrice)

        result := firstDistance <= secondDistance ? firstTarget : secondTarget


    result


// ============================================================================
// SECTION 16: PROFILE PERIOD CHANGE
// ============================================================================

bool newProfilePeriod = timeframe.change(profileTF)


// ============================================================================
// SECTION 17: FINALIZE CLOSED PROFILE PERIOD
// ============================================================================

if newProfilePeriod

    // ------------------------------------------------------------------------
    // SAVE CLOSED SESSION POC
    // ------------------------------------------------------------------------

    if array.size(sessStartIdx) > 0

        int previousStart = array.get(
             sessStartIdx,
             array.size(sessStartIdx) - 1
        )

        int previousEnd = array.size(barHi) - 1


        if previousEnd >= previousStart

            float closedPOC = f_pocOnly(
                 previousStart,
                 previousEnd
            )


            if useNakedPOC

                array.push(
                     nakedPOCs,
                     closedPOC
                )


                if array.size(nakedPOCs) > 30

                    array.shift(nakedPOCs)


    // ------------------------------------------------------------------------
    // BUILD ROLLING PROFILE FROM CLOSED PERIODS
    // ------------------------------------------------------------------------

    int sessionCount = array.size(sessStartIdx)


    if sessionCount > 0

        int firstSession = math.max(
             0,
             sessionCount - lookbackPeriods
        )

        int profileStart = array.get(
             sessStartIdx,
             firstSession
        )

        int profileEnd = array.size(barHi) - 1


        if profileEnd > profileStart

            [
                 calculatedPOC,
                 calculatedVAH,
                 calculatedVAL
            ] = f_buildProfile(
                 profileStart,
                 profileEnd
            )


            profPOC := calculatedPOC

            profVAH := calculatedVAH

            profVAL := calculatedVAL


    // ------------------------------------------------------------------------
    // NEW PERIOD START INDEX
    // ------------------------------------------------------------------------

    array.push(
         sessStartIdx,
         array.size(barHi)
    )


    // ------------------------------------------------------------------------
    // PRUNE OLD BAR STORAGE
    // ------------------------------------------------------------------------

    if array.size(sessStartIdx) > lookbackPeriods + 2

        int sessionsToDrop = array.size(sessStartIdx) - (lookbackPeriods + 2)

        int cutOffset = array.get(
             sessStartIdx,
             sessionsToDrop
        )


        if cutOffset > 0

            barHi := array.copy(
                 array.slice(
                     barHi,
                     cutOffset,
                     array.size(barHi)
                 )
            )

            barLo := array.copy(
                 array.slice(
                     barLo,
                     cutOffset,
                     array.size(barLo)
                 )
            )

            barVol := array.copy(
                 array.slice(
                     barVol,
                     cutOffset,
                     array.size(barVol)
                 )
            )


            for index = 0 to array.size(sessStartIdx) - 1

                array.set(
                     sessStartIdx,
                     index,
                     array.get(sessStartIdx, index) - cutOffset
                )


        for dropIndex = 1 to sessionsToDrop

            array.shift(sessStartIdx)


// ============================================================================
// SECTION 18: STORE CURRENT BAR
// ============================================================================

array.push(
     barHi,
     high
)

array.push(
     barLo,
     low
)

array.push(
     barVol,
     nz(volume)
)


// ============================================================================
// SECTION 19: REMOVE TOUCHED NAKED POC
// ============================================================================

if array.size(nakedPOCs) > 0

    int nakedIndex = array.size(nakedPOCs) - 1


    while nakedIndex >= 0

        float nakedLevel = array.get(
             nakedPOCs,
             nakedIndex
        )


        if high >= nakedLevel and low <= nakedLevel

            array.remove(
                 nakedPOCs,
                 nakedIndex
            )


        nakedIndex -= 1


// ============================================================================
// SECTION 20: SWING STRUCTURE
// ============================================================================

float swingHighValue = ta.pivothigh(
     high,
     pivotLen,
     pivotLen
)

float swingLowValue = ta.pivotlow(
     low,
     pivotLen,
     pivotLen
)

var float lastSwingHigh = na

var float lastSwingLow = na


if not na(swingHighValue)

    lastSwingHigh := swingHighValue


if not na(swingLowValue)

    lastSwingLow := swingLowValue


// ============================================================================
// SECTION 21: FILTERS
// ============================================================================

float volumeAverage = ta.sma(
     volume,
     volAvgLen
)

float rsiValue = ta.rsi(
     close,
     rsiLen
)

float atrValue = ta.atr(
     atrLen
)

bool haveProfile = (
     not na(profPOC) and
     not na(profVAH) and
     not na(profVAL) and
     profVAL < profPOC and
     profPOC < profVAH
)


// ============================================================================
// SECTION 22: SOURCE FADE SIGNAL
// ============================================================================

bool fadeLongSetup = (
     haveProfile and
     low[1] < profVAL and
     close > profVAL and
     close < profPOC and
     close > close[1] and
     rsiValue <= rsiOS
)


bool fadeShortSetup = (
     haveProfile and
     high[1] > profVAH and
     close < profVAH and
     close > profPOC and
     close < close[1] and
     rsiValue >= rsiOB
)


// ============================================================================
// SECTION 23: BREAKOUT SIGNAL
// ============================================================================

bool breakoutLongSetup = (
     haveProfile and
     ta.crossover(close, profVAH) and
     not na(volumeAverage) and
     volume > volumeAverage * volMult
)


bool breakoutShortSetup = (
     haveProfile and
     ta.crossunder(close, profVAL) and
     not na(volumeAverage) and
     volume > volumeAverage * volMult
)


// ============================================================================
// SECTION 24: MODE
// ============================================================================

bool allowFade = tradeMode != "Breakout only"

bool allowBreakout = tradeMode != "Fade only"

bool useBreakoutLong = allowBreakout and breakoutLongSetup

bool useBreakoutShort = allowBreakout and breakoutShortSetup

bool sourceLongSignal = (allowFade and fadeLongSetup) or useBreakoutLong

bool sourceShortSignal = (allowFade and fadeShortSetup) or useBreakoutShort


// ============================================================================
// SECTION 25: LONG LEVELS
// ============================================================================

float prospectiveLongStop = na

float prospectiveLongTarget = na

float prospectiveLongRisk = na

string prospectiveLongType = ""


if sourceLongSignal

    if useBreakoutLong

        prospectiveLongType := "BREAKOUT"

        float atrStop = profVAH - atrStopMult * atrValue

        prospectiveLongStop := na(lastSwingLow) ? atrStop : math.min(atrStop, lastSwingLow)


    else

        prospectiveLongType := "FADE"

        float atrStop = low - atrStopMult * atrValue * 0.5

        prospectiveLongStop := na(lastSwingLow) ? atrStop : math.min(atrStop, lastSwingLow)


    prospectiveLongRisk := close - prospectiveLongStop


    if prospectiveLongRisk > 0

        if useBreakoutLong

            float fallbackTarget = close + prospectiveLongRisk * fallbackR

            float hvnTarget = f_nearestZoneAbove(close)

            float nakedTarget = useNakedPOC ? f_nearestNakedAbove(close) : na

            float structuralTarget = f_pickNearer(
                 hvnTarget,
                 nakedTarget,
                 close
            )

            prospectiveLongTarget := na(structuralTarget) ? fallbackTarget : structuralTarget


        else

            prospectiveLongTarget := profPOC


// ============================================================================
// SECTION 26: SHORT LEVELS
// ============================================================================

float prospectiveShortStop = na

float prospectiveShortTarget = na

float prospectiveShortRisk = na

string prospectiveShortType = ""


if sourceShortSignal

    if useBreakoutShort

        prospectiveShortType := "BREAKOUT"

        float atrStop = profVAL + atrStopMult * atrValue

        prospectiveShortStop := na(lastSwingHigh) ? atrStop : math.max(atrStop, lastSwingHigh)


    else

        prospectiveShortType := "FADE"

        float atrStop = high + atrStopMult * atrValue * 0.5

        prospectiveShortStop := na(lastSwingHigh) ? atrStop : math.max(atrStop, lastSwingHigh)


    prospectiveShortRisk := prospectiveShortStop - close


    if prospectiveShortRisk > 0

        if useBreakoutShort

            float fallbackTarget = close - prospectiveShortRisk * fallbackR

            float hvnTarget = f_nearestZoneBelow(close)

            float nakedTarget = useNakedPOC ? f_nearestNakedBelow(close) : na

            float structuralTarget = f_pickNearer(
                 hvnTarget,
                 nakedTarget,
                 close
            )

            prospectiveShortTarget := na(structuralTarget) ? fallbackTarget : structuralTarget


        else

            prospectiveShortTarget := profPOC


// ============================================================================
// SECTION 27: TARGET VALIDATION
// ============================================================================

bool longTargetOK = (
     not na(prospectiveLongTarget) and
     prospectiveLongTarget > close
)

bool shortTargetOK = (
     not na(prospectiveShortTarget) and
     prospectiveShortTarget < close
)


// ============================================================================
// SECTION 28: RISK VALIDATION
// ============================================================================

bool longRiskOK = (
     not na(prospectiveLongRisk) and
     prospectiveLongRisk > syminfo.mintick and
     longTargetOK
)

bool shortRiskOK = (
     not na(prospectiveShortRisk) and
     prospectiveShortRisk > syminfo.mintick and
     shortTargetOK
)


if longRiskOK and minimumRiskPoints > 0

    longRiskOK := prospectiveLongRisk >= minimumRiskPoints


if longRiskOK and maximumRiskPoints > 0

    longRiskOK := prospectiveLongRisk <= maximumRiskPoints


if shortRiskOK and minimumRiskPoints > 0

    shortRiskOK := prospectiveShortRisk >= minimumRiskPoints


if shortRiskOK and maximumRiskPoints > 0

    shortRiskOK := prospectiveShortRisk <= maximumRiskPoints


// ============================================================================
// SECTION 29: SESSION
// ============================================================================

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

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

int entryStartTotal = entryStartHour * 60 + entryStartMinute

int lastEntryTotal = lastEntryHour * 60 + lastEntryMinute

int forceCloseTotal = forceCloseHour * 60 + forceCloseMinute

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


// ============================================================================
// SECTION 30: DAILY 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 31: 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 32: COOLDOWN
// ============================================================================

var int lastExitBar = na


if positionJustClosed

    lastExitBar := bar_index


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

bool tradeLimitOK = maximumTradesPerDay == 0 or tradesToday < maximumTradesPerDay


// ============================================================================
// SECTION 33: FINAL ENTRY SIGNALS
// ============================================================================

bool commonEntryOK = (
     flat and
     entrySession and
     currentCloseMinute < forceCloseTotal and
     cooldownOK and
     tradeLimitOK and
     barstate.isconfirmed
)


bool finalLongSignal = (
     commonEntryOK and
     allowLong and
     sourceLongSignal and
     longRiskOK
)


bool finalShortSignal = (
     commonEntryOK and
     allowShort and
     sourceShortSignal and
     shortRiskOK and
     not finalLongSignal
)


// ============================================================================
// SECTION 34: PENDING TRADE LEVELS
// ============================================================================

var float pendingStop = na

var float pendingTarget = na

var string pendingSetup = ""

var float activeStop = na

var float activeTarget = na

var string activeSetup = ""

var int activeEntryBar = na


// ============================================================================
// SECTION 35: LONG ENTRY
// ============================================================================

if finalLongSignal

    pendingStop := prospectiveLongStop

    pendingTarget := prospectiveLongTarget

    pendingSetup := prospectiveLongType


    strategy.entry(
         "Long",
         strategy.long,
         qty=3,
         comment=prospectiveLongType == "BREAKOUT" ? "VP BO L" : "VP FADE L",
         alert_message=prospectiveLongType == "BREAKOUT" ? "ENTRY_LONG_BREAKOUT_CNPS40" : "ENTRY_LONG_FADE_CNPS40"
    )


    strategy.exit(
         "Long Exit",
         from_entry="Long",
         stop=prospectiveLongStop,
         limit=prospectiveLongTarget,
         alert_profit="EXIT_LONG_TP_CNPS40",
         alert_loss="EXIT_LONG_SL_CNPS40",
         disable_alert=disableSLTPAlerts
    )


    tradesToday += 1


// ============================================================================
// SECTION 36: SHORT ENTRY
// ============================================================================

if finalShortSignal

    pendingStop := prospectiveShortStop

    pendingTarget := prospectiveShortTarget

    pendingSetup := prospectiveShortType


    strategy.entry(
         "Short",
         strategy.short,
         qty=3,
         comment=prospectiveShortType == "BREAKOUT" ? "VP BO S" : "VP FADE S",
         alert_message=prospectiveShortType == "BREAKOUT" ? "ENTRY_SHORT_BREAKOUT_CNPS40" : "ENTRY_SHORT_FADE_CNPS40"
    )


    strategy.exit(
         "Short Exit",
         from_entry="Short",
         stop=prospectiveShortStop,
         limit=prospectiveShortTarget,
         alert_profit="EXIT_SHORT_TP_CNPS40",
         alert_loss="EXIT_SHORT_SL_CNPS40",
         disable_alert=disableSLTPAlerts
    )


    tradesToday += 1


// ============================================================================
// SECTION 37: CAPTURE ACTIVE LONG
// ============================================================================

if newLongPosition

    activeStop := pendingStop

    activeTarget := pendingTarget

    activeSetup := pendingSetup

    activeEntryBar := bar_index

    pendingStop := na

    pendingTarget := na

    pendingSetup := ""


// ============================================================================
// SECTION 38: CAPTURE ACTIVE SHORT
// ============================================================================

if newShortPosition

    activeStop := pendingStop

    activeTarget := pendingTarget

    activeSetup := pendingSetup

    activeEntryBar := bar_index

    pendingStop := na

    pendingTarget := na

    pendingSetup := ""


// ============================================================================
// SECTION 39: CLEANUP
// ============================================================================

if positionJustClosed

    activeStop := na

    activeTarget := na

    activeSetup := ""

    activeEntryBar := na


// ============================================================================
// SECTION 40: FORCE CLOSE
// ============================================================================

bool forceCloseBar = currentMinute < forceCloseTotal and currentCloseMinute >= forceCloseTotal


if forceCloseBar and strategy.position_size != 0

    strategy.close_all(
         immediately=true,
         comment="EOD",
         alert_message="SESSION_END_CNPS40"
    )


// ============================================================================
// SECTION 41: PROFILE PLOTS
// ============================================================================

plot(
     showProfileLevels ? profVAH : na,
     title="VAH",
     color=color.blue,
     linewidth=1,
     style=plot.style_stepline
)


plot(
     showProfileLevels ? profPOC : na,
     title="POC",
     color=color.orange,
     linewidth=2,
     style=plot.style_stepline
)


plot(
     showProfileLevels ? profVAL : na,
     title="VAL",
     color=color.blue,
     linewidth=1,
     style=plot.style_stepline
)


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

plot(
     showActiveSLTP and strategy.position_size != 0 ? activeStop : na,
     title="Active Stop",
     color=color.red,
     linewidth=2,
     style=plot.style_linebr
)


plot(
     showActiveSLTP and strategy.position_size != 0 ? activeTarget : na,
     title="Active Target",
     color=color.green,
     linewidth=2,
     style=plot.style_linebr
)


// ============================================================================
// SECTION 43: SIGNAL MARKERS
// ============================================================================

plotshape(
     showSignals and finalLongSignal,
     title="Long Signal",
     style=shape.triangleup,
     location=location.belowbar,
     color=color.green,
     size=size.small,
     text="L"
)


plotshape(
     showSignals and finalShortSignal,
     title="Short Signal",
     style=shape.triangledown,
     location=location.abovebar,
     color=color.red,
     size=size.small,
     text="S"
)


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

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


if showDiagnostics and barstate.islast

    string valueAreaState = "NO PROFILE"


    if haveProfile

        valueAreaState := "VALID"


    else if not na(profVAH) and not na(profVAL)

        valueAreaState := "INVALID VA"


    string positionText = "FLAT"


    if inLong

        positionText := "LONG"


    else if inShort

        positionText := "SHORT"


    float relativeVolume = na


    if not na(volumeAverage) and volumeAverage > 0

        relativeVolume := volume / volumeAverage


    table.cell(diagnostics, 0, 0, "CNPS40 V1.2")
    table.cell(diagnostics, 1, 0, timeframe.period)

    table.cell(diagnostics, 0, 1, "Profile")
    table.cell(diagnostics, 1, 1, valueAreaState)

    table.cell(diagnostics, 0, 2, "VAH")
    table.cell(diagnostics, 1, 2, str.tostring(profVAH, format.mintick))

    table.cell(diagnostics, 0, 3, "POC")
    table.cell(diagnostics, 1, 3, str.tostring(profPOC, format.mintick))

    table.cell(diagnostics, 0, 4, "VAL")
    table.cell(diagnostics, 1, 4, str.tostring(profVAL, format.mintick))

    table.cell(diagnostics, 0, 5, "RSI")
    table.cell(diagnostics, 1, 5, str.tostring(rsiValue, "0.0"))

    table.cell(diagnostics, 0, 6, "Rel Volume")
    table.cell(diagnostics, 1, 6, str.tostring(relativeVolume, "0.00"))

    table.cell(diagnostics, 0, 7, "HVN")
    table.cell(diagnostics, 1, 7, str.tostring(array.size(zoneLo)))

    table.cell(diagnostics, 0, 8, "Naked POC")
    table.cell(diagnostics, 1, 8, str.tostring(array.size(nakedPOCs)))

    table.cell(diagnostics, 0, 9, "Fade L")
    table.cell(diagnostics, 1, 9, fadeLongSetup ? "YES" : "NO")

    table.cell(diagnostics, 0, 10, "Fade S")
    table.cell(diagnostics, 1, 10, fadeShortSetup ? "YES" : "NO")

    table.cell(diagnostics, 0, 11, "BO L")
    table.cell(diagnostics, 1, 11, breakoutLongSetup ? "YES" : "NO")

    table.cell(diagnostics, 0, 12, "BO S")
    table.cell(diagnostics, 1, 12, breakoutShortSetup ? "YES" : "NO")

    table.cell(diagnostics, 0, 13, "Trades Today")
    table.cell(
         diagnostics,
         1,
         13,
         maximumTradesPerDay == 0 ? str.tostring(tradesToday) + " / INF" : str.tostring(tradesToday) + " / " + str.tostring(maximumTradesPerDay)
    )

    table.cell(diagnostics, 0, 14, "Position")
    table.cell(diagnostics, 1, 14, positionText)


// ============================================================================
// SECTION 45: TIMEFRAME CHECK
// ============================================================================

if barstate.islast and timeframe.in_seconds() >= timeframe.in_seconds(profileTF)

    runtime.error(
         "CNPS40: Chart timeframe must be lower than Profile Period."
    )