Skip to main content

Strategy Deployment 🚀

Preparing for Live Trading​

Moving a strategy from backtesting to live trading is one of the most critical transitions in algorithmic trading. A strategy that looks profitable on historical data can behave very differently in real-time markets. Understanding the key differences and preparing systematically will save you from costly surprises.

What Changes Between Backtesting and Live​

In backtesting, every bar is already complete when your script runs. You know the open, high, low, and close before making any decisions. In live trading, the current bar is still forming. Your script re-executes on every tick, and signals can appear and disappear before the bar closes. This is called repainting, and it is the single most common source of disappointment when deploying strategies.

Other critical differences include:

  • Fill assumptions: Backtests assume your orders fill at the exact price you specify. Live markets involve slippage, partial fills, and order queue priority.
  • Latency: There is a delay between signal generation and order execution. On volatile instruments, prices can move significantly in that window.
  • Data feed differences: Historical data is cleaned and adjusted. Live data can have gaps, bad ticks, and feed interruptions.
  • Commission and fees: Even if you model commission in backtests, real-world fees (exchange fees, ECN rebates, regulatory fees) are more nuanced.

Pre-Deployment Checklist​

Before turning on any live strategy, walk through these questions:

  1. Does the strategy use close to generate entry signals? If so, it may repaint on live data.
  2. Are all input parameters set to their intended live values (not leftover optimization values)?
  3. Is position sizing based on current equity, not a hardcoded backtest value?
  4. Have you accounted for realistic slippage and commission in the strategy() declaration?
  5. Does the strategy handle missing or invalid indicator data (e.g., na values for ATR on the first bars)?
  6. Have you tested on the exact symbol and timeframe you intend to trade live?

Strategy Validation Checklist​

The following script runs a set of automated pre-flight checks every bar. It validates that the ATR is available, that position sizing stays within safe bounds, and that the strategy is running on an appropriate timeframe. If any check fails, trading is disabled for that bar.

//@version=5
strategy("Deployment Validator", overlay=true, initial_capital=100000, default_qty_type=strategy.percent_of_equity, default_qty_value=10)

// --- Inputs ---
float riskPerTrade = input.float(1.0, "Risk per Trade %", minval=0.1, maxval=5.0)
int atrLength = input.int(14, "ATR Length", minval=1)
float maxPositionPct = input.float(20.0, "Max Position % of Equity", minval=1.0, maxval=50.0)

// --- Validation state ---
var bool tradingAllowed = true
tradingAllowed := true // Reset each bar, then run checks

// CHECK 1: Valid timeframe (intraday only, 4h or less)
// timeframe.in_seconds() returns the chart timeframe in seconds.
// 14400 seconds = 4 hours.
bool validTimeframe = timeframe.in_seconds() <= 14400 and not timeframe.isdwm
if not validTimeframe
tradingAllowed := false

// CHECK 2: ATR is available and positive
float atr = ta.atr(atrLength)
if na(atr) or atr <= 0
tradingAllowed := false

// CHECK 3: Position size within limits
float proposedSize = na(atr) ? 0.0 : strategy.equity * (riskPerTrade / 100) / atr
float positionValue = proposedSize * close
float maxAllowed = strategy.equity * (maxPositionPct / 100)
if positionValue > maxAllowed
proposedSize := maxAllowed / close

// CHECK 4: Minimum equity threshold
if strategy.equity < 1000
tradingAllowed := false

// --- Visual feedback ---
bgcolor(tradingAllowed ? na : color.new(color.red, 90))

// --- Example trade logic (only when all checks pass) ---
bool longSignal = ta.crossover(ta.sma(close, 10), ta.sma(close, 20))
bool shortSignal = ta.crossunder(ta.sma(close, 10), ta.sma(close, 20))

if tradingAllowed and longSignal
strategy.entry("Long", strategy.long, qty=proposedSize)

if tradingAllowed and shortSignal
strategy.close("Long", comment="Exit signal")

The script first resets tradingAllowed to true on every bar, then each check can set it to false. The background turns red on any bar where trading is blocked. Notice that timeframe.in_seconds() is used instead of comparing the string timeframe.period, and timeframe.isdwm filters out daily/weekly/monthly charts.

Market Hours and Session Filters​

Many strategies should only trade during regular market hours when liquidity is highest and spreads are tightest. Pine Script does not provide a simple boolean for "are we in the regular session," but you can build one using the time() function with a session string.

Detecting Regular Trading Hours​

The time() function returns na when the current bar falls outside the specified session. By checking not na(time(...)), you get a reliable boolean for session membership.

//@version=5
strategy("Session Filter Strategy", overlay=true, initial_capital=100000, default_qty_type=strategy.percent_of_equity, default_qty_value=10)

// --- Session definitions ---
// "0930-1600:23456" = 9:30 AM to 4:00 PM, Monday(2) through Friday(6)
// "0400-0930:23456" = Pre-market session
// "1600-2000:23456" = After-hours session
bool inRegularSession = not na(time(timeframe.period, "0930-1600:23456"))
bool inPreMarket = not na(time(timeframe.period, "0400-0930:23456"))
bool inAfterHours = not na(time(timeframe.period, "1600-2000:23456"))

// --- Visual session markers ---
bgcolor(inRegularSession ? color.new(color.green, 93) : na)
bgcolor(inPreMarket ? color.new(color.orange, 93) : na)
bgcolor(inAfterHours ? color.new(color.blue, 93) : na)

// --- Strategy logic: only trade during regular hours ---
float fastMA = ta.sma(close, 9)
float slowMA = ta.sma(close, 21)

bool longCondition = ta.crossover(fastMA, slowMA)
bool shortCondition = ta.crossunder(fastMA, slowMA)

if inRegularSession and longCondition
strategy.entry("Long", strategy.long)

if inRegularSession and shortCondition
strategy.entry("Short", strategy.short)

// Close all positions before market close (3:55 PM)
bool nearClose = not na(time(timeframe.period, "1555-1600:23456"))
if nearClose
strategy.close_all(comment="EOD flatten")

This strategy enters trades only during regular hours (9:30 AM--4:00 PM ET) and flattens all positions five minutes before the close. The session strings follow the format "HHMM-HHMM:days" where days are numbered 1 (Sunday) through 7 (Saturday). Note that session.regular is a string constant representing the exchange's regular session specification -- it is not a boolean and cannot be used in if statements directly.

Alert-to-Webhook Pipeline​

TradingView does not execute trades directly on a brokerage. Instead, you create alerts that fire when your strategy generates signals. These alerts can send HTTP POST requests (webhooks) to external services that then place real orders.

How the Pipeline Works​

  1. Your Pine Script strategy calls alert() with a JSON payload.
  2. TradingView fires the alert when the condition is met.
  3. The alert sends an HTTP POST to your webhook URL.
  4. Your execution service (a bot, a middleware like 3Commas, or a custom server) parses the JSON and submits orders to your broker.

Webhook-Ready Strategy​

//@version=5
strategy("Webhook Strategy", overlay=true, initial_capital=100000, default_qty_type=strategy.percent_of_equity, default_qty_value=10)

// --- Inputs ---
string webhookToken = input.string("YOUR_TOKEN", "Webhook Auth Token")
string exchange = input.string("binance", "Exchange Name")
string ticker = input.string("BTCUSDT", "Ticker Symbol")
float riskPct = input.float(1.0, "Risk %", minval=0.1, maxval=5.0)

// --- Indicators ---
float ema12 = ta.ema(close, 12)
float ema26 = ta.ema(close, 26)

// --- Signals ---
bool buySignal = ta.crossover(ema12, ema26)
bool sellSignal = ta.crossunder(ema12, ema26)

// --- Position sizing ---
float atr = ta.atr(14)
float qty = na(atr) or atr <= 0 ? 0.0 : strategy.equity * (riskPct / 100) / atr
string qtyStr = str.tostring(qty, "#.####")
string priceStr = str.tostring(close, "#.##")
string timeStr = str.tostring(year) + "-" + str.tostring(month, "00") + "-" + str.tostring(dayofmonth, "00")

// --- Generic REST webhook format ---
if buySignal and qty > 0
strategy.entry("Long", strategy.long, qty=qty)
alert('{"action":"buy","token":"' + webhookToken + '","exchange":"' + exchange + '","ticker":"' + ticker + '","qty":"' + qtyStr + '","price":"' + priceStr + '","time":"' + timeStr + '"}', alert.freq_once_per_bar)

if sellSignal
strategy.close("Long", comment="Sell signal")
alert('{"action":"sell","token":"' + webhookToken + '","exchange":"' + exchange + '","ticker":"' + ticker + '","price":"' + priceStr + '","time":"' + timeStr + '"}', alert.freq_once_per_bar)

When creating the alert in TradingView, set the "Webhook URL" field to your endpoint and leave the alert message as {{strategy.order.alert_message}} or use the JSON directly. The alert.freq_once_per_bar setting prevents duplicate messages on intra-bar recalculations.

Common Webhook Formats​

3Commas-style payloads typically use a simpler structure:

{"message_type":"bot","bot_id":"12345","email_token":"abc-def","delay_seconds":0,"action":"start_long_deal"}

Generic REST format (shown in the example above) includes the action, symbol, quantity, and price so your middleware can construct the appropriate broker API call.

Always include an authentication token in your payload so your receiving server can verify the request originated from your TradingView alert and not from an unauthorized source.

Order Types​

Pine Script provides two primary order functions: strategy.entry() and strategy.order(). Understanding when to use each is essential for correct strategy behavior.

strategy.entry() vs strategy.order()​

strategy.entry() is position-aware. If you call strategy.entry("Long", strategy.long) while already in a long position with the same ID, it does nothing. If you call it while in a short position, it reverses. This is the correct function for most trend-following and mean-reversion strategies.

strategy.order() is a raw order. It always submits, regardless of current position state. Use it when you need precise control, such as scaling into positions or building custom order management logic.

Complete Order Types Example​

//@version=5
strategy("Order Types Demo", overlay=true, initial_capital=100000, default_qty_type=strategy.fixed, default_qty_value=100)

// --- Indicators ---
float atr = ta.atr(14)
float upperBand = ta.sma(close, 20) + 2 * atr
float lowerBand = ta.sma(close, 20) - 2 * atr
float midLine = ta.sma(close, 20)

// --- Plot reference levels ---
plot(upperBand, "Upper Band", color=color.red)
plot(lowerBand, "Lower Band", color=color.green)
plot(midLine, "Mid Line", color=color.gray)

// === 1. Market order (default) ===
// Executes at the next available price
bool marketBuy = ta.crossover(close, midLine) and strategy.position_size == 0
if marketBuy
strategy.entry("Market Long", strategy.long)

// === 2. Limit order ===
// Only fills if price reaches the limit level or better
bool limitSetup = close > midLine and strategy.position_size == 0
if limitSetup
strategy.entry("Limit Long", strategy.long, limit=lowerBand)

// === 3. Stop order (breakout entry) ===
// Triggers when price moves THROUGH the stop level
bool breakoutSetup = close < upperBand and strategy.position_size == 0
if breakoutSetup
strategy.entry("Breakout Long", strategy.long, stop=upperBand)

// === 4. Stop-limit order ===
// Triggers at the stop level, then places a limit order
if close < upperBand and strategy.position_size == 0
strategy.entry("StopLimit Long", strategy.long, stop=upperBand, limit=upperBand + atr * 0.5)

// === 5. Exit with stop loss and take profit ===
if strategy.position_size > 0
float entryPx = strategy.position_avg_price
strategy.exit("Exit", from_entry="Market Long", stop=entryPx - 2 * atr, limit=entryPx + 4 * atr)

Key points about order types:

  • Market orders (no stop or limit parameter) execute immediately at the next bar's open.
  • Limit orders (limit=price) fill only at the specified price or better. They are used for pullback entries.
  • Stop orders (stop=price) trigger when price reaches the stop level. They are used for breakout entries.
  • Stop-limit orders (both stop and limit) trigger at the stop, then attempt to fill at the limit price.
  • strategy.exit() combines stop-loss and take-profit into a single bracket order.

Risk Controls for Live Trading​

Position Management​

Before entering any new trade, verify that you are not already overexposed. The following checks prevent position concentration risk.

//@version=5
strategy("Position Manager", overlay=true, initial_capital=100000, default_qty_type=strategy.percent_of_equity, default_qty_value=10)

// --- Inputs ---
int maxOpenTrades = input.int(3, "Max Open Trades", minval=1, maxval=10)
float maxExposurePct = input.float(60.0, "Max Portfolio Exposure %", minval=10.0, maxval=100.0)
float riskPerTrade = input.float(1.0, "Risk Per Trade %", minval=0.1, maxval=5.0)

// --- Current exposure calculation ---
float currentExposure = math.abs(strategy.position_size * close)
float exposurePct = strategy.equity > 0 ? (currentExposure / strategy.equity) * 100 : 0.0
bool belowTradeLimit = strategy.opentrades < maxOpenTrades
bool belowExposureLimit = exposurePct < maxExposurePct

// --- Example signal ---
float atr = ta.atr(14)
bool longEntry = ta.crossover(ta.ema(close, 9), ta.ema(close, 21))
float qty = na(atr) or atr <= 0 ? 0.0 : strategy.equity * (riskPerTrade / 100) / atr

// --- Enter only when within risk limits ---
if longEntry and belowTradeLimit and belowExposureLimit and qty > 0
strategy.entry("Long", strategy.long, qty=qty)

The script calculates the current portfolio exposure as a percentage of equity, then gates all new entries behind two checks: the number of open trades and the total exposure percentage.

Emergency Stop (Circuit Breaker)​

A circuit breaker automatically halts trading when losses exceed predefined thresholds. This is your last line of defense against cascading losses in abnormal market conditions.

//@version=5
strategy("Circuit Breaker", overlay=true, initial_capital=100000, default_qty_type=strategy.percent_of_equity, default_qty_value=10)

// --- Inputs ---
float maxDrawdownPct = input.float(10.0, "Max Drawdown % to Halt", minval=1.0, maxval=50.0)
int maxConsecLosses = input.int(3, "Max Consecutive Losses", minval=1, maxval=20)

// --- Drawdown calculation ---
float currentDD = strategy.equity_high > 0 ? (strategy.equity - strategy.equity_high) / strategy.equity_high * 100 : 0.0

// --- Consecutive losses tracking ---
var int consecLosses = 0
if strategy.closedtrades > 0
float lastProfit = strategy.closedtrades.profit(strategy.closedtrades - 1)
// Detect new trade closure by checking trade count change
if strategy.closedtrades != strategy.closedtrades[1]
if lastProfit < 0
consecLosses += 1
else
consecLosses := 0

// --- Circuit breaker logic ---
bool drawdownBreached = currentDD <= -maxDrawdownPct
bool consecLossBreached = consecLosses >= maxConsecLosses
var bool circuitBroken = false

if drawdownBreached or consecLossBreached
circuitBroken := true

// --- Force close all if breaker trips ---
if circuitBroken and strategy.position_size != 0
strategy.close_all(comment="CIRCUIT BREAKER")

// --- Normal strategy logic (only when breaker is not tripped) ---
float fastEma = ta.ema(close, 9)
float slowEma = ta.ema(close, 21)

if not circuitBroken and ta.crossover(fastEma, slowEma)
strategy.entry("Long", strategy.long)

if not circuitBroken and ta.crossunder(fastEma, slowEma)
strategy.close("Long", comment="Signal exit")

// --- Dashboard display ---
var table breakerTable = table.new(position.top_right, 2, 3, border_width=1)
if barstate.islast
string statusText = circuitBroken ? "HALTED" : "ACTIVE"
color statusColor = circuitBroken ? color.red : color.green
table.cell(breakerTable, 0, 0, "System Status", bgcolor=color.gray, text_color=color.white)
table.cell(breakerTable, 1, 0, statusText, bgcolor=statusColor, text_color=color.white)
table.cell(breakerTable, 0, 1, "Drawdown", bgcolor=color.gray, text_color=color.white)
table.cell(breakerTable, 1, 1, str.tostring(currentDD, "#.##") + "%", text_color=currentDD < -5 ? color.red : color.green)
table.cell(breakerTable, 0, 2, "Consec Losses", bgcolor=color.gray, text_color=color.white)
table.cell(breakerTable, 1, 2, str.tostring(consecLosses), text_color=consecLosses >= 2 ? color.orange : color.green)

The circuit breaker tracks two independent conditions: portfolio drawdown from the equity high-water mark and consecutive losing trades. Once either threshold is breached, circuitBroken latches to true and remains there for the rest of the session. The strategy immediately closes all open positions and refuses to enter new trades. A small table in the top-right corner shows the current system status.

Performance Monitoring Dashboard​

A comprehensive monitoring dashboard gives you at-a-glance visibility into how your live strategy is performing. The following example builds a table with key metrics.

//@version=5
strategy("Performance Dashboard", overlay=true, initial_capital=100000, default_qty_type=strategy.percent_of_equity, default_qty_value=10)

// --- Strategy logic (simple MA cross for demonstration) ---
float ema9 = ta.ema(close, 9)
float ema21 = ta.ema(close, 21)

if ta.crossover(ema9, ema21)
strategy.entry("Long", strategy.long)
if ta.crossunder(ema9, ema21)
strategy.close("Long")

// --- Circuit breaker state ---
float maxDDPct = input.float(15.0, "Max DD % to Halt")
int maxConsec = input.int(5, "Max Consecutive Losses")
float currentDD = strategy.equity_high > 0 ? (strategy.equity - strategy.equity_high) / strategy.equity_high * 100 : 0.0
var int consecL = 0

if strategy.closedtrades > 0 and strategy.closedtrades != strategy.closedtrades[1]
float lp = strategy.closedtrades.profit(strategy.closedtrades - 1)
if lp < 0
consecL += 1
else
consecL := 0

bool systemHalted = currentDD <= -maxDDPct or consecL >= maxConsec

// --- Calculations ---
int totalTrades = strategy.closedtrades
int winTrades = strategy.wintrades
int lossTrades = strategy.losstrades
float winRate = totalTrades > 0 ? (winTrades * 1.0 / totalTrades) * 100 : 0.0
float avgWin = winTrades > 0 ? strategy.grossprofit / winTrades : 0.0
float avgLoss = lossTrades > 0 ? strategy.grossloss / lossTrades : 0.0
float profitFactor = strategy.grossloss != 0 ? math.abs(strategy.grossprofit / strategy.grossloss) : 0.0
float openPnL = strategy.openprofit
float closedPnL = strategy.netprofit

// --- Build dashboard table ---
var table dash = table.new(position.top_right, 2, 9, bgcolor=color.new(color.black, 80), border_width=1)

if barstate.islast
// Header
table.cell(dash, 0, 0, "METRIC", bgcolor=color.new(color.blue, 20), text_color=color.white, text_size=size.small)
table.cell(dash, 1, 0, "VALUE", bgcolor=color.new(color.blue, 20), text_color=color.white, text_size=size.small)

// Open P&L
color openColor = openPnL >= 0 ? color.green : color.red
table.cell(dash, 0, 1, "Open P&L", text_color=color.white, text_size=size.small)
table.cell(dash, 1, 1, "$" + str.tostring(openPnL, "#.##"), text_color=openColor, text_size=size.small)

// Closed P&L
color closedColor = closedPnL >= 0 ? color.green : color.red
table.cell(dash, 0, 2, "Closed P&L", text_color=color.white, text_size=size.small)
table.cell(dash, 1, 2, "$" + str.tostring(closedPnL, "#.##"), text_color=closedColor, text_size=size.small)

// Win Rate
table.cell(dash, 0, 3, "Win Rate", text_color=color.white, text_size=size.small)
table.cell(dash, 1, 3, str.tostring(winRate, "#.#") + "%", text_color=color.white, text_size=size.small)

// Avg Win / Avg Loss
table.cell(dash, 0, 4, "Avg Win", text_color=color.white, text_size=size.small)
table.cell(dash, 1, 4, "$" + str.tostring(avgWin, "#.##"), text_color=color.green, text_size=size.small)

table.cell(dash, 0, 5, "Avg Loss", text_color=color.white, text_size=size.small)
table.cell(dash, 1, 5, "$" + str.tostring(math.abs(avgLoss), "#.##"), text_color=color.red, text_size=size.small)

// Drawdown
table.cell(dash, 0, 6, "Current DD", text_color=color.white, text_size=size.small)
table.cell(dash, 1, 6, str.tostring(currentDD, "#.##") + "%", text_color=currentDD < -5 ? color.red : color.white, text_size=size.small)

// Trade Count
table.cell(dash, 0, 7, "Total Trades", text_color=color.white, text_size=size.small)
table.cell(dash, 1, 7, str.tostring(totalTrades), text_color=color.white, text_size=size.small)

// System Status
string sysStatus = systemHalted ? "HALTED" : "TRADING"
color sysColor = systemHalted ? color.red : color.green
table.cell(dash, 0, 8, "System Status", text_color=color.white, text_size=size.small)
table.cell(dash, 1, 8, sysStatus, text_color=sysColor, text_size=size.small)

The dashboard displays eight key metrics in a compact table: open P&L, closed P&L, win rate, average win, average loss, current drawdown, total trade count, and system status. Color coding provides instant visual feedback -- green for positive values, red for negative ones, and the system status shows whether the circuit breaker has been tripped.

Paper Trading vs Live​

Differences in Execution​

Paper trading on TradingView simulates order fills, but the simulation is optimistic:

  • Fills: Paper trading assumes your limit orders fill whenever price touches your level. In live markets, your order sits in a queue and may not fill if there is not enough liquidity at that price.
  • Slippage: Paper trading applies the slippage setting from your strategy() declaration uniformly. Real slippage is variable -- it increases dramatically during news events, at market open, and on illiquid instruments.
  • Market impact: If you are trading a meaningful size relative to average volume, your own orders will move the price. Paper trading does not model this.

Validating Paper Results​

Before going live, run your strategy in paper mode for at least 30 trading days (roughly 6 calendar weeks). During this period, track:

  1. Fill rate: How often do your limit orders actually fill? If your backtest assumes 100% fill on limits, your live results will be worse.
  2. Signal stability: Do signals appear and disappear intra-bar? If so, your strategy repaints and needs barstate.isconfirmed guards.
  3. Timing alignment: Verify that your alerts fire at the expected time relative to bar close.
  4. Edge consistency: Is the paper P&L within one standard deviation of the backtest P&L? If paper results are significantly worse, investigate before going live.
PhaseDurationCapitalPurpose
BacktestN/ASimulatedValidate logic and edge
Paper trade30+ trading daysSimulatedValidate execution assumptions
Live (small)30+ trading days10-25% of target sizeValidate real fills and psychology
Live (full)OngoingFull allocationProduction trading

Scale into full size gradually. The psychological difference between paper and real money is substantial, and it affects your willingness to let the system operate without interference.

Strategy Documentation​

Well-documented code is essential when you return to a strategy months later, share it with collaborators, or debug unexpected behavior in live trading. Pine Script supports structured comments that serve as documentation.

//@version=5
// ============================================================================
// Strategy: Momentum Breakout System
// Version: 2.1.0
// Author: Your Name
// Created: 2025-01-15
// Modified: 2025-03-20
// Description: Enters long on ATR-based breakouts above the 20-period high
// with trailing stop exits. Designed for US equities on the
// 15-minute timeframe.
//
// Changelog:
// v2.1.0 - Added circuit breaker and webhook alerts
// v2.0.0 - Switched to ATR-based position sizing
// v1.0.0 - Initial version with fixed position sizes
// ============================================================================
strategy("Momentum Breakout v2.1", overlay=true, initial_capital=100000, default_qty_type=strategy.percent_of_equity, default_qty_value=10, commission_type=strategy.commission.percent, commission_value=0.1, slippage=2)

// @function calcPositionSize Calculates risk-adjusted position size
// @param equity Current account equity
// @param riskPct Percentage of equity to risk per trade
// @param atrVal Current ATR value for stop distance
// @returns float Number of shares/contracts to trade
calcPositionSize(float equity, float riskPct, float atrVal) =>
float riskAmount = equity * (riskPct / 100)
float size = atrVal > 0 ? riskAmount / atrVal : 0.0
size

// @function isValidSetup Checks whether current bar meets all entry prerequisites
// @param atrVal Current ATR value
// @param minVolume Minimum volume threshold
// @returns bool True if all conditions are met
isValidSetup(float atrVal, float minVolume) =>
bool atrOk = not na(atrVal) and atrVal > 0
bool volumeOk = volume > minVolume
bool sessionOk = not na(time(timeframe.period, "0930-1600:23456"))
atrOk and volumeOk and sessionOk

// --- Inputs ---
float riskPct = input.float(1.0, "Risk %", minval=0.1, maxval=5.0)
int atrLen = input.int(14, "ATR Length")
int breakoutLen = input.int(20, "Breakout Lookback")
float minVol = input.float(100000, "Min Volume")

// --- Calculations ---
float atr = ta.atr(atrLen)
float upperLevel = ta.highest(high, breakoutLen)
float qty = calcPositionSize(strategy.equity, riskPct, atr)

// --- Entry ---
if isValidSetup(atr, minVol) and close > upperLevel[1] and qty > 0
strategy.entry("Breakout", strategy.long, qty=qty)
strategy.exit("Trail", "Breakout", trail_points=atr * 2, trail_offset=atr)

Key documentation practices:

  • File header: Include strategy name, version, author, dates, and a concise description.
  • Changelog: Record what changed in each version so you can roll back decisions.
  • Function annotations: Use @function, @param, and @returns tags for every custom function.
  • Inline comments: Explain why a decision was made, not what the code does (the code itself shows what).

Practice Exercises​

Put your deployment knowledge into practice with these exercises:

  1. Webhook-Ready Alert Strategy: Create a strategy that generates structured JSON alert payloads for buy, sell, and close-all signals. Include the symbol, action, quantity, price, and a timestamp in each payload. Test that the JSON is well-formed by copying the alert text into a JSON validator.

  2. Monitoring Dashboard: Build a performance dashboard that shows daily P&L, cumulative win rate, current drawdown, profit factor, and total number of trades in a table. Use color coding to highlight when metrics exceed warning thresholds (e.g., drawdown worse than -5%).

  3. Circuit Breaker Implementation: Implement a strategy with a circuit breaker that halts all trading after 3 consecutive losses OR when drawdown exceeds 8%. Display the circuit breaker status on the chart. Bonus: add a "reset" mechanism that re-enables trading after N bars of being halted.

Pro Tips
  • Always use barstate.isconfirmed to guard entry signals if you want to avoid repainting. This ensures the signal is evaluated only on the confirmed (closed) bar.
  • Set realistic slippage and commission_value in your strategy() declaration. A strategy that is profitable with zero costs may be a net loser with realistic friction.
  • Use alert.freq_once_per_bar instead of alert.freq_all for webhook alerts. Firing on every tick can send hundreds of duplicate messages and trigger rate limits on your execution service.
  • Test your webhook JSON payloads with a service like webhook.site before connecting to a live execution engine.
  • Keep a deployment log (outside Pine Script) that records when you started live trading, what parameters you used, and any incidents. This is invaluable for post-mortem analysis.
Common Pitfalls
  • Do NOT compare timeframe.period with numeric operators like <= or >=. It is a string. Use timeframe.in_seconds() for numeric comparisons or timeframe.isdwm for category checks.
  • session.regular, session.premarket, and session.postmarket are string constants for use in session specification functions, NOT booleans. Do not write if session.regular -- it will not work as intended.
  • Pine Script does not have runtime.error(), try/catch, or math.random(). Error handling must be done through conditional checks and na() guards.
  • Backtested equity curves always look smoother than live results. Budget for at least 1.5x the maximum backtest drawdown when setting your circuit breaker threshold.
  • Do not deploy a strategy that you have only tested on a single symbol or timeframe. Market microstructure varies significantly across instruments.

Next Steps​

Ready to learn about advanced Pine Script techniques? Move on to the next chapter! 🚀