--- name: pinescript-strategy description: "Write TradingView Pine Script v6 strategies that behave the same live as in backtest — entries, exits, position sizing, commission and slippage, and the repainting and lookahead traps that make a backtest lie. Includes validated scaffolds and webhook alert payloads for broker automation. Use when asked to write, build or backtest a trading strategy; to add entries, exits, stops or take-profits; when a strategy performs differently live than in testing; or to automate alerts to a broker." license: MIT metadata: "pinescript-plugin/version": "0.4.2" "pinescript-plugin/triggers": "write a strategy, trading strategy, backtest, strategy.entry, strategy.exit, stop loss, take profit, position sizing, repainting, backtest doesn't match live, webhook alert, automate to broker" "pinescript-plugin/pine-version": "v6" --- # Pine v6 strategies ## The thing that actually goes wrong A strategy that compiles cleanly and backtests beautifully can still lose money, because **the backtest was measuring something the live market will not repeat.** Practitioners are blunt about this: *"one overlooked mistake — like a repainting signal or scope error — can invalidate months of backtesting."* ([PickMyTrade](https://blog.pickmytrade.io/debugging-tradingview-strategies-10-common-pine-script-mistakes/)) The validator now catches three of the five traps below — **S1** (repainting), **S2** (`ta.*` in a conditional) and **S9** (entry with no exit). Call `validate_pine_script` and act on those findings. The other two — unconfirmed-bar entries and optimistic fill assumptions — are invisible to any static check. **Read for those by eye, every time, before anyone risks capital.** ## The five traps, in order of cost ### 1. Repainting higher-timeframe data — detected as **S1** ```pine // WRONG — the current daily bar is still forming; history and live disagree d = request.security(syminfo.tickerid, "D", close) // RIGHT — the previous, closed bar d = request.security(syminfo.tickerid, "D", close[1], lookahead=barmerge.lookahead_off) ``` `lookahead_on` is worse than useless in a strategy: it lets history see data that did not exist yet, so the backtest is fiction. ### 2. Acting on an unconfirmed bar — NOT detectable Intrabar, `close` moves. A condition true mid-bar may be false at the close, so the signal appears and vanishes. ```pine // Deterministic — evaluate only on a closed bar if barstate.isconfirmed and longCondition strategy.entry("Long", strategy.long) ``` Or set `calc_on_every_tick=false` (the default) and leave it alone. ### 3. `ta.*` inside a conditional — detected as **S2** ```pine // WRONG — ta.rsi only advances when the branch is taken; its state is corrupted v = useRsi ? ta.rsi(close, 14) : na // RIGHT — compute every bar, select afterwards rsiValue = ta.rsi(close, 14) v = useRsi ? rsiValue : na ``` Compiles. Silently wrong. The indicator's internal history has gaps. ### 4. Accumulator lifetime — both directions are wrong ```pine // WRONG — resets every bar, so it is always 1 int wins = 0 wins := wins + 1 // WRONG in v6 — `and` short-circuits, so the increment is skipped // whenever the left side is false ok = condition and (wins := wins + 1) > 0 // RIGHT — persist with var, and never hide state changes inside and/or. // The same applies to ta.*: `x and ta.rsi(close,14) > 50` is a CONDITIONAL ta.* call, // because v6 short-circuits. Compute it on its own line first. var int wins = 0 if tradeClosed and profitable wins := wins + 1 ``` **Now the inverse, which is more common and more expensive.** `var` persists, so a loop that re-accumulates every bar without a reset grows without bound: ```pine // WRONG — var means this is never cleared; it adds 10 more closes every bar var float sum = 0.0 for i = 0 to 9 sum := sum + close[i] // RIGHT — a per-bar total must NOT be var float sum = 0.0 for i = 0 to 9 sum += close[i] // ALSO RIGHT — keep var, but reset before the loop var float sum = 0.0 sum := 0.0 for i = 0 to 9 sum += close[i] ``` Same trap with `while`, and here it is silent rather than wrong: ```pine // WRONG — on bar 2 counter is already 5, so the body never runs again var int counter = 0 while counter < 5 counter += 1 ``` **The question to ask of every accumulator: does its lifetime match its meaning?** "Total so far" wants `var`. "Total for this bar" must not have it. The validator does not check this (S3), so it is on you. v6 introduced short-circuit evaluation. Any assignment tucked into the right-hand side of `and`/`or` will sometimes not run. ### 5. Optimistic fills — NOT detectable Default backtest assumptions are generous. State them explicitly: ```pine strategy("S", overlay=true, initial_capital=10000, default_qty_type=strategy.percent_of_equity, default_qty_value=10, commission_type=strategy.commission.percent, commission_value=0.05, slippage=2, process_orders_on_close=true, calc_on_every_tick=false) ``` Zero commission and zero slippage flatter every strategy, and flatter high-frequency ones most. ## Entries and exits ```pine strategy.entry("Long", strategy.long, comment="MA cross") strategy.exit("Long Exit", from_entry="Long", stop=stopPrice, limit=targetPrice) strategy.close("Long", comment="Signal flip") strategy.close_all(comment="Flat") ``` - `strategy.entry` **reverses** an opposing position by default. To avoid that, close first. - `strategy.exit` needs `from_entry` matching the entry `id`, or it attaches to everything. - An entry with no exit is unbounded risk. Every `strategy.entry` needs a matching `strategy.exit` or `strategy.close` — detected as **S9**. Note `strategy.cancel` withdraws a *pending order*; it does not close a position and does not count. - `pyramiding` defaults to 0 — additional same-direction entries are ignored unless you raise it. ## Stops and targets ```pine longStop = strategy.position_avg_price * (1 - stopPercent / 100) longTarget = strategy.position_avg_price * (1 + targetPercent / 100) if strategy.position_size > 0 strategy.exit("X", from_entry="Long", stop=longStop, limit=longTarget) ``` Derive levels from `strategy.position_avg_price`, not from `close` at signal time — the fill is not the signal bar's close. For ATR-based stops, compute ATR **unconditionally**, then use it. ## Runtime state | Variable | Meaning | |---|---| | `strategy.position_size` | Signed; `> 0` long, `< 0` short, `0` flat | | `strategy.position_avg_price` | Average fill of the open position | | `strategy.equity` | Capital including open P/L | | `strategy.netprofit` | Closed-trade profit | | `strategy.opentrades` / `strategy.closedtrades` | Counts | ## Webhook automation Strategies are commonly automated by posting JSON to a broker bridge — TradersPost, PickMyTrade, CrossTrade. The alert message carries the payload: ```pine alertMessage = '{"ticker":"' + syminfo.ticker + '","action":"buy","sentiment":"long"}' if longCondition strategy.entry("Long", strategy.long, alert_message=alertMessage) ``` `alert_message` on the order call is what the webhook posts. Keep it valid JSON — build it with `str.tostring()` and explicit concatenation, and check the receiving platform's required fields (`ticker`, `action`, `sentiment`, `quantity` are common). `alertcondition()` messages are **static** and cannot carry runtime values; use `alert()` or order-level `alert_message` when the payload must vary. ## Scaffolds `references/scaffolds/` — validated in CI: | Scaffold | Shows | |---|---| | `basic-strategy.pine` | Entries, exits, sizing, commission, confirmed-bar gating | | `risk-managed-strategy.pine` | ATR stops, position sizing from risk %, webhook payloads | ## Checklist before anyone risks money - [ ] `validate_pine_script` clean - [ ] Every `request.security` uses `[1]` or an explicit `lookahead_off` - [ ] Entries gated on `barstate.isconfirmed`, or `calc_on_every_tick=false` - [ ] All `ta.*` calls unconditional — including on the right of `and`/`or`, which short-circuits - [ ] Every accumulator's lifetime matches its meaning: `var` for a running total, no `var` (or a reset before the loop) for a per-bar one - [ ] Commission and slippage set to something realistic - [ ] Every entry has a matching exit - [ ] Backtest sample is long enough to include a regime change - [ ] `alert_message` JSON validated against the broker's schema