Skip to content

About

Executes Pine Script over supplied OHLCV candles and returns the plotted series. Wraps PineTS (AGPL-3.0).

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

pine-runner

A tiny standalone HTTP service that executes Pine Script over OHLCV candles you supply and returns the plotted series as JSON, ready to draw on a chart.

It exists so the GAIIN platform can run generated Pine (not just show the code) without embedding a Pine runtime in the proprietary app. It wraps PineTS by LuxAlgo.

Why it is a separate service (licensing)

PineTS is AGPL-3.0. This service links PineTS, so this service is also AGPL-3.0 and its source is published here. Everything else in the platform talks to it only over the HTTP boundary below (JSON in, JSON out), which is an arm's-length interface, so no other codebase is affected by the copyleft. Per AGPL §13 the source is advertised to network users via the X-Source-Repository response header and GET /source.

API

GET /healthz

Liveness. Open (no auth). { status: "ok", service, source }.

GET /source

The AGPL source pointer. { service, source, license }.

POST /run

Gated by X-Internal-Auth when SAMWISE_INTERNAL_SECRET is set.

Request:

{
  "source": "//@version=6\nindicator(\"EMA\", overlay=true)\nplot(ta.ema(close, 21))",
  "candles": [{ "time": 1704067200, "open": 100, "high": 105, "low": 99, "close": 103, "volume": 1000 }],
  "market": { "symbol": "NSE:RELIANCE", "timeframe": "60", "timezone": "Asia/Kolkata",
              "mintick": 0.05, "session": "0915-1530", "currency": "INR" }
}

time is UNIX SECONDS (lightweight-charts style).

market is optional and describes the instrument the script thinks it is on. It is worth sending. Pine reads it as syminfo.*, so without it low - syminfo.mintick, ordinary generated Pine for a stop a tick below a low, throws and takes the whole run down, and hour, dayofweek and every session built-in are computed in the wrong timezone. Defaults describe an NSE equity; timeframe is inferred from the spacing of the candles when absent.

Response:

{
  "ok": true,
  "overlay": true,
  "plots": [
    { "name": "EMA", "color": "#2196F3", "data": [{ "time": 1704067200, "value": 101.2 }] }
  ],
  "warnings": []
}
  • overlay — true => draw on the price pane; false => a separate oscillator pane.
  • On failure: { "ok": false, "error": "...", "warnings": [...] } (still HTTP 200).

The strategy block

A strategy(...) script is backtested by PineTS's broker emulator, and the run also returns what it did:

{
  "strategy": {
    "title": "RSI Divergence + BoS",
    "initial_capital": 100000,
    "orders_filled": 14,
    "trades_closed": 14, "trades_open": 0, "wins": 6, "losses": 8,
    "net_profit": 713.19, "net_profit_pct": 0.71,
    "gross_profit": 1443.14, "gross_loss": 729.95,
    "max_drawdown": 491.0, "max_drawdown_pct": 0.49,
    "sharpe": -0.7, "buy_and_hold_pct": 7.25, "equity": 100713.19,
    "trades": [{ "entry_time": 1738053900, "entry_price": 1239.3,
                 "exit_time": 1738133100, "exit_price": 1242.65,
                 "size": 8.07, "profit": 21.02, "status": "closed" }],
    "trades_truncated": false
  }
}

Read orders_filled first. PineTS rejects an order it cannot fund without a warning, so a script can run cleanly, plot correctly and take no trades at all, and then every metric above is zero and reads like a flat but valid backtest. The commonest cause is the commonest idiom: default_qty_type=strategy.percent_of_equity, default_qty_value=100 cannot be funded under the runtime's default full-collateral margin. Measured on identical scripts: 0 trades at 100%, 3 at 10%, 3 at 100% with margin_long=0, margin_short=0. Zero orders_filled adds a warning; trades is capped at the last 500, which trades_truncated says and the counts never are.

What the runtime does not do

process_orders_on_close is ignored. Pine's way of asking for an entry at the close of the signal candle is parsed and never acted on: the identifier appears once in PineTS's bundle, in the table that records an argument's name, type and default. Measured on a signal hard-coded at bar_index == 10 over twenty candles, the fill is bar 11's open with the flag and without it, identically.

So every market entry here fills at the next bar's open. TradingView honours the flag, which means a script declaring it is correct on a chart and these numbers are about a different entry price. /run says so in warnings whenever a strategy script declares it, rather than leaving each caller to find out.

It matters most where bars gap. On contiguous intraday candles the next bar's open is the previous close, so the two fills coincide and the difference hides; on daily bars it is the size of the gap, on every trade.

Run locally

bun install
bun run src/service/index.ts   # listens on :8085

Notes / hardening

  • Execution has a per-request timeout (default 12s) and a candle cap. Because PineTS transpiles Pine to JS and evaluates it, run this container unprivileged, with tight CPU/memory limits and no outbound network (candles are always supplied in the request; the built-in data providers are unused).
  • Only plot() line series are returned today. Drawing objects (labels, lines, boxes, tables, shapes) are ignored.
  • One set of candles per run, so request.security for a second timeframe gets the same bars back. Do not send a script that calls it.
  • bun test covers the contract above, including the two runtime behaviours that are easy to mistake for our own bugs: the silent rejection of a full-equity order, and syminfo being absent unless a provider supplies it.

About

Executes Pine Script over supplied OHLCV candles and returns the plotted series. Wraps PineTS (AGPL-3.0).

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages