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.

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
getCandles integration with temporal context.pine files or pass code strings directlyplot() outputs to structured 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 values from plots 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' }
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:
Fork/PR on GitHub.
MIT Β© tripolskypetr