Skip to main content

Add Pine Script Indicators

TradeJS supports two indicator authoring paths:

  • TypeScript indicator plugins (recommended for reusable pane indicators)
  • Pine plots inside standalone Pine strategies (for strategy-native visuals/signals)

Important: a standalone Pine indicator plugin (indicators) is not supported yet. Pine indicators are rendered from plot(...) series produced by a Pine strategy.

1. TypeScript Indicator Path (Standalone Pane Indicator)

Use plugin indicators when you need reusable chart panes independent of one strategy.

See Write Custom Indicators.

2. Pine Indicator Path (Inside Pine Strategy)

For Pine strategies (example: AdaptiveMomentumRibbon), indicator lines come from Pine plot(...) outputs and are converted to figures.

If you only extend an existing Pine strategy, you usually edit:

  • strategy .pine file
  • strategy config (*_LINE_PLOTS)

Example (AdaptiveMomentumRibbon):

adaptiveMomentumRibbon.pine

rsiValue = ta.rsi(close, 14)
plot(rsiValue, "rsi")

strategy config

{
"AMR_LINE_PLOTS": [
"kcMidline",
"kcUpper",
"kcLower",
"invalidationLevel",
"rsi"
]
}

Then run:

npx @tradejs/cli backtest --user root --config AdaptiveMomentumRibbon:amr-default

or:

npx @tradejs/cli signals --user root --cacheOnly

The new Pine plot appears in figures.lines.

3. Full Custom Pine Indicator Module (All Files)

If you want a fully custom user Pine indicator, create a custom Pine strategy module.

Minimal file set (user project):

src/strategies/MyPineIndicator/
myPineIndicator.pine
config.ts
figures.ts
core.ts
strategy.ts
manifest.ts
src/plugins/myPineIndicator.plugin.ts
tradejs.config.ts

myPineIndicator.pine

//@version=5
indicator("My Pine Indicator", shorttitle="MPI", overlay=true)

fast = ta.ema(close, 9)
slow = ta.ema(close, 21)
osc = fast - slow

entryLong = ta.crossover(osc, 0)
entryShort = ta.crossunder(osc, 0)
invalidated = false

plot(osc, "myOsc")
plot(entryLong ? 1 : 0, "entryLong")
plot(entryShort ? 1 : 0, "entryShort")
plot(invalidated ? 1 : 0, "invalidated")

config.ts

import type { BacktestPriceMode, StrategyConfig } from '@tradejs/types';

export const config = {
ENV: 'BACKTEST',
INTERVAL: '15',
BACKTEST_PRICE_MODE: 'mid' as BacktestPriceMode,
MY_LOOKBACK_BARS: 400,
MY_LINE_PLOTS: ['myOsc'],
LONG: { enable: true, direction: 'LONG', TP: 2, SL: 1 },
SHORT: { enable: true, direction: 'SHORT', TP: 2, SL: 1 },
} as const;

export type MyPineIndicatorConfig = StrategyConfig & typeof config;

figures.ts

import {
PineContextLike,
getPinePlotSeries,
toFiniteNumber,
} from '@tradejs/node/pine';
import type { StrategyEntryModelFigures } from '@tradejs/types';

export const buildMyPineFigures = (
pineContext: PineContextLike,
): StrategyEntryModelFigures => {
const points = getPinePlotSeries(pineContext, 'myOsc')
.slice(-180)
.map((item) => {
const timestamp = toFiniteNumber(item?.time);
const value = toFiniteNumber(item?.value);
if (timestamp == null || value == null) return null;
return { timestamp, value };
})
.filter(Boolean) as { timestamp: number; value: number }[];

return {
lines: [
{
id: 'my-osc-line',
kind: 'my_osc',
points,
color: '#22d3ee',
width: 2,
style: 'solid',
},
],
};
};

core.ts

import {
getLatestPineRawPlotValue,
runPineScript,
toPineBoolean,
} from '@tradejs/node/pine';
import type { CreateStrategyCore } from '@tradejs/types';
import { MyPineIndicatorConfig } from './config';
import { buildMyPineFigures } from './figures';

export const createMyPineIndicatorCore: CreateStrategyCore<
MyPineIndicatorConfig
> = async ({ config, symbol, loadPineScriptFile, strategyApi }) => {
const script = loadPineScriptFile('myPineIndicator.pine');

return async () => {
if (!script) {
return strategyApi.skip('PINE_SCRIPT_EMPTY');
}

const { fullData, currentPrice } = await strategyApi.getMarketData();
const pineContext = await runPineScript({
candles: fullData.slice(-Number(config.MY_LOOKBACK_BARS ?? 400)),
script,
symbol,
timeframe: String(config.INTERVAL ?? '15'),
});

const entryLong = toPineBoolean(
getLatestPineRawPlotValue(pineContext, 'entryLong'),
);
const entryShort = toPineBoolean(
getLatestPineRawPlotValue(pineContext, 'entryShort'),
);

if (!entryLong && !entryShort) {
return strategyApi.skip('NO_SIGNAL');
}

const mode = entryLong ? config.LONG : config.SHORT;
if (!mode.enable) {
return strategyApi.skip('SIDE_DISABLED');
}

const { stopLossPrice, takeProfitPrice } =
strategyApi.getDirectionalTpSlPrices({
price: currentPrice,
direction: mode.direction,
takeProfitDelta: mode.TP,
stopLossDelta: mode.SL,
unit: 'percent',
});

return strategyApi.entry({
code: 'MY_PINE_INDICATOR_ENTRY',
direction: mode.direction,
figures: buildMyPineFigures(pineContext),
orderPlan: {
qty: 1,
stopLossPrice,
takeProfits: [{ rate: 1, price: takeProfitPrice }],
},
});
};
};

strategy.ts

import { createStrategyRuntime } from '@tradejs/node/strategies';
import { config, MyPineIndicatorConfig } from './config';
import { createMyPineIndicatorCore } from './core';
import { myPineIndicatorManifest } from './manifest';

export const MyPineIndicatorStrategyCreator =
createStrategyRuntime<MyPineIndicatorConfig>({
strategyName: 'MyPineIndicator',
defaults: config as MyPineIndicatorConfig,
createCore: createMyPineIndicatorCore,
manifest: myPineIndicatorManifest,
strategyDirectory: __dirname,
});

manifest.ts

import type { StrategyManifest } from '@tradejs/types';

export const myPineIndicatorManifest: StrategyManifest = {
name: 'MyPineIndicator',
};

src/plugins/myPineIndicator.plugin.ts

import { defineStrategyPlugin } from '@tradejs/core/config';
import { myPineIndicatorManifest } from '../strategies/MyPineIndicator/manifest';
import { MyPineIndicatorStrategyCreator } from '../strategies/MyPineIndicator/strategy';

export default defineStrategyPlugin({
strategyEntries: [
{
manifest: myPineIndicatorManifest,
creator: MyPineIndicatorStrategyCreator,
},
],
});

tradejs.config.ts

import { defineConfig } from '@tradejs/core/config';
import { basePreset } from '@tradejs/base';

export default defineConfig(basePreset, {
strategies: ['./src/plugins/myPineIndicator.plugin.ts'],
});

4. Validate in Backtest/Signals

npx @tradejs/cli backtest --user root --config MyPineIndicator:default
npx @tradejs/cli signals --user root --cacheOnly

5. Common Pitfalls

  • Plot name mismatch: config MY_LINE_PLOTS names must match Pine plot(..., "name").
  • Missing required runtime plots: your Pine script must expose entryLong, entryShort, invalidated.
  • Flat/incorrect line: increase lookback (MY_LOOKBACK_BARS) and check Pine warmup.

For a full production-ready reference, see Pine Strategy Step by Step.