Page 1 of 3

Beating cutting winners early as a scalper: rules that stuck

Posted: Tue Sep 22, 2026 2:18 pm
by LondonScalper
Cutting winners early was my quiet expectancy leak for years.

The scalp would go green, fear of giving it back would tap me on the shoulder, and I would scratch for crumbs while the level I trusted still had room. The fix was not "be braver." It was rules that remove the mid-trade negotiation.

Rules that stuck
1. Pre-commit partial and runner plan before entry; no improvising at +0.3R.
2. If I scratch early without a rule, tag cut_early and reduce next size.
3. Time stops and structure invalidation decide exits — not the P&L colour alone.

I still take partials. I just refuse to let anxiety invent a new plan every minute.

What rule actually stopped you from harvesting winners too early — partials, time, or something else?

On runners I accept that some will reverse after partials. That is tuition for not cutting the whole idea at the first tick of green. The journal tracks cut_early rate per month so the leak cannot hide inside overall win rate.

I allow one discretionary early exit per week for true structure change — not for comfort. More than that and the tag rate climbs.

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:40 am
by FTtrader
LondonScalper wrote: Tue Sep 22, 2026 2:18 pm Cutting winners early was my quiet expectancy leak for years.

The scalp would go green, fear of giving it back would tap me on the shoulder, and I would scratch for crumbs while the level I trusted still had room. The fix was not "be braver." It was rules that remove the mid-trade negotiation.

Rules that stuck
1. Pre-commit partial and runner plan before entry; no improvising at +0.3R.
2. If I scratch early without a rule, tag cut_early and reduce next size.
3. Time stops and structure invalidation decide exits — not the P&L colour alone.

I still take partials. I just refuse to let anxiety invent a new plan every minute.

What rule actually stopped you from harvesting winners too early — partials, time, or something else?

On runners I accept that some will reverse after partials. That is tuition for not cutting the whole idea at the first tick of green. The journal tracks cut_early rate per month so the leak cannot hide inside overall win rate.

I allow one discretionary early exit per week for true structure change — not for comfort. More than that and the tag rate climbs.
Hello LondonScalper,

The realization that some runners will inevitably reverse is one of the biggest psychological hurdles a trader can cross. Calling it "tuition for not cutting the whole idea" is a brilliant reframe. It shifts the mid-trade anxiety from "I'm losing my unrealized profits" to "I am paying the standard market fee to see if my 15-minute structural read was a home run."

To answer your question directly: The rule that truly stops the premature harvesting of winners is the mechanical break-even trigger coupled with an automated first partial.

When trading raw price action, staring at a floating P&L is toxic because it introduces a metric completely disconnected from market structure. By hard-coding a rule where 50% of the position is automatically harvested at +1R, and the stop-loss on the runner is instantly moved to the exact entry price, the mid-trade negotiation is silenced. The trade mathematically becomes a "free look." If the structure holds and it hits the runner target, great. If it reverses and stops out at break-even, the trade was still net-profitable. The time stop acts as the final failsafe—if the market isn't validating the idea within a set number of candles, the momentum is dead, and the capital is better deployed elsewhere.

Tracking the cut_early tag in a journal is the exact right way to measure this expectancy leak. It keeps you accountable to the system rather than the outcome of a single trade.

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:40 am
by FTtrader
Here is a Pine Script v5 strategy designed precisely around your rules. It isn't meant to replace your discretionary price action entries, but rather to visualize and backtest the exact management framework you described: pre-committed partials, a runner left for structure, break-even automation, and strict time stops.

Code: Select all

//@version=5
strategy("PA Trade Management Engine: Partials & Time Stops", overlay=true, initial_capital=10000, default_qty_type=strategy.percent_of_equity, default_qty_value=2)

// ==============================================================================
// INPUTS: MANAGEMENT RULES
// ==============================================================================
grp_management = "Trade Management Rules"
partialR       = input.float(1.0, title="Partial Target (R-Multiple)", step=0.1, group=grp_management, tooltip="Where to take the first partial and move SL to Break-Even")
runnerR        = input.float(3.0, title="Runner Target (R-Multiple)", step=0.5, group=grp_management, tooltip="Final target for the remaining position")
partialPct     = input.int(50, title="Partial Size (%)", minval=10, maxval=90, step=10, group=grp_management)
timeStopBars   = input.int(12, title="Time Stop (Bars)", minval=1, group=grp_management, tooltip="Close trade if open for this many bars without hitting the partial")

// ==============================================================================
// MOCK ENTRY LOGIC (For Demonstration)
// In a live scenario, this is replaced by your raw PA triggers
// ==============================================================================
// Simple trigger: Bullish/Bearish Engulfing for demonstration
bullEngulf = close > open and close[1] < open[1] and close > open[1] and open < close[1]
bearEngulf = close < open and close[1] > open[1] and close < open[1] and open > close[1]

// ==============================================================================
// STATE VARIABLES
// ==============================================================================
var float trade_sl   = na
var float trade_tp1  = na
var float trade_tp2  = na
var float entry_p    = na
var int   entry_bar  = na
var bool  partial_hit = false

// ==============================================================================
// EXECUTION & CALCULATION
// ==============================================================================
// Long Entry
if (bullEngulf and strategy.position_size == 0)
    trade_sl   := ta.lowest(low, 3) // Stop behind recent structure
    risk       = close - trade_sl
    trade_tp1  := close + (risk * partialR)
    trade_tp2  := close + (risk * runnerR)
    entry_p    := close
    entry_bar  := bar_index
    partial_hit := false
    strategy.entry("Long", strategy.long)

// Short Entry
if (bearEngulf and strategy.position_size == 0)
    trade_sl   := ta.highest(high, 3)
    risk       = trade_sl - close
    trade_tp1  := close - (risk * partialR)
    trade_tp2  := close - (risk * runnerR)
    entry_p    := close
    entry_bar  := bar_index
    partial_hit := false
    strategy.entry("Short", strategy.short)

// ==============================================================================
// IN-TRADE MANAGEMENT (The "No Negotiation" Rules)
// ==============================================================================
if (strategy.position_size != 0)
    bars_in_trade = bar_index - entry_bar
    
    // Rule 3: Time Stop (Momentum died, exit before P&L bleeds)
    if (bars_in_trade >= timeStopBars and not partial_hit)
        strategy.close_all(comment="Time Stop")
        trade_sl := na

    // Rule 1 & 2: Pre-commit to Partials and BE
    if (strategy.position_size > 0) // Managing Longs
        // Send the partial exit order
        strategy.exit("Take Partial", "Long", qty_percent=partialPct, limit=trade_tp1, stop=trade_sl)
        
        // Detect if partial was hit to move SL to BE for the runner
        if (high >= trade_tp1 and not partial_hit)
            partial_hit := true
            trade_sl   := entry_p // Move stop to Break Even
            
        // Manage the runner
        if (partial_hit)
            strategy.exit("Take Runner", "Long", limit=trade_tp2, stop=trade_sl)

    if (strategy.position_size < 0) // Managing Shorts
        strategy.exit("Take Partial", "Short", qty_percent=partialPct, limit=trade_tp1, stop=trade_sl)
        
        if (low <= trade_tp1 and not partial_hit)
            partial_hit := true
            trade_sl   := entry_p
            
        if (partial_hit)
            strategy.exit("Take Runner", "Short", limit=trade_tp2, stop=trade_sl)

// Reset visuals when flat
if (strategy.position_size == 0)
    trade_sl  := na
    trade_tp1 := na
    trade_tp2 := na

// ==============================================================================
// VISUALIZATION
// ==============================================================================
plot(strategy.position_size != 0 ? trade_sl : na, color=color.red, style=plot.style_linebr, linewidth=2, title="Stop Loss / BE")
plot(strategy.position_size != 0 and not partial_hit ? trade_tp1 : na, color=color.blue, style=plot.style_linebr, linewidth=2, title="Partial Target (+1R)")
plot(strategy.position_size != 0 ? trade_tp2 : na, color=color.green, style=plot.style_linebr, linewidth=2, title="Runner Target")

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:40 am
by FTtrader
How this enforces the rules:

Pre-commitment: The moment an entry is triggered, the script mathematically locks in the trade_tp1 (Partial) and trade_tp2 (Runner) based on structural risk. There is no manual dragging of targets to "scratch for crumbs."

The Time Stop Failsafe: If the setup takes more than timeStopBars to trigger your partial, it aggressively flattens the position. This prevents the slow, agonizing bleed of a trade that has lost its structural momentum.

The "Free Look" Engine: Once the partial target is touched, the trade_sl variable instantly updates to the entry price (entry_p). The remaining runner is now managed entirely independently of fear, protected by a hard break-even floor.

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:42 am
by FTtrader
To make this "pro," we need to elevate the script from a basic retail retail indicator to an institutional-grade algorithmic engine. Given your background in C# and enterprise architecture, you’ll appreciate that Pine Script v5 now supports User-Defined Types (UDTs) and methods. We can structure this almost like an object-oriented class to handle state, rather than relying on messy global variables.

A professional scalping script needs three things the previous version lacked:

Dynamic Risk Sizing: Fixed percent-of-equity is flawed. Position size must be calculated dynamically based on the exact pip/tick distance of the structural stop-loss and a strict risk percentage per trade (e.g., risking exactly 1% of the account whether the SL is 5 pips or 15 pips).

Structure Invalidation Exits: Instead of a fixed final target, the runner should trail its stop strictly based on new market structure (swing highs/lows).

Clean Chart Execution: Continuous plot() lines clutter the chart. A pro script draws specific line and label objects only during an active trade and deletes them afterward.

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:42 am
by FTtrader
Here is the refactored, production-ready Engine.

Code: Select all

//@version=5
strategy("Pro PA Management Engine", overlay=true, initial_capital=10000, calc_on_every_tick=true, commission_type=strategy.commission.cash_per_contract, commission_value=3)

// ==============================================================================
// 1. INPUTS & CONFIGURATION
// ==============================================================================
grp_risk = "Risk & Sizing"
riskPerTrade = input.float(1.0, "Risk Per Trade (%)", step=0.1, group=grp_risk)

grp_mgmt = "Trade Management Rules"
partialR     = input.float(1.0, "First Partial (R)", step=0.1, group=grp_mgmt)
partialPct   = input.int(50, "Partial Size (%)", step=10, group=grp_mgmt)
timeStopBars = input.int(15, "Time Stop (Bars)", group=grp_mgmt, tooltip="Max bars to hold if partial isn't hit")
trailLookback= input.int(5, "Trailing Structure (Bars)", group=grp_mgmt, tooltip="Lookback for swing high/low to trail the runner")

// ==============================================================================
// 2. STATE MANAGEMENT (C#-style OOP)
// ==============================================================================
type TradeState
    string id
    float  entryPrice
    float  stopLoss
    float  partialTarget
    bool   isPartialHit
    int    entryBar

var TradeState activeTrade = TradeState.new()

// Method to clear state
method reset(TradeState this) =>
    this.id := ""
    this.entryPrice := na
    this.stopLoss := na
    this.partialTarget := na
    this.isPartialHit := false
    this.entryBar := na

// ==============================================================================
// 3. MARKET STRUCTURE & LIQUIDITY SWEEP LOGIC (Trigger)
// ==============================================================================
// Using a 5-bar pivot to define local structure
pivotHigh = ta.pivothigh(high, 5, 5)
pivotLow  = ta.pivotlow(low, 5, 5)

var float lastSwingHigh = na
var float lastSwingLow = na

if not na(pivotHigh)
    lastSwingHigh := pivotHigh
if not na(pivotLow)
    lastSwingLow := pivotLow

// Pro Setup: Liquidity Sweep (Price pokes below last swing low, but closes strongly above)
bullishSweep = low < lastSwingLow and close > lastSwingLow and close > open
// Bearish Setup: Price pokes above last swing high, but closes below
bearishSweep = high > lastSwingHigh and close < lastSwingHigh and close < open

// ==============================================================================
// 4. DYNAMIC RISK SIZING & EXECUTION
// ==============================================================================
get_position_size(stopDistance) =>
    accountRisk = strategy.equity * (riskPerTrade / 100)
    tickValue = syminfo.mintick * syminfo.pointvalue
    qty = accountRisk / (stopDistance / syminfo.mintick * tickValue)
    math.max(qty, 0.01) // Minimum size safeguard

if (strategy.position_size == 0)
    activeTrade.reset() // Ensure clean state
    
    if (bullishSweep)
        sl_price = low - (syminfo.mintick * 2) // Stop just below the sweep candle
        risk_dist = close - sl_price
        
        if risk_dist > 0
            activeTrade.id := "Long"
            activeTrade.entryPrice := close
            activeTrade.stopLoss := sl_price
            activeTrade.partialTarget := close + (risk_dist * partialR)
            activeTrade.entryBar := bar_index
            
            qty = get_position_size(risk_dist)
            strategy.entry("Long", strategy.long, qty=qty)

    else if (bearishSweep)
        sl_price = high + (syminfo.mintick * 2) // Stop just above the sweep candle
        risk_dist = sl_price - close
        
        if risk_dist > 0
            activeTrade.id := "Short"
            activeTrade.entryPrice := close
            activeTrade.stopLoss := sl_price
            activeTrade.partialTarget := close - (risk_dist * partialR)
            activeTrade.entryBar := bar_index
            
            qty = get_position_size(risk_dist)
            strategy.entry("Short", strategy.short, qty=qty)

// ==============================================================================
// 5. TRADE MANAGEMENT ENGINE (No Mid-Trade Negotiation)
// ==============================================================================
if (strategy.position_size != 0)
    barsInTrade = bar_index - activeTrade.entryBar
    
    // RULE 1: Time Stop (Kill the trade if momentum is dead)
    if (barsInTrade >= timeStopBars and not activeTrade.isPartialHit)
        strategy.close_all(comment="Time Stop (Scratch)")
        activeTrade.reset()
        
    // RULE 2: Pre-Committed Partial & Break Even
    if (activeTrade.id == "Long")
        // Exit partial
        strategy.exit("Take Partial", "Long", qty_percent=partialPct, limit=activeTrade.partialTarget, stop=activeTrade.stopLoss)
        
        // Check if partial was hit
        if (high >= activeTrade.partialTarget and not activeTrade.isPartialHit)
            activeTrade.isPartialHit := true
            activeTrade.stopLoss := activeTrade.entryPrice // Move to BE
            
        // RULE 3: Trailing Structure on Runner (Invalidation determines exit)
        if (activeTrade.isPartialHit)
            // Trail stop based on recent swing lows, never moving backwards
            dynamicTrail = ta.lowest(low, trailLookback) - (syminfo.mintick * 2)
            activeTrade.stopLoss := math.max(activeTrade.stopLoss, dynamicTrail)
            strategy.exit("Runner", "Long", stop=activeTrade.stopLoss)

    else if (activeTrade.id == "Short")
        strategy.exit("Take Partial", "Short", qty_percent=partialPct, limit=activeTrade.partialTarget, stop=activeTrade.stopLoss)
        
        if (low <= activeTrade.partialTarget and not activeTrade.isPartialHit)
            activeTrade.isPartialHit := true
            activeTrade.stopLoss := activeTrade.entryPrice 
            
        if (activeTrade.isPartialHit)
            dynamicTrail = ta.highest(high, trailLookback) + (syminfo.mintick * 2)
            activeTrade.stopLoss := math.min(activeTrade.stopLoss, dynamicTrail)
            strategy.exit("Runner", "Short", stop=activeTrade.stopLoss)

// ==============================================================================
// 6. UI / CHART VISUALS (Clean, Object-Based Rendering)
// ==============================================================================
var line slLine = na
var line tpLine = na

if (strategy.position_size != 0)
    // Draw or update active trade lines
    if (na(slLine))
        slLine := line.new(activeTrade.entryBar, activeTrade.stopLoss, bar_index, activeTrade.stopLoss, color=color.new(color.red, 20), width=2, style=line.style_dashed)
        tpLine := line.new(activeTrade.entryBar, activeTrade.partialTarget, bar_index, activeTrade.partialTarget, color=color.new(color.blue, 20), width=2)
    else
        line.set_x2(slLine, bar_index)
        line.set_y1(slLine, activeTrade.stopLoss) // Updates dynamically as runner trails
        line.set_y2(slLine, activeTrade.stopLoss)
        
        if (not activeTrade.isPartialHit)
            line.set_x2(tpLine, bar_index)
        else
            line.delete(tpLine) // Remove TP line once runner is active
else
    // Clean up chart when flat
    line.delete(slLine)
    line.delete(tpLine)
    slLine := na
    tpLine := na

// Dashboard to track the "Expectancy Leak" stats
var table dash = table.new(position.bottom_right, 2, 4, border_width=1)
if (barstate.islast)
    table.cell(dash, 0, 0, "Total Trades", text_color=color.gray)
    table.cell(dash, 1, 0, str.tostring(strategy.closedtrades))
    table.cell(dash, 0, 1, "Win Rate", text_color=color.gray)
    table.cell(dash, 1, 1, str.tostring(math.round((strategy.wintrades / math.max(1, strategy.closedtrades)) * 100, 1)) + "%")

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:43 am
by FTtrader
Why this architecture fits your rules:

The TradeState UDT: Instead of scattering global variables, state is tracked inside a single data object. You immediately know if a trade is valid, where the stops are, and if the BE phase has been activated.

True Structural Runners: In the first script, the runner had a hard TP (e.g., 3R). That violates the rule of letting structure invalidate the trade. This pro version dynamically calculates the trailing stop based on the ta.lowest() or ta.highest() of the last N bars only after the partial is hit. The market decides when you're done, not a fixed number.

Dynamic Risk Alignment: The get_position_size function normalizes your risk. Whether a setup sweeps liquidity on a 1-minute chart (tight stop) or a 15-minute chart (wider stop), the script will automatically calculate contract size so a loss exactly equals your predefined risk (e.g., 1%).

Liquidity Sweep Triggers: Instead of generic engulfing patterns, the entry engine now looks for actual PA context: price must probe beyond a recent 5-bar pivot and close back inside.

Clean UI Engine: Standard indicator plotting is banned during the trade. Active trades instantiate line.new objects that self-update and then instantly delete upon closure. No more scrolling back and seeing thousands of messy red/green lines across your historical data.

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:44 am
by FTtrader
To move this from a professional script to an institutional-grade algorithmic architecture, we need to strip away retail logic entirely.

A true algorithmic engine relies on four pillars:

Finite State Machine (FSM): Boolean flags (isPartialHit) fail at scale. The trade must exist in explicit, immutable states (Flat, Phase1_Risk, Phase2_Runner).

Cost-Aware Break-Even: True break-even is not your entry price. It is your entry price plus an offset to cover the spread, slippage, and round-trip commissions.

Session Filtering (Killzones): Scalping raw price action outside of high-volume liquidity windows (London/NY overlaps) introduces unnecessary noise.

Automated Exit Tagging: To track your specific "expectancy leak," the engine must categorize and log the exact reason a trade closed (Full Stop, Time Stop, BE Runner, Trailed Runner).

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:45 am
by FTtrader
Here is the refactored engine utilizing Pine Script v5's switch statements, array tracking, and a strict State Machine architecture.

Code: Select all

//@version=5
strategy("FSM Trade Management Engine", overlay=true, initial_capital=10000, calc_on_every_tick=true, commission_type=strategy.commission.cash_per_contract, commission_value=3, slippage=1)

// ==============================================================================
// 1. CONFIGURATION & SESSION MACROS
// ==============================================================================
grp_risk = "Risk & Sizing"
riskPerTrade = input.float(1.0, "Risk Per Trade (%)", step=0.1, group=grp_risk)
beBuffer     = input.int(2, "BE Cost Buffer (Ticks)", group=grp_risk, tooltip="Offset added to BE to cover commissions/spread")

grp_mgmt = "FSM Management Rules"
partialR     = input.float(1.0, "First Partial (R)", step=0.1, group=grp_mgmt)
partialPct   = input.int(50, "Partial Size (%)", step=10, group=grp_mgmt)
timeStopBars = input.int(12, "Time Stop (Bars)", group=grp_mgmt)
trailLookback= input.int(5, "Trailing Structure (Bars)", group=grp_mgmt)

grp_time = "Session Killzones"
sessionTime  = input.session("0800-1130;1330-1600", "Active Trading Windows (NY Time)", group=grp_time)
tz           = input.string("America/New_York", "Timezone", group=grp_time)

// ==============================================================================
// 2. FINITE STATE MACHINE (FSM) ARCHITECTURE
// ==============================================================================
// State Enums
var int STATE_FLAT       = 0 // No active trade
var int STATE_RISK       = 1 // Trade active, original stop loss in place
var int STATE_RUNNER     = 2 // Partial secured, stop at BE + Buffer

type TradeContext
    int    state
    int    dir          // 1 = Long, -1 = Short
    float  entryPrice
    float  stopLoss
    float  targetPrice  // Partial target
    int    entryBar
    string exitReason   // Tagging system for journaling

var TradeContext ctx = TradeContext.new(STATE_FLAT, 0, na, na, na, na, "")

// Exit Reason Logging Arrays (For the Dashboard)
var int countFullStop = 0
var int countTimeStop = 0
var int countBE       = 0
var int countTrailed  = 0

method reset(TradeContext this) =>
    this.state       := STATE_FLAT
    this.dir         := 0
    this.entryPrice  := na
    this.stopLoss    := na
    this.targetPrice := na
    this.entryBar    := na
    this.exitReason  := ""

// ==============================================================================
// 3. SESSION & STRUCTURE ENGINE
// ==============================================================================
inSession = not na(time(timeframe.period, sessionTime, tz))

pivotHigh = ta.pivothigh(high, 5, 5)
pivotLow  = ta.pivotlow(low, 5, 5)
var float swingHigh = na
var float swingLow  = na
if not na(pivotHigh)
    swingHigh := pivotHigh
if not na(pivotLow)
    swingLow := pivotLow

// High-Probability Sweep (Only valid inside session)
validLongSweep  = inSession and low < swingLow and close > swingLow and close > open
validShortSweep = inSession and high > swingHigh and close < swingHigh and close < open

// ==============================================================================
// 4. EXECUTION CONTROLLER
// ==============================================================================
calc_size(stopDist) =>
    riskAmt = strategy.equity * (riskPerTrade / 100)
    tickVal = syminfo.mintick * syminfo.pointvalue
    math.max(0.01, riskAmt / (stopDist / syminfo.mintick * tickVal))

// Entry Router
if (ctx.state == STATE_FLAT and strategy.position_size == 0)
    if (validLongSweep)
        sl = low - (syminfo.mintick * 2)
        dist = close - sl
        if dist > 0
            ctx.state       := STATE_RISK
            ctx.dir         := 1
            ctx.entryPrice  := close
            ctx.stopLoss    := sl
            ctx.targetPrice := close + (dist * partialR)
            ctx.entryBar    := bar_index
            strategy.entry("Long", strategy.long, qty=calc_size(dist))

    else if (validShortSweep)
        sl = high + (syminfo.mintick * 2)
        dist = sl - close
        if dist > 0
            ctx.state       := STATE_RISK
            ctx.dir         := -1
            ctx.entryPrice  := close
            ctx.stopLoss    := sl
            ctx.targetPrice := close - (dist * partialR)
            ctx.entryBar    := bar_index
            strategy.entry("Short", strategy.short, qty=calc_size(dist))

// ==============================================================================
// 5. THE "NO NEGOTIATION" MANAGEMENT FSM
// ==============================================================================
if (strategy.position_size != 0)
    barsHeld = bar_index - ctx.entryBar
    buffer = syminfo.mintick * beBuffer

    switch ctx.state
        // ---------------------------------------------------------
        // PHASE 1: INITIAL RISK EXPOSURE
        // ---------------------------------------------------------
        STATE_RISK =>
            // Failsafe 1: Time Stop
            if (barsHeld >= timeStopBars)
                strategy.close_all(comment="Time Stop")
                ctx.exitReason := "Time"
                
            if (ctx.dir == 1)
                strategy.exit("Partial", "Long", qty_percent=partialPct, limit=ctx.targetPrice, stop=ctx.stopLoss)
                // State Transition Check: Did we hit the partial?
                if (high >= ctx.targetPrice)
                    ctx.state    := STATE_RUNNER
                    ctx.stopLoss := ctx.entryPrice + buffer // BE + Cost Buffer
            else
                strategy.exit("Partial", "Short", qty_percent=partialPct, limit=ctx.targetPrice, stop=ctx.stopLoss)
                if (low <= ctx.targetPrice)
                    ctx.state    := STATE_RUNNER
                    ctx.stopLoss := ctx.entryPrice - buffer 

        // ---------------------------------------------------------
        // PHASE 2: RISK-FREE RUNNER (STRUCTURAL TRAIL)
        // ---------------------------------------------------------
        STATE_RUNNER =>
            if (ctx.dir == 1)
                // Lock into higher lows, never retreating
                newTrail = ta.lowest(low, trailLookback) - (syminfo.mintick * 2)
                if (newTrail > ctx.stopLoss)
                    ctx.stopLoss := newTrail
                strategy.exit("Runner", "Long", stop=ctx.stopLoss)
            else
                newTrail = ta.highest(high, trailLookback) + (syminfo.mintick * 2)
                if (newTrail < ctx.stopLoss)
                    ctx.stopLoss := newTrail
                strategy.exit("Runner", "Short", stop=ctx.stopLoss)

// ==============================================================================
// 6. EVENT LISTENER: TRADE CLOSURE & LOGGING
// ==============================================================================
// Detect exact moment we go flat to log the result
wentFlat = strategy.position_size == 0 and strategy.position_size[1] != 0

if (wentFlat)
    // Determine exit reason based on state at time of closure
    if (ctx.exitReason == "Time")
        countTimeStop += 1
    else if (ctx.state == STATE_RISK)
        countFullStop += 1
    else if (ctx.state == STATE_RUNNER)
        // Check if stopped out at the BE buffer or deep in profit
        isBE = (ctx.dir == 1 and close <= ctx.entryPrice + (syminfo.mintick * beBuffer * 2)) or 
               (ctx.dir == -1 and close >= ctx.entryPrice - (syminfo.mintick * beBuffer * 2))
        
        if isBE
            countBE += 1
        else
            countTrailed += 1
            
    ctx.reset()

// ==============================================================================
// 7. VISUAL & ANALYTICS ENGINE
// ==============================================================================
// Draw session background
bgcolor(inSession ? color.new(color.blue, 96) : na, title="Killzone")

var line slLine = na
var line tpLine = na

if (strategy.position_size != 0)
    if (na(slLine))
        colorSl = ctx.state == STATE_RISK ? color.new(color.red, 20) : color.new(color.purple, 20)
        slLine := line.new(ctx.entryBar, ctx.stopLoss, bar_index, ctx.stopLoss, color=colorSl, width=2, style=line.style_dashed)
        if (ctx.state == STATE_RISK)
            tpLine := line.new(ctx.entryBar, ctx.targetPrice, bar_index, ctx.targetPrice, color=color.new(color.blue, 20), width=2)
    else
        line.set_x2(slLine, bar_index)
        line.set_y1(slLine, ctx.stopLoss)
        line.set_y2(slLine, ctx.stopLoss)
        if (ctx.state == STATE_RISK)
            line.set_x2(tpLine, bar_index)
        else
            line.delete(tpLine)
            line.set_color(slLine, color.new(color.purple, 20)) // Turn SL purple when at BE/Trailing
else
    line.delete(slLine)
    line.delete(tpLine)
    slLine := na
    tpLine := na

// Analytics Dashboard (The Expectancy Leak Tracker)
var table dash = table.new(position.bottom_right, 2, 5, border_width=1, border_color=color.gray)
if (barstate.islast)
    total = math.max(1, countFullStop + countTimeStop + countBE + countTrailed)
    
    table.cell(dash, 0, 0, "Exit Analytics", text_color=color.white, bgcolor=color.new(color.blue, 50))
    table.cell(dash, 1, 0, str.tostring(total) + " Trades", text_color=color.white, bgcolor=color.new(color.blue, 50))
    
    table.cell(dash, 0, 1, "Full Stops (Risk)", text_color=color.red)
    table.cell(dash, 1, 1, str.tostring(math.round(countFullStop/total*100, 1)) + "%")
    
    table.cell(dash, 0, 2, "Time Scratches", text_color=color.orange)
    table.cell(dash, 1, 2, str.tostring(math.round(countTimeStop/total*100, 1)) + "%")
    
    table.cell(dash, 0, 3, "Runner BE Exits", text_color=color.gray)
    table.cell(dash, 1, 3, str.tostring(math.round(countBE/total*100, 1)) + "%")
    
    table.cell(dash, 0, 4, "Runner Trailed", text_color=color.green)
    table.cell(dash, 1, 4, str.tostring(math.round(countTrailed/total*100, 1)) + "%")

Re: Beating cutting winners early as a scalper: rules that stuck

Posted: Thu Sep 24, 2026 10:45 am
by FTtrader
Architectural Upgrades

The Finite State Machine (switch ctx.state): Instead of evaluating a chaotic stack of if statements, the engine now formally transitions between STATE_RISK and STATE_RUNNER. Once state transitions to STATE_RUNNER, the logic for STATE_RISK is fundamentally bypassed by the compiler. It is impossible to accidentally execute Phase 1 logic during Phase 2.

Cost-Aware Trailing (beBuffer): Institutional algorithms do not set Break-Even to the exact entry price. They set it to Entry + (Spread + Commission Offset). This script calculates the required buffer in ticks, ensuring a "Break-Even" stop is genuinely a zero-cost trade, not a minor loss.

Session Filtering (Killzones): The inSession boolean ensures entries only trigger during high-liquidity overlaps. This prevents the algorithmic engine from entering structural sweeps during the dead Asian session where momentum is unlikely to carry to the runner target.

The Expectancy Leak Analytics Dashboard: An event listener is triggered at the exact tick strategy.position_size returns to 0. It queries the FSM to determine the context of the exit. It then outputs a live GUI dashboard on the chart tracking your exit tags (Full Stops vs. Time Scratches vs. BE Outs vs. Profit Trails)—automating the journaling process you rely on to track early cuts.