Fixed-$ Risk, ATR Stops, R-Multiples, and Practical Guardrails in Pine Script v6.
A lot of TradingView strategy builders can create an entry condition. Far fewer can build a complete trading system. This guide covers fixed-$ risk sizing, ATR-based stops, R-multiples, expectancy, drawdown control, and operational guardrails such as max daily loss, max trades per day, session limits, end-of-day flattening, and end-of-week exits.
That distinction matters. An indicator builder asks, "When should I buy or sell?" A system builder asks, "How much should I risk, where does the trade become invalid, how many trades should I allow today, and when should I stop trading altogether?" In live trading, those second questions often matter more than the signal itself. That is also why this guide sits naturally beside The 8 Rules of Genuine Backtesting in TradingView, where the focus is on building honest tests before trusting any result.
From a backtesting perspective, this is one of the biggest dividing lines between scripts that merely look clever and scripts that are actually useful. TradingView strategies run through a broker emulator, and by default orders are filled on the next available tick; with standard bar-close recalculation, that usually means the next bar's open. The emulator also makes default assumptions about intrabar movement unless you use Bar Magnifier. That means your position size, stop distance, and entry model all need to be aligned, otherwise your "risk per trade" in the backtest may not be the risk you think you are taking.
The practical goal of this article is simple: move from signal-building to risk-building. We will look at fixed-$ risk sizing, ATR-based stops, R-multiples, expectancy, drawdown control, and operational guardrails such as max daily loss, max trades per day, session limits, end-of-day flattening, and end-of-week exits.
Fixed-$ risk sizing means you decide in advance how much you are prepared to lose if the trade is wrong. For example: risk $200 per trade.
That sounds simple, but it changes everything. Instead of entering a random size on every setup, you calculate quantity from the distance between entry and stop. If the stop is wide, your size must be smaller. If the stop is tight, your size can be larger. This makes trade outcomes comparable and gives your backtest a much more honest structure.
The core idea is:
Position size = cash risk per trade / cash risk per unit
In Pine, the per-unit cash risk depends on the gap between your planned entry and planned stop, and on the symbol's contract characteristics. Pine exposes useful symbol data through built-ins such as syminfo.pointvalue, syminfo.mintick, and syminfo.mincontract, which are exactly the sort of building blocks you need when translating price distance into actual position size. Even so, you should always validate the result against the contract specification of the market and broker you actually trade. The How I Build a TradingView Strategy That Matches My Broker's Constraints guide goes deeper into that execution-aware layer.
//@version=6
strategy("Fixed $ Risk Helper", overlay=true, pyramiding=0)
riskPerTrade = input.float(200.0, "Risk per trade ($)", minval=1)
entryPrice = input.float(100.0, "Planned entry price")
stopPrice = input.float(98.5, "Planned stop price")
roundDownToStep(value, step) =>
step > 0 ? math.floor(value / step) * step : value
priceRiskPerUnit = math.abs(entryPrice - stopPrice)
cashRiskPerUnit = priceRiskPerUnit * syminfo.pointvalue
rawQty = cashRiskPerUnit > 0 ? riskPerTrade / cashRiskPerUnit : 0.0
qty = roundDownToStep(rawQty, syminfo.mincontract)
plot(qty, "Calculated Qty")
This is the foundation. Once you size this way, the strategy stops being "one lot because that feels right" and starts becoming a controlled process.
A fixed stop of 10 points might be sensible on one market and absurd on another. Even on the same market, it may be too wide in quiet conditions and too tight in volatile ones. That is why ATR-based stops are so useful in systematic strategy design.
ATR does not tell you direction. It tells you how much the market is moving. When you use ATR to place a stop, you are saying: "I want my invalidation distance to adapt to current volatility." That produces a much more defensible stop model for backtesting.
The key systems insight is this:
That is exactly how fixed-risk sizing is supposed to behave.
//@version=6
strategy("ATR Fixed Risk Example", overlay=true, pyramiding=0)
riskPerTrade = input.float(200.0, "Risk per trade ($)", minval=1)
atrLen = input.int(14, "ATR Length", minval=1)
atrMult = input.float(2.0, "ATR Stop Multiplier", minval=0.1)
rrTarget = input.float(2.0, "Reward multiple", minval=0.1)
fastEMA = ta.ema(close, 10)
slowEMA = ta.ema(close, 20)
atrVal = ta.atr(atrLen)
roundDownToStep(value, step) =>
step > 0 ? math.floor(value / step) * step : value
longSignal = ta.crossover(fastEMA, slowEMA) and barstate.isconfirmed and strategy.position_size == 0
entryEstimate = close
stopPrice = entryEstimate - atrVal * atrMult
riskPerUnit = math.abs(entryEstimate - stopPrice) * syminfo.pointvalue
rawQty = riskPerUnit > 0 ? riskPerTrade / riskPerUnit : 0.0
qty = roundDownToStep(rawQty, syminfo.mincontract)
targetPrice = entryEstimate + (entryEstimate - stopPrice) * rrTarget
if longSignal and qty > 0
strategy.entry("L", strategy.long, qty=qty)
strategy.exit("L-Exit", "L", stop=stopPrice, limit=targetPrice)
There is an important backtesting caveat here. In a standard bar-close strategy, the signal is computed on the close, but the market order is generally filled on the next available tick, which for historical bars is usually the next bar's open. So if you size off the close but the next bar opens away from that price, your actual simulated risk can differ from your intended risk. This is precisely why risk model and order model must be considered together.
This is where many otherwise decent backtests become misleading.
A strategy that enters at market should not be sized as though it entered at a perfect pullback level. A limit-entry strategy should be sized from the intended limit price, not from the current close. A breakout stop-entry strategy should be sized from the breakout trigger level, not from where price happens to be when you place the order.
That sounds obvious, but many scripts mix these concepts. The result is that the reported "$200 risk" is not really $200 risk at all.
//@version=6
strategy("Limit Entry Sized by Planned Entry", overlay=true, pyramiding=0)
riskPerTrade = input.float(200.0, "Risk per trade ($)")
atrLen = input.int(14, "ATR Length")
atrMult = input.float(2.0, "ATR Stop Multiplier")
fastEMA = ta.ema(close, 10)
slowEMA = ta.ema(close, 20)
atrVal = ta.atr(atrLen)
roundDownToStep(value, step) =>
step > 0 ? math.floor(value / step) * step : value
setup = ta.crossover(fastEMA, slowEMA) and barstate.isconfirmed and strategy.position_size == 0
plannedEntry = close - atrVal * 0.5
stopPrice = plannedEntry - atrVal * atrMult
riskPerUnit = math.abs(plannedEntry - stopPrice) * syminfo.pointvalue
qty = riskPerUnit > 0 ? roundDownToStep(riskPerTrade / riskPerUnit, syminfo.mincontract) : 0.0
if setup and qty > 0
strategy.entry("L-Limit", strategy.long, qty=qty, limit=plannedEntry)
strategy.exit("L-Limit-Exit", "L-Limit", stop=stopPrice)
//@version=6
strategy("Stop Entry Sized by Planned Trigger", overlay=true, pyramiding=0)
riskPerTrade = input.float(200.0, "Risk per trade ($)")
atrLen = input.int(14, "ATR Length")
atrMult = input.float(2.0, "ATR Stop Multiplier")
lookback = input.int(20, "Breakout lookback")
atrVal = ta.atr(atrLen)
rangeHi = ta.highest(high, lookback)
roundDownToStep(value, step) =>
step > 0 ? math.floor(value / step) * step : value
setup = close > ta.ema(close, 50) and barstate.isconfirmed and strategy.position_size == 0
plannedEntry = rangeHi + syminfo.mintick
stopPrice = plannedEntry - atrVal * atrMult
riskPerUnit = math.abs(plannedEntry - stopPrice) * syminfo.pointvalue
qty = riskPerUnit > 0 ? roundDownToStep(riskPerTrade / riskPerUnit, syminfo.mincontract) : 0.0
if setup and qty > 0
strategy.entry("L-Stop", strategy.long, qty=qty, stop=plannedEntry)
strategy.exit("L-Stop-Exit", "L-Stop", stop=stopPrice)
This links directly to the earlier order-types discussion: the order type is not a cosmetic choice. It changes fill behaviour, trade participation, and the realism of your backtest. TradingView supports market, limit, stop, and stop-limit orders, and the broker emulator uses chart data plus documented assumptions to simulate fills. For that reason, this article pairs naturally with Strategy Order Types Explained.
Once you standardise risk, you can start thinking in R.
If your fixed risk per trade is $200:
This is much more useful than reading raw profit figures in isolation. R-multiples let you compare trade quality across different instruments, timeframes, and stop models. They also make expectancy much easier to understand.
Expectancy is the average amount your system makes per trade over time. In R terms, it becomes cleaner because the denominator is standardised.
//@version=6
strategy("R-Multiple Tracking", overlay=true, pyramiding=0)
riskPerTrade = input.float(200.0, "Risk per trade ($)", minval=1)
var float sumR = 0.0
var float sumWinR = 0.0
var float sumLossR = 0.0
var int wins = 0
var int trades = 0
if ta.change(strategy.closedtrades) > 0
int idx = strategy.closedtrades - 1
float lastPnL = strategy.closedtrades.profit(idx) - strategy.closedtrades.commission(idx)
float lastR = lastPnL / riskPerTrade
sumR += lastR
trades += 1
if lastR > 0
wins += 1
sumWinR += lastR
else
sumLossR += math.abs(lastR)
losses = trades - wins
winRate = trades > 0 ? float(wins) / trades : na
avgWinR = wins > 0 ? sumWinR / wins : na
avgLossR = losses > 0 ? sumLossR / losses : na
expectancy = trades > 0 and not na(avgWinR) and not na(avgLossR) ? (winRate * avgWinR) - ((1 - winRate) * avgLossR) : na
plot(expectancy, "Expectancy (R)")
TradingView's strategy namespace includes closed-trade metrics such as profit and commission, which makes this type of post-trade analysis possible directly inside a strategy script.
A good strategy does not just know when to enter. It also knows when to stop.
Pine includes strategy-level risk commands such as strategy.risk.max_drawdown(), strategy.risk.max_intraday_loss(), strategy.risk.max_intraday_filled_orders(), and strategy.risk.max_position_size(). These are powerful because they are enforced by the strategy engine itself. When triggered, they can cancel pending orders, close positions, and halt further trading according to the documented rule.
//@version=6
strategy("Built-in Risk Caps", overlay=true, pyramiding=0)
maxOrdersPerDay = input.int(3, "Max filled orders per day", minval=1)
maxDDPct = input.float(15.0, "Max drawdown %", minval=0.1)
maxLossDay = input.float(500.0, "Max intraday loss cash", minval=1)
strategy.risk.max_intraday_filled_orders(maxOrdersPerDay)
strategy.risk.max_drawdown(maxDDPct, strategy.percent_of_equity)
strategy.risk.max_intraday_loss(maxLossDay, strategy.cash)
Then add operational rules around time. Session filters and flattening windows are often where a lot of live robustness comes from. Pine's input.session() and time() functions are designed for this sort of logic, including optional time-zone handling.
//@version=6
strategy("Session Guardrails", overlay=true, pyramiding=0)
entrySession = input.session("0800-1600", "Allowed entry session")
inEntrySession = not na(time(timeframe.period, entrySession, "America/New_York"))
isPreCloseWindow = hour(time, "America/New_York") == 15 and minute(time, "America/New_York") >= 55
isFridayCutoff = dayofweek(time, "America/New_York") == dayofweek.friday and
hour(time, "America/New_York") == 15 and minute(time, "America/New_York") >= 55
longSignal = ta.crossover(ta.ema(close, 10), ta.ema(close, 20)) and barstate.isconfirmed
if longSignal and inEntrySession and strategy.position_size == 0
strategy.entry("L", strategy.long)
if isPreCloseWindow
strategy.close_all("EOD flat")
if isFridayCutoff
strategy.close_all("EOW flat")
Practical examples of these guardrails include:
These are not "extras." They are part of the system.
All of this becomes more meaningful if the signal itself is tested honestly.
barstate.isconfirmed is true on the closing update of a realtime bar, and historical bars are already confirmed. In practice, it helps stop your strategy from reacting to a condition that appears intrabar and disappears before the candle closes. That matters for entries, stop placement, and position size calculations. For a fuller discussion, see The 8 Rules of Genuine Backtesting in TradingView, but the core point is simple: do not build serious risk logic on top of unstable signals.
The real shift from indicator builder to system builder happens when you stop asking only, "What is my entry?" and start asking, "What is my risk?"
Fixed-$ risk sizing gives you consistency. ATR stops give you volatility awareness. R-multiples and expectancy give you a cleaner way to evaluate edge. Guardrails such as max daily loss, trade caps, session limits, and forced flattening help keep the strategy operationally sane.
Most importantly, all of these pieces have to agree with the backtesting model. If the order type, fill assumptions, and position sizing logic do not line up, the backtest can look disciplined while the underlying execution model is not.
That is the deeper lesson: in automated trading, risk model, order model, and backtest model are all part of the strategy. Treat them separately, and the numbers can lie. Treat them as one system, and the backtest starts becoming much more meaningful.
No treatment of position sizing is complete without addressing the Kelly Criterion — the mathematical formula for the optimal fraction of capital to risk on each trade to maximise long-term geometric growth.
The full Kelly formula for a binary win/loss system is:
f = (W × R − L) / R
For example, a strategy with a 55% win rate and a 2:1 R/R would produce a Kelly fraction of approximately 32.5% — meaning full Kelly suggests risking 32.5% of capital per trade. In practice, this is catastrophically aggressive for any real trading scenario. A single losing streak at full Kelly creates drawdowns that most traders cannot psychologically or financially survive.
Fractional Kelly — typically quarter-Kelly or half-Kelly — is the practical standard. Quarter-Kelly in the above example would mean risking around 8% per trade, which is still aggressive but geometrically sound. Most systematic traders use far less than this (1–2% per trade) because they are optimising for drawdown tolerance and operational sustainability, not pure geometric growth.
The reason Kelly is worth understanding even if you do not apply it directly: it proves that there is an optimal risk level, that over-risking is provably worse than under-risking in the long run, and that ruin is a mathematical certainty at sufficiently high risk fractions regardless of edge. That is not an opinion. It is a function of compounding arithmetic.
Fixed-$ risk per trade assumes each trade is independent. In practice, it often is not.
If you are running three concurrent strategies — all on NAS100, all long-biased, all triggered by similar momentum conditions — a sharp market reversal can trigger all three at the same time. What looks like three separate 1% risks is actually 3% of portfolio exposure in a single correlated event.
The same trap exists across correlated instruments. EURUSD and GBPUSD, NAS100 and SPX500, BTC and ETH — these pairs frequently move together during periods of macro stress, precisely when you most want your portfolio to behave as though the positions are independent.
The practical adjustments:
Correlation is not a theoretical concern. It is a practical one that determines whether your diversified portfolio actually protects you.
Most traders understand that losses are part of trading. Far fewer have done the arithmetic to understand what their strategy's expected losing streak actually looks like.
For a strategy with a 40% win rate, the probability of a losing streak of length N is:
(1 − 0.40)^N = 0.60^N
With a 1,000-trade backtest, a 10-loss streak is not just possible — it is likely to have occurred at least once. The question is whether the strategy survived it without account ruin, and whether the trader survived it psychologically.
This calculation should directly inform position sizing. If a 10-loss streak would create an unacceptable drawdown at 2% per trade (10 × 2% = 20% equity drawdown), and 20% is psychologically and operationally manageable, then 2% is appropriate. If it is not, the risk must come down.
The math is not pessimistic — it is honest. And running this calculation before trading is far more useful than discovering the streak mid-sequence and making emotional decisions about whether to continue.