A clean-room Pine Script v6 engine in TypeScript — compile and run Pine indicators and strategies anywhere, browser-first. Designed from the public TradingView v6 docs only.
Piner is published here as a standalone open-source library, and it powers two primary use cases:
- fractalchart.com — the charting front end: author Pine in the browser and see indicators, strategies, and drawings render on a live chart.
- pinestack — the headless side.
Where TradingView keeps Pine inside one chart, one symbol, and one timeframe,
pinestack turns the engine into a programmable, parallel execution surface for
scanning universes, backtesting, parameter sweeps, walk-forward validation,
portfolio aggregation, and forward execution — driven by the
pinerunCLI or thepinetopterminal UI. If you want to run Pine outside a chart, start there rather than wiring the engine up yourself.
Piner itself stays a pure, browser-safe library; both layers build on top of it.
fractalchart.com — Pine authored and rendered on a live chart.
pinestack — the same engine headless: pinetop's backtest page over a pinerun run.
compile(src)lexes → parses → analyzes → emits JS and an interpreter oracle; the two backends are cross-checked for byte-for-byte identical output. The full v6 language runs end-to-end, plus broad built-in, input, drawing,request.security, andstrategycoverage. Seedocs/compiler-design.md.
All docs live in docs/:
- Architecture — the engine design.
- Pine semantics — the v6 spec the engine implements.
npm install @heyphat/piner
# or: bun add @heyphat/piner / pnpm add @heyphat/piner / yarn add @heyphat/pinerShips ESM + CJS builds and TypeScript types. Works in the browser and in Node ≥ 18.
Requires Bun ≥ 1.2.
bun install
bun test # full suite incl. the two-backend (js vs interp) cross-check
bun run typecheck # tsc --noEmit
bun run build # ESM + CJS (bun) + d.ts (tsc) into dist/See CONTRIBUTING.md for the clean-room policy and the two-backend invariant before opening a PR.
Compile Pine v6 source and run it against a data feed:
import { compile, Engine, ArrayFeed } from '@heyphat/piner';
const compiled = compile(`//@version=6
indicator("SMA cross", overlay=true)
fast = ta.sma(close, 5)
slow = ta.sma(close, 20)
plot(fast, title="fast")
plot(slow, title="slow")
plotshape(ta.crossover(fast, slow), title="up")
`);
const engine = new Engine(compiled, new ArrayFeed(bars)); // backend: 'js' (default) | 'interp'
await engine.run({ symbol: 'BTCUSD', timeframe: '60' });
engine.outputs.plots.get(0); // → { id, title, data: number[] }
// realtime: each tick re-runs the open bar (repaint), commit on close
engine.tick(liveBar, /* isClose */ false);compiled.interpret is the AST-interpreter backend over the same runtime — used
as the correctness oracle (cross-checked against the generated JS). The runtime
can also execute a hand-written ScriptFn directly (see test/runtime-core.test.ts).
Need to run this across many symbols and timeframes, from a CLI, with data providers and caching already wired up? Don't build that here — use pinestack, which layers an OHLCV data ring (
pinery), a job model with parallel workers and a determinism cache (pinerun), a terminal UI (pinetop), and a forward-execution runner (pinelive) on top of this engine. It installs as self-contained binaries — no Node or Bun needed.
Pine import Publisher/Lib/Version [as alias] resolves from an in-memory
registry you pass to compile — no network, no filesystem, fully deterministic:
import { compile, Engine, ArrayFeed } from '@heyphat/piner';
const compiled = compile(
`//@version=6
indicator("uses a library")
import alice/mathlib/1 as ml
plot(ml.zscore(close, 20), title="z")
`,
{
libraries: [
{
key: 'alice/mathlib/1',
source: `//@version=6
library("MathLib")
export zscore(float src, int len) => (src - ta.sma(src, len)) / ta.stdev(src, len)
`,
},
],
},
);Registry keys are "Publisher/Lib/Version" or { user, lib, version }. Exported
functions, UDTs (type), enums, and methods resolve through the alias. Versions
match exactly; transitive imports resolve (depth cap 32) with cycles rejected;
export-constraint violations (e.g. plot inside an export) are compile errors.
Imported symbols are inline-merged, so the two backends stay byte-for-byte identical.
An alias equal to a builtin namespace (e.g. ta) is rejected — piner does not
implement TradingView's builtin-namespace extension.
The core compile() is pure and browser-safe (no I/O). For Node/CLI/server use, the
optional @heyphat/piner/node entry point builds a registry from .pine files on disk —
so you don't assemble it by hand. It's never bundled into the browser build.
import { loadLibraryDir, compile } from '@heyphat/piner/node';
// Scans <root>/<publisher>/<lib>/<version>.pine (mirrors the import identity):
// pine-libs/PineCoders/AllTimeHighLow/1.pine → "PineCoders/AllTimeHighLow/1"
const libraries = loadLibraryDir('./pine-libs');
const compiled = compile(source, { libraries });loadLibraryManifest('libs/manifest.json') is also available for non-conventional layouts
(a JSON map of "Publisher/Lib/Version" → source-file path). The identity comes from the
path, not the file's library("…") title; multiple <version>.pine files coexist.
When sources live behind async I/O (an HTTP CDN, a database) or in a large on-disk tree you
don't want to scan eagerly, compileAsync(src, { resolveLibrary }) walks the import graph and
fetches ONLY the libraries actually imported (transitively), then calls the pure compile():
import { compileAsync } from '@heyphat/piner';
const compiled = await compileAsync(source, {
resolveLibrary: async ({ canonical }) => {
const res = await fetch(`https://cdn.example.com/pine/${canonical}.pine`);
return res.ok ? await res.text() : undefined; // undefined ⇒ missing-library error
},
});The provider may be sync or async. Node's fsLibrarySource('./pine-libs') (from
@heyphat/piner/node) is a ready-made lazy provider that reads only the single
<publisher>/<lib>/<version>.pine files that are imported. compile() itself stays
synchronous and pure — compileAsync just gathers sources first (see
resolveLibraryClosure for the standalone gatherer).
Contributions are welcome — bug reports, fixes, new built-in coverage, and docs. Please read CONTRIBUTING.md (especially the clean-room policy) and the Code of Conduct first.
GNU AGPL-3.0 © Phat Huynh.
Piner is a clean-room reimplementation from public TradingView documentation. No third-party Pine engine code is used or copied.

