Most retail traders use fixed lot sizes. One lot of Nifty futures every trade, or 0.01 BTC every entry. This sounds disciplined but it's not — a fixed lot size means your risk per trade fluctuates with market volatility. When ATR is high, you're risking far more than when ATR is low. Dynamic position sizing solves this by making your risk per trade constant in monetary terms.
The Core Concept: Risk-Based Sizing
The principle is simple: decide what percentage of your account you're willing to lose on any single trade (typically 1–2%), then calculate how many units to buy based on the distance from entry to stop-loss.
The formula:
position_size = (account_size × risk_percent) / (entry_price − stop_price)
If your account is ₹5,00,000, you risk 1% per trade (₹5,000), and your stop is 50 points below entry — you buy 100 units. If the stop is 100 points below entry, you buy 50 units. Your maximum loss is always ₹5,000 regardless of how wide the stop is.
Implementing in Pine Script v6
Pine Script's strategy.entry() accepts a qty parameter. We calculate this dynamically on each signal:
//@version=6
strategy("Risk-Based Position Sizing",
default_qty_type=strategy.fixed,
initial_capital=500000,
commission_type=strategy.commission.percent,
commission_value=0.05)
// --- Inputs ---
risk_pct = input.float(1.0, "Risk per Trade (%)", minval=0.1, maxval=5.0, step=0.1) / 100
atr_period = input.int(14, "ATR Period")
atr_mult = input.float(2.0, "Stop ATR Multiplier")
// --- ATR-based stop distance ---
atr_val = ta.atr(atr_period)
stop_dist = atr_val * atr_mult
// --- Dynamic position size ---
account_eq = strategy.equity
risk_amount = account_eq * risk_pct
qty_calc = math.floor(risk_amount / stop_dist)
qty_safe = math.max(qty_calc, 1)
// --- Signal (replace with your logic) ---
fast_ma = ta.ema(close, 20)
slow_ma = ta.ema(close, 50)
long_signal = ta.crossover(fast_ma, slow_ma)
short_signal = ta.crossunder(fast_ma, slow_ma)
// --- Entries ---
stop_long = close - stop_dist
stop_short = close + stop_dist
if long_signal
strategy.entry("Long", strategy.long, qty=qty_safe)
strategy.exit("Long Exit", "Long", stop=stop_long)
if short_signal
strategy.entry("Short", strategy.short, qty=qty_safe)
strategy.exit("Short Exit", "Short", stop=stop_short)
Key Implementation Details
Use strategy.equity, not initial_capital
Always calculate your risk amount from strategy.equity (current account value), not strategy.initial_capital. This ensures position sizes scale with your account — growing as you profit, shrinking as you draw down. This is called "fixed fractional" position sizing and it's the standard approach for serious algo traders.
ATR vs. Fixed Pip/Point Stops
Using ATR to set your stop distance is superior to fixed-point stops because it adapts to market volatility. A 50-point stop on Nifty during a low-volatility period is very different from a 50-point stop during a high-volatility event. ATR normalises this — your stops are always proportional to current market conditions.
For NSE futures, your position size must be a multiple of the lot size. Nifty 50 futures have a lot size of 25. Wrap your qty_safe calculation in math.floor(qty_safe / lot_size) * lot_size to ensure you're always trading whole lots.
Adding a Daily Loss Limit
Position sizing alone doesn't protect you from a bad day where you take 10 consecutive losses. Add a daily loss limit to halt trading if the account drops beyond a threshold in a single session:
// Input at top of script (with other inputs)
max_daily_loss = input.float(-3.0, "Max Daily Loss (%)", step=0.5)
// Daily loss limit guard
var float day_start_equity = na
is_new_day = ta.change(time("D")) != 0
if is_new_day or na(day_start_equity)
day_start_equity := strategy.equity
daily_loss_pct = (strategy.equity - day_start_equity) / day_start_equity * 100
trading_halted = daily_loss_pct <= max_daily_loss
// Only enter if not halted
if long_signal and not trading_halted
strategy.entry("Long", strategy.long, qty=qty_safe)
Volatility-Adjusted Sizing (Advanced)
A further refinement is to reduce position size not just based on stop distance but also based on overall market volatility. During high-volatility regimes, even a wide ATR stop might be hit by noise. You can add a volatility scalar:
// Normalised volatility scalar
hist_atr_avg = ta.sma(atr_val, 100)
vol_scalar = math.min(hist_atr_avg / atr_val, 1.0) // never exceed standard size
qty_adj = math.floor(qty_safe * vol_scalar)
qty_final = math.max(qty_adj, 1)
This reduces position size when current ATR is higher than its 100-bar average — i.e., when the market is more volatile than normal. It's a simple but effective regime filter.
Download the Complete Risk Framework
The BotJockie free Algo Toolkit includes a production-ready Pine Script v6 risk management module with position sizing, daily loss limits, and volatility adjustment — ready to drop into any strategy.
Risk Management Framework (.pine)
Production-ready Pine Script v6 risk module. Free, no card required.
Download Free Toolkit →