Skip to content

Piner

CI npm License: AGPL v3

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 pinerun CLI or the pinetop terminal 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.

Fractal Chart, powered by Piner

fractalchart.com — Pine authored and rendered on a live chart.

The pinetop BACKTEST page: strategy list and backtest flags on the left, price chart with entry/exit markers plus equity and drawdown curves in the middle, tearsheet (returns, risk, trades, top drawdowns) on the right, monthly returns and trades below

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, and strategy coverage. See docs/compiler-design.md.

Documentation

All docs live in docs/:

Install

npm install @heyphat/piner
# or: bun add @heyphat/piner / pnpm add @heyphat/piner / yarn add @heyphat/piner

Ships ESM + CJS builds and TypeScript types. Works in the browser and in Node ≥ 18.

Develop

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.

What works today

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.

Library import / export

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.

Loading libraries from the filesystem (Node)

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.

Async / lazy resolution (HTTP, CDN, large trees)

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).

Contributing

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.

License

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.

About

Clean-room Pine Script v6 engine for the browser and Node

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

21 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages