Adding a hard cap is the final failsafe of professional algorithmic trading. Even if your rolling 100-trade window gets extraordinarily "hot" (e.g., a 90% win rate) and the Kelly formula tells you it is mathematically optimal to risk 40% of your account, a hard cap prevents a single "black swan" event from destroying your portfolio on the 101st trade.
In Pine Script, you achieve this by adding a new user input for the maximum limit, and then wrapping your final risk calculation in a math.min() function. This forces the script to compare your calculated Kelly risk against your hard limit and always choose the smaller of the two numbers.
Code: Select all
//@version=5
strategy("Dynamic Kelly + Hard Cap", 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 (The Hard Cap)
// =========================================================================
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
// =========================================================================
// 3. 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
// =========================================================================
// 4. THE KELLY MATH + HARD CAP
// =========================================================================
fullKelly = dynWinRate - ((1.0 - dynWinRate) / dynRR)
// Step 1: Apply fractional safety brake (and bound to 0 if edge is negative)
fractionalKelly = math.max(0, fullKelly * i_fraction)
// Step 2: Apply the Hard Cap (picks the smaller of your Cap or the Kelly calculation)
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
// =========================================================================
// 5. DUMMY ENTRY LOGIC
// =========================================================================
fastSma = ta.sma(close, 10)
slowSma = ta.sma(close, 20)
longTrigger = ta.crossover(fastSma, slowSma)
shortTrigger = ta.crossunder(fastSma, slowSma)
// =========================================================================
// 6. EXECUTION
// =========================================================================
if longTrigger and strategy.opentrades == 0 and appliedRisk > 0
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
strategy.entry("Short", strategy.short, qty=positionSize)
strategy.exit("Exit Short", "Short", stop=close + stopDist, limit=close - (stopDist * dynRR))
// =========================================================================
// 7. DASHBOARD UI
// =========================================================================
var table infoTable = table.new(position.top_right, 2, 5, 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))
// Highlights red if the cap is actively restricting the trade size
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)
table.cell(infoTable, 0, 4, "Current Risk ($)", text_color=color.white, bgcolor=color.new(color.gray, 20))
table.cell(infoTable, 1, 4, "$" + str.tostring(riskAmount, "#.##"), text_color=color.white, bgcolor=color.new(color.gray, 20))