Skip to main content

Overview

VibeTrader supports custom indicators written in PineScript v6. Indicators can be overlaid on the price chart or displayed in separate panels.

Indicator Structure

Basic Template

//@version=6
indicator("My Custom Indicator", overlay = false)

// 1. Define inputs
length = input.int(14, "Period", minval=1)

// 2. Calculate values
value = ta.sma(close, length)

// 3. Plot results
plot(value, color=color.blue, title="SMA")

Overlay Indicators

Set overlay = true to display on the price chart:
ema.pine
//@version=6
indicator("EMA", overlay = true)

// Inputs
input9 = input.int(9, "Fast MA")
input21 = input.int(21, "Slow MA")
input36 = input.int(36, "Slowest MA")

// Calculations
ma9 = ta.ema(close, input9)
ma21 = ta.ema(close, input21)
ma36 = ta.ema(close, input36)

// Plotting
plot(ma9, color = #FF6D00, title="EMA-9")
plot(ma21, color = #2962FF, title="EMA-21")
plot(ma36, color = #aec7e8, title="EMA-36")

Stacked Indicators

Set overlay = false for separate panels:
rsi.pine
//@version=6
indicator("Simple RSI", overlay = false)

// Get User Input
rsiLength = input.int(13, "RSI Length", minval = 1)
rsiSource = input.source(close, "Source")

// Calculate RSI
rsiValue = ta.rsi(rsiSource, rsiLength)

// Plot
plot(rsiValue, "RSI", color = #FF6D00, linewidth = 2)

// Horizontal lines for Overbought/Oversold levels
h0 = hline(70, "Upper Band", color = color.gray, linestyle = hline.style_dashed)
h1 = hline(30, "Lower Band", color = color.gray, linestyle = hline.style_dashed)
fill(h0, h1, color = color.rgb(126, 87, 194, 90), title = "Background")

Plot Types

VibeTrader supports all PineScript plot styles. The Plot type is defined in src/lib/charting/plot/Plot.ts:1:
src/lib/charting/plot/Plot.ts
export type Plot = {
    data: PineData[],
    options: PlotOptions,
    title: string,
    _plotKey: string,
}

export type PlotOptions = {
    color?: string;
    linewidth?: number;
    linestyle?: string;
    style?: string;  // 'line' | 'stepline' | 'histogram' | 'columns' | 'area' | 'shape' | 'char'
    location?: Location;  // 'abovebar' | 'belowbar' | 'top' | 'bottom' | 'absolute'
    force_overlay?: boolean;
    // ... additional options
};

Line Plots

plot(ma, color=color.blue, linewidth=2, title="Moving Average")

Histogram

macd.pine
//@version=6
indicator("MACD", overlay=false)

// MACD Inputs
fast_length = 12
slow_length = 26
signal_length = 9

// Calculation
[macdLine, signalLine, histLine] = ta.macd(close, fast_length, slow_length, signal_length)

// Plotting
plot(macdLine, color=color.blue, title="MACD Line")
plot(signalLine, color=color.orange, title="Signal Line")

// Histogram with color based on value
histColor = histLine >= 0 ? (histLine > histLine[1] ? #26A69A : #B2DFDB) : (histLine < histLine[1] ? #EF5350 : #FFCDD2)
plot(histLine, color=histColor, style=plot.style_columns, title="Histogram")
hline(0, "Zero Line", color=color.gray)

Shapes and Characters

plotshape(crossover(fast, slow), style=shape.triangleup, location=location.belowbar, color=color.green)
plotchar(crossunder(fast, slow), char="▼", location=location.abovebar, color=color.red)

Advanced Features

Fill Between Plots

bb.pine
//@version=6
indicator("Bollinger Bands Example", overlay=true)

// Define inputs for the Bollinger Bands
length = input.int(20, minval=1)
mult = input.float(2.0, minval=0.001, maxval=50)

// Calculate the Bollinger Bands using ta.bb()
[middleBand, upperBand, lowerBand] = ta.bb(close, length, mult)

// Plot the bands on the chart
plot(middleBand, color=color.blue, title="Middle Band")
p1 = plot(upperBand, color=color.red, title="Upper Band")
p2 = plot(lowerBand, color=color.red, title="Lower Band")

// Fill the area between bands
fill(p1, p2, color=color.rgb(33, 150, 243, 90), title="Background")

Dynamic Line Objects

PineScript can create dynamic line objects that VibeTrader renders:
lines.pine
//@version=6
indicator("Reading line values demo", overlay = true)

int length = input.int(2, "Length", 2)
var line directionLine = na

bool rising = ta.rising(hlc3, length)
bool falling = ta.falling(hlc3, length)
bool newDirection = (rising and not rising[1]) or (falling and not falling[1])

// Update the line when direction changes
if newDirection
    directionLine := line.new(bar_index - length, hlc3[length], bar_index, hlc3, width = 3)

float slope = (directionLine.get_y2() - directionLine.get_y1()) / (directionLine.get_x2() - directionLine.get_x1())
float lineValue = line.get_price(directionLine, bar_index)

color slopeColor = slope > 0 ? color.green : color.red

directionLine.set_color(slopeColor)
plot(lineValue, "Extrapolated value", slopeColor, 3, plot.style_circles)
Line objects are represented by the LineObject type in PineData.ts:3.

Plot Categorization

VibeTrader automatically categorizes plots as overlay or stacked based on these rules:
KlineViewContainer.tsx:331
const isOverlayOutputShapeAndLocation = (
    (style === 'shape' || style === 'char') && 
    (location === 'abovebar' || location === 'belowbar')
)

if (isOverlayOutputShapeAndLocation || isForceOverlay) {
    overlayOutputs.push(output)
} else {
    if (notForceOverlay) {
        stackedOutputs.push(output)
    } else {
        if (isOverlayIndicator) {
            overlayOutputs.push(output)
        } else {
            stackedOutputs.push(output)
        }
    }
}

Force Overlay

Use force_overlay option to control placement:
plot(myValue, title="My Plot", force_overlay=true)  // Force onto main chart

Loading Indicators

Predefined Indicators

Place PineScript files in public/indicators/:
public/indicators/
├── ema.pine
├── rsi.pine
├── macd.pine
├── bb.pine
└── custom.pine
They will be auto-loaded at startup:
KlineViewContainer.tsx:407
this.fetchOPredefinedScripts(allIndTags).then(scripts => {
    this.predefinedScripts = new Map(scripts.map(p => [p.scriptName, p.script]))
})

Runtime Execution

Execute scripts dynamically:
// From KlineViewContainer component
const scripts = [
    '//@version=6\nindicator("Custom")\nplot(close)'
];

klineViewContainer.runScripts(scripts);

Best Practices

  • Minimize complex calculations in loops
  • Use built-in ta.* functions when possible
  • Avoid excessive plot calls
  • Cache input values
  • Choose distinct colors for multiple plots
  • Use appropriate line widths (1-3 pixels)
  • Add meaningful titles to all plots
  • Use hlines for reference levels
  • Provide sensible default input values
  • Add min/max constraints to inputs
  • Use descriptive input names
  • Document calculation methodology in comments
Plot data is logged to console:
console.log(scriptName + ' data\n', data)
console.log(scriptName + ' options\n', plots.map(x => JSON.stringify(x.options)))

Common Patterns

Multi-Timeframe Analysis

//@version=6
indicator("MTF EMA", overlay=true)

timeframe = input.timeframe("D", "Timeframe")
length = input.int(20, "Length")

higherEma = request.security(syminfo.tickerid, timeframe, ta.ema(close, length))
plot(higherEma, color=color.purple, linewidth=2)

Conditional Coloring

//@version=6
indicator("Dynamic Color")

value = ta.rsi(close, 14)
valueColor = value > 70 ? color.red : value < 30 ? color.green : color.gray
plot(value, color=valueColor, linewidth=2)

Multiple Outputs

//@version=6
indicator("Trend System", overlay=true)

fast = ta.ema(close, 12)
slow = ta.ema(close, 26)

plot(fast, color=color.blue, title="Fast EMA")
plot(slow, color=color.red, title="Slow EMA")

bgColor = fast > slow ? color.new(color.green, 90) : color.new(color.red, 90)
bgcolor(bgColor)

Next Steps

PineScript Integration

Learn how PineTS integrates with VibeTrader

Custom Drawings

Create custom drawing tools for chart analysis

Build docs developers (and LLMs) love