Indicator
Indicator is the SSOT wrapper around a Pine/JS script. It owns the source code, the transpiled function, the parsed input schema, and the parsed declaration-prop schema. You typically pass an Indicator to pine.run() / pine.stream() instead of a raw string — that gives you:
- Lazy, cached transpile (re-running the same Indicator across multiple
pine.run()calls compiles the script once) - A title-keyed
.inputview to read or override input defaults - A name-keyed
.propview to read or overrideindicator()/strategy()declaration arguments - Schema introspection (
getInputsMeta(),getPropsMeta()) for UI builders
Every code snippet on this page is exercised by the verification harness at PineTS/.scratchpad/indicator-doc-examples.cjs (kept in lock-step with the docs during authoring). If something doesn’t behave as documented, the harness fails.
Table of Contents
- Quickstart
- Constructor
- Indicator.from()
- Visible-range detection
- Inputs —
.inputandgetInputsMeta() - Declaration props —
.propandgetPropsMeta() - Reusing the same Indicator across runs
- JS-function source
- Backward compatibility — constructor
inputsmap
Quickstart
The simplest path — wrap a Pine string in new Indicator(...), pass it to pine.run():
const { PineTS, Indicator } = require('pinets');
const code = `
//@version=6
indicator("SMA Demo")
len = input.int(14, "Length")
plot(ta.sma(close, len))
`;
const ind = new Indicator(code);
const pine = new PineTS(/* market data or provider */);
const ctx = await pine.run(ind);
Raw strings and raw functions also work — they get wrapped in a throwaway Indicator internally:
await pine.run(code); // raw Pine string
await pine.run(($) => {
/* ... */
}); // raw JS function
You’d choose the explicit Indicator form when you want to:
- Override input or prop values before the run.
- Reuse the same compiled script across multiple
pine.run()calls. - Inspect the script’s schema from the host UI.
Constructor
new Indicator(source: string | Function, inputs?: Record<string, unknown>)
| Argument | Type | Required | Notes |
|---|---|---|---|
source | string (Pine code) or Function (JS callback) | Yes | The script body. |
inputs | Record<string, unknown> | No | Legacy — title-keyed override map. Kept for back-compat; prefer the .input API. |
Both forms accepted:
// Pine string source
const a = new Indicator(`
//@version=6
indicator("X")
plot(close)
`);
// JS function source
const b = new Indicator(($) => {
const { indicator, plot } = $.pine;
indicator('JS X');
plot($.data.close);
});
Indicator.from()
A static normalizer used by pine.run() and friends. Accepts any of the three input shapes and returns an Indicator:
Indicator.from(code); // wraps the raw Pine string
Indicator.from(($) => {
/* ... */
}); // wraps the raw function
Indicator.from(existingInd) === existingInd; // pass-through
Use it when you write code that needs to operate on Indicators regardless of how the user provided the source.
Visible-range detection
Statically tags whether the script references any host-bound built-in (chart.left_visible_bar_time, chart.right_visible_bar_time, …). Used by pine.update() to skip re-runs for non-viewport scripts.
const vr = new Indicator(`
//@version=6
indicator("VR", overlay=true)
left = chart.left_visible_bar_time
plot(close)
`);
const noVR = new Indicator(`//@version=6\nindicator("Plain")\nplot(close)`);
vr.usesVisibleRange(); // → true
noVR.usesVisibleRange(); // → false
See Initialization and Usage → Host Environment (Visible Range) for the full visible-range flow.
Inputs — .input and getInputsMeta()
Reading and writing values
.input is a Proxy keyed primarily by varId — the variable each input is assigned to (length = input.int(…) → "length"). Keying by varId is the recommended way to read and override inputs: it works even when titles are empty or duplicated. The input’s title also resolves as a secondary alias for the common case, and every input also answers to its declaration id (in_0, in_1, … — see Where inputs can be declared). The container is frozen — you mutate values per key, but you can’t replace the whole object.
const code = `
//@version=6
indicator("Demo")
length = input.int(14, "Length", minval=2, maxval=200)
src = input.source(close, "Source")
maType = input.string("EMA", "MA Type", options=["EMA", "SMA", "WMA"])
plot(close)
`;
const ind = new Indicator(code);
// Recommended: key by varId (the variable name).
ind.input['length']; // → 14 (default from source)
ind.input['src']; // → "close"
ind.input['maType']; // → "EMA"
ind.input['length'] = 50; // ✓ valid override
ind.input['length']; // → 50
// Title still works as a fallback alias (same underlying slot).
ind.input['Length']; // → 50
ind.input['MA Type'] = 'SMA';
varId takes priority. Resolution tries the varId first, then falls back to title. If a varId and some other input’s title happen to collide, the varId wins.
Object.keys(ind.input)enumerates varIds; titles resolve but aren’t listed.
When you pass the Indicator to pine.run(), the override flows into the runtime:
const code = `
//@version=6
indicator("E2E")
len = input.int(14, "Length")
plot(ta.sma(close, len))
`;
const ind = new Indicator(code);
ind.input['len'] = 5; // by varId (recommended)
const ctx = await pine.run(ind);
// ctx.inputs.len === 5 (overrides are forwarded under the varId)
// the SMA in the script was computed with length=5
Empty or duplicated titles
Because varId is the primary key, inputs that are awkward to address by title still have a stable handle:
const code = `
//@version=6
indicator("Dup")
fast = input.int(10, "Length")
slow = input.int(20, "Length") // same title as 'fast'
hidden = input.int(5, "") // empty title
plot(close)
`;
const ind = new Indicator(code);
ind.input['slow'] = 50; // ✓ targets the second input precisely
ind.input['hidden'] = 7; // ✓ overridable despite the empty title
ind.input['Length']; // → 10 (a shared title aliases the FIRST input, 'fast')
ind.getInputsMeta().length; // → 3 (duplicates and empty-titled inputs all appear)
Where inputs can be declared
As on TradingView, an input is declared wherever its call appears — at global scope, inside if / switch / loop bodies, inside function bodies, or directly as a call argument. Inputs are numbered in_0, in_1, … in source order (TradingView’s input ids) and labelled like the settings dialog, exposed as name in getInputsMeta():
- the
titleargument when given (an explicit""stays empty); - else the variable the input is assigned to, even deep inside the expression (
k := input(14),a = ta.sma(close, input(14))→"a"); - else the enclosing function’s name (
f() => input(14)→"f"); - else
"untitled"(plot(ta.sma(close, input(14)))).
const code = `
//@version=6
indicator("Local inputs")
var k = 0
if barstate.isfirst
k := input(14)
plot(ta.sma(close, k) - ta.sma(close, input(28)))
`;
const ind = new Indicator(code);
ind.getInputsMeta().map((m) => [m.id, m.name, m.defval]);
// → [['in_0', 'k', 14], ['in_1', 'untitled', 28]]
Object.keys(ind.input); // → ['k', 'in_1']
ind.input['in_1'] = 50; // an untitled input is addressed by its id
An input without an assigned variable or title — or whose variable/title is already taken by an earlier input (e.g. k = cond ? input(1) : input(2)) — is keyed by its id. Overrides are forwarded to the runtime under the id, so each input is targeted precisely.
A local input is declared even if its block never runs; its value only reaches the script when the block executes (k stays 0 above if the if never fires). A function called several times still declares a single input. A branch whose condition is a compile-time constant false — if false, a constant false variable, false and …, a constant ternary or switch subject — is dropped together with its inputs.
Arguments are evaluated at compile time. Defaults and other arguments may be calculations over literals and constants — arithmetic, comparisons, ternaries, string concatenation, math.*, int()/float(), str.upper/str.lower/str.length, color.new/color.rgb, timestamp("…"):
k = input(2 + 2) // defval 4
t = input.time(timestamp("2024-01-01 00:00 +0000") + 3600000) // defval 1704070800000
n = input.int(int(10 / 3), "N", minval = 1 + 1) // defval 3, minval 2
w = input(7 / 2) // int input, defval 3
An int-typed calculation is truncated once evaluated, as on TradingView: input(7 / 2) is 3 and input(1 / 2 * 4) is 2, while input(7 / 2.0) is 3.5. The script sees the same value at runtime.
Because they are evaluated at compile time, input arguments can only reference constants. These are compile errors, as on TradingView:
for i = 0 to 9
k = input(i) // Undeclared identifier "i" (loop counter)
f(x) => input(x) // Undeclared identifier "x" (function parameter)
a = input(5)
b = input(a) // Arguments of input function must be of constant type, or "source" builtin variables.
k = input.int(bar_index > 40000 ? 14 : 200)
// Cannot call "input.int" with argument "defval"="call "operator ?:" (series int)". An argument of "series int" type was used but a "const int" is expected.
s = input.source(close * 2)
// Invalid value for the "defval" parameter of the "input.source" function. Possible values: [open, high, low, close, hl2, hlc3, ohlc4, hlcc4].
g(a = input(14)) => a // The default value cannot be a function, variable or calculation.
h(a = 2 + 2) => a // The default value assigned to a parameter must be either a literal value (e.g., "5") or a built-in variable (e.g., "close").
Runtime values — series such as bar_index, close, time, barstate.*, ta.* results, and simple values such as timeframe.multiplier, syminfo.mintick or timestamp(2024, 1, 1) — are rejected in any input argument (defval, title, minval, …), directly or through a variable. A default that must depend on the chart is written with local inputs instead:
var k = 0
if bar_index > 40000
k := input(14)
else
k := input(200) // two inputs, both labelled "k"
The other argument rules are enforced too, with TradingView’s messages and positions:
- each argument must be a constant of the parameter’s type —
input.int(2.5),input.bool(1),input.color("red"),input.int(10, step = 0.5)andoptions = [1, "x"]on a numeric input are rejected, as is a typed input defaulting tona; - the default must lie within
minval/maxvaland be one ofoptions(input's defval should be in options, but 4 is not in [1, 2, 3]); input.enumfields must all belong to one enum;- named arguments must exist on the function (
input.text_areahas noinline,input.boolhas nooptions); activeaccepts an input bool (active = showInput).
Validation
Writes are validated against the input’s schema. Failures throw immediately with a tailored message:
ind.input['nope'] = 1; // ✗ Error: [Indicator.input] unknown input key "nope". Known: length, src, maType, Length, in_0, Source, in_1, MA Type, in_2
ind.input['length'] = 1; // ✗ Error: [Indicator.input] "length" value 1 is below minval 2
ind.input['maType'] = 'HMA';// ✗ Error: [Indicator.input] "maType" value "HMA" is not one of: "EMA", "SMA", "WMA"
ind.input = { foo: 1 }; // ✗ Error: [Indicator.input] .input cannot be replaced — mutate individual keys (…)
Schema introspection — getInputsMeta()
Returns the parsed schema as an array of IPineInput. Useful for UI builders that need to render input controls.
const meta = ind.getInputsMeta();
// [
// { id: 'in_0', name: 'Length', type: 'int', varId: 'length', title: 'Length', defval: 14, minval: 2, maxval: 200 },
// { id: 'in_1', name: 'Source', type: 'source', varId: 'src', title: 'Source', defval: 'close' },
// { id: 'in_2', name: 'MA Type', type: 'string', varId: 'maType', title: 'MA Type', defval: 'EMA', options: ['EMA','SMA','WMA'] },
// { id: 'in_3', name: 'Line', type: 'color', varId: 'lineCol',title: 'Line', defval: '#F23645FF' },
// ]
Each entry carries everything the scanner harvested — id, name, type, defval, varId, title, tooltip, group, inline, display, options, minval, maxval, step, active, confirm. id is unique and always accepted by .input; name is the settings-dialog label; varId (the assigned variable name) is present whenever the input is assigned to a variable and is the preferred .input override key. title is only set when the script passes one. display is always set: when the script does not pass it, it is 'none' for bool, color, time and text_area inputs and 'all' otherwise, as on TradingView. options is also filled in when the dropdown is implicit: an input.enum without options lists every field title of its enum, and a source input lists the eight selectable sources (open, high, low, close, hl2, hlc3, hlcc4, ohlc4 — a volume override is still accepted). The exported types IPineInput, PineInputType, and PineInputDisplay describe the shape.
Color defaults are normalized. color-typed inputs report defval as a canonical 8-digit RGBA hex string #RRGGBBAA (uppercase; FF = fully opaque), regardless of how the source wrote it:
#RRGGBB/#RRGGBBAAhex literals- a named constant —
color.red→#F23645FF - the constructor calls
color.new(col, transp)andcolor.rgb(r, g, b, transp?), statically evaluated (Pine’stranspis 0–100 transparency, socolor.new(#26a69a, 50)→#26A69A80,color.rgb(6, 162, 47)→#06A22FFF)
Reading the same key back via .input['Line'] returns the identical normalized string. This gives UI color pickers a single, parseable format to bind to. (The normalization is presentational — it doesn’t affect what the running script computes when the input isn’t overridden.) A color default that can’t be resolved statically — e.g. one built from a runtime variable or color.from_gradient(...) — is reported as undefined.
Constant arguments are resolved. When an input argument references a constant or simple variable (global, or local to the input’s block) or a calculation over them, the scanner reports the evaluated value — so defval, group, inline, tooltip, options, etc. report the value, not the variable name. This composes with the enum, color, and chained-const logic.
const string GRP = "Indicator Settings"
const int DEF = 14
len = input.int(DEF, "Length", group = GRP)
// → { id: 'in_0', name: 'Length', type: 'int', varId: 'len', title: 'Length', defval: 14, group: 'Indicator Settings' }
A runtime value (e.g. a computed series like ta.sma(close, 5)) is not a valid input argument — see the compile errors in Where inputs can be declared.
Declaration props — .prop and getPropsMeta()
The script’s indicator(...) or strategy(...) declaration takes its own set of arguments — overlay, precision, initial_capital, pyramiding, currency, etc. The .prop view exposes those for read/override, mirroring the .input API but keyed by arg name (each arg name is unique, so there’s no varId/title distinction here).
Reading source-code defaults
When you construct an Indicator, the source’s declaration call is parsed and its arg values seed .prop. Spec defaults from the Pine docs apply to args the source didn’t set.
const code = `
//@version=6
strategy("Demo Strat", overlay=true, initial_capital=50000, pyramiding=3,
currency=currency.EUR, default_qty_type=strategy.percent_of_equity,
commission_type=strategy.commission.cash_per_order)
plot(close)
`;
const ind = new Indicator(code);
ind.prop['overlay']; // → true (from source)
ind.prop['initial_capital']; // → 50000 (from source)
ind.prop['pyramiding']; // → 3 (from source)
ind.prop['currency']; // → "EUR" (enum resolved to runtime string)
ind.prop['default_qty_type']; // → "percent_of_equity"
ind.prop['commission_type']; // → "cash_per_order" (nested namespace resolved)
ind.prop['slippage']; // → 0 (spec default — source didn't set it)
Namespaced enum constants (currency.EUR, strategy.percent_of_equity, strategy.commission.cash_per_order) are resolved to the bare runtime string — matching what TradingView’s str.tostring() would print.
Writing overrides
ind.prop['initial_capital'] = 75000;
ind.prop['pyramiding'] = 4;
const ctx = await pine.run(ind);
// ctx.strategy.config.initial_capital === 75000 (override wins)
// ctx.strategy.config.pyramiding === 4
// ctx.strategy.config.currency === "EUR" (source-only value still applies)
Indicator scripts work the same way against ctx.indicator:
const code = `//@version=6\nindicator("E2E", overlay=false, precision=2)\nplot(close)`;
const ind = new Indicator(code);
ind.prop['overlay'] = true;
ind.prop['precision'] = 6;
const ctx = await pine.run(ind);
// ctx.indicator.overlay === true
// ctx.indicator.precision === 6
Validation
Same strictness as .input:
ind.prop['nope'] = 1; // ✗ Error: [Indicator.prop] unknown key "nope". Known: …
ind.prop['currency'] = 'XYZ'; // ✗ Error: [Indicator.prop] "currency" value "XYZ" is not one of: …
ind.prop['title'] = 'X'; // ✗ Error: [Indicator.prop] unknown key "title". Known: … (title is mutable: false)
ind.prop = { foo: 1 }; // ✗ Error: [Indicator] .prop cannot be replaced — (…)
Schema introspection — getPropsMeta()
Returns an IPineProp[] filtered to props applicable to the detected script type (indicator vs strategy). Includes title and shorttitle with mutable: false so UI builders can render them even though they’re not writable.
const ind = new Indicator(`//@version=6\nstrategy("X")\nplot(close)`);
const meta = ind.getPropsMeta();
// Each entry: { name, type, defval, options?, minval?, maxval?, mutable, appliesTo, version? }
const currency = meta.find((m) => m.name === 'currency');
// {
// name: "currency",
// type: "enum",
// defval: "USD",
// options: ["AED", "ARS", "AUD", ..., "USDT", ..., "ZAR"],
// mutable: true,
// appliesTo: "strategy",
// }
getDeclarationType() reports which schema the Indicator is using:
new Indicator(`//@version=6\nstrategy("X")\nplot(close)`).getDeclarationType(); // → 'strategy'
new Indicator(`//@version=6\nindicator("X")\nplot(close)`).getDeclarationType(); // → 'indicator'
new Indicator(`//@version=6\nplot(close)`).getDeclarationType(); // → null (falls back to indicator schema)
Exported schemas for UI builders
The complete prop schemas are also exported as top-level constants. UI code that wants the full list (independent of any Indicator instance) can import them directly:
const { INDICATOR_PROPS, STRATEGY_PROPS, propsForDeclaration } = require('pinets');
INDICATOR_PROPS; // IPineProp[] — 17 entries (incl. title/shorttitle)
STRATEGY_PROPS; // IPineProp[] — 33 entries
propsForDeclaration('strategy'); // → STRATEGY_PROPS
propsForDeclaration(null); // → INDICATOR_PROPS (fallback)
Every enum-typed entry in propsSchema.ts has a one-line comment enumerating accepted values and pointing to the corresponding exported Pine enum (enum format, enum scale, enum currency, …) in src/namespaces/Types.ts so you can import the source-of-truth values rather than hard-coding strings in UI code.
Reusing the same Indicator across runs
The transpiled function is cached on the Indicator instance, so passing the same Indicator to multiple pine.run() calls compiles the script once. Live overrides on .input / .prop between runs are picked up automatically.
const code = `
//@version=6
indicator("Reusable")
len = input.int(14, "Length")
plot(ta.sma(close, len))
`;
const ind = new Indicator(code);
ind.input['Length'] = 7;
const ctxA = await new PineTS(dataA).run(ind); // first run: transpiles + scans
const ctxB = await new PineTS(dataB).run(ind); // second run: reuses prepared artifact
// Both runs computed ta.sma(close, 7)
This matters for hosts that run the same script across many datasets (e.g. a chart with multiple symbols, or a backtest sweep over parameter ranges).
JS-function source
When the source is a JS callback, the AST scan for inputs returns [] (Pine inputs are written in Pine syntax; the JS form expresses them differently). The .input view is empty and writes throw “unknown input key”. The legacy inputs constructor map still works for runtime overrides via the JS path.
const fn = ($) => {
const { indicator, plot } = $.pine;
indicator('JS', { overlay: true, precision: 4 });
plot($.data.close);
};
const ind = new Indicator(fn);
ind.getInputsMeta(); // → []
ind.getDeclarationType(); // → 'indicator' (acorn parse of the function body)
ind.prop['overlay']; // → true (from the JS call)
ind.prop['precision']; // → 4 (from the JS call)
Declaration detection works for the JS path because the function body is parsed with acorn; the scanner walks for CallExpression(indicator | strategy, ...) regardless of whether it’s destructured from $.pine first.
Backward compatibility — constructor inputs map
The legacy second constructor argument (a title-keyed override map) is still honored. It’s most useful when you build an Indicator and want to ship a default override set up front, without touching .input later.
const code = `
//@version=6
indicator("BC")
len = input.int(14, "Length")
plot(ta.sma(close, len))
`;
const ind = new Indicator(code, { Length: 21 }); // keyed by title
const ctx = await pine.run(ind);
// ctx.inputs.Length === 21 (the runtime resolves it via the title fallback)
Precedence when both are set: a .input write (varId-keyed) wins over the constructor map (title-keyed), because the runtime resolves varId before title. Both entries coexist in ctx.inputs — they’re distinct keys — and the varId one is chosen:
const ind = new Indicator(code, { Length: 21 });
ind.input['len'] = 3; // varId override
const ctx = await pine.run(ind);
// ctx.inputs === { Length: 21, len: 3 }
// resolveInput checks varId 'len' first → the script computes ta.sma(close, 3)
New code should prefer the varId-keyed .input API. The constructor inputs argument (and title-keyed .input access) stay for callers that have existing code relying on them.
Related
- Initialization and Usage — full surface of
pine.run(),pine.stream(),pine.update(). - Strategy Namespace —
strategy.*API surface (the Pine side of strategy props). - API Coverage — implementation status of every Pine
input.*and namespace.