//@version=6
strategy(
     title="CNPS15 LIQ-SWEEP",
     shorttitle="CNPS15 LIQ-SWEEP",
     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
)


// ╔══════════════════════════════════════════════════════════════════════╗
// ║             CNPS18 LIQ-SWEEP-PULLBACK V2                          ║
// ║          SWEEP -> RECLAIM -> DISPLACEMENT -> PULLBACK             ║
// ╠══════════════════════════════════════════════════════════════════════╣
// ║ CHART          : M1                                                ║
// ║ POSITION       : 3 CONTRACTS                                       ║
// ║ COMMISSION     : VND 10,000 / CONTRACT                            ║
// ║                                                                    ║
// ║ LIQUIDITY LEVELS                                                    ║
// ║ - Previous Day High / Low                                          ║
// ║ - Opening Range High / Low 09:00-09:15                            ║
// ║                                                                    ║
// ║ SHORT                                                              ║
// ║ Sweep high liquidity                                               ║
// ║ -> Reclaim below level                                             ║
// ║ -> Bearish displacement                                            ║
// ║ -> FVG midpoint if available                                       ║
// ║    otherwise 50% displacement body                                 ║
// ║ -> First pullback                                                   ║
// ║ -> Rejection                                                       ║
// ║ -> SHORT                                                           ║
// ║                                                                    ║
// ║ LONG                                                               ║
// ║ Sweep low liquidity                                                ║
// ║ -> Reclaim above level                                             ║
// ║ -> Bullish displacement                                            ║
// ║ -> FVG midpoint if available                                       ║
// ║    otherwise 50% displacement body                                 ║
// ║ -> First pullback                                                   ║
// ║ -> Rejection                                                       ║
// ║ -> LONG                                                            ║
// ║                                                                    ║
// ║ DEFAULT EXIT                                                       ║
// ║ SL = 5 POINTS                                                      ║
// ║ TP = 15 POINTS                                                     ║
// ╚══════════════════════════════════════════════════════════════════════╝


// ============================================================================
// SECTION 1: LIQUIDITY LEVELS
// ============================================================================

usePreviousDayLevels = input.bool(
     true,
     title="Use Previous Day High / Low?",
     group="Liquidity Levels"
)

useOpeningRangeLevels = input.bool(
     true,
     title="Use Opening Range High / Low?",
     group="Liquidity Levels"
)

oneTradePerLevelPerDay = input.bool(
     true,
     title="Maximum One ENTRY Per Level / Day?",
     group="Liquidity Levels"
)


// ============================================================================
// SECTION 2: OPENING RANGE
// ============================================================================

openingRangeStartHour = input.int(
     9,
     title="Opening Range Start Hour",
     minval=0,
     maxval=23,
     group="Opening Range"
)

openingRangeStartMinute = input.int(
     0,
     title="Opening Range Start Minute",
     minval=0,
     maxval=59,
     group="Opening Range"
)

openingRangeEndHour = input.int(
     9,
     title="Opening Range End Hour",
     minval=0,
     maxval=23,
     group="Opening Range"
)

openingRangeEndMinute = input.int(
     15,
     title="Opening Range End Minute",
     minval=0,
     maxval=59,
     group="Opening Range"
)


// ============================================================================
// SECTION 3: LIQUIDITY SWEEP
// ============================================================================

minimumSweepPoints = input.float(
     0.25,
     title="Minimum Sweep Beyond Level",
     minval=0.0,
     step=0.25,
     group="Liquidity Sweep"
)

maximumSweepPoints = input.float(
     8.0,
     title="Maximum Sweep Beyond Level",
     minval=0.5,
     step=0.25,
     group="Liquidity Sweep"
)

returnInsideBars = input.int(
     3,
     title="Maximum Bars To Reclaim Level",
     minval=0,
     maxval=10,
     group="Liquidity Sweep"
)


// ============================================================================
// SECTION 4: DISPLACEMENT
// ============================================================================

atrLength = input.int(
     14,
     title="ATR Length",
     minval=5,
     maxval=50,
     group="Displacement"
)

displacementExpiryBars = input.int(
     4,
     title="Maximum Bars After Reclaim For Displacement",
     minval=1,
     maxval=15,
     group="Displacement"
)

minimumDisplacementBodyATR = input.float(
     0.55,
     title="Minimum Displacement Body / ATR",
     minval=0.1,
     maxval=3.0,
     step=0.05,
     group="Displacement"
)

minimumDisplacementBodyRatio = input.float(
     0.50,
     title="Minimum Displacement Body / Candle Range",
     minval=0.1,
     maxval=1.0,
     step=0.05,
     group="Displacement"
)

displacementCloseExtremePercent = input.float(
     35.0,
     title="Displacement Close Near Extreme (%)",
     minval=5.0,
     maxval=50.0,
     step=5.0,
     group="Displacement"
)


// ============================================================================
// SECTION 5: PULLBACK ZONE
// ============================================================================

preferFVGWhenAvailable = input.bool(
     true,
     title="Prefer FVG Midpoint When Available?",
     group="Pullback Zone"
)

minimumFVGSize = input.float(
     0.25,
     title="Minimum FVG Size",
     minval=0.0,
     step=0.25,
     group="Pullback Zone"
)

displacementRetracementPercent = input.float(
     50.0,
     title="Displacement Body Retracement (%)",
     minval=20.0,
     maxval=80.0,
     step=5.0,
     group="Pullback Zone"
)

pullbackExpiryBars = input.int(
     8,
     title="Maximum Bars To Wait For First Pullback",
     minval=1,
     maxval=30,
     group="Pullback Zone"
)

pullbackTouchTolerance = input.float(
     0.50,
     title="Pullback Touch Tolerance",
     minval=0.0,
     step=0.25,
     group="Pullback Zone"
)


// ============================================================================
// SECTION 6: PULLBACK INVALIDATION
// ============================================================================

liquidityLevelInvalidationBuffer = input.float(
     1.0,
     title="Close Invalidation Beyond Liquidity Level",
     minval=0.0,
     step=0.25,
     group="Pullback Invalidation"
)


// ============================================================================
// SECTION 7: REJECTION CONFIRMATION
// ============================================================================

requireDirectionalRejection = input.bool(
     false,
     title="Require Directional Rejection Candle?",
     group="Rejection"
)

minimumRejectionBodyRatio = input.float(
     0.10,
     title="Minimum Rejection Body / Range",
     minval=0.0,
     maxval=1.0,
     step=0.05,
     group="Rejection"
)

requireCloseBackBeyondPullbackLevel = input.bool(
     true,
     title="Require Close Back Beyond Pullback Level?",
     group="Rejection"
)

rejectionCloseExtremePercent = input.float(
     50.0,
     title="Rejection Close Near Extreme (%)",
     minval=20.0,
     maxval=50.0,
     step=5.0,
     group="Rejection"
)


// ============================================================================
// SECTION 8: RISK MANAGEMENT
// ============================================================================

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

manualSLPoints = input.float(
     5.0,
     title="Manual Stop Loss (Points)",
     minval=0.1,
     step=0.5,
     group="Risk Management"
)

manualTPPoints = input.float(
     15.0,
     title="Manual Take Profit (Points)",
     minval=0.1,
     step=0.5,
     group="Risk Management"
)

structureStopBuffer = input.float(
     0.50,
     title="Structure SL Buffer Beyond Sweep Extreme",
     minval=0.0,
     step=0.25,
     group="Risk Management"
)

structureRR = input.float(
     3.0,
     title="Structure TP Reward / Risk",
     minval=0.5,
     maxval=10.0,
     step=0.25,
     group="Risk Management"
)

maximumStructureRisk = input.float(
     12.0,
     title="Maximum Structure Risk",
     minval=1.0,
     step=0.5,
     group="Risk Management"
)


// ============================================================================
// SECTION 9: TRADE CONTROL
// ============================================================================

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

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


// ============================================================================
// SECTION 10: SESSION
// ============================================================================

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

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

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

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

resetSetupAcrossLargeGap = input.bool(
     true,
     title="Reset Active Setup Across Lunch / Large Gap?",
     group="Session"
)

maximumBarGapMinutes = input.int(
     5,
     title="Maximum Allowed Gap Between Bars",
     minval=2,
     maxval=60,
     group="Session"
)


// ============================================================================
// SECTION 11: ALERT CONTROL
// ============================================================================

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


// ============================================================================
// SECTION 12: DISPLAY
// ============================================================================

showLiquidityLevels = input.bool(
     true,
     title="Show Liquidity Levels?",
     group="Display"
)

showPullbackZone = input.bool(
     true,
     title="Show Active Pullback Level?",
     group="Display"
)

showFVG = input.bool(
     false,
     title="Show Optional FVG?",
     group="Display"
)

showSignalMarkers = input.bool(
     false,
     title="Show Setup Markers?",
     group="Display"
)

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

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


// ============================================================================
// SECTION 13: M1 / SESSION STATE
// ============================================================================

bool isM1Chart = (
     timeframe.in_seconds(timeframe.period) == 60
)

int sessionStartTotal = (
     sessionStartHour * 60 +
     sessionStartMinute
)

int sessionEndTotal = (
     sessionEndHour * 60 +
     sessionEndMinute
)

int openingRangeStartTotal = (
     openingRangeStartHour * 60 +
     openingRangeStartMinute
)

int openingRangeEndTotal = (
     openingRangeEndHour * 60 +
     openingRangeEndMinute
)

int currentOpenTotal = (
     hour(time) * 60 +
     minute(time)
)

int currentCloseTotal = (
     hour(time_close) * 60 +
     minute(time_close)
)

bool inSession = (
     currentOpenTotal >= sessionStartTotal and
     currentOpenTotal < sessionEndTotal
)

bool dayChanged = (
     bar_index > 0 and
     (
         year(time) != year(time[1]) or
         month(time) != month(time[1]) or
         dayofmonth(time) != dayofmonth(time[1])
     )
)

bool newTradingDay = (
     isM1Chart and
     inSession and
     (
         barstate.isfirst or
         dayChanged
     )
)

bool finalSessionBar = (
     isM1Chart and
     inSession and
     currentCloseTotal >= sessionEndTotal
)

bool allowEntry = (
     isM1Chart and
     inSession and
     currentCloseTotal < sessionEndTotal and
     barstate.isconfirmed
)

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

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

bool largeBarGap = (
     bar_index > 0 and
     time - time[1] >
     maximumBarGapMinutes *
     60 *
     1000
)


// ============================================================================
// SECTION 14: PREVIOUS DAY HIGH / LOW
// ============================================================================

float previousDayHigh = request.security(
     syminfo.tickerid,
     "D",
     high[1],
     gaps=barmerge.gaps_off,
     lookahead=barmerge.lookahead_on
)

float previousDayLow = request.security(
     syminfo.tickerid,
     "D",
     low[1],
     gaps=barmerge.gaps_off,
     lookahead=barmerge.lookahead_on
)


// ============================================================================
// SECTION 15: OPENING RANGE
// ============================================================================

var float openingRangeHigh = na
var float openingRangeLow = na

bool insideOpeningRange = (
     inSession and
     currentOpenTotal >= openingRangeStartTotal and
     currentOpenTotal < openingRangeEndTotal
)

bool openingRangeReady = (
     inSession and
     currentOpenTotal >= openingRangeEndTotal and
     not na(openingRangeHigh) and
     not na(openingRangeLow)
)


if newTradingDay

    openingRangeHigh := na
    openingRangeLow := na


if insideOpeningRange

    openingRangeHigh := (
         na(openingRangeHigh)
         ? high
         : math.max(
             openingRangeHigh,
             high
         )
    )

    openingRangeLow := (
         na(openingRangeLow)
         ? low
         : math.min(
             openingRangeLow,
             low
         )
    )


// ============================================================================
// SECTION 16: LEVEL USAGE FLAGS
// ============================================================================
//
// IMPORTANT:
//
// Levels are NOT marked used when a sweep occurs.
// They are marked used only after a real strategy ENTRY.
//
// 1 = PDH
// 2 = PDL
// 3 = ORH
// 4 = ORL
//
// ============================================================================

var bool tradedPDH = false
var bool tradedPDL = false
var bool tradedORH = false
var bool tradedORL = false


if newTradingDay

    tradedPDH := false
    tradedPDL := false
    tradedORH := false
    tradedORL := false


// ============================================================================
// SECTION 17: ATR / CANDLE METRICS
// ============================================================================

float atr = ta.atr(
     atrLength
)

float candleRange = (
     high -
     low
)

float candleBody = (
     math.abs(
         close -
         open
     )
)

float candleBodyRatio = (
     candleRange > 0
     ? candleBody /
       candleRange
     : 0.0
)

float closeLocation = (
     candleRange > 0
     ? (
         close -
         low
       ) /
       candleRange
     : 0.5
)


// ============================================================================
// SECTION 18: DISPLACEMENT QUALITY
// ============================================================================

bool bullishDisplacementQuality = (
     close > open and
     candleBody >=
     atr *
     minimumDisplacementBodyATR and
     candleBodyRatio >=
     minimumDisplacementBodyRatio and
     closeLocation >=
     1.0 -
     displacementCloseExtremePercent /
     100.0
)

bool bearishDisplacementQuality = (
     close < open and
     candleBody >=
     atr *
     minimumDisplacementBodyATR and
     candleBodyRatio >=
     minimumDisplacementBodyRatio and
     closeLocation <=
     displacementCloseExtremePercent /
     100.0
)


// ============================================================================
// SECTION 19: REJECTION QUALITY
// ============================================================================

bool bullishRejectionQuality = (
     (
         not requireDirectionalRejection or
         close > open
     ) and
     candleBodyRatio >=
     minimumRejectionBodyRatio and
     closeLocation >=
     1.0 -
     rejectionCloseExtremePercent /
     100.0
)

bool bearishRejectionQuality = (
     (
         not requireDirectionalRejection or
         close < open
     ) and
     candleBodyRatio >=
     minimumRejectionBodyRatio and
     closeLocation <=
     rejectionCloseExtremePercent /
     100.0
)


// ============================================================================
// SECTION 20: COOLDOWN
// ============================================================================

var int lastEntryBar = na

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


// ============================================================================
// SECTION 21: SETUP STATE
// ============================================================================
//
// setupDirection:
//
//  1 = LONG
// -1 = SHORT
//
// setupStage:
//
// 0 = Scan liquidity
// 1 = Sweep active -> wait reclaim
// 2 = Reclaimed -> wait displacement
// 3 = Displacement confirmed -> wait first pullback
//
// ============================================================================

var int setupDirection = 0
var int setupStage = 0

var int activeLevelId = 0
var float activeLevelPrice = na

var int sweepStartBar = na
var int reclaimBar = na
var int displacementBar = na

var float sweepExtreme = na

var float displacementOpen = na
var float displacementClose = na
var float displacementHigh = na
var float displacementLow = na

var float pullbackEntryLevel = na

var bool fvgDetected = false
var float fvgLower = na
var float fvgUpper = na


// ============================================================================
// SECTION 22: DIAGNOSTIC COUNTERS
// ============================================================================

var int sweepCount = 0
var int reclaimCount = 0
var int displacementCount = 0
var int fvgCount = 0
var int pullbackCount = 0
var int entryCount = 0


// ============================================================================
// SECTION 23: DAILY RESET
// ============================================================================

if newTradingDay

    setupDirection := 0
    setupStage := 0

    activeLevelId := 0
    activeLevelPrice := na

    sweepStartBar := na
    reclaimBar := na
    displacementBar := na

    sweepExtreme := na

    displacementOpen := na
    displacementClose := na
    displacementHigh := na
    displacementLow := na

    pullbackEntryLevel := na

    fvgDetected := false
    fvgLower := na
    fvgUpper := na

    lastEntryBar := na


// ============================================================================
// SECTION 24: LARGE GAP RESET
// ============================================================================

if (
     resetSetupAcrossLargeGap and
     largeBarGap and
     setupStage != 0
)

    setupDirection := 0
    setupStage := 0

    activeLevelId := 0
    activeLevelPrice := na

    sweepStartBar := na
    reclaimBar := na
    displacementBar := na

    sweepExtreme := na

    displacementOpen := na
    displacementClose := na
    displacementHigh := na
    displacementLow := na

    pullbackEntryLevel := na

    fvgDetected := false
    fvgLower := na
    fvgUpper := na


// ============================================================================
// SECTION 25: HIGH LEVEL AVAILABILITY
// ============================================================================

bool pdhAvailable = (
     usePreviousDayLevels and
     not na(previousDayHigh) and
     (
         not oneTradePerLevelPerDay or
         not tradedPDH
     )
)

bool orhAvailable = (
     useOpeningRangeLevels and
     openingRangeReady and
     not na(openingRangeHigh) and
     (
         not oneTradePerLevelPerDay or
         not tradedORH
     )
)

float pdhOvershoot = (
     pdhAvailable
     ? high -
       previousDayHigh
     : na
)

float orhOvershoot = (
     orhAvailable
     ? high -
       openingRangeHigh
     : na
)

bool pdhSweepCandidate = (
     pdhAvailable and
     pdhOvershoot >=
     minimumSweepPoints and
     pdhOvershoot <=
     maximumSweepPoints
)

bool orhSweepCandidate = (
     orhAvailable and
     orhOvershoot >=
     minimumSweepPoints and
     orhOvershoot <=
     maximumSweepPoints
)


// ============================================================================
// SECTION 26: LOW LEVEL AVAILABILITY
// ============================================================================

bool pdlAvailable = (
     usePreviousDayLevels and
     not na(previousDayLow) and
     (
         not oneTradePerLevelPerDay or
         not tradedPDL
     )
)

bool orlAvailable = (
     useOpeningRangeLevels and
     openingRangeReady and
     not na(openingRangeLow) and
     (
         not oneTradePerLevelPerDay or
         not tradedORL
     )
)

float pdlOvershoot = (
     pdlAvailable
     ? previousDayLow -
       low
     : na
)

float orlOvershoot = (
     orlAvailable
     ? openingRangeLow -
       low
     : na
)

bool pdlSweepCandidate = (
     pdlAvailable and
     pdlOvershoot >=
     minimumSweepPoints and
     pdlOvershoot <=
     maximumSweepPoints
)

bool orlSweepCandidate = (
     orlAvailable and
     orlOvershoot >=
     minimumSweepPoints and
     orlOvershoot <=
     maximumSweepPoints
)


// ============================================================================
// SECTION 27: SELECT HIGH LIQUIDITY LEVEL
// ============================================================================

int highCandidateId = 0
float highCandidatePrice = na


if pdhSweepCandidate

    highCandidateId := 1
    highCandidatePrice := previousDayHigh


else if orhSweepCandidate

    highCandidateId := 3
    highCandidatePrice := openingRangeHigh


// ============================================================================
// SECTION 28: SELECT LOW LIQUIDITY LEVEL
// ============================================================================

int lowCandidateId = 0
float lowCandidatePrice = na


if pdlSweepCandidate

    lowCandidateId := 2
    lowCandidatePrice := previousDayLow


else if orlSweepCandidate

    lowCandidateId := 4
    lowCandidatePrice := openingRangeLow


bool highSweepAvailable = (
     highCandidateId != 0
)

bool lowSweepAvailable = (
     lowCandidateId != 0
)

bool ambiguousSweep = (
     highSweepAvailable and
     lowSweepAvailable
)


// ============================================================================
// SECTION 29: ARM SHORT SWEEP
// ============================================================================

if (
     setupStage == 0 and
     strategy.position_size == 0 and
     allowEntry and
     cooldownOK and
     allowShort and
     highSweepAvailable and
     not ambiguousSweep
)

    setupDirection := -1
    setupStage := 1

    activeLevelId := (
         highCandidateId
    )

    activeLevelPrice := (
         highCandidatePrice
    )

    sweepStartBar := (
         bar_index
    )

    sweepExtreme := (
         high
    )

    sweepCount += 1


// ============================================================================
// SECTION 30: ARM LONG SWEEP
// ============================================================================

if (
     setupStage == 0 and
     strategy.position_size == 0 and
     allowEntry and
     cooldownOK and
     allowLong and
     lowSweepAvailable and
     not ambiguousSweep
)

    setupDirection := 1
    setupStage := 1

    activeLevelId := (
         lowCandidateId
    )

    activeLevelPrice := (
         lowCandidatePrice
    )

    sweepStartBar := (
         bar_index
    )

    sweepExtreme := (
         low
    )

    sweepCount += 1


// ============================================================================
// SECTION 31: SHORT SWEEP -> RECLAIM
// ============================================================================

if (
     setupDirection == -1 and
     setupStage == 1
)

    sweepExtreme := (
         na(sweepExtreme)
         ? high
         : math.max(
             sweepExtreme,
             high
         )
    )

    float overshoot = (
         sweepExtreme -
         activeLevelPrice
    )

    bool sweepTooLarge = (
         overshoot >
         maximumSweepPoints
    )

    bool reclaimExpired = (
         bar_index -
         sweepStartBar >
         returnInsideBars
    )

    bool reclaimedInside = (
         close <
         activeLevelPrice
    )


    if (
         sweepTooLarge or
         reclaimExpired
    )

        setupDirection := 0
        setupStage := 0

        activeLevelId := 0
        activeLevelPrice := na

        sweepStartBar := na
        sweepExtreme := na


    else if reclaimedInside

        setupStage := 2

        reclaimBar := (
             bar_index
        )

        reclaimCount += 1


// ============================================================================
// SECTION 32: LONG SWEEP -> RECLAIM
// ============================================================================

if (
     setupDirection == 1 and
     setupStage == 1
)

    sweepExtreme := (
         na(sweepExtreme)
         ? low
         : math.min(
             sweepExtreme,
             low
         )
    )

    float overshoot = (
         activeLevelPrice -
         sweepExtreme
    )

    bool sweepTooLarge = (
         overshoot >
         maximumSweepPoints
    )

    bool reclaimExpired = (
         bar_index -
         sweepStartBar >
         returnInsideBars
    )

    bool reclaimedInside = (
         close >
         activeLevelPrice
    )


    if (
         sweepTooLarge or
         reclaimExpired
    )

        setupDirection := 0
        setupStage := 0

        activeLevelId := 0
        activeLevelPrice := na

        sweepStartBar := na
        sweepExtreme := na


    else if reclaimedInside

        setupStage := 2

        reclaimBar := (
             bar_index
        )

        reclaimCount += 1


// ============================================================================
// SECTION 33: DISPLACEMENT EXPIRATION
// ============================================================================

bool displacementExpired = (
     setupStage == 2 and
     not na(reclaimBar) and
     bar_index -
     reclaimBar >
     displacementExpiryBars
)

if displacementExpired

    setupDirection := 0
    setupStage := 0

    activeLevelId := 0
    activeLevelPrice := na

    sweepStartBar := na
    reclaimBar := na

    sweepExtreme := na


// ============================================================================
// SECTION 34: BEARISH DISPLACEMENT
// ============================================================================

bool bearishDisplacementSignal = false


if (
     setupDirection == -1 and
     setupStage == 2 and
     bearishDisplacementQuality
)

    bearishDisplacementSignal := true

    displacementBar := (
         bar_index
    )

    displacementOpen := (
         open
    )

    displacementClose := (
         close
    )

    displacementHigh := (
         high
    )

    displacementLow := (
         low
    )


    // ------------------------------------------------------------------------
    // Default entry zone:
    // Retracement into the displacement candle BODY.
    //
    // For bearish displacement:
    // Entry is measured upward from the close toward the open.
    // ------------------------------------------------------------------------

    float bodySize = (
         displacementOpen -
         displacementClose
    )

    pullbackEntryLevel := (
         displacementClose +
         bodySize *
         displacementRetracementPercent /
         100.0
    )

    fvgDetected := false
    fvgLower := na
    fvgUpper := na

    setupStage := 3

    displacementCount += 1


// ============================================================================
// SECTION 35: BULLISH DISPLACEMENT
// ============================================================================

bool bullishDisplacementSignal = false


if (
     setupDirection == 1 and
     setupStage == 2 and
     bullishDisplacementQuality
)

    bullishDisplacementSignal := true

    displacementBar := (
         bar_index
    )

    displacementOpen := (
         open
    )

    displacementClose := (
         close
    )

    displacementHigh := (
         high
    )

    displacementLow := (
         low
    )


    // ------------------------------------------------------------------------
    // For bullish displacement:
    // Entry is measured downward from close toward open.
    // ------------------------------------------------------------------------

    float bodySize = (
         displacementClose -
         displacementOpen
    )

    pullbackEntryLevel := (
         displacementClose -
         bodySize *
         displacementRetracementPercent /
         100.0
    )

    fvgDetected := false
    fvgLower := na
    fvgUpper := na

    setupStage := 3

    displacementCount += 1


// ============================================================================
// SECTION 36: OPTIONAL FVG OVERRIDE
// ============================================================================
//
// FVG is OPTIONAL.
//
// If a valid standard 3-candle FVG forms immediately after the
// displacement candle, use its midpoint instead.
//
// Otherwise the strategy simply keeps using the displacement
// retracement level.
//
// ============================================================================

if (
     setupStage == 3 and
     preferFVGWhenAvailable and
     not fvgDetected and
     not na(displacementBar) and
     bar_index ==
     displacementBar + 1 and
     bar_index >= 2
)

    bool threeBarContinuity = (
         time -
         time[2] <=
         maximumBarGapMinutes *
         60 *
         1000
    )


    // ------------------------------------------------------------------------
    // Bullish FVG
    //
    // Current low > high[2]
    // ------------------------------------------------------------------------

    bool bullishFVG = (
         setupDirection == 1 and
         threeBarContinuity and
         low >
         high[2] and
         (
             low -
             high[2]
         ) >=
         minimumFVGSize
    )


    // ------------------------------------------------------------------------
    // Bearish FVG
    //
    // Current high < low[2]
    // ------------------------------------------------------------------------

    bool bearishFVG = (
         setupDirection == -1 and
         threeBarContinuity and
         high <
         low[2] and
         (
             low[2] -
             high
         ) >=
         minimumFVGSize
    )


    if bullishFVG

        fvgDetected := true

        fvgLower := (
             high[2]
        )

        fvgUpper := (
             low
        )

        pullbackEntryLevel := (
             (
                 fvgLower +
                 fvgUpper
             ) /
             2.0
        )

        fvgCount += 1


    else if bearishFVG

        fvgDetected := true

        fvgLower := (
             high
        )

        fvgUpper := (
             low[2]
        )

        pullbackEntryLevel := (
             (
                 fvgLower +
                 fvgUpper
             ) /
             2.0
        )

        fvgCount += 1


// ============================================================================
// SECTION 37: PULLBACK EXPIRATION
// ============================================================================

bool pullbackExpired = (
     setupStage == 3 and
     not na(displacementBar) and
     bar_index -
     displacementBar >
     pullbackExpiryBars
)

if pullbackExpired

    setupDirection := 0
    setupStage := 0

    activeLevelId := 0
    activeLevelPrice := na

    sweepStartBar := na
    reclaimBar := na
    displacementBar := na

    sweepExtreme := na

    displacementOpen := na
    displacementClose := na
    displacementHigh := na
    displacementLow := na

    pullbackEntryLevel := na

    fvgDetected := false
    fvgLower := na
    fvgUpper := na


// ============================================================================
// SECTION 38: PRE-ENTRY THESIS INVALIDATION
// ============================================================================
//
// After reclaim + displacement, the liquidity level should remain held.
//
// LONG:
// Closing materially back below swept low liquidity invalidates setup.
//
// SHORT:
// Closing materially back above swept high liquidity invalidates setup.
//
// ============================================================================

bool longSetupInvalidated = (
     setupDirection == 1 and
     setupStage == 3 and
     close <
     activeLevelPrice -
     liquidityLevelInvalidationBuffer
)

bool shortSetupInvalidated = (
     setupDirection == -1 and
     setupStage == 3 and
     close >
     activeLevelPrice +
     liquidityLevelInvalidationBuffer
)

if (
     longSetupInvalidated or
     shortSetupInvalidated
)

    setupDirection := 0
    setupStage := 0

    activeLevelId := 0
    activeLevelPrice := na

    sweepStartBar := na
    reclaimBar := na
    displacementBar := na

    sweepExtreme := na

    displacementOpen := na
    displacementClose := na
    displacementHigh := na
    displacementLow := na

    pullbackEntryLevel := na

    fvgDetected := false
    fvgLower := na
    fvgUpper := na


// ============================================================================
// SECTION 39: LONG FIRST PULLBACK
// ============================================================================

bool longPullbackSignal = false


if (
     setupDirection == 1 and
     setupStage == 3 and
     bar_index >
     displacementBar and
     not na(pullbackEntryLevel)
)

    bool touchedPullback = (
         low <=
         pullbackEntryLevel +
         pullbackTouchTolerance
    )


    if touchedPullback

        bool closeBackOK = (
             not requireCloseBackBeyondPullbackLevel or
             close >=
             pullbackEntryLevel
        )

        bool validRejection = (
             bullishRejectionQuality and
             closeBackOK
        )


        // --------------------------------------------------------------------
        // FIRST PULLBACK ONLY.
        //
        // Weak first touch = setup consumed.
        // --------------------------------------------------------------------

        if validRejection

            longPullbackSignal := true

            pullbackCount += 1


        else

            setupDirection := 0
            setupStage := 0

            activeLevelId := 0
            activeLevelPrice := na

            sweepStartBar := na
            reclaimBar := na
            displacementBar := na

            sweepExtreme := na

            pullbackEntryLevel := na

            fvgDetected := false
            fvgLower := na
            fvgUpper := na


// ============================================================================
// SECTION 40: SHORT FIRST PULLBACK
// ============================================================================

bool shortPullbackSignal = false


if (
     setupDirection == -1 and
     setupStage == 3 and
     bar_index >
     displacementBar and
     not na(pullbackEntryLevel)
)

    bool touchedPullback = (
         high >=
         pullbackEntryLevel -
         pullbackTouchTolerance
    )


    if touchedPullback

        bool closeBackOK = (
             not requireCloseBackBeyondPullbackLevel or
             close <=
             pullbackEntryLevel
        )

        bool validRejection = (
             bearishRejectionQuality and
             closeBackOK
        )


        if validRejection

            shortPullbackSignal := true

            pullbackCount += 1


        else

            setupDirection := 0
            setupStage := 0

            activeLevelId := 0
            activeLevelPrice := na

            sweepStartBar := na
            reclaimBar := na
            displacementBar := na

            sweepExtreme := na

            pullbackEntryLevel := na

            fvgDetected := false
            fvgLower := na
            fvgUpper := na


// ============================================================================
// SECTION 41: STRUCTURE RISK CHECK
// ============================================================================

float estimatedLongStructureSL = (
     not na(sweepExtreme)
     ? sweepExtreme -
       structureStopBuffer
     : na
)

float estimatedShortStructureSL = (
     not na(sweepExtreme)
     ? sweepExtreme +
       structureStopBuffer
     : na
)

float estimatedLongStructureRisk = (
     longPullbackSignal and
     not na(estimatedLongStructureSL)
     ? close -
       estimatedLongStructureSL
     : na
)

float estimatedShortStructureRisk = (
     shortPullbackSignal and
     not na(estimatedShortStructureSL)
     ? estimatedShortStructureSL -
       close
     : na
)

bool longRiskOK = (
     exitMode == "Manual Points" or
     (
         not na(estimatedLongStructureRisk) and
         estimatedLongStructureRisk > 0 and
         estimatedLongStructureRisk <=
         maximumStructureRisk
     )
)

bool shortRiskOK = (
     exitMode == "Manual Points" or
     (
         not na(estimatedShortStructureRisk) and
         estimatedShortStructureRisk > 0 and
         estimatedShortStructureRisk <=
         maximumStructureRisk
     )
)


// ============================================================================
// SECTION 42: FINAL ENTRY SIGNALS
// ============================================================================

bool finalLongSignal = (
     longPullbackSignal and
     allowEntry and
     allowLong and
     cooldownOK and
     longRiskOK and
     strategy.position_size == 0
)

bool finalShortSignal = (
     shortPullbackSignal and
     allowEntry and
     allowShort and
     cooldownOK and
     shortRiskOK and
     strategy.position_size == 0
)


// ============================================================================
// SECTION 43: FROZEN TRADE STATE
// ============================================================================

var float frozenTradeSweepExtreme = na

var float activeLongSL = na
var float activeLongTP = na

var float activeShortSL = na
var float activeShortTP = na


// ============================================================================
// SECTION 44: LONG ENTRY
// ============================================================================

if finalLongSignal

    int entryLevelId = (
         activeLevelId
    )

    frozenTradeSweepExtreme := (
         sweepExtreme
    )

    strategy.entry(
         "Long",
         strategy.long,
         alert_message="ENTRY_LONG_CNPS18_V2"
    )

    lastEntryBar := (
         bar_index
    )

    entryCount += 1


    // ------------------------------------------------------------------------
    // Only NOW mark the liquidity level as used.
    // ------------------------------------------------------------------------

    if oneTradePerLevelPerDay

        if entryLevelId == 2
            tradedPDL := true

        if entryLevelId == 4
            tradedORL := true


    setupDirection := 0
    setupStage := 0

    activeLevelId := 0
    activeLevelPrice := na

    sweepStartBar := na
    reclaimBar := na
    displacementBar := na

    sweepExtreme := na

    displacementOpen := na
    displacementClose := na
    displacementHigh := na
    displacementLow := na

    pullbackEntryLevel := na

    fvgDetected := false
    fvgLower := na
    fvgUpper := na


// ============================================================================
// SECTION 45: SHORT ENTRY
// ============================================================================

if finalShortSignal

    int entryLevelId = (
         activeLevelId
    )

    frozenTradeSweepExtreme := (
         sweepExtreme
    )

    strategy.entry(
         "Short",
         strategy.short,
         alert_message="ENTRY_SHORT_CNPS18_V2"
    )

    lastEntryBar := (
         bar_index
    )

    entryCount += 1


    if oneTradePerLevelPerDay

        if entryLevelId == 1
            tradedPDH := true

        if entryLevelId == 3
            tradedORH := true


    setupDirection := 0
    setupStage := 0

    activeLevelId := 0
    activeLevelPrice := na

    sweepStartBar := na
    reclaimBar := na
    displacementBar := na

    sweepExtreme := na

    displacementOpen := na
    displacementClose := na
    displacementHigh := na
    displacementLow := na

    pullbackEntryLevel := na

    fvgDetected := false
    fvgLower := na
    fvgUpper := na


// ============================================================================
// SECTION 46: LONG POSITION MANAGEMENT
// ============================================================================

if strategy.position_size > 0

    float entryPrice = (
         strategy.position_avg_price
    )


    if exitMode == "Manual Points"

        activeLongSL := (
             entryPrice -
             manualSLPoints
        )

        activeLongTP := (
             entryPrice +
             manualTPPoints
        )


    else

        activeLongSL := (
             not na(frozenTradeSweepExtreme)
             ? frozenTradeSweepExtreme -
               structureStopBuffer
             : na
        )

        float risk = (
             not na(activeLongSL)
             ? entryPrice -
               activeLongSL
             : na
        )

        activeLongTP := (
             not na(risk) and
             risk > 0
             ? entryPrice +
               risk *
               structureRR
             : na
        )


    bool validGeometry = (
         not na(activeLongSL) and
         not na(activeLongTP) and
         activeLongSL <
         entryPrice and
         activeLongTP >
         entryPrice
    )


    if validGeometry

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


    else

        strategy.close(
             "Long",
             immediately=true,
             comment="Invalid Long Geometry",
             alert_message="EXIT_LONG_INVALID_CNPS18_V2"
        )


// ============================================================================
// SECTION 47: SHORT POSITION MANAGEMENT
// ============================================================================

if strategy.position_size < 0

    float entryPrice = (
         strategy.position_avg_price
    )


    if exitMode == "Manual Points"

        activeShortSL := (
             entryPrice +
             manualSLPoints
        )

        activeShortTP := (
             entryPrice -
             manualTPPoints
        )


    else

        activeShortSL := (
             not na(frozenTradeSweepExtreme)
             ? frozenTradeSweepExtreme +
               structureStopBuffer
             : na
        )

        float risk = (
             not na(activeShortSL)
             ? activeShortSL -
               entryPrice
             : na
        )

        activeShortTP := (
             not na(risk) and
             risk > 0
             ? entryPrice -
               risk *
               structureRR
             : na
        )


    bool validGeometry = (
         not na(activeShortSL) and
         not na(activeShortTP) and
         activeShortSL >
         entryPrice and
         activeShortTP <
         entryPrice
    )


    if validGeometry

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


    else

        strategy.close(
             "Short",
             immediately=true,
             comment="Invalid Short Geometry",
             alert_message="EXIT_SHORT_INVALID_CNPS18_V2"
        )


// ============================================================================
// SECTION 48: POSITION CLEANUP
// ============================================================================

bool positionJustClosed = (
     strategy.position_size == 0 and
     bar_index > 0 and
     strategy.position_size[1] != 0
)

if positionJustClosed

    frozenTradeSweepExtreme := na

    activeLongSL := na
    activeLongTP := na

    activeShortSL := na
    activeShortTP := na


// ============================================================================
// SECTION 49: SESSION END
// ============================================================================

if finalSessionBar

    setupDirection := 0
    setupStage := 0

    activeLevelId := 0
    activeLevelPrice := na

    sweepStartBar := na
    reclaimBar := na
    displacementBar := na

    sweepExtreme := na

    displacementOpen := na
    displacementClose := na
    displacementHigh := na
    displacementLow := na

    pullbackEntryLevel := na

    fvgDetected := false
    fvgLower := na
    fvgUpper := na


    if strategy.position_size != 0

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


// ============================================================================
// SECTION 50: LIQUIDITY LEVEL DISPLAY
// ============================================================================

plot(
     showLiquidityLevels and
     usePreviousDayLevels
     ? previousDayHigh
     : na,
     title="Previous Day High",
     color=color.new(
         color.red,
         25
     ),
     linewidth=1,
     style=plot.style_linebr
)

plot(
     showLiquidityLevels and
     usePreviousDayLevels
     ? previousDayLow
     : na,
     title="Previous Day Low",
     color=color.new(
         color.green,
         25
     ),
     linewidth=1,
     style=plot.style_linebr
)

plot(
     showLiquidityLevels and
     useOpeningRangeLevels and
     openingRangeReady
     ? openingRangeHigh
     : na,
     title="Opening Range High",
     color=color.new(
         color.orange,
         20
     ),
     linewidth=1,
     style=plot.style_linebr
)

plot(
     showLiquidityLevels and
     useOpeningRangeLevels and
     openingRangeReady
     ? openingRangeLow
     : na,
     title="Opening Range Low",
     color=color.new(
         color.orange,
         20
     ),
     linewidth=1,
     style=plot.style_linebr
)


// ============================================================================
// SECTION 51: PULLBACK LEVEL DISPLAY
// ============================================================================

plot(
     showPullbackZone and
     setupStage == 3
     ? pullbackEntryLevel
     : na,
     title="Active Pullback Entry Level",
     color=color.yellow,
     linewidth=2,
     style=plot.style_linebr
)


// ============================================================================
// SECTION 52: OPTIONAL FVG DISPLAY
// ============================================================================

plot(
     showFVG and
     setupStage == 3 and
     fvgDetected
     ? fvgUpper
     : na,
     title="FVG Upper",
     color=color.aqua,
     linewidth=1,
     style=plot.style_linebr
)

plot(
     showFVG and
     setupStage == 3 and
     fvgDetected
     ? fvgLower
     : na,
     title="FVG Lower",
     color=color.aqua,
     linewidth=1,
     style=plot.style_linebr
)


// ============================================================================
// SECTION 53: OPTIONAL SIGNAL MARKERS
// ============================================================================

plotshape(
     showSignalMarkers and
     bullishDisplacementSignal,
     title="Bullish Displacement",
     style=shape.circle,
     location=location.belowbar,
     color=color.lime,
     size=size.tiny,
     text="D",
     textcolor=color.white
)

plotshape(
     showSignalMarkers and
     bearishDisplacementSignal,
     title="Bearish Displacement",
     style=shape.circle,
     location=location.abovebar,
     color=color.red,
     size=size.tiny,
     text="D",
     textcolor=color.white
)

plotshape(
     showSignalMarkers and
     finalLongSignal,
     title="Long Entry Signal",
     style=shape.triangleup,
     location=location.belowbar,
     color=color.lime,
     size=size.small,
     text="L",
     textcolor=color.white
)

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


// ============================================================================
// SECTION 54: ACTIVE SL / TP
// ============================================================================

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

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

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

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


// ============================================================================
// SECTION 55: DIAGNOSTICS
// ============================================================================

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

if showDiagnostics and barstate.islast

    string levelText = (
         activeLevelId == 1
         ? "PDH"
         : (
             activeLevelId == 2
             ? "PDL"
             : (
                 activeLevelId == 3
                 ? "ORH"
                 : (
                     activeLevelId == 4
                     ? "ORL"
                     : "-"
                 )
             )
         )
    )

    string directionText = (
         setupDirection == 1
         ? "LONG"
         : (
             setupDirection == -1
             ? "SHORT"
             : "-"
         )
    )

    string stageText = (
         setupStage == 0
         ? "SCAN"
         : (
             setupStage == 1
             ? "WAIT RECLAIM"
             : (
                 setupStage == 2
                 ? "WAIT DISP"
                 : "WAIT PULLBACK"
             )
         )
    )

    string zoneText = (
         setupStage == 3
         ? (
             fvgDetected
             ? "FVG"
             : "DISP 50%"
         )
         : "-"
    )

    string positionText = (
         strategy.position_size > 0
         ? "LONG"
         : (
             strategy.position_size < 0
             ? "SHORT"
             : "FLAT"
         )
    )


    table.cell(
         diagnostics,
         0,
         0,
         "CNPS18 V2",
         text_color=color.white,
         bgcolor=color.black
    )

    table.cell(
         diagnostics,
         1,
         0,
         isM1Chart
         ? "M1 OK"
         : "USE M1",
         text_color=color.white,
         bgcolor=color.black
    )

    table.cell(diagnostics, 0, 1, "Setup")
    table.cell(diagnostics, 1, 1, directionText)

    table.cell(diagnostics, 0, 2, "Stage")
    table.cell(diagnostics, 1, 2, stageText)

    table.cell(diagnostics, 0, 3, "Level")
    table.cell(diagnostics, 1, 3, levelText)

    table.cell(diagnostics, 0, 4, "Level Price")
    table.cell(
         diagnostics,
         1,
         4,
         str.tostring(
             activeLevelPrice
         )
    )

    table.cell(diagnostics, 0, 5, "Sweep Extreme")
    table.cell(
         diagnostics,
         1,
         5,
         str.tostring(
             sweepExtreme
         )
    )

    table.cell(diagnostics, 0, 6, "Zone Source")
    table.cell(diagnostics, 1, 6, zoneText)

    table.cell(diagnostics, 0, 7, "Pullback Level")
    table.cell(
         diagnostics,
         1,
         7,
         str.tostring(
             pullbackEntryLevel
         )
    )

    table.cell(diagnostics, 0, 8, "Sweeps")
    table.cell(
         diagnostics,
         1,
         8,
         str.tostring(
             sweepCount
         )
    )

    table.cell(diagnostics, 0, 9, "Reclaims")
    table.cell(
         diagnostics,
         1,
         9,
         str.tostring(
             reclaimCount
         )
    )

    table.cell(diagnostics, 0, 10, "Displacements")
    table.cell(
         diagnostics,
         1,
         10,
         str.tostring(
             displacementCount
         )
    )

    table.cell(diagnostics, 0, 11, "FVGs")
    table.cell(
         diagnostics,
         1,
         11,
         str.tostring(
             fvgCount
         )
    )

    table.cell(diagnostics, 0, 12, "Pullbacks")
    table.cell(
         diagnostics,
         1,
         12,
         str.tostring(
             pullbackCount
         )
    )

    table.cell(diagnostics, 0, 13, "Entries")
    table.cell(
         diagnostics,
         1,
         13,
         str.tostring(
             entryCount
         )
    )

    table.cell(diagnostics, 0, 14, "Exit")
    table.cell(
         diagnostics,
         1,
         14,
         exitMode
    )

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