Re: The 1% Rule Isn't Optional
Posted: Sat Sep 26, 2026 10:43 am
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:
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))