πŸ“œ @backtest-kit/pinets

Run TradingView Pine Script strategies in Node.js self hosted enviroment. Execute your existing Pine Script indicators and generate trading signals - pure technical analysis with 1:1 syntax compatibility.

screenshot

Ask DeepWiki npm TypeScript

Port your TradingView strategies to backtest-kit with zero rewrite. Powered by PineTS - an open-source Pine Script transpiler and runtime.

πŸ“š Backtest Kit Docs | 🌟 GitHub | πŸ“œ PineTS Docs

New to backtest-kit? The fastest way to get a real, production-ready setup is to clone the reference implementation β€” a fully working news-sentiment AI trading system with LLM forecasting, multi-timeframe data, and a documented February 2026 backtest. Start there instead of from scratch.

  • πŸ“œ Pine Script v5/v6: Native TradingView syntax with 1:1 compatibility
  • 🎯 60+ Indicators: SMA, EMA, RSI, MACD, Bollinger Bands, ATR, Stochastic, and more
  • πŸ”Œ Backtest Integration: Seamless getCandles integration with temporal context
  • πŸ“ File or Code: Load .pine files or pass code strings directly
  • πŸ—ΊοΈ Plot Extraction: Flexible mapping from Pine plot() outputs to structured data
  • ⚑ Cached Execution: Memoized file reads for repeated strategy runs
  • πŸ›‘οΈ Type Safe: Full TypeScript support with generics for extracted data

@backtest-kit/pinets executes TradingView Pine Script and extracts trading signals for backtest-kit:

Function Description
getSignal() Run Pine Script and get structured ISignalDto (position, TP/SL, estimated time)
run() Run Pine Script and return raw plot data
extract() Extract the latest bar values from plots with custom mapping
extractRows() Extract all bars as a timestamped row array with custom mapping
dumpPlotData() Dump plot data to markdown files for debugging
usePine() Register custom Pine constructor
setLogger() Configure custom logger
File.fromPath() Load Pine Script from .pine file
Code.fromString() Use inline Pine Script code
npm install @backtest-kit/pinets pinets backtest-kit

Create a Pine Script file (strategy.pine):

//@version=5
indicator("Signal Strategy 100 candles of 1H timeframe")

// Indicators - faster settings for 1H
rsi = ta.rsi(close, 10)
atr = ta.atr(10)
ema_fast = ta.ema(close, 7)
ema_slow = ta.ema(close, 16)

// Conditions
long_cond = ta.crossover(ema_fast, ema_slow) and rsi < 65
short_cond = ta.crossunder(ema_fast, ema_slow) and rsi > 35

// Levels - tighter SL, wider TP for better RR
sl_long = close - atr * 1.5
tp_long = close + atr * 3
sl_short = close + atr * 1.5
tp_short = close - atr * 3

// Plots for extraction
plot(close, "Close")
plot(long_cond ? 1 : short_cond ? -1 : 0, "Signal")
plot(long_cond ? sl_long : sl_short, "StopLoss")
plot(long_cond ? tp_long : tp_short, "TakeProfit")
plot(60, "EstimatedTime")  // 1 hour in minutes

Use it in your strategy:

import { File, getSignal } from '@backtest-kit/pinets';
import { addStrategy } from 'backtest-kit';

addStrategy({
strategyName: 'pine-ema-cross',
interval: '5m',
riskName: 'demo',
getSignal: async (symbol) => {
const source = File.fromPath('strategy.pine');

return await getSignal(source, {
symbol,
timeframe: '1h',
limit: 100,
});
}
});

No file needed - pass Pine Script as a string:

import { Code, getSignal } from '@backtest-kit/pinets';

const pineScript = `
//@version=5
indicator("RSI Strategy")

rsi = ta.rsi(close, 14)
atr = ta.atr(14)

long_cond = rsi < 30
short_cond = rsi > 70

plot(close, "Close")
plot(long_cond ? 1 : short_cond ? -1 : 0, "Signal")
plot(close - atr * 2, "StopLoss")
plot(close + atr * 3, "TakeProfit")
`;

const source = Code.fromString(pineScript);
const signal = await getSignal(source, {
symbol: 'BTCUSDT',
timeframe: '15m',
limit: 100,
});

For advanced use cases, extract any Pine plot() with custom mapping:

import { File, run, extract } from '@backtest-kit/pinets';

const source = File.fromPath('indicators.pine');

const plots = await run(source, {
symbol: 'ETHUSDT',
timeframe: '1h',
limit: 200,
});

const data = await extract(plots, {
// Simple: plot name -> number
rsi: 'RSI',
macd: 'MACD',

// Advanced: with transform and lookback
prevRsi: {
plot: 'RSI',
barsBack: 1, // Previous bar value
},
trendStrength: {
plot: 'ADX',
transform: (v) => v > 25 ? 'strong' : 'weak',
},
});

// data = { rsi: 55.2, macd: 12.5, prevRsi: 52.1, trendStrength: 'strong' }

extractRows() returns every bar as a typed row with a timestamp field β€” useful for building datasets, detecting crossovers across history, or feeding data into downstream analysis.

import { File, run, extractRows } from '@backtest-kit/pinets';

const source = File.fromPath('indicators.pine');

const plots = await run(source, {
symbol: 'ETHUSDT',
timeframe: '1h',
limit: 200,
});

const rows = await extractRows(plots, {
// Simple: plot name -> number | null
rsi: 'RSI',
macd: 'MACD',

// Advanced: with lookback and optional transform
prevRsi: {
plot: 'RSI',
barsBack: 1,
},
trend: {
plot: 'ADX',
transform: (v) => v > 25 ? 'strong' : 'weak',
},
});

// rows[0] = { timestamp: '2024-01-01T00:00:00.000Z', rsi: 48.3, macd: -2.1, prevRsi: null, trend: 'weak' }
// rows[1] = { timestamp: '2024-01-01T01:00:00.000Z', rsi: 52.1, macd: -1.5, prevRsi: 48.3, trend: 'weak' }
// ...

Difference between extract() and extractRows():

extract() extractRows()
Returns Single object (latest bar) Array of objects (all bars)
Missing value 0 (fallback) null
timestamp field No Yes β€” ISO string from the bar's time
barsBack Looks back from the last bar Looks back from each bar's own index
Use case Signal generation at current bar Dataset export, historical analysis

Dump plot data to markdown files for analysis and debugging:

import { File, run, dumpPlotData } from '@backtest-kit/pinets';

const source = File.fromPath('strategy.pine');

const plots = await run(source, {
symbol: 'BTCUSDT',
timeframe: '1h',
limit: 100,
});

// Dump plots to ./dump/ta directory
await dumpPlotData('signal-001', plots, 'ema-cross', './dump/ta');

Register a custom Pine constructor for advanced configurations:

import { usePine } from '@backtest-kit/pinets';
import { Pine } from 'pinets';

// Use custom Pine instance
usePine(Pine);

Configure logging for debugging:

import { setLogger } from '@backtest-kit/pinets';

setLogger({
log: (method, data) => console.log(`[${method}]`, data),
info: (method, data) => console.info(`[${method}]`, data),
error: (method, data) => console.error(`[${method}]`, data),
});

For getSignal() to work, your Pine Script must include these plots:

Plot Name Value Description
"Signal" 1 / -1 / 0 Long / Short / No signal
"Close" close Entry price
"StopLoss" price Stop loss level
"TakeProfit" price Take profit level
"EstimatedTime" minutes Hold duration (optional, default: 240)

Using custom plots is also possible with run, it allows to reconfigure the mapper

Instead of rewriting your TradingView strategies:

// ❌ Without pinets (manual rewrite)
import { getCandles } from 'backtest-kit';
import { RSI, EMA, ATR } from 'technicalindicators';

const candles = await getCandles('BTCUSDT', '5m', 100);
const closes = candles.map(c => c.close);
const rsi = RSI.calculate({ values: closes, period: 14 });
const emaFast = EMA.calculate({ values: closes, period: 9 });
const emaSlow = EMA.calculate({ values: closes, period: 21 });
// ... rewrite all your Pine Script logic in JS
// βœ… With pinets (copy-paste from TradingView)
import { File, getSignal } from '@backtest-kit/pinets';

const signal = await getSignal(File.fromPath('strategy.pine'), {
symbol: 'BTCUSDT',
timeframe: '5m',
limit: 100,
});

Benefits:

  • πŸ“œ Use existing TradingView Pine Script as-is
  • 🎯 60+ built-in indicators (no manual calculation)
  • ⚑ Same code for backtest and live trading
  • πŸ”„ Full time-series semantics with lookback support
  • πŸ›‘οΈ Type-safe extraction with TypeScript generics

Fork/PR on GitHub.

MIT Β© tripolskypetr