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.
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.
Liveness. Open (no auth). { status: "ok", service, source }.
The AGPL source pointer. { service, source, license }.
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).
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.
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.
bun install
bun run src/service/index.ts # listens on :8085- 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.securityfor a second timeframe gets the same bars back. Do not send a script that calls it. bun testcovers 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, andsyminfobeing absent unless a provider supplies it.