agentleFS
maxenkomaxenko/claude-skills/pine-script-pro/SKILL.md

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.

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
  1. Distinctive:

    When you cannot determine intent from the user's request, ask one focused question before coding.

    line 17
  2. Very specific:

    Default to indicator() unless the user explicitly asks to simulate trades or measure performance.

    line 38
  3. Distinctive:

    Every script you write follows this order, with section banners:

    line 42
  4. Very specific:

    For request.security() of higher timeframes: use the safe wrapper below, never inline request.security(syminfo.tickerid, "D", close).

    line 100
  5. Very specific:

    For lower-timeframe data: use request.security_lower_tf(), not request.security() with a smaller timeframe — only the former is safe across …

    line 101
  6. Distinctive:

    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 …

    line 156
  7. Distinctive:

    All three are bar-anchored (travel with the bar through zoom/scroll).

    line 226
  8. Very specific:

    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 …

    line 228
  9. Very specific:

    Never create a new line/label/box unconditionally on every bar.

    line 251
  10. Very specific:

    Use strategy.exit() with both stop and limit for bracket orders.

    line 291
  11. Very specific:

    Always use the f_secure wrapper above for HTF data — never inline a raw request.security.

    line 301
  12. Very specific:

    Use var for state that must accumulate across bars (counters, persistent line/label references, last-signal price).

    line 316
  13. Very specific:

    Add tooltip = "..." for any input whose effect is not obvious.

    line 323
  14. Very specific:

    Use input.color() for any colour the user might want to theme.

    line 324
  15. Very specific:

    Use inline = "row1" to put related inputs on one row (e.g.

    line 325
  16. Very specific:

    Default max_bars_back = 500.

    line 342
  17. Very specific:

    Default 50 lines/labels/boxes.

    line 343
  18. Very specific:

    Drawing-object limits declared: if the script uses lines/labels/boxes (max_lines_count, max_labels_count, max_boxes_count, …

    line 386
  19. Distinctive:

    If any check fails, fix before reporting done.

    line 393
  20. Distinctive:

    When delivering a script, output:

    line 397
  21. Distinctive:

    Configuration notes: if any input deserves explanation beyond its tooltip.

    line 402
  22. Distinctive:

    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 …

    line 405

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

  1. Classify the artifact. Indicator, strategy, or library? See the decision matrix below.
  2. Identify the analytical core. What is being computed? On what timeframe(s)? What inputs control it?
  3. 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.
  4. Design the visualization. Plot, plotshape, label, line, box, table, fill, polyline — each has a niche. See "Visualization decision matrix" below.
  5. Write the script following the structure in "Script structure" and the rules in "Repainting safety".
  6. Self-validate with the checklist at the end before reporting done.

Decision matrix: indicator vs strategy vs library

UseDeclarationWhen
indicator()Plotting, alerts, screening, custom UI on chartDefault for any "show me / alert me when" request. No trade simulation.
strategy()Backtested entry/exit logic with the TradingView Strategy TesterUser says backtest, equity curve, win rate, drawdown, position sizing.
library()Reusable exported functions for other scriptsUser 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

  1. Live-bar fluctuation — close on the current unconfirmed bar moves with every tick. A signal based on ta.crossover(close, ma) flickers on/off until the bar closes, then locks in. The plotted history shows only the locked-in version.
  2. request.security() future-leak — calling with lookahead = barmerge.lookahead_on and without offsetting the expression by [1] pulls the higher-timeframe bar's close before it was knowable.
  3. barstate.isnew — fires at bar open in realtime but at bar close on history. Different timing in the two regimes.
  4. varip and timenow — both carry realtime-only state that cannot be reproduced on history.

The non-negotiable rules

  • For any signal feeding an alertcondition(), alert(), or strategy.entry() trigger: gate it with barstate.isconfirmed or 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 inline request.security(syminfo.tickerid, "D", close).
  • For lower-timeframe data: use request.security_lower_tf(), not request.security() with a smaller timeframe — only the former is safe across history and realtime.
  • Avoid varip unless you genuinely need intrabar accumulation (e.g. tick-volume profiles). Document the repainting cost in a comment when you use it.
  • plotshape() and label.new() placed in the past (e.g. pivot detected n bars later) must use an offset and 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.

NeedUseWhy
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 seriesfill(plotA, plotB, color)Cheaper than polygons; chains naturally with two plots.
Horizontal static levelhline()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, zigzagline.new()From line budget. Use line.set_* to mutate, not delete-and-create.
Rectangle / order block / supply zonebox.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, fanspolyline.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:

PropertyWhat it meansPrimitives
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:

  1. Stay anchored to the bar through zoom/pan — they want bar-tethered. Default for almost everything that isn't a table.
  2. Grow visually when I zoom in — they want chart-coord-sized body. Need box or polyline.
  3. Stay the same pixel size at every zoom — they want pixel-sized body. Need plotshape or label.
  4. 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 when yloc.belowbar/abovebar is used, but in practice supplying y = na produces 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 AND yloc.belowbar/abovebar produces 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:

  1. Supply BOTH y = low/high AND yloc = yloc.belowbar/abovebar. Do not pass y = na.
  2. textcolor matches color. A contrasting textcolor would put a visible glyph inside the circle.
  3. style = label.style_circle for the round shape; label.style_square/label.style_diamond work the same way for other shapes.
  4. size = size.tiny keeps the marker subtle. size on label.new() accepts series string, so it can be input-driven (unlike plotshape's const string size).

Other causes of bar-tracking failure to rule out before changing primitives:

  • location.top / location.bottom on a plotshape — 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/box unconditionally on every bar. Either gate on a real event or mutate one persistent object via var 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:

  • camelCase for variables, functions, and inputs: maFast, lengthInput, roundedOhlc().
  • SNAKE_CASE for 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), not ma = ta.ema(close, 20). Distinguishes = declarations from := reassignments and surfaces type-mismatch errors early.
  • const for true constants, not var. var carries a per-bar copy cost; const is compile-time.
  • input.*() with group, tooltip, and inline parameters. 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) not x*(-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 signal

Reasoning: 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.isconfirmed if process_orders_on_close = false, or accept the one-bar lag if true.
  • Use strategy.exit() with both stop and limit for bracket orders. Use strategy.close() only for unconditional exits.
  • For pyramiding, set pyramiding = N on the declaration and rely on strategy.entry() (which honours pyramiding), not strategy.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_secure wrapper above for HTF data — never inline a raw request.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 to request.security() — it returns only one of the LTF bars per chart bar.
  • Set gaps = barmerge.gaps_off on HTF requests unless you specifically want na for unclosed bars.

See references/multi-timeframe.md for the bundled-request pattern, intrabar iteration, and timeframe-string validation.

Persistence: var vs varip vs regular

DeclarationPersists across bars?Persists across intrabar ticks?Repaints?
x = ...No (reset every bar's first iteration)N/ANo
var x = ...Yes (initialized once on bar 0, then mutable)No — resets on each tick of the realtime barNo
varip x = ...YesYes — survives every realtime tickYes — 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 is const 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 is series string, so you can str.format("Price = {0,number}", close) at runtime. Fires from inside a single Any alert() function call alert.

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 references someSeries[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:

  1. Declares //@version=6 at the top.
  2. Script type is intentional: indicator() for visualization/alerts, strategy() for backtested trades, library() for shared code.
  3. No accidental repainting: any signal feeding an alert or order is gated on barstate.isconfirmed OR uses [1]-offset data OR the script title says "(live mode — repaints)".
  4. HTF requests use the safe f_secure wrapper (or the inline equivalent: expression[1] + lookahead_on).
  5. Strategies set commission and slippage explicitly. No silently-zero defaults.
  6. Drawing-object limits declared if the script uses lines/labels/boxes (max_lines_count, max_labels_count, max_boxes_count, max_polylines_count).
  7. Naming follows style: camelCase identifiers, SNAKE_CASE constants, Input/Array/Table suffixes where they help.
  8. Inputs grouped and tooltipped. Ranges enforced via minval/maxval.
  9. Functions defined in global scope (Pine forbids nesting).
  10. No dead code, no commented-out blocks shipped. Pine Logs removed or gated behind a debug input.
  11. 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:

  1. One sentence stating what it does and on what timeframe(s) it operates.
  2. The full script in a single ```pine fenced code block. Complete — no // ... rest of code placeholders.
  3. Repainting status in one line: "Non-repainting (confirmed-bar signals)" OR "Live mode — signals may flicker until bar close, document this to the user".
  4. Configuration notes if any input deserves explanation beyond its tooltip.
  5. 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 length bars".
  • "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_tf details.
  • 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.