Advertisement IC Markets

The 1% Rule Isn't Optional

Discuss 1-minute to 15-minute price action setups, fading intraday momentum, key support/resistance zones, and proven short-term trading methodologies.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

Institutional algorithms use "circuit breakers" to protect capital from extreme market regimes, anomalous news events, or malfunctioning edge models. When a daily drawdown limit is breached, the algorithm mathematically unplugs itself from the exchange until the next rollover.

In Pine Script, you build this by capturing the account equity at the open of a new daily session (ta.change(time("D"))), calculating the percentage deviation on every subsequent bar, and switching a boolean flag to block all strategy.entry commands if the threshold is crossed.

Here is the fully integrated script with the Daily Drawdown Circuit Breaker and an updated live dashboard:

Code: Select all

//@version=5
strategy("Kelly + Cap + Circuit Breaker", overlay=true, initial_capital=10000, default_qty_type=strategy.cash)

// =========================================================================
// 1. BOOTSTRAP INPUTS 
// =========================================================================
i_initialWinRate = input.float(50.0, title="Bootstrap Win Rate (%)", minval=1.0, maxval=99.0, group="Kelly Parameters") / 100.0
i_initialRR      = input.float(2.0, title="Bootstrap Reward/Risk", minval=0.1, group="Kelly Parameters")
i_fraction       = input.float(0.5, title="Kelly Fraction (0.5 = Half Kelly)", minval=0.1, maxval=1.0, step=0.1, group="Kelly Parameters")
i_lookback       = input.int(100, title="Rolling Trade Window", minval=10, maxval=500, group="Kelly Parameters")

// =========================================================================
// 2. RISK MANAGEMENT INPUTS (Hard Cap & Circuit Breaker)
// =========================================================================
i_atrLen     = input.int(14, title="ATR Length for Stop", group="Risk Management")
i_atrMult    = input.float(1.5, title="ATR Multiplier for Stop Distance", group="Risk Management")
i_maxRisk    = input.float(5.0, title="Absolute Max Risk Cap (%)", minval=0.1, maxval=20.0, step=0.5, group="Risk Management") / 100.0
i_maxDailyDD = input.float(3.0, title="Max Daily Drawdown (%)", minval=0.5, maxval=10.0, step=0.5, group="Risk Management") / 100.0

// =========================================================================
// 3. DAILY DRAWDOWN CIRCUIT BREAKER LOGIC
// =========================================================================
var float startOfDayEquity = strategy.initial_capital
var bool  circuitBreakerTripped = false

// Detect the start of a new trading day (resets at 00:00 exchange time)
isNewDay = ta.change(time("D"))
if isNewDay
    startOfDayEquity := strategy.equity
    circuitBreakerTripped := false // Reset the breaker for the new day

// Calculate the current daily P&L percentage relative to the day's open
currentDailyDD = (strategy.equity - startOfDayEquity) / startOfDayEquity

// Trip the breaker if equity drops below the threshold
if currentDailyDD <= -i_maxDailyDD
    circuitBreakerTripped := true

// =========================================================================
// 4. DYNAMIC EDGE CALCULATION
// =========================================================================
var float dynWinRate = i_initialWinRate
var float dynRR      = i_initialRR

int totalTrades = strategy.closedtrades

if totalTrades > 0
    int wins = 0
    float grossProfit = 0.0
    float grossLoss = 0.0
    int winCount = 0
    int lossCount = 0

    int startIdx = math.max(0, totalTrades - i_lookback)
    int tradesToAnalyze = totalTrades - startIdx

    if tradesToAnalyze > 0
        for i = startIdx to totalTrades - 1
            float profit = strategy.closedtrades.profit(i)
            if profit > 0
                wins += 1
                grossProfit += profit
                winCount += 1
            else if profit < 0
                grossLoss += math.abs(profit)
                lossCount += 1

        dynWinRate := wins / tradesToAnalyze
        float avgWin = winCount > 0 ? grossProfit / winCount : 0.0
        float avgLoss = lossCount > 0 ? grossLoss / lossCount : 0.0
        dynRR := avgLoss > 0 ? avgWin / avgLoss : i_initialRR

// =========================================================================
// 5. THE KELLY MATH + HARD CAP
// =========================================================================
fullKelly       = dynWinRate - ((1.0 - dynWinRate) / dynRR)
fractionalKelly = math.max(0, fullKelly * i_fraction)
appliedRisk     = math.min(i_maxRisk, fractionalKelly)

riskAmount      = strategy.equity * appliedRisk
atr             = ta.atr(i_atrLen)
stopDist        = atr * i_atrMult

positionSize    = stopDist > 0 ? (riskAmount / stopDist) : 0

// =========================================================================
// 6. DUMMY ENTRY LOGIC
// =========================================================================
fastSma      = ta.sma(close, 10)
slowSma      = ta.sma(close, 20)
longTrigger  = ta.crossover(fastSma, slowSma)
shortTrigger = ta.crossunder(fastSma, slowSma)

// =========================================================================
// 7. EXECUTION (Blocked by Circuit Breaker)
// =========================================================================
// Added "and not circuitBreakerTripped" to the entry conditions
if longTrigger and strategy.opentrades == 0 and appliedRisk > 0 and not circuitBreakerTripped
    strategy.entry("Long", strategy.long, qty=positionSize)
    strategy.exit("Exit Long", "Long", stop=close - stopDist, limit=close + (stopDist * dynRR))

if shortTrigger and strategy.opentrades == 0 and appliedRisk > 0 and not circuitBreakerTripped
    strategy.entry("Short", strategy.short, qty=positionSize)
    strategy.exit("Exit Short", "Short", stop=close + stopDist, limit=close - (stopDist * dynRR))

// =========================================================================
// 8. DASHBOARD UI
// =========================================================================
var table infoTable = table.new(position.top_right, 2, 6, border_width=1, border_color=color.gray, frame_color=color.black, frame_width=1)
if barstate.islast
    table.cell(infoTable, 0, 0, "Rolling Win Rate", text_color=color.white, bgcolor=color.new(color.purple, 20))
    table.cell(infoTable, 1, 0, str.tostring(dynWinRate * 100, "#.##") + "%", text_color=color.white, bgcolor=color.new(color.purple, 20))
    
    table.cell(infoTable, 0, 1, "Rolling R/R", text_color=color.white, bgcolor=color.new(color.purple, 20))
    table.cell(infoTable, 1, 1, str.tostring(dynRR, "#.##"), text_color=color.white, bgcolor=color.new(color.purple, 20))
    
    table.cell(infoTable, 0, 2, "Fractional Kelly %", text_color=color.white, bgcolor=color.new(color.blue, 20))
    table.cell(infoTable, 1, 2, str.tostring(fractionalKelly * 100, "#.##") + "%", text_color=color.white, bgcolor=color.new(color.blue, 20))
    
    isCapped = fractionalKelly > i_maxRisk
    capColor = isCapped ? color.new(color.red, 0) : color.new(color.teal, 20)
    table.cell(infoTable, 0, 3, isCapped ? "RISK CAPPED AT MAX" : "Applied Risk", text_color=color.white, bgcolor=capColor)
    table.cell(infoTable, 1, 3, str.tostring(appliedRisk * 100, "#.##") + "%", text_color=color.white, bgcolor=capColor)
    
    // Circuit Breaker UI Logic
    ddColor = circuitBreakerTripped ? color.new(color.red, 0) : color.new(color.gray, 20)
    table.cell(infoTable, 0, 4, circuitBreakerTripped ? "CIRCUIT BREAKER ACTIVE" : "Daily Drawdown", text_color=color.white, bgcolor=ddColor)
    table.cell(infoTable, 1, 4, str.tostring(currentDailyDD * 100, "#.##") + "%", text_color=color.white, bgcolor=ddColor)

    table.cell(infoTable, 0, 5, "Current Risk ($)", text_color=color.white, bgcolor=color.new(color.gray, 20))
    table.cell(infoTable, 1, 5, "$" + str.tostring(riskAmount, "#.##"), text_color=color.white, bgcolor=color.new(color.gray, 20))
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Recommended broker for automated trading & scalping IC Markets
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

Key Functional Additions

The ta.change(time("D")) Anchor: This automatically fires exactly once when the exchange clock ticks over to a new calendar day. It grabs your current equity and locks it into the startOfDayEquity variable. (Note: In 24/7 markets like crypto, this triggers at 00:00 UTC).

The Boolean Trap (circuitBreakerTripped): If your open equity bleeds past your limit (e.g., -3%), this flag permanently flips to true for the remainder of the session. It does not reset until the time("D") anchor fires the next day.

Execution Blockade: The single addition of and not circuitBreakerTripped to your strategy.entry block ensures no new orders can fire, effectively shutting the algorithm down to preserve capital.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

To elevate this from a retail script to an institutional-grade algorithmic execution model, we must address the three silent killers of quantitative trading: Market Friction, Leverage Constraints, and Market Regimes.

A professional script doesn't just calculate theoretical edge; it accounts for execution reality.

Here is the "Institutional Grade" upgrade.

What Makes This Script "Pro"?

Friction Integration (Slippage & Commissions): The strategy() header now natively deducts a 0.03% commission and 2 ticks of slippage per trade. Your rolling Kelly calculation now uses your net edge, preventing the algo from sizing up on gross profits that are actually being eaten by broker fees.

Maximum Leverage Override: If your stop loss is extremely tight (e.g., $0.05 away), the Kelly formula might tell you to buy 15,000 shares to reach your 2% risk. If your account doesn't have the margin to cover a 15,000-share position, the exchange rejects the order. The script now calculates your broker's Max Leverage limits and automatically chokes the position size down to fit your buying power.

High-Water Mark (HWM) Kill Switch: Professional desks don't just track daily drawdowns; they track Peak-to-Trough drawdowns. The script logs your highest all-time equity balance (the High-Water Mark). If you ever drop a set percentage (e.g., 10%) from your peak, the algorithm unplugs itself permanently for review.

Volatility Regime Filter (ATR %): The algorithm measures current volatility as a percentage of the asset's price. If volatility drops below your minimum threshold (chop zone) or spikes above your maximum (news events/flash crashes), it blocks entries.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

Pinescript Pro

Code: Select all

//@version=5
// Native commission & slippage deduction added to strategy header for accurate Net Edge calculations
strategy("Inst. Kelly Algorithmic Engine", overlay=true, initial_capital=10000, default_qty_type=strategy.cash, commission_type=strategy.commission.percent, commission_value=0.03, slippage=2)

// =========================================================================
// 1. DYNAMIC EDGE INPUTS (Net Kelly Parameters)
// =========================================================================
i_initialWinRate = input.float(50.0, "Bootstrap Win Rate (%)", minval=1.0, maxval=99.0, group="Kelly Parameters") / 100.0
i_initialRR      = input.float(2.0, "Bootstrap Reward/Risk", minval=0.1, group="Kelly Parameters")
i_fraction       = input.float(0.5, "Kelly Fraction (0.5 = Half Kelly)", minval=0.1, maxval=1.0, step=0.1, group="Kelly Parameters")
i_lookback       = input.int(100, "Rolling Trade Window", minval=10, maxval=500, group="Kelly Parameters")

// =========================================================================
// 2. RISK & EXECUTION LIMITS (Institutional Controls)
// =========================================================================
i_atrLen       = input.int(14, "ATR Length for Stop", group="Risk Limits")
i_atrMult      = input.float(1.5, "ATR Multiplier (Stop Distance)", group="Risk Limits")
i_maxRisk      = input.float(5.0, "Absolute Max Risk per Trade (%)", maxval=20.0, step=0.5, group="Risk Limits") / 100.0
i_maxLeverage  = input.float(10.0, "Max Broker Leverage Constraint (x)", minval=1.0, step=1.0, group="Risk Limits", tooltip="Caps position quantity to prevent margin rejections.")

// =========================================================================
// 3. CIRCUIT BREAKERS & REGIME FILTERS
// =========================================================================
i_maxDailyDD = input.float(3.0, "Max Daily Drawdown (%)", minval=0.5, maxval=10.0, step=0.5, group="Circuit Breakers") / 100.0
i_hwmDD      = input.float(15.0, "Max Peak-to-Trough Drawdown (%)", minval=5.0, maxval=50.0, step=1.0, group="Circuit Breakers") / 100.0
i_minVol     = input.float(0.1, "Min Volatility Filter (ATR %)", step=0.1, group="Regime Filters", tooltip="Blocks trades if market is too choppy/flat.")
i_maxVol     = input.float(3.0, "Max Volatility Filter (ATR %)", step=0.1, group="Regime Filters", tooltip="Blocks trades during extreme news/flash crashes.")

// =========================================================================
// 4. CIRCUIT BREAKER LOGIC (Daily & Peak-to-Trough)
// =========================================================================
var float hwmEquity = strategy.initial_capital
var float startOfDayEquity = strategy.initial_capital
var bool  dailyBreakerTripped = false
var bool  hwmBreakerTripped = false

// Update High-Water Mark
hwmEquity := math.max(hwmEquity, strategy.equity)

// Detect New Day for Daily Breaker
isNewDay = ta.change(time("D"))
if isNewDay
    startOfDayEquity := strategy.equity
    dailyBreakerTripped := false 

// Check Drawdown Status
currentDailyDD = (strategy.equity - startOfDayEquity) / startOfDayEquity
currentHwmDD   = (strategy.equity - hwmEquity) / hwmEquity

if currentDailyDD <= -i_maxDailyDD
    dailyBreakerTripped := true
if currentHwmDD <= -i_hwmDD
    hwmBreakerTripped := true

bool systemHalted = dailyBreakerTripped or hwmBreakerTripped

// =========================================================================
// 5. VOLATILITY REGIME CHECK
// =========================================================================
atrVal   = ta.atr(i_atrLen)
atrPct   = (atrVal / close) * 100
bool isVolatileEnough = atrPct >= i_minVol and atrPct <= i_maxVol

// =========================================================================
// 6. DYNAMIC NET EDGE CALCULATION (The Rolling Window)
// =========================================================================
var float dynWinRate = i_initialWinRate
var float dynRR      = i_initialRR
int totalTrades      = strategy.closedtrades

if totalTrades > 0
    int wins = 0
    float netProfit = 0.0
    float netLoss = 0.0
    int winCount = 0
    int lossCount = 0

    int startIdx = math.max(0, totalTrades - i_lookback)
    int tradesToAnalyze = totalTrades - startIdx

    if tradesToAnalyze > 0
        for i = startIdx to totalTrades - 1
            // This profit figure natively includes the slippage and commission defined in the strategy header
            float tradeProfit = strategy.closedtrades.profit(i)
            if tradeProfit > 0
                wins += 1
                netProfit += tradeProfit
                winCount += 1
            else if tradeProfit < 0
                netLoss += math.abs(tradeProfit)
                lossCount += 1

        dynWinRate := wins / tradesToAnalyze
        float avgWin = winCount > 0 ? netProfit / winCount : 0.0
        float avgLoss = lossCount > 0 ? netLoss / lossCount : 0.0
        dynRR := avgLoss > 0 ? avgWin / avgLoss : i_initialRR

// =========================================================================
// 7. MULTI-FACTOR POSITION SIZING MATH
// =========================================================================
// Calculate Edge Risk
fullKelly       = dynWinRate - ((1.0 - dynWinRate) / dynRR)
fractionalKelly = math.max(0, fullKelly * i_fraction)
appliedRisk     = math.min(i_maxRisk, fractionalKelly)

// Calculate Theoretical Quantity
riskAmount      = strategy.equity * appliedRisk
stopDist        = atrVal * i_atrMult
theoreticalQty  = stopDist > 0 ? (riskAmount / stopDist) : 0

// Apply Margin/Leverage Constraint Override
maxAllowedQty   = (strategy.equity * i_maxLeverage) / close
bool isMarginCapped = theoreticalQty > maxAllowedQty
finalQty        = math.min(theoreticalQty, maxAllowedQty)

// =========================================================================
// 8. DUMMY ENTRY LOGIC (Replace with your trigger)
// =========================================================================
longTrigger  = ta.crossover(ta.sma(close, 10), ta.sma(close, 20))
shortTrigger = ta.crossunder(ta.sma(close, 10), ta.sma(close, 20))

bool canTrade = not systemHalted and isVolatileEnough and appliedRisk > 0

// =========================================================================
// 9. EXECUTION
// =========================================================================
if longTrigger and strategy.opentrades == 0 and canTrade
    strategy.entry("Long", strategy.long, qty=finalQty)
    strategy.exit("Exit Long", "Long", stop=close - stopDist, limit=close + (stopDist * dynRR))

if shortTrigger and strategy.opentrades == 0 and canTrade
    strategy.entry("Short", strategy.short, qty=finalQty)
    strategy.exit("Exit Short", "Short", stop=close + stopDist, limit=close - (stopDist * dynRR))

// =========================================================================
// 10. INSTITUTIONAL DASHBOARD UI
// =========================================================================
var table t = table.new(position.top_right, 2, 7, border_width=1, border_color=color.gray, frame_color=color.black, frame_width=1)
if barstate.islast
    // System Status
    sysColor = hwmBreakerTripped ? color.red : dailyBreakerTripped ? color.orange : color.green
    sysText  = hwmBreakerTripped ? "HWM KILL SWITCH ACTIVE" : dailyBreakerTripped ? "DAILY BREAKER ACTIVE" : "SYSTEM ONLINE"
    table.cell(t, 0, 0, "Engine Status", text_color=color.white, bgcolor=sysColor)
    table.cell(t, 1, 0, sysText, text_color=color.white, bgcolor=sysColor)
    
    // Net Rolling Edge
    table.cell(t, 0, 1, "Net Rolling Win Rate", text_color=color.white, bgcolor=color.new(color.purple, 20))
    table.cell(t, 1, 1, str.tostring(dynWinRate * 100, "#.##") + "%", text_color=color.white, bgcolor=color.new(color.purple, 20))
    
    table.cell(t, 0, 2, "Net Rolling R/R", text_color=color.white, bgcolor=color.new(color.purple, 20))
    table.cell(t, 1, 2, str.tostring(dynRR, "#.##"), text_color=color.white, bgcolor=color.new(color.purple, 20))
    
    // Risk Metrics
    riskColor = (fractionalKelly > i_maxRisk) ? color.new(color.red, 0) : color.new(color.teal, 20)
    table.cell(t, 0, 3, "Target vs Max Risk", text_color=color.white, bgcolor=riskColor)
    table.cell(t, 1, 3, str.tostring(fractionalKelly*100, "#.##") + "% -> " + str.tostring(appliedRisk*100, "#.##") + "%", text_color=color.white, bgcolor=riskColor)
    
    // Margin Utilization
    marginColor = isMarginCapped ? color.new(color.orange, 0) : color.new(color.gray, 20)
    table.cell(t, 0, 4, isMarginCapped ? "MARGIN REJECTION CAPPED" : "Leverage Constraint", text_color=color.white, bgcolor=marginColor)
    table.cell(t, 1, 4, isMarginCapped ? "Capped to " + str.tostring(i_maxLeverage) + "x" : "Clear", text_color=color.white, bgcolor=marginColor)
    
    // Regime Filter
    volColor = isVolatileEnough ? color.new(color.gray, 20) : color.new(color.orange, 0)
    table.cell(t, 0, 5, isVolatileEnough ? "Volatility Filter" : "MARKET REGIME FILTERED", text_color=color.white, bgcolor=volColor)
    table.cell(t, 1, 5, str.tostring(atrPct, "#.##") + "%", text_color=color.white, bgcolor=volColor)

    // Capital
    table.cell(t, 0, 6, "Peak-to-Trough DD", text_color=color.white, bgcolor=color.new(color.gray, 20))
    table.cell(t, 1, 6, str.tostring(currentHwmDD * 100, "#.##") + "%", text_color=color.white, bgcolor=color.new(color.gray, 20))
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

To execute partial profit-taking in Pine Script v5, you must issue multiple strategy.exit commands that target the exact same entry ID (e.g., "Long").

When you specify qty in the first exit command, the exchange engine will pull only that fraction of the shares/lots for the first limit order. By leaving the qty parameter blank on the second exit command, Pine Script automatically assigns the entire remaining balance to the final target, preventing orphaned fractional shares.

Replace Section 9 (EXECUTION) in your script with this updated logic:

Code: Select all

// =========================================================================
// 9. EXECUTION (With 50% Scale-Out)
// =========================================================================
if longTrigger and strategy.opentrades == 0 and canTrade
    strategy.entry("Long", strategy.long, qty=finalQty)
    
    // Calculate precise price levels
    float stopLevel = close - stopDist
    float tp1Level  = close + stopDist                // 1:1 Reward/Risk
    float tp2Level  = close + (stopDist * dynRR)      // Dynamic Target
    
    // Exit 1: Dump 50% of the position at 1:1 R/R
    strategy.exit("TP1 Long", "Long", qty=finalQty * 0.5, stop=stopLevel, limit=tp1Level)
    
    // Exit 2: Let the remaining balance run to the dynamic target
    // Omitting the 'qty' parameter here forces it to close 100% of whatever is left
    strategy.exit("TP2 Long", "Long", stop=stopLevel, limit=tp2Level)

if shortTrigger and strategy.opentrades == 0 and canTrade
    strategy.entry("Short", strategy.short, qty=finalQty)
    
    // Calculate precise price levels
    float stopLevel = close + stopDist
    float tp1Level  = close - stopDist                // 1:1 Reward/Risk
    float tp2Level  = close - (stopDist * dynRR)      // Dynamic Target
    
    // Exit 1: Dump 50% of the position at 1:1 R/R
    strategy.exit("TP1 Short", "Short", qty=finalQty * 0.5, stop=stopLevel, limit=tp1Level)
    
    // Exit 2: Let the remaining balance run to the dynamic target
    strategy.exit("TP2 Short", "Short", stop=stopLevel, limit=tp2Level)
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

The Institutional Reality of Scaling Out

Scaling out mathematically alters your strategy's expectancy. Because you are taking 50% of your size off the table at 1R, your average winning trade size will decrease.

If your previous historical R/R was 2.0 on a full position, the math now looks like this:

50% secured at 1.0R

50% secured at 2.0R

New Expected Value: 1.5R

Scaling out intentionally lowers your total gross return in exchange for a massive boost to your Win Rate and a drastically smoother equity curve. For a high-frequency scalper utilizing aggressive fractional Kelly sizing, this trade-off is almost always mathematically superior because it defends against the deep drawdowns that trigger the circuit breakers we built.

Key insight: Pine Script's backtester evaluates strategy.exit limits using high/low wick data intra-bar. If the wick hits both your TP1 and your Stop Loss in the exact same 1-minute candle, Pine assumes the worst-case scenario and triggers the Stop Loss first.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

To move a stop loss dynamically in Pine Script, you must transition from issuing static orders to building a state machine. Because strategy.exit orders remain active on the exchange server, you can modify an existing order simply by re-issuing it with the exact same ID ("TP2 Long") but with updated parameters.

Instead of writing logic that guesses if the price wicked your TP1 level, professional scripts monitor the actual broker fill. If your open position size shrinks but doesn't hit zero, it means the exchange executed your TP1 limit order.

Here is the complete Execution & Trade Management section to replace Section 9 in your script:

Code: Select all

// =========================================================================
// 9. EXECUTION & DYNAMIC TRADE MANAGEMENT
// =========================================================================
// State tracking variables stored in memory across bars
var float entryPrice = na
var float stopLevel  = na
var float tp2Level   = na
var bool  tp1Hit     = false

// --- ENTRY LOGIC ---
if longTrigger and strategy.opentrades == 0 and canTrade
    strategy.entry("Long", strategy.long, qty=finalQty)
    
    // Lock in the levels at the moment of execution
    entryPrice := close
    stopLevel  := close - stopDist
    float tp1Level = close + stopDist
    tp2Level   := close + (stopDist * dynRR)
    tp1Hit     := false
    
    // Issue the initial dual-exit orders
    strategy.exit("TP1 Long", "Long", qty=finalQty * 0.5, stop=stopLevel, limit=tp1Level)
    strategy.exit("TP2 Long", "Long", stop=stopLevel, limit=tp2Level)

if shortTrigger and strategy.opentrades == 0 and canTrade
    strategy.entry("Short", strategy.short, qty=finalQty)
    
    entryPrice := close
    stopLevel  := close + stopDist
    float tp1Level = close - stopDist
    tp2Level   := close - (stopDist * dynRR)
    tp1Hit     := false
    
    strategy.exit("TP1 Short", "Short", qty=finalQty * 0.5, stop=stopLevel, limit=tp1Level)
    strategy.exit("TP2 Short", "Short", stop=stopLevel, limit=tp2Level)

// --- BREAK-EVEN MANAGEMENT ---
// If our current position size is smaller than the previous bar's size (and > 0), 
// it confirms our TP1 partial limit order was successfully filled by the exchange engine.

// Long BE Trigger
if strategy.position_size > 0 and strategy.position_size < strategy.position_size[1]
    if not tp1Hit
        tp1Hit := true
        stopLevel := entryPrice // Move the variable to Break-Even
        
        // Re-issue the order with the identical ID to override the previous stop
        strategy.exit("TP2 Long", "Long", stop=stopLevel, limit=tp2Level)

// Short BE Trigger (Short sizes are negative, so we check if it became *greater*)
if strategy.position_size < 0 and strategy.position_size > strategy.position_size[1]
    if not tp1Hit
        tp1Hit := true
        stopLevel := entryPrice // Move the variable to Break-Even
        
        strategy.exit("TP2 Short", "Short", stop=stopLevel, limit=tp2Level)
        
// Reset the state machine when the trade fully closes
if strategy.position_size == 0
    tp1Hit := false
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

The "Fill-Verification" Edge

Many retail scripts use if high >= tp1Level to trigger a break-even move. That is a critical flaw. In live markets, the price can wick your target by a single tick, but due to order book liquidity spreads, your limit order may not actually get filled. Moving your stop to break-even before you've actually secured the TP1 cash leaves you exposed.

By tracking strategy.position_size[1], the algorithm explicitly waits for the exchange engine to confirm the scale-out fill before defending the remaining position. In high-frequency 1-minute and 5-minute price action scalping, leaving the stop at its original location after securing 1R is mathematically disastrous. When liquidity sweeps occur and the structure reverses against you abruptly, this exact break-even mechanic preserves the geometric growth of the Kelly curve we built earlier.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

MetaTrader 5 processes execution fundamentally differently than Pine Script. Pine operates on closed bars and manages states via simulated exchange engine rules. MQL5 is an event-driven C++ environment where you must manually interact with the live broker order book, account history pools, and tick-level Bid/Ask spreads.

The most critical difference for a scalper is how partials are handled. Instead of placing two simultaneous limit orders, professional MT5 EAs issue a single market order. The EA then monitors the tick stream (OnTick) and manually executes a partial close (PositionClosePartial) when the price crosses the 1R threshold, subsequently modifying the stop loss (PositionModify) on the remaining volume.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: The 1% Rule Isn't Optional

Post by PTScalper »

MQL5 Expert Advisor Translation

Save this as an .mq5 file. It utilizes the standard CTrade library for execution and abstracts the Kelly math and history parsing into clean, C#-style methods.

Code: Select all

//+------------------------------------------------------------------+
//|                                     Inst_Kelly_Engine_Scalper.mq5|
//+------------------------------------------------------------------+
#property strict
#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\SymbolInfo.mqh>

CTrade         trade;
CPositionInfo  position;
CSymbolInfo    symbolInfo;

// =========================================================================
// 1. INPUT PARAMETERS (Matching Pine Script)
// =========================================================================
sinput string  grp1 = "--- Dynamic Edge (Kelly) ---";
input double   InpBootstrapWinRate = 50.0;    // Bootstrap Win Rate (%)
input double   InpBootstrapRR      = 2.0;     // Bootstrap Reward/Risk
input double   InpKellyFraction    = 0.5;     // Kelly Fraction (0.5 = Half)
input int      InpLookback         = 100;     // Rolling Trade Window

sinput string  grp2 = "--- Risk & Execution Limits ---";
input int      InpAtrPeriod        = 14;      // ATR Period
input double   InpAtrMultiplier    = 1.5;     // ATR Stop Multiplier
input double   InpMaxRisk          = 5.0;     // Max Risk per Trade (%)
input double   InpMaxLeverage      = 10.0;    // Max Leverage (Margin Cap)

sinput string  grp3 = "--- Circuit Breakers ---";
input double   InpMaxDailyDD       = 3.0;     // Max Daily Drawdown (%)
input double   InpHwmDD            = 15.0;    // Max Peak-to-Trough DD (%)

// =========================================================================
// 2. GLOBAL STATE VARIABLES
// =========================================================================
int            atrHandle, smaFastHandle, smaSlowHandle;
double         hwmEquity;
double         startOfDayEquity;
int            currentDay;
bool           systemHalted = false;

// Trade Management State
bool           tp1Hit = false;
double         initialVolume = 0.0;
double         entryPrice = 0.0;
double         tp1Level = 0.0;

// =========================================================================
// INITIALIZATION
// =========================================================================
int OnInit()
{
    symbolInfo.Name(_Symbol);
    trade.SetExpertMagicNumber(1337);
    
    atrHandle = iATR(_Symbol, PERIOD_CURRENT, InpAtrPeriod);
    smaFastHandle = iMA(_Symbol, PERIOD_CURRENT, 10, 0, MODE_SMA, PRICE_CLOSE);
    smaSlowHandle = iMA(_Symbol, PERIOD_CURRENT, 20, 0, MODE_SMA, PRICE_CLOSE);
    
    hwmEquity = AccountInfoDouble(ACCOUNT_EQUITY);
    startOfDayEquity = hwmEquity;
    currentDay = TimeDay(TimeCurrent());
    
    return(INIT_SUCCEEDED);
}

// =========================================================================
// CORE EVENT LOOP
// =========================================================================
void OnTick()
{
    if(!symbolInfo.RefreshRates()) return;

    // 1. Update Circuit Breakers & High-Water Mark
    CheckCircuitBreakers();
    if(systemHalted) return;

    // 2. Dynamic Trade Management (Scale-out & Break-even)
    ManageOpenPositions();

    // 3. Entry Logic (Only if no open positions)
    if(PositionsTotal() == 0)
    {
        // Reset state
        tp1Hit = false; 
        initialVolume = 0;

        double fastSMA[], slowSMA[], atrArr[];
        CopyBuffer(smaFastHandle, 0, 0, 2, fastSMA);
        CopyBuffer(smaSlowHandle, 0, 0, 2, slowSMA);
        CopyBuffer(atrHandle, 0, 0, 1, atrArr);
        
        bool longTrigger = (fastSMA[1] <= slowSMA[1] && fastSMA[0] > slowSMA[0]);
        bool shortTrigger = (fastSMA[1] >= slowSMA[1] && fastSMA[0] < slowSMA[0]);

        if(longTrigger || shortTrigger)
        {
            double stopDist = atrArr[0] * InpAtrMultiplier;
            double lotSize = CalculatePositionSize(stopDist);
            
            if(lotSize > 0)
            {
                if(longTrigger) ExecuteTrade(ORDER_TYPE_BUY, lotSize, stopDist);
                if(shortTrigger) ExecuteTrade(ORDER_TYPE_SELL, lotSize, stopDist);
            }
        }
    }
}

// =========================================================================
// DYNAMIC KELLY & LOT SIZING
// =========================================================================
double CalculatePositionSize(double stopDistancePoints)
{
    double dynWinRate = InpBootstrapWinRate / 100.0;
    double dynRR = InpBootstrapRR;
    
    // Parse History for Net Edge Calculation
    HistorySelect(0, TimeCurrent());
    int totalDeals = HistoryDealsTotal();
    int analyzed = 0, wins = 0, losses = 0;
    double grossProfit = 0, grossLoss = 0;
    
    for(int i = totalDeals - 1; i >= 0 && analyzed < InpLookback; i--)
    {
        ulong ticket = HistoryDealGetTicket(i);
        if(HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_OUT)
        {
            double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT) + 
                            HistoryDealGetDouble(ticket, DEAL_COMMISSION) + 
                            HistoryDealGetDouble(ticket, DEAL_SWAP); // Net Profit
            
            if(profit > 0) { wins++; grossProfit += profit; }
            else if(profit < 0) { losses++; grossLoss += MathAbs(profit); }
            analyzed++;
        }
    }
    
    if(analyzed > 0)
    {
        dynWinRate = (double)wins / analyzed;
        double avgWin = (wins > 0) ? grossProfit / wins : 0;
        double avgLoss = (losses > 0) ? grossLoss / losses : 0;
        if(avgLoss > 0) dynRR = avgWin / avgLoss;
    }
    
    // Kelly Math
    double fullKelly = dynWinRate - ((1.0 - dynWinRate) / dynRR);
    double fracKelly = MathMax(0.0, fullKelly * InpKellyFraction);
    double appliedRisk = MathMin(InpMaxRisk / 100.0, fracKelly);
    
    if(appliedRisk <= 0) return 0.0;

    // Convert Risk $ to Lot Size
    double riskAmount = AccountInfoDouble(ACCOUNT_EQUITY) * appliedRisk;
    double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
    double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
    
    if(tickValue == 0 || tickSize == 0) return 0.0;
    
    double pointsLoss = stopDistancePoints / tickSize;
    double calcLots = riskAmount / (pointsLoss * tickValue);
    
    // Normalize and apply constraints
    double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
    double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
    double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
    
    calcLots = MathFloor(calcLots / lotStep) * lotStep;
    calcLots = MathMax(minLot, MathMin(maxLot, calcLots));
    
    return calcLots;
}

// =========================================================================
// EXECUTION & DYNAMIC MANAGEMENT
// =========================================================================
void ExecuteTrade(ENUM_ORDER_TYPE type, double volume, double stopDist)
{
    double price = (type == ORDER_TYPE_BUY) ? symbolInfo.Ask() : symbolInfo.Bid();
    double sl = (type == ORDER_TYPE_BUY) ? price - stopDist : price + stopDist;
    
    if(trade.PositionOpen(_Symbol, type, volume, price, sl, 0))
    {
        entryPrice = price;
        initialVolume = volume;
        tp1Level = (type == ORDER_TYPE_BUY) ? price + stopDist : price - stopDist;
        tp1Hit = false;
    }
}

void ManageOpenPositions()
{
    if(PositionsTotal() > 0 && position.Select(_Symbol))
    {
        if(!tp1Hit)
        {
            double currentPrice = (position.PositionType() == POSITION_TYPE_BUY) ? symbolInfo.Bid() : symbolInfo.Ask();
            bool hitTarget = (position.PositionType() == POSITION_TYPE_BUY) ? (currentPrice >= tp1Level) : (currentPrice <= tp1Level);
            
            if(hitTarget)
            {
                // 1. Close 50%
                double partialVol = NormalizeDouble(position.Volume() / 2.0, 2);
                double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
                if(partialVol >= minLot)
                {
                    trade.PositionClosePartial(_Symbol, partialVol);
                }
                
                // 2. Move Stop to Break-Even (Entry Price)
                trade.PositionModify(_Symbol, entryPrice, 0);
                tp1Hit = true;
            }
        }
    }
}

// =========================================================================
// CIRCUIT BREAKER UTILITY
// =========================================================================
void CheckCircuitBreakers()
{
    double equity = AccountInfoDouble(ACCOUNT_EQUITY);
    hwmEquity = MathMax(hwmEquity, equity);
    
    int today = TimeDay(TimeCurrent());
    if(today != currentDay)
    {
        startOfDayEquity = equity;
        currentDay = today;
        systemHalted = false; // Reset daily
    }
    
    double dailyDD = (equity - startOfDayEquity) / startOfDayEquity;
    double peakDD = (equity - hwmEquity) / hwmEquity;
    
    if(dailyDD <= -(InpMaxDailyDD / 100.0) || peakDD <= -(InpHwmDD / 100.0))
    {
        systemHalted = true;
        if(PositionsTotal() > 0) trade.PositionClose(_Symbol); // Liquidate
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply