pine-script-pro
Skill1 starsChanged 4 months agoMITView on GitHub
Authors production-grade Pine Script v6 for TradingView (indicators, strategies, libraries). Use when the user asks to "write a Pine Script", "build a TradingView indicator", "create a strategy", "code an oscillator/overlay/screener", "Pine Script v6", "TradingView alert", or describes any trading idea they want on a chart ("alert when RSI crosses 70 on the 4h", "show me a divergence indicator", "backtest a moving-average crossover"). Triggers even when the user does not say the word "Pine". Do NOT use for other platforms (NinjaScript, MQL4/MQL5, ThinkScript, EasyLanguage), non-TradingView backtesting frameworks, or general trading-strategy advice that does not require code.
In this repository24 other files it gives its agents.
- app-hardenSkill
- arch-auditSkill
- arch-conformanceSkill
- ballot-advisorSkill
- bug-huntSkill
- canon-c50Skill
- canon-r6-mark-iiSkill
- code-scrutinySkill
- davinci-resolveSkill
- desktop-ui-craftSkill
- fsharp-upliftSkill
- pattern-architectSkill
- plan-evalSkill
- plan-executeSkill
- plan-featureSkill
- refactor-proSkill
- rust-ci-greenSkill
- rust-commitSkill
- rust-test-separateSkill
- rust-upliftSkill
- rust-warn-fixSkill
- skill-forgeSkill
- skill-refineSkill
- sprintSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
22 rules. About 0 are common, 8 are distinctive, and 14 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈8
Distinctive · estimate
This author's own choices. Worth a look.
14
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 17
When you cannot determine intent from the user's request, ask one focused question before coding.
- Very specific: line 38
Default to indicator() unless the user explicitly asks to simulate trades or measure performance.
- Distinctive: line 42
Every script you write follows this order, with section banners:
- Very specific: line 100
For
request.security()of higher timeframes: use the safe wrapper below, never inline request.security(syminfo.tickerid, "D", close). - Very specific: line 101
For lower-timeframe data: use request.security_lower_tf(), not request.security() with a smaller timeframe — only the former is safe across …
- Distinctive: line 156
If the user's wording is ambiguous, ask: "When you zoom in vertically, do you want the circle to (a) stay the same visual size and just …
- Distinctive: line 226
All three are bar-anchored (travel with the bar through zoom/scroll).
- Very specific: line 228
If the user genuinely wants the marker's body to grow with chart zoom, fall back to box.new() (square dot) or polyline.new() (filled circle …
- Very specific: line 251
Never create a new line/label/box unconditionally on every bar.
- Very specific: line 291
Use strategy.exit() with both stop and limit for bracket orders.
- Very specific: line 301
Always use the f_secure wrapper above for HTF data — never inline a raw request.security.
- Very specific: line 316
Use var for state that must accumulate across bars (counters, persistent line/label references, last-signal price).
- Very specific: line 323
Add tooltip = "..." for any input whose effect is not obvious.
- Very specific: line 324
Use input.color() for any colour the user might want to theme.
- Very specific: line 325
Use inline = "row1" to put related inputs on one row (e.g.
- Very specific: line 342
Default max_bars_back = 500.
- Very specific: line 343
Default 50 lines/labels/boxes.
- Very specific: line 386
Drawing-object limits declared: if the script uses lines/labels/boxes (max_lines_count, max_labels_count, max_boxes_count, …
- Distinctive: line 393
If any check fails, fix before reporting done.
- Distinctive: line 397
When delivering a script, output:
- Distinctive: line 402
Configuration notes: if any input deserves explanation beyond its tooltip.
- Distinctive: line 405
Do not pad with installation instructions ("Paste this into Pine Editor and click Add to Chart") — users who ask for Pine code know how to …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Pine Script Pro
You author Pine Script v6 for TradingView at the level of a senior PineCoder: indicators that visualize cleanly on charts, strategies that hold up to honest backtesting, and analytical logic that does not silently lie about the past.
The single biggest gap between amateur and expert Pine work is repainting awareness. Roughly 95% of scripts on TradingView repaint in some form. Most authors do not know it. Your scripts will not repaint unintentionally, and when a script deliberately uses live data, you will say so out loud.
Operating principle
Pine Script runs once per bar, top-to-bottom, across the entire visible history. Every variable is a series — a vector of values aligned to bars. Things that look like scalars are not. Internalize this and most pitfalls disappear.
When you cannot determine intent from the user's request, ask one focused question before coding. Trading ideas often hide three different scripts (alert-only indicator, visual indicator, or full strategy). Confirm which one.
Workflow
For every request:
- Classify the artifact. Indicator, strategy, or library? See the decision matrix below.
- Identify the analytical core. What is being computed? On what timeframe(s)? What inputs control it?
- Check repainting risk. Will signals reference live bar data, higher-timeframe data, or future-leaking patterns? Decide upfront whether the script is "confirmed-bar only" or "live mode" — and label it in the title and a header comment.
- Design the visualization. Plot, plotshape, label, line, box, table, fill, polyline — each has a niche. See "Visualization decision matrix" below.
- Write the script following the structure in "Script structure" and the rules in "Repainting safety".
- Self-validate with the checklist at the end before reporting done.
Decision matrix: indicator vs strategy vs library
| Use | Declaration | When |
indicator() | Plotting, alerts, screening, custom UI on chart | Default for any "show me / alert me when" request. No trade simulation. |
strategy() | Backtested entry/exit logic with the TradingView Strategy Tester | User says backtest, equity curve, win rate, drawdown, position sizing. |
library() | Reusable exported functions for other scripts | User asks for a "library" or you are splitting a large codebase. |
Default to indicator() unless the user explicitly asks to simulate trades or measure performance. Strategies cost more compute and trigger backtest concerns the user may not want.
Script structure (mandatory order)
Every script you write follows this order, with section banners:
// SPDX-License-Identifier: MIT (or user's chosen license)
//@version=6
indicator("Name", shorttitle = "Short", overlay = true,
max_lines_count = 200, max_labels_count = 200)
// ──────────────────────────────────────────────────────────────────────────
// CONSTANTS
// ──────────────────────────────────────────────────────────────────────────
const color BULL_COLOR = color.new(#26a69a, 0)
const color BEAR_COLOR = color.new(#ef5350, 0)
const int MAX_LOOKBACK = 500
// ──────────────────────────────────────────────────────────────────────────
// INPUTS
// ──────────────────────────────────────────────────────────────────────────
lengthInput = input.int(14, "Length", minval = 1, maxval = 500, group = "Calculation")
sourceInput = input.source(close, "Source", group = "Calculation")
confirmedInput = input.bool(true, "Wait for bar close", group = "Signals",
tooltip = "When on, signals only fire after the bar closes (non-repainting). When off, signals may flicker on the live bar.")
showLabelsInput = input.bool(true, "Show labels", group = "Display")
// ──────────────────────────────────────────────────────────────────────────
// FUNCTIONS
// ──────────────────────────────────────────────────────────────────────────
// All user-defined functions live in global scope (Pine forbids nesting).
// ──────────────────────────────────────────────────────────────────────────
// CALCULATIONS
// ──────────────────────────────────────────────────────────────────────────
// ──────────────────────────────────────────────────────────────────────────
// VISUALS
// ──────────────────────────────────────────────────────────────────────────
// ──────────────────────────────────────────────────────────────────────────
// ALERTS
// ──────────────────────────────────────────────────────────────────────────Reasoning: TradingView's style guide mandates this order. Constants → inputs → functions → calc → visuals → alerts. Inputs grouped logically with group = make settings panes readable.
Repainting safety (highest-impact rule)
A repainting script is one whose plotted history disagrees with what a trader would have seen live. Repainting is silent — backtests look great, live trading bleeds.
The four repainting traps
- Live-bar fluctuation —
closeon the current unconfirmed bar moves with every tick. A signal based onta.crossover(close, ma)flickers on/off until the bar closes, then locks in. The plotted history shows only the locked-in version. request.security()future-leak — calling withlookahead = barmerge.lookahead_onand without offsetting the expression by[1]pulls the higher-timeframe bar's close before it was knowable.barstate.isnew— fires at bar open in realtime but at bar close on history. Different timing in the two regimes.varipandtimenow— both carry realtime-only state that cannot be reproduced on history.
The non-negotiable rules
- For any signal feeding an
alertcondition(),alert(), orstrategy.entry()trigger: gate it withbarstate.isconfirmedor reference only confirmed-bar data (close[1],ma[1]). If the user wants the live-tick version, they must ask for it explicitly and you must label the script "(live mode — repaints)" in the title. - For
request.security()of higher timeframes: use the safe wrapper below, never inlinerequest.security(syminfo.tickerid, "D", close). - For lower-timeframe data: use
request.security_lower_tf(), notrequest.security()with a smaller timeframe — only the former is safe across history and realtime. - Avoid
varipunless you genuinely need intrabar accumulation (e.g. tick-volume profiles). Document the repainting cost in a comment when you use it. plotshape()andlabel.new()placed in the past (e.g. pivot detectednbars later) must use anoffsetand a comment explaining the look-back delay, so users understand the signal wasn't visible at that bar.
The safe HTF-request wrapper — bundle this in every multi-timeframe script
//@function Non-repainting higher-timeframe request. Offsets by one bar and uses
// lookahead_on so historical and realtime values match the *previously closed* HTF bar.
f_secure(simple string sym, simple string tf, series float expr) =>
request.security(sym, tf, expr[1], lookahead = barmerge.lookahead_on)
// Usage:
htfClose = f_secure(syminfo.tickerid, "D", close)This is the PineCoders canonical pattern. Use it unchanged.
For details and edge cases (multiple values bundled in one request, request.security_lower_tf with intrabar arrays, barstate.isconfirmed patterns in alerts), read references/repainting.md.
Visualization decision matrix
Pine has overlapping drawing primitives. Pick the right one or your script gets ugly and slow.
| Need | Use | Why |
| Continuous line (moving average, oscillator) | plot() | Cheapest; new v6 supports linestyle = plot.style_line_dashed etc. |
| Histogram (MACD hist, volume) | plot(series, style = plot.style_histogram) or plot(series, style = plot.style_columns) | Native, no drawing-object budget. |
| Filled region between two series | fill(plotA, plotB, color) | Cheaper than polygons; chains naturally with two plots. |
| Horizontal static level | hline() | Free, axis-aligned. |
| Static or repeating shape on a bar (cross, triangle, arrow) | plotshape() / plotchar() / plotarrow() | Free (not subject to label/line limits). Use over label.new() for arrows on every signal bar. |
| Dynamic text per bar (e.g. value labels) | label.new() | Costs from the label budget (default 50, max 500). Manage with max_labels_count. |
| Trendline, S/R line, zigzag | line.new() | From line budget. Use line.set_* to mutate, not delete-and-create. |
| Rectangle / order block / supply zone | box.new() | From box budget. Same mutation pattern. |
| Floating UI panel (dashboard, scoreboard, multi-symbol screener) | table.new() | Anchored to viewport, not bars. Update once per bar with table.cell(). |
| Multi-point polygon, channels, fans | polyline.new() | New in v6; up to 100 default. Replaces fragile multi-line patterns. |
Anchoring vs sizing — two separate axes
Drawing primitives differ on two independent properties that users often conflate:
| Property | What it means | Primitives |
| Bar-anchored (X position follows the bar) | The drawing moves with its bar through every zoom, pan, resize. As bars scroll left, the drawing scrolls with them. | All bar-tethered primitives: plotshape, plotchar, plotarrow, label.new(bar_index, …), line.new, box.new(left = bar_index, …), polyline of chart.point.from_index. |
| Viewport-anchored (X position fixed to chart pane) | The drawing stays at the same pixel position regardless of which bars are visible. | table.new only. |
| Pixel-sized body (visual size constant under zoom) | The drawing's shape is rendered at a fixed pixel size; zooming makes it cover more or fewer bars/price, but the dot/triangle/circle itself is the same physical size. | plotshape, plotchar, plotarrow, label.new with any style = label.style_* (including label.style_circle). The size parameter only picks among discrete pixel sizes (size.tiny…size.huge). |
| Chart-coord-sized body (visual size grows/shrinks with zoom) | The drawing's dimensions are expressed in time × price; zooming in makes it physically larger on screen, zooming out makes it smaller. | line.new, box.new, polyline.new. Width/height/path are all in chart units. |
This is the #1 source of "the marker doesn't behave the way I want" complaints. When a user says "respect scale", "scale with the chart", or "stay glued to the bar", they may mean any of:
- Stay anchored to the bar through zoom/pan — they want bar-tethered. Default for almost everything that isn't a
table. - Grow visually when I zoom in — they want chart-coord-sized body. Need
boxorpolyline. - Stay the same pixel size at every zoom — they want pixel-sized body. Need
plotshapeorlabel. - Offset from the bar should be in price units, not pixels — they want a custom price-space offset (e.g. ATR-based) with a primitive of either kind.
If the user's wording is ambiguous, ask: "When you zoom in vertically, do you want the circle to (a) stay the same visual size and just move with the bar, or (b) grow proportionally with the bars?" That single question saves three rewrites.
Empirical default: when traders say a marker should "respect the chart's coordinate system" or "scale with the chart" without further qualification, they usually mean option (b) — chart-coord-sized. A plotshape that stays the same pixel size at every zoom looks "stuck" or "static" to them, even though it's correctly tracking its bar. Reach for box/polyline first, and only fall back to plotshape/label if the user pushes back.
When you use a chart-coord-sized primitive, make the default radius generous enough that the scaling effect is visually obvious on a typical chart — a 0.05-ATR marker on a high-priced stock can be so small that the user perceives no scaling and reports it as "static." Default to roughly 0.15-0.25 ATR vertically and 0.4-0.5 bar widths horizontally, and expose both as inputs.
Diagnostic: "marker doesn't follow vertical zoom/pan"
If a user reports that markers position correctly on the initial render but fail to track the bar when they zoom or pan vertically, the issue is in how the label/shape's y-coordinate is being supplied to Pine. There is a subtle and badly-documented quirk in label.new() that affects exactly this:
label.new(x = bar_index, y = na, yloc = yloc.belowbar, …)— the y-argument is supposedly ignored whenyloc.belowbar/abovebaris used, but in practice supplyingy = naproduces a label whose anchoring detaches during vertical interaction on some chart configurations.label.new(x = bar_index, y = low, yloc = yloc.belowbar, …)— supplying both an explicit price y ANDyloc.belowbar/abovebarproduces a label that tracks the bar reliably through any zoom/pan.
Always supply both y and yloc for label-based bar markers. The y-argument doubles as an anchoring hint even when yloc ostensibly overrides it.
The Skyrexio reference pattern (verified working)
This is the exact field-tested pattern for a small filled circle glued to a bar — both bullish-below and bearish-above variants:
// Below the bar
label.new(x = bar_index, y = low, yloc = yloc.belowbar, text = "",
color = bullColor, style = label.style_circle,
textcolor = bullColor, size = size.tiny)
// Above the bar
label.new(x = bar_index, y = high, yloc = yloc.abovebar, text = "",
color = bearColor, style = label.style_circle,
textcolor = bearColor, size = size.tiny)The four critical bits, in order of how often they are mis-set:
- Supply BOTH
y = low/highANDyloc = yloc.belowbar/abovebar. Do not passy = na. textcolormatchescolor. A contrastingtextcolorwould put a visible glyph inside the circle.style = label.style_circlefor the round shape;label.style_square/label.style_diamondwork the same way for other shapes.size = size.tinykeeps the marker subtle.sizeonlabel.new()acceptsseries string, so it can be input-driven (unlikeplotshape'sconst stringsize).
Other causes of bar-tracking failure to rule out before changing primitives:
location.top/location.bottomon aplotshape— these are pane-relative, not bar-anchored. The marker stays at the top/bottom of the pane regardless of bar position. Almost never what the user wants for "marker on a bar."table.new— tables are viewport-anchored, not bar-anchored. Confusing only if the user mistook a table for a marker.- Chart auto-scale — if the user is on auto-scale and reports "vertical pan doesn't move bars," the chart is auto-fitting and they're not actually panning. Suggest toggling auto-scale off (right-click price axis → uncheck "Auto") to verify.
Pixel-sized marker just above/below a bar — the standard patterns
This is the most common visualization request and the most over-thought. Three idiomatic answers, in order of simplicity:
// (1) Uniform marker on every signal bar — free, no budget cost.
plotshape(signal, style = shape.circle, location = location.belowbar,
color = color.green, size = size.tiny)
// (2) Per-bar marker with a different color each time — costs from label budget.
if signal
label.new(bar_index, na, text = "",
yloc = yloc.belowbar, // or yloc.abovebar
style = label.style_circle, // or .style_label_up, .style_xcross …
color = perBarColor,
size = size.tiny)
// (3) Custom price-space offset (e.g. ATR-based gap from the bar).
priceY = isBullish ? low - atr * 0.5 : high + atr * 0.5
if signal
label.new(bar_index, priceY, text = "",
style = label.style_circle, // y is interpreted as price (default yloc.price)
color = perBarColor,
size = size.tiny)All three are bar-anchored (travel with the bar through zoom/scroll). All three have a pixel-sized body (don't grow with zoom). The choice is between TradingView's default offset (1, 2) and a custom price-space offset (3).
If the user genuinely wants the marker's body to grow with chart zoom, fall back to box.new() (square dot) or polyline.new() (filled circle from N points). Both consume budget; both render in chart-coordinate space.
plotshape const-string gotcha
plotshape()'s size parameter is typed const string — a hard compile-time constant. You cannot drive it from an input.string switch because the result of a switch over an input is simple string, one qualifier looser than const. Compile error: CE10123 — An argument of "simple string" type was used but a "const string" is expected.
Workarounds:
// Hardcode the size (cheapest):
plotshape(signal, style = shape.circle, size = size.tiny, …)
// Or branch with the size literal at each call site:
plotshape(signal and sizePref == "Tiny", style = shape.circle, size = size.tiny, …)
plotshape(signal and sizePref == "Small", style = shape.circle, size = size.small, …)
plotshape(signal and sizePref == "Normal", style = shape.circle, size = size.normal, …)label.new()'s size parameter, by contrast, accepts series string — so for label-based markers you can drive size from an input switch.
Drawing-budget rules
- Declare every limit you need explicitly:
indicator(..., max_lines_count = 500, max_labels_count = 500, max_boxes_count = 500, max_polylines_count = 100). These default to 50 and TradingView silently deletes the oldest objects when you exceed. - Never create a new
line/label/boxunconditionally on every bar. Either gate on a real event or mutate one persistent object viavar line myLine = na+line.set_xy*updates. - For interactive overlays (S/R levels, order blocks), draw once when the event triggers, keep a reference, and mutate it on subsequent bars rather than redrawing.
For the full visualization toolkit including v6's polylines, text formatting (bold/italic), chart.point, and color-gradient techniques, see references/visualization.md.
Style conventions
Following TradingView's official style guide and the PineCoders conventions:
camelCasefor variables, functions, and inputs:maFast,lengthInput,roundedOhlc().SNAKE_CASEfor constants:BULL_COLOR,MAX_LOOKBACK.- Qualifying suffixes that disambiguate provenance:
lengthInput,volumesArray,resultsTable,maPlotId. This makes a 300-line script readable. - Explicit types on declarations:
float ma = ta.ema(close, 20), notma = ta.ema(close, 20). Distinguishes=declarations from:=reassignments and surfaces type-mismatch errors early. constfor true constants, notvar.varcarries a per-bar copy cost;constis compile-time.input.*()withgroup,tooltip, andinlineparameters. A script with 8 ungrouped inputs is unusable; with tooltips it teaches the user the indicator.- Spaces around binary operators, no space for unary minus:
x * (-1)notx*(-1). - Function definitions in global scope only. Pine forbids nested function defs. If you need closure-like behavior, pass state as parameters or use UDTs.
Strategy realism (when writing a strategy())
The default strategy() is a lie machine: zero commission, zero slippage, fills at bar close. Backtests look beautiful and live results crater. Every strategy you write declares realism upfront:
strategy("Name", overlay = true,
initial_capital = 10000,
default_qty_type = strategy.percent_of_equity,
default_qty_value = 10, // 10% of equity per trade
commission_type = strategy.commission.percent,
commission_value = 0.05, // 0.05% per side — adjust to broker
slippage = 2, // 2 ticks — bump for illiquid instruments
pyramiding = 0, // 0 means one trade per direction
calc_on_every_tick = false, // true causes realtime/history divergence
process_orders_on_close = true) // simulate fills at the close that produced the signalReasoning: calc_on_every_tick = true makes the strategy repaint between history and realtime. process_orders_on_close = true removes the one-bar fill delay that confuses authors. Commission and slippage are non-optional in any honest backtest — a 500-trade strategy at 0% commission can flip from profitable to unprofitable when fees are added.
Other strategy rules:
- Gate entries on
barstate.isconfirmedifprocess_orders_on_close = false, or accept the one-bar lag iftrue. - Use
strategy.exit()with bothstopandlimitfor bracket orders. Usestrategy.close()only for unconditional exits. - For pyramiding, set
pyramiding = Non the declaration and rely onstrategy.entry()(which honours pyramiding), notstrategy.order()(which does not). - Position sizing by risk-per-trade beats fixed-quantity. The pattern is
qty = (equity * riskPct) / (atr * atrStopMult).
For pyramiding edge cases, bracket exits, and the order-fill model in detail, see references/strategy.md.
Multi-timeframe analysis
The single most-misused Pine feature. Rules:
- Always use the
f_securewrapper above for HTF data — never inline a rawrequest.security. - Bundle multiple HTF values into one call by returning a tuple from an expression function:
[htfO, htfH, htfL, htfC] = f_secureBundle(...). This is dramatically faster than four separate calls. - For LTF data (intrabar analysis), use
request.security_lower_tf()which returns an array of intrabar values. Do not pass a smaller timeframe torequest.security()— it returns only one of the LTF bars per chart bar. - Set
gaps = barmerge.gaps_offon HTF requests unless you specifically wantnafor unclosed bars.
See references/multi-timeframe.md for the bundled-request pattern, intrabar iteration, and timeframe-string validation.
Persistence: var vs varip vs regular
| Declaration | Persists across bars? | Persists across intrabar ticks? | Repaints? |
x = ... | No (reset every bar's first iteration) | N/A | No |
var x = ... | Yes (initialized once on bar 0, then mutable) | No — resets on each tick of the realtime bar | No |
varip x = ... | Yes | Yes — survives every realtime tick | Yes — historical and realtime diverge |
Use var for state that must accumulate across bars (counters, persistent line/label references, last-signal price). Use plain assignment for everything per-bar. Only reach for varip when you genuinely need tick-level accumulation (e.g. tick-volume profiles, time-since-last-tick) and document the repainting cost.
Inputs: design for the user
A great Pine script is configurable without code edits. Conventions:
- Group related inputs:
group = "Signal logic",group = "Display",group = "Alerts". - Add
tooltip = "..."for any input whose effect is not obvious. The tooltip teaches. - Use
input.color()for any colour the user might want to theme. - Use
inline = "row1"to put related inputs on one row (e.g. fast-length + fast-source). - For mode switches use
input.string(..., options = ["Mode A", "Mode B"])not booleans — the option labels self-document. - Validate ranges with
minval =/maxval =. Pine input fields do not enforce sanity otherwise.
Alerts
Two functions, one trap:
alertcondition()— older. Message isconst string, so{{placeholder}}syntax is your only way to inject values. Each alertcondition shows up as a selectable condition in TradingView's "Create Alert" dialog.alert()— newer. Message isseries string, so you canstr.format("Price = {0,number}", close)at runtime. Fires from inside a singleAny alert() function callalert.
Pick alert() for dynamic JSON-for-webhook messages. Pick alertcondition() for menu-style alerts users can independently toggle.
Repainting interaction: gate alert-triggering events with barstate.isconfirmed so alerts only fire on confirmed bars, matching what a live trader would have seen. Otherwise the user gets dozens of false-positive alerts that vanish when the bar closes.
Limits to remember
- Default
max_bars_back = 500. If a script referencessomeSeries[1000], set it explicitly:indicator(..., max_bars_back = 1500). Otherwise you get the dreaded "Pine can't determine the referencing length" error. - Default 50 lines/labels/boxes. Always declare what you need.
- Hard cap of 500 plots per script. Use a table for dense screener output instead of dozens of plots.
- Series-of-string is limited (no arrays of strings indexed by series in old code, though v6 relaxed this).
- 100 polylines max.
The chart-instance cache — read this before you blame your code
TradingView caches the compiled indicator instance on the chart separately from the source code in the Pine Editor. Saving the script in the editor does not always invalidate that cache. Symptoms of cache-staleness:
- User reports "the fix didn't work" after you made what should have been a corrective change.
- The behavior matches a prior version of the code, not the current source.
- The bug is unreproducible when you walk through the logic on paper.
- Edits to drawings (label/box/polyline) "don't take" but edits to inputs do (inputs reset the instance; drawings reuse it).
Before assuming your code is wrong, ask the user to remove the indicator from the chart and re-add it. Right-click the indicator's pane → Remove indicator → re-open Pine Editor → click "Add to Chart." This forces a fresh compile and re-instantiation against the latest source.
This is the single most common reason a "logically correct" fix appears to fail across multiple iterations. If you ever find yourself on a third rewrite of the same primitive without progress, the cache is the likely culprit. Suggest the re-add before the fourth rewrite.
(The Pine Editor's "Save" + chart-instance cache disconnect is a TradingView UX wart, not a Pine language issue. There is no Pine-side workaround.)
Pine Logs (v6, for debugging)
For any non-trivial script, sprinkle log statements during development:
log.info("rsi = {0,number,#.##}, signal = {1}", rsi, signalText)
log.warning("Volatility regime change at bar {0}", bar_index)
log.error("Unexpected na in lookback")Open via the editor's "More" menu → "Pine Logs…". Clicking a log entry jumps to the bar where it fired — by far the fastest way to diagnose "why didn't my signal trigger here?" Remove or gate behind a debug input before publishing.
The new Pine Profiler (also in v6) reveals per-line execution time; reach for it if a script is sluggish.
Validation checklist — run before reporting done
For every script you produce, confirm:
- Declares
//@version=6at the top. - Script type is intentional:
indicator()for visualization/alerts,strategy()for backtested trades,library()for shared code. - No accidental repainting: any signal feeding an alert or order is gated on
barstate.isconfirmedOR uses[1]-offset data OR the script title says "(live mode — repaints)". - HTF requests use the safe
f_securewrapper (or the inline equivalent:expression[1]+lookahead_on). - Strategies set commission and slippage explicitly. No silently-zero defaults.
- Drawing-object limits declared if the script uses lines/labels/boxes (
max_lines_count,max_labels_count,max_boxes_count,max_polylines_count). - Naming follows style:
camelCaseidentifiers,SNAKE_CASEconstants,Input/Array/Tablesuffixes where they help. - Inputs grouped and tooltipped. Ranges enforced via
minval/maxval. - Functions defined in global scope (Pine forbids nesting).
- No dead code, no commented-out blocks shipped. Pine Logs removed or gated behind a debug input.
- Script compiles cleanly mentally — walk it bar-by-bar in your head: does the first bar work? does the last bar work? does an intrabar tick break anything?
If any check fails, fix before reporting done.
Output format
When delivering a script, output:
- One sentence stating what it does and on what timeframe(s) it operates.
- The full script in a single ```pine fenced code block. Complete — no
// ... rest of codeplaceholders. - Repainting status in one line: "Non-repainting (confirmed-bar signals)" OR "Live mode — signals may flicker until bar close, document this to the user".
- Configuration notes if any input deserves explanation beyond its tooltip.
- Known limitations or honest caveats — if a strategy's backtest is suspiciously profitable, say why it might overfit; if an HTF indicator only matches between specific timeframe pairs, say so.
Do not pad with installation instructions ("Paste this into Pine Editor and click Add to Chart") — users who ask for Pine code know how to use it.
Confidence qualifiers
Pine sits between code and analysis — much of trading is uncertain even when the code is right. When asserting:
- "This is mathematically equivalent to TradingView's built-in RSI" — only after you have verified, otherwise say "this approximates RSI; minor divergence possible at the first
lengthbars". - "This strategy is profitable" — never assert. Backtest results live in the Strategy Tester, not in your claims. Use "the backtest on this sample reports X, with Y caveats".
- "This will not repaint" — only when every signal is gated correctly and no
varip,barstate.isnew, or naked HTF call exists. Otherwise: "live behavior may diverge from history; here is why".
A precise caveat is more valuable than an unqualified promise.
References
Heavy domain detail lives in references/ and is read on demand:
references/v6-language.md— type system, operators, control flow, UDTs, methods, arrays/maps/matrices, v5→v6 migration deltas.references/built-in-functions.md—ta.*(indicators),math.*,request.*,str.*,array.*,chart.point, common gotchas per family.references/visualization.md— every drawing primitive with idiomatic use, performance notes, polylines, gradients, tables-as-dashboards pattern.references/strategy.md— entry/exit/exit-bracket idioms, pyramiding, position sizing by risk, realistic backtest tuning, common backtest-overfitting patterns.references/repainting.md— every repaint cause with code examples and the fix, the PineCoders safe-security pattern,request.security_lower_tfdetails.references/multi-timeframe.md— HTF/LTF patterns, tuple bundling, timeframe-string parsing, intrabar iteration.references/debugging.md— Pine Logs, Pine Profiler, common compiler errors and what they actually mean.
For routine indicators you may not need to consult these. For anything multi-timeframe, multi-symbol, or with a strategy backtest, read the relevant reference before writing.
Bundled starting templates live in ${CLAUDE_SKILL_DIR}/assets/ — indicator-template.pine, strategy-template.pine, library-template.pine. Read and adapt rather than writing from scratch when the user's request maps cleanly onto one.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

