Most Pine Script strategies treat risk as an afterthought — a fixed lot size bolted on after the signal logic is working. This is backwards. In professional algo trading, risk management is the foundation that signal logic sits on top of. Get the risk layer right first, and the strategy becomes far more robust regardless of the specific edge you're trading.
This guide builds a complete, reusable risk management module in Pine Script v6. It covers every layer: per-trade risk sizing, daily loss limits, trailing stop mechanics, and maximum drawdown protection. By the end, you'll have a framework you can copy into any strategy and immediately improve its live performance characteristics.
Architecture: The Risk Module Approach
Rather than scattering risk logic throughout a strategy, we'll build it as a self-contained block at the top of the script. This makes it easy to copy between strategies, adjust parameters in one place, and reason about the risk behaviour independently from the signal logic.
The module has four layers, each building on the previous:
- Account state tracking — equity, peak, daily start balance
- Limit checks — daily loss, total drawdown, position guards
- Position sizing — ATR-based risk-per-trade calculation
- Stop management — initial stop, breakeven move, trailing stop
Layer 1 — Account State Tracking
//@version=6
strategy("BotJockie Risk Framework",
initial_capital = 500000,
default_qty_type = strategy.fixed,
commission_type = strategy.commission.percent,
commission_value = 0.05,
slippage = 2)
// ─── INPUTS ───────────────────────────────────────────
const string GRP_RISK = "Risk Management"
risk_per_trade_pct = input.float(1.0, "Risk per Trade (%)", group=GRP_RISK, minval=0.1, maxval=5.0, step=0.1) / 100
max_daily_loss_pct = input.float(3.0, "Max Daily Loss (%)", group=GRP_RISK, minval=0.5, maxval=10.0, step=0.5) / 100
max_drawdown_pct = input.float(8.0, "Max Drawdown (%)", group=GRP_RISK, minval=1.0, maxval=20.0, step=1.0) / 100
atr_period = input.int(14, "ATR Period", group=GRP_RISK)
atr_stop_mult = input.float(2.0, "Stop ATR Multiplier", group=GRP_RISK, step=0.25)
be_trigger_r = input.float(1.0, "Breakeven Trigger (R)", group=GRP_RISK, step=0.25)
trail_start_r = input.float(2.0, "Trail Start (R)", group=GRP_RISK, step=0.25)
trail_atr_mult = input.float(1.5, "Trail ATR Multiplier", group=GRP_RISK, step=0.25)
// ─── ACCOUNT STATE ────────────────────────────────────
var float peak_equity = strategy.initial_capital
var float session_start_eq = strategy.initial_capital
new_session = ta.change(time("D")) != 0 or bar_index == 0
if new_session
session_start_eq := strategy.equity
peak_equity := math.max(peak_equity, strategy.equity)
Layer 2 — Limit Checks
// ─── LIMIT CALCULATIONS ───────────────────────────────
daily_pnl_pct = (strategy.equity - session_start_eq) / session_start_eq
total_dd_pct = (strategy.equity - peak_equity) / peak_equity
daily_limit_hit = daily_pnl_pct <= -max_daily_loss_pct
total_dd_hit = total_dd_pct <= -max_drawdown_pct
already_long = strategy.position_size > 0
already_short = strategy.position_size < 0
// Master kill switch — no new entries if any limit is breached
trading_allowed = not daily_limit_hit and not total_dd_hit
// Emergency close — if total drawdown breached, flatten everything
if total_dd_hit and strategy.position_size != 0
strategy.close_all(comment="MAX DD HIT")
Set max_daily_loss_pct slightly below your broker's or prop firm's hard limit. If the firm's limit is 5%, set yours to 4.5%. This gives you a buffer against slippage on the forced close.
Layer 3 — Dynamic Position Sizing
// ─── POSITION SIZING ──────────────────────────────────
atr_val = ta.atr(atr_period)
stop_dist = atr_val * atr_stop_mult
risk_amount = strategy.equity * risk_per_trade_pct
raw_qty = risk_amount / stop_dist
calc_qty = math.max(math.floor(raw_qty), 1)
// NSE lot size compliance (comment out if not NSE futures)
// lot_size = 25 // Nifty lot size
// calc_qty := math.max(math.floor(calc_qty / lot_size) * lot_size, lot_size)
Layer 4 — Stop Management (Initial, Breakeven, Trailing)
This is where most frameworks fall short. A good risk module doesn't just set an initial stop — it manages the stop through the life of the trade. We implement three stop phases:
- Initial stop — ATR-based, set at entry
- Breakeven move — once trade reaches 1R profit, move stop to entry
- Trailing stop — once trade reaches 2R, trail with ATR
// ─── STOP MANAGEMENT ─────────────────────────────────
var float entry_price = na
var float initial_stop = na
var float current_stop = na
// Reset state when flat
if strategy.position_size == 0
entry_price := na
initial_stop := na
current_stop := na
// On new long entry (transitioned from 0 to long)
if strategy.position_size > 0 and nz(strategy.position_size[1]) <= 0
entry_price := strategy.opentrades.entry_price(strategy.opentrades - 1)
initial_stop := entry_price - stop_dist
current_stop := initial_stop
// On new short entry (transitioned from 0 to short)
if strategy.position_size < 0 and nz(strategy.position_size[1]) >= 0
entry_price := strategy.opentrades.entry_price(strategy.opentrades - 1)
initial_stop := entry_price + stop_dist
current_stop := initial_stop
// Current R multiple (na-safe)
trade_r = na(entry_price) ? na :
strategy.position_size > 0 ? (close - entry_price) / stop_dist :
strategy.position_size < 0 ? (entry_price - close) / stop_dist : na
// Breakeven: move stop to entry once trade reaches be_trigger_r
if not na(trade_r) and trade_r >= be_trigger_r and not na(current_stop)
if strategy.position_size > 0
current_stop := math.max(current_stop, entry_price)
if strategy.position_size < 0
current_stop := math.min(current_stop, entry_price)
// Trailing stop: once trade reaches trail_start_r, trail with ATR
if not na(trade_r) and trade_r >= trail_start_r and not na(current_stop)
if strategy.position_size > 0
trail_level = close - atr_val * trail_atr_mult
current_stop := math.max(current_stop, trail_level)
if strategy.position_size < 0
trail_level = close + atr_val * trail_atr_mult
current_stop := math.min(current_stop, trail_level)
// Apply the current stop to open positions
if strategy.position_size > 0 and not na(current_stop)
strategy.exit("Long SL", from_entry="Long", stop=current_stop)
if strategy.position_size < 0 and not na(current_stop)
strategy.exit("Short SL", from_entry="Short", stop=current_stop)
Wiring Your Signal Logic In
With the risk framework in place, your signal logic just needs to call the entry function with calc_qty and check trading_allowed:
// ─── SIGNAL LOGIC (replace with your own) ────────────
fast_ma = ta.ema(close, 20)
slow_ma = ta.ema(close, 50)
long_signal = ta.crossover(fast_ma, slow_ma) and close > ta.ema(close, 200)
short_signal = ta.crossunder(fast_ma, slow_ma) and close < ta.ema(close, 200)
// Entries — always gated by trading_allowed
if long_signal and trading_allowed and not already_long
strategy.entry("Long", strategy.long, qty=calc_qty)
if short_signal and trading_allowed and not already_short
strategy.entry("Short", strategy.short, qty=calc_qty)
Visualising Risk State on the Chart
// ─── DASHBOARD TABLE ─────────────────────────────────
var table risk_table = table.new(position.top_right, 2, 5,
bgcolor=color.new(color.navy, 80), border_color=color.new(color.blue, 60), border_width=1)
if barstate.islast
table.cell(risk_table, 0, 0, "Daily P&L", text_color=color.gray, text_size=size.small)
table.cell(risk_table, 1, 0, str.tostring(math.round(daily_pnl_pct * 100, 2)) + "%",
text_color=daily_pnl_pct >= 0 ? color.green : color.red, text_size=size.small)
table.cell(risk_table, 0, 1, "Drawdown", text_color=color.gray, text_size=size.small)
table.cell(risk_table, 1, 1, str.tostring(math.round(total_dd_pct * 100, 2)) + "%",
text_color=total_dd_pct >= -0.03 ? color.green : color.orange, text_size=size.small)
table.cell(risk_table, 0, 2, "Trade R", text_color=color.gray, text_size=size.small)
table.cell(risk_table, 1, 2, na(trade_r) ? "—" : str.tostring(math.round(trade_r, 2)) + "R",
text_color=color.white, text_size=size.small)
table.cell(risk_table, 0, 3, "Status", text_color=color.gray, text_size=size.small)
table.cell(risk_table, 1, 3, trading_allowed ? "ACTIVE" : "HALTED",
text_color=trading_allowed ? color.green : color.red, text_size=size.small)
table.cell(risk_table, 0, 4, "Position", text_color=color.gray, text_size=size.small)
table.cell(risk_table, 1, 4, str.tostring(calc_qty) + " units",
text_color=color.white, text_size=size.small)
Get the Complete Framework
The complete production version of this framework — including additional features like a weekly drawdown limit, a volatility regime filter, and news hour blocking — is included in the free BotJockie Algo Toolkit. Download it below, or book a call if you want it integrated into your specific strategy.
Complete Risk Framework (.pine)
Production-ready Pine Script v6 with all layers above + extras. Free, no card required.