Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-kit-docs/llms.txt

Use this file to discover all available pages before exploring further.

Traders who have already built and validated strategies in TradingView’s Pine Script can run them unchanged inside Backtest Kit — no rewrite required. The @backtest-kit/pinets package embeds the open-source PineTS runtime, which provides 1:1 Pine Script v5/v6 syntax compatibility with 60+ built-in indicators. Your .pine file is loaded as-is; getSignal receives the computed plot outputs and translates them into standard Backtest Kit position parameters.

Backtest results (December 2025)

The strategy ran on the first half of December 2025 against a choppy BTC market that oscillated between ~85kand 85k and ~94k with sharp intra-day reversals.
MetricValue
PeriodDec 1 – Dec 15, 2025 (first half only)
Net PNL**+2.40(+2.402.40 (+2.40%)** on 900 deployed
Sharpe ratio0.06
Total trades9 (6 LONG / 3 SHORT)
Win rate66.7% (6 / 9)
Best trade+1.61% (Dec 1 SHORT)
Worst trade−2.41% (Dec 5 SHORT — false breakout)
Worst drawdown−2.35% per position
Avg hold time~20 hours
Exit methodFixed ±2% TP/SL bracket
Three trades hit the hard 2% stop-loss (trades #4, #5, #7), all false breakouts where price briefly crossed the Bollinger Band then reversed. The remaining six trades all hit the take-profit side.

Install dependencies

npm install @backtest-kit/pinets pinets backtest-kit

How it works

The strategy registers on a 1h interval. On each tick, it calls run(File.fromPath(...), options) from @backtest-kit/pinets, which executes btc_dec2025_range.pine inside the PineTS runtime. The result is an array of bar-by-bar output objects, one per candle, each containing the values of all plot() calls defined in the Pine Script file.Execution can be cached per hour via Cache.fn from backtest-kit — wrapping the run() call so the Pine Script executes only once per hour boundary, not once per minute, which keeps backtest speed high even with a large candle window.
The run() call returns a plot data structure. extract() reads the latest bar values from named plots. The strategy extracts six values:
Plot nameDescription
bbUpper / bbLower / bbBasisBollinger Bands (20-period, 2σ)
rangeHigh / rangeLowDetected horizontal range boundaries
signal+1 = bullish breakout, −1 = bearish breakout, 0 = no signal
isRanging1 = price still inside range → skip signal
volSpike1 = volume confirmation present
Only signal and isRanging gate trade entry. volSpike is tracked in the log but not used as a hard filter — the one trade without volume confirmation (trade #6) still hit take-profit.
Even when signal !== 0, two additional checks prevent entering on moves that have already run:
  • LONG (signal === 1): skip if currentPrice > plot.close — the breakout has already extended beyond where the signal fired.
  • SHORT (signal === -1): skip if currentPrice < plot.close — the breakdown has already run.
  • Both: skip if isRanging === 1 — price is still consolidating within the detected range and has not confirmed a breakout.
These filters are critical in a choppy, high-volatility environment like December 2025, where signals can fire at a candle close but the price has already moved 0.5–1% by the time the next candle opens.
Valid signals open a Position.bracket with a symmetric ±2% take-profit and stop-loss. There is no trailing stop, no DCA, and no time-based exit — positions either hit +2% (take-profit) or −2% (stop-loss). The minuteEstimatedTime: Infinity flag ensures the position is held until one of the two exits fires, regardless of how long it takes.This simplicity keeps the strategy easy to reason about: every trade risks exactly 2tomake2 to make 2, and the edge comes entirely from signal accuracy (66.7% win rate in December).

Pine Script indicator file

The indicator computes Bollinger Bands, detects a horizontal range, and emits a directional breakout signal. All outputs that the strategy needs to read must be declared with display=display.data_window.
//@version=5
indicator("Range Breakout", overlay=true)

// Bollinger Bands
basis = ta.sma(close, 20)
dev = ta.stdev(close, 20)
upper = basis + dev * 2
lower = basis - dev * 2

// Range detection (simplified)
rangeHigh = ta.highest(high, 20)
rangeLow  = ta.lowest(low, 20)
isRanging = (rangeHigh - rangeLow) / rangeLow < 0.03 ? 1 : 0

// Volume spike
volSpike = volume > ta.sma(volume, 20) * 1.5 ? 1 : 0

// Signal: 1 = long breakout, -1 = short breakout, 0 = no signal
signal = close > upper ? 1 : close < lower ? -1 : 0

// All plots that getSignal needs to read must use display.data_window
plot(signal,    "signal",    display=display.data_window)
plot(upper,     "bbUpper",   display=display.data_window)
plot(lower,     "bbLower",   display=display.data_window)
plot(basis,     "bbBasis",   display=display.data_window)
plot(rangeHigh, "rangeHigh", display=display.data_window)
plot(rangeLow,  "rangeLow",  display=display.data_window)
plot(isRanging, "isRanging", display=display.data_window)
plot(volSpike,  "volSpike",  display=display.data_window)

Strategy code

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

addStrategySchema({
  strategyName: 'dec_2025_strategy',
  interval: '1h',
  getSignal: async (symbol, when, currentPrice) => {
    const plots = await run(File.fromPath('./btc_dec2025_range.pine'), {
      symbol,
      timeframe: '1h',
      limit: 100,
    });

    const latest = await extract(plots, {
      close:     'close',
      signal:    'signal',
      isRanging: 'isRanging',
      volSpike:  'volSpike',
    });

    // No signal or still ranging — skip
    if (latest.signal === 0) return null;
    if (latest.isRanging === 1) return null;

    const position = latest.signal === 1 ? 'long' : 'short';

    // Skip if price has already moved past the signal close
    if (position === 'long'  && currentPrice > latest.close) return null;
    if (position === 'short' && currentPrice < latest.close) return null;

    return {
      position,
      priceOpen: currentPrice,
      priceTakeProfit:
        position === 'long'
          ? currentPrice * 1.02
          : currentPrice * 0.98,
      priceStopLoss:
        position === 'long'
          ? currentPrice * 0.98
          : currentPrice * 1.02,
      minuteEstimatedTime: Infinity,
      cost: 100,
    };
  },
});

Plot extraction

run() returns an array of objects where each key matches the second argument of a plot() call in the Pine Script. For the breakout indicator:
import { run, File, extract } from '@backtest-kit/pinets';

const plots = await run(File.fromPath('./btc_dec2025_range.pine'), {
  symbol,
  timeframe: '1h',
  limit: 100,
});

// plots[i] contains the raw plot arrays returned by PineTS.
// Use extract() to read the latest bar values by plot name:
const latest = await extract(plots, {
  close:      'close',
  signal:     'signal',     // +1, -1, or 0
  bbUpper:    'bbUpper',
  bbLower:    'bbLower',
  bbBasis:    'bbBasis',
  rangeHigh:  'rangeHigh',
  rangeLow:   'rangeLow',
  isRanging:  'isRanging',  // 0 or 1
  volSpike:   'volSpike',   // 0 or 1
});
The plot names are case-sensitive and must match the string literal in plot(value, "name", ...). When using the @backtest-kit/cli --pine mode, only plots declared with display=display.data_window are exported as output columns — see the Tip at the bottom of this page for details.

60+ built-in indicators

The PineTS runtime bundles the same indicator library available in TradingView. You can use any of these directly in your .pine file:
CategoryAvailable indicators
Moving averagesSMA, EMA, WMA, VWMA, DEMA, TEMA, HMA, ALMA
OscillatorsRSI, MACD, Stochastic, CCI, Williams %R, MFI
VolatilityBollinger Bands, ATR, Keltner Channels, Donchian
TrendADX, Aroon, PSAR, Ichimoku, SuperTrend
VolumeOBV, VWAP, CMF, Force Index

Running the backtest

npm start -- --backtest --symbol BTCUSDT \
  --strategy dec_2025_strategy \
  --exchange ccxt-exchange \
  --frame dec_2025_frame \
  ./content/dec_2025.strategy/dec_2025.strategy.ts

# With visual dashboard
npm start -- --backtest --symbol BTCUSDT --ui \
  ./content/dec_2025.strategy/dec_2025.strategy.ts
Source code: dec_2025.strategy
Pine Script plots are only exported to the data window if the plot() call includes display=display.data_window. Plots that use the default display=display.all are rendered on the chart overlay in TradingView but are not accessible as named keys in the run() output. If a key you expect is missing from the output, check that the corresponding plot() in your .pine file has the correct display flag.

Build docs developers (and LLMs) love