Pine Script is small, but its rules are unusually strict — repainting, request.security lag, and per-bar execution order trip up even experienced traders. Claude is useful here for a specific reason: it drafts correct, idiomatic Pine Script v5 on the first pass, which means you spend your time on strategy logic instead of debugging syntax. This guide walks through building an advanced, webhook-ready strategy step by step — session filtering, percentage-based risk sizing, dynamic take-profit/stop-loss, realistic slippage modelling, and sending entry/exit alerts straight into your PickMyTrade webhook.
Table of Contents
- 1. Why draft Pine Script with Claude
- 2. Prompting for a clean strategy skeleton
- 3. Trading session & day filters
- 4. Percentage-based SL/TP sizing
- 5. Dynamic TP/SL (ATR & structure)
- 6. Modelling slippage honestly
- Backtest-side: the strategy() declaration
- Order-side: buffer the price you actually send
- 7. Sending entries/exits to PickMyTrade
- Attach a payload to every order
- Wire it up once, in the alert dialog
- 8. The full reference strategy
- 9. Before you go live
1. Why draft Pine Script with Claude
Treat Claude as a pair programmer, not an oracle. It’s excellent at:
- Translating a rule into code — “close above the 20 EMA with RSI under 40, filtered to the NY session” becomes a working
longConditionin one pass. - Converting Pine v4 to v5, or converting an indicator’s logic into a
strategy()so it can backtest and fire alerts. - Explaining execution order — why your stop got skipped, why
strategy.exitdidn’t fire, why a plot repaints.
It is not a substitute for walking through the compiled result on a chart. Every code block below is meant to be pasted into the Pine Editor and read against real candles before you trust it.
2. Prompting for a clean strategy skeleton
The quality of generated Pine Script depends almost entirely on how specific the prompt is. Vague prompts produce vague strategies with hidden repainting bugs. A prompt pattern that works well:
“Write a Pine Script v5
strategy(), notindicator(). Entry: [your rule]. Exit: [your rule]. Position size: percent of equity, configurable via input. Include a trading-session filter and day-of-week filter as inputs. Norequest.securityunless I ask for a higher timeframe. Addalert_messagestrings on everystrategy.entryandstrategy.exitformatted as JSON with ticker, action, quantity, stop loss and take profit.”
Naming the exact building blocks — inputs, alert_message, no security() — removes most of the ambiguity that causes a model to guess at conventions you didn’t ask for. The rest of this guide builds each of those blocks out individually, then assembles them into one strategy in Step 8.
3. Trading session & day filters
Most strategies only perform inside a specific window — the London open, the first two hours of the NY cash session, or weekdays only. Build the filter once as an input, and reuse the same boolean everywhere else in the script.
// Trading session + day-of-week filter
sessionInput = input.session("0930-1600", "Trading Session (exchange time)")
daysInput = input.string("1234567", "Active Days",
tooltip="1=Mon..7=Sun. Remove a digit to disable that day, e.g. 12345")
inSession = not na(time(timeframe.period, sessionInput + ":" + daysInput))
// Flatten automatically when the session ends
if not inSession and inSession[1]
strategy.close_all(comment="Session end")
input.session gives traders a native TradingView time picker instead of raw text, and the days string lets you disable Friday or weekends without touching the logic. Gate every entry condition with and inSession:
longCondition = ta.crossover(ta.ema(close,9), ta.ema(close,21)) and inSession
Watch for:
input.sessionresolves in the chart’s exchange timezone, not your local timezone. If you trade multiple instruments across time zones, make the timezone an explicit input too —input.string("America/New_York", "Session Timezone")— and pass it as the third argument totime().
4. Percentage-based SL/TP sizing
Fixed tick or fixed-dollar stops break the moment you switch instruments or timeframes. Sizing everything off percentages — risk per trade, stop distance, target distance — keeps the strategy portable and ties position size to account equity, not to a hardcoded contract count.
// Inputs: everything expressed as a percentage
riskPct = input.float(1.0, "Risk per trade (% equity)", minval=0.1, step=0.1) / 100
slPct = input.float(1.0, "Stop Loss (% from entry)", minval=0.05, step=0.05) / 100
tpPct = input.float(2.0, "Take Profit (% from entry)", minval=0.05, step=0.05) / 100
if longCondition
entryPrice = close
slPrice = entryPrice * (1 - slPct)
tpPrice = entryPrice * (1 + tpPct)
riskPerUnit = entryPrice - slPrice
qty = (strategy.equity * riskPct) / riskPerUnit
strategy.entry("Long", strategy.long, qty=qty)
strategy.exit("Long Exit", from_entry="Long", stop=slPrice, limit=tpPrice)
The important line is qty: instead of a fixed contract size, position size is derived from strategy.equity, the risk percentage, and the actual stop distance. Risking 1% with a 0.5% stop produces a bigger position than risking 1% with a 2% stop — which is the correct behavior for consistent per-trade risk.
| Stop distance | Risk / trade | Resulting size (10k acct) |
|---|---|---|
| 0.5% | 1% | ~2.0x equity notional |
| 1.0% | 1% | ~1.0x equity notional |
| 2.0% | 1% | ~0.5x equity notional |
5. Dynamic TP/SL (ATR & structure)
Fixed percentages are a good starting point but ignore volatility. A 1% stop is tight on a quiet day and gets run every time on a volatile one. Basing the stop on ATR — and the target on a fixed reward:risk multiple of that same stop — makes both move with the market.
atrLen = input.int(14, "ATR Length")
atrMult = input.float(1.5, "ATR Multiplier - Stop")
rr = input.float(2.0, "Reward : Risk")
atrVal = ta.atr(atrLen)
swingLow = ta.lowest(low, 10)
// Whichever is tighter: volatility stop or last swing low
dynamicSL = math.max(close - atrVal * atrMult, swingLow)
riskDist = close - dynamicSL
dynamicTP = close + riskDist * rr
if longCondition
strategy.entry("Long", strategy.long)
strategy.exit("Long Exit", from_entry="Long", stop=dynamicSL, limit=dynamicTP)
For a trailing variant — lock in profit as price runs instead of holding a single fixed target — strategy.exit accepts trail_points/trail_offset, both expressed in ticks:
trailTicks = (atrVal * atrMult) / syminfo.mintick
offsetTicks = (atrVal * 0.5) / syminfo.mintick
strategy.exit("Long Trail", from_entry="Long",
trail_points=trailTicks, trail_offset=offsetTicks, stop=dynamicSL)
Why math.max and not just ATR: pure ATR stops ignore market structure — they’ll happily place a stop inside the middle of a range. Clamping to the recent swing low keeps the stop below an actual level, and ATR keeps it from ever getting unreasonably wide.
6. Modelling slippage honestly
A backtest with zero slippage is a fantasy — it flatters strategies that scalp small moves on thin instruments. Pine gives you two separate levers: a backtest-side slippage model, and a broker-side buffer you can bake into the price you actually send.
Backtest-side: the strategy() declaration
strategy("Session ORB — realistic fills", overlay=true,
default_qty_type=strategy.percent_of_equity, default_qty_value=10,
slippage=3, // ticks added against you on every fill
commission_type=strategy.commission.percent, commission_value=0.04,
process_orders_on_close=false)
slippage is a tick count applied to every fill — entries, stops, and limits alike — in the direction that costs you money. It’s a blunt instrument, but it’s honest: a strategy that only looks profitable at slippage=0 is not tradeable.
Order-side: buffer the price you actually send
Because PickMyTrade forwards your alert to a live broker, the fill you get depends on real order-book depth at that instant — not on what Pine assumed. For instruments prone to gaps or thin liquidity, send a small buffer on stop and limit prices rather than the exact theoretical level:
slipBufferTicks = input.int(2, "Manual Slippage Buffer (ticks)")
adjStop = dynamicSL - slipBufferTicks * syminfo.mintick // long stop, extra room below
adjLimit = dynamicTP - slipBufferTicks * syminfo.mintick // long target, slightly closer
Watch for: don’t buffer in both the backtest and the live payload and then compare results directly — you’ll be double-counting slippage and the numbers won’t reconcile. Keep the backtest’s
slippageparameter as your baseline estimate, and only add the manual buffer to the live alert once you’ve compared actual PickMyTrade fill prices against the strategy’s assumed levels for a few weeks.
7. Sending entries/exits to PickMyTrade
TradingView alerts only know how to send text. The trick is generating that text as JSON at the exact moment an order is placed, using alert_message on strategy.entry/strategy.exit, then wiring a single alert to forward it.
Attach a payload to every order
if longCondition
entryMsg = '{"ticker":"' + syminfo.ticker + '","action":"buy","quantity":' + str.tostring(qty) +
',"entry":' + str.tostring(close) + ',"stoploss":' + str.tostring(dynamicSL) +
',"takeprofit":' + str.tostring(dynamicTP) + ',"orderType":"market"}'
strategy.entry("Long", strategy.long, qty=qty, alert_message=entryMsg)
exitMsg = '{"ticker":"' + syminfo.ticker + '","action":"close"}'
strategy.exit("Long Exit", from_entry="Long", stop=dynamicSL, limit=dynamicTP, alert_message=exitMsg)
Field names are illustrative: the exact JSON keys PickMyTrade expects (ticker/symbol, action, quantity, stop loss, take profit, order type, and your PMT account key) are defined on your PickMyTrade webhook setup screen — copy the sample payload from there and keep the structure above, just with the confirmed key names.
Wire it up once, in the alert dialog
- Add the strategy to the chart, then click Alert.
- Condition: select your strategy name, then toggle Order fills only.
- Message box: enter exactly
{{strategy.order.alert_message}}— this pulls whatever string you built inalert_messagefor that specific fill. - Webhook URL: paste the PickMyTrade endpoint from your dashboard.
- Set expiration to open-ended and leave the alert running — one alert now covers every entry and exit the strategy generates.
Watch for: if you rebuild
entryMsg/exitMsgas plain strings without wiring them throughalert_message,{{strategy.order.alert_message}}will be empty. The message has to be attached to the order call itself, not justalert()‘d separately, or fill-specific fields like fill price won’t line up with the right order.
8. The full reference strategy
Everything above, assembled into one script: session/day filter, ATR+structure stop, reward:risk target, equity-percent sizing, realistic slippage/commission, and JSON alerts ready for a webhook.
//@version=5
strategy("EMA Cross — session filtered, PMT-ready", overlay=true,
default_qty_type=strategy.percent_of_equity, default_qty_value=10,
slippage=3, commission_type=strategy.commission.percent, commission_value=0.04)
// --- Session ---
sessionInput = input.session("0930-1600", "Trading Session")
daysInput = input.string("12345", "Active Days")
inSession = not na(time(timeframe.period, sessionInput + ":" + daysInput))
// --- Risk inputs ---
riskPct = input.float(1.0, "Risk per trade (%)") / 100
atrLen = input.int(14, "ATR Length")
atrMult = input.float(1.5, "ATR Multiplier")
rr = input.float(2.0, "Reward:Risk")
// --- Signal ---
fastEma = ta.ema(close, 9)
slowEma = ta.ema(close, 21)
longCondition = ta.crossover(fastEma, slowEma) and inSession
// --- Dynamic stop / target ---
atrVal = ta.atr(atrLen)
swingLow = ta.lowest(low, 10)
dynamicSL = math.max(close - atrVal * atrMult, swingLow)
riskDist = close - dynamicSL
dynamicTP = close + riskDist * rr
qty = (strategy.equity * riskPct) / riskDist
// --- Orders + alerts ---
if longCondition
entryMsg = '{"ticker":"' + syminfo.ticker + '","action":"buy","quantity":' + str.tostring(qty) +
',"stoploss":' + str.tostring(dynamicSL) + ',"takeprofit":' + str.tostring(dynamicTP) + '}'
strategy.entry("Long", strategy.long, qty=qty, alert_message=entryMsg)
exitMsg = '{"ticker":"' + syminfo.ticker + '","action":"close"}'
strategy.exit("Long Exit", from_entry="Long", stop=dynamicSL, limit=dynamicTP, alert_message=exitMsg)
if not inSession and inSession[1]
strategy.close_all(comment="Session end", alert_message='{"ticker":"' + syminfo.ticker + '","action":"close"}')
9. Before you go live
A strategy that backtests well and a strategy that’s safe to wire to a live broker are two different bars to clear.
- Run it on bar replay first. Step through the exact bars where entries fired and confirm the plotted stop/target match what the alert payload would have sent.
- Paper-trade through PickMyTrade before funding it. Compare the fill prices PickMyTrade reports against
dynamicSL/dynamicTPto calibrate your manual slippage buffer from Step 6. - Confirm the alert fires on order fills, not just conditions. “Once Per Bar Close” plus “Order fills only” avoids duplicate or premature signals.
- Log every payload. Keep a few weeks of the raw JSON PickMyTrade received — it’s the fastest way to diagnose a mismatch between what Pine intended and what the broker executed.
Once your alert_message payload matches your PickMyTrade webhook schema, the rest is configuration — no code changes needed to add a new broker or account.
