Skip to content

Introduction to scripting

Axel De Acetis edited this page May 6, 2023 · 11 revisions

Indicator scripting involves using JavaScript code with some specialized elements that are specific to aggr.
The code is executed whenever the chart needs to be refreshed, typically at a default interval of 1 second, which is specified by the Refresh Rate chart option.
To create effective indicator scripts, it is important to focus on concise, direct code and to minimize the use of loops and conditional statements wherever possible.

Built in variables

vbuy amount of buys (reported amount of buys at market)
vsell amount of sells (reported amount of sells at market)
cbuy number of market orders executed on buy side
csell number of market orders executed on sell side
lbuy amount of short liquidations
lsell amount of long liquidations
time alias for renderer.localTimestamp
renderer (or bar, alias of renderer) internal object describing the current state of the chart

{
  type: 'time' | 'tick' // type of timescale
  timeframe: number // current timeframe in second for time type, number of ticks for tick type
  timestamp: number // epoch time for time type, origin bar epoch time + number of ticks for tick type
  localTimestamp: number // timestamp adjusted  with timezone offset
  length: number // number of bars drawn (including this one)
  bar: Bar // simplified bar with volume sums of each activated sources
  sources: { [name: string]: Bar } // all sources bars (including the hidden ones)
}

series internal array containing the series API of the current indicator

fns internal array containing all utility function states

Built in functions

  • avg_close(bar: Bar): number
    return the averaged close of all activated sources in the given bar object

  • avg_ohlc(bar: Bar): {OHLC}
    return the averaged ohlc object of all activated sources in the given bar object

  • avg_heikinashi(bar: Bar): {OHLC}
    return the averaged Heikin Ashi ohlc object of all activated sources in the given bar object

  • ohlc(value: number): {OHLC}
    create a ohlc object out of a single value
    wicks will only be visible using realtime data (as the script will be executed multiple for each bars)

  • cum(value: number): number
    return the current value + value of the previous candle

  • cum_ohlc(value: number): {OHLC}
    same as ohlc except the output is cumulative

  • highest(value: number, length: number): number
    return the highest value within the last n bars

  • lowest(value: number, length: number): number
    return the lowest value within the last n bars

  • pivot_high(value: number, left: number, right: number): number | null
    will search for high price that was not exceeded during n bars to the left (past data) and n bars to the right (future data)

  • pivot_low(value: number, left: number, right: number): number | null
    same as pivot_high but instead will search for the low

  • avg(value: number[]): number
    simple average of a given set of values

  • sum(value: number, length: number): number
    return the sum of the last n values

  • linreg(value: number, length: number): number
    return the linear regression of a source (non optimized)

  • ema(value: number, length: number): number
    return the exponential moving average of a source

  • sma(value: number, length: number): number
    return the simple moving average of a source

Plotting

The basics of plotting

  • line(value: number)
    Plot line

  • candlestick(ohlc: OHLC)

  • candlestick(open: number, high: number, low: number, close: number)
    Plot candles

  • bar(ohlc: OHLC)

  • bar(open: number, high: number, low: number, close: number)
    Plot OHLC (the one without body)

  • histogram(value: number)
    Plot histogram bars, supports dynamic coloring

  • area(value: number)
    Plot line with a gradient fill below it (from 0 to line)

  • cloudarea(lowerValue: number, higherValue: number)
    Fill a continuous range, supports dynamic coloring
    Used for ichimoku, ma ribbon, bb

  • brokenarea(lowerValue: number, higherValue: number)
    Same as cloudarea except it can have breaks (much like floating rectangles) Used to plot S/R, pivot points

Customization

All plotting functions take options arguments that you add after the ones described above. All series options are documented here (and a few sections which follow that).

Simple line coloring and a title

line($price.close, color=red, title=BTCUSD)

Simple line with custom color and width using option

line($price.close, color=options.color, lineWidth=4)

The histogram series custom coloring require that you enter the full plot object into the plotting function

histogram({
  time: time,
  value: vbuy - vsell,
  color: vbuy > vsell ? 'green' : 'red'
})

Cloud area use dedicated option for when higherValue is above or below lowerValue

ma50=sma($price.close, 50)
ma20=sma($price.close, 20)

cloudarea(ma50, ma20, positiveColor=options.bullColor, negativeColor=options.bearColor)

Note that all series options are set when the serie is created and CANNOT change during it's execution.
Because of that, sadly, the following script won't work :

var change = myvar - myvar[1]
var color = myvar > 0 ? 'green' : 'red'
line($price.close, color=color)

Plots ordering

Within an indicator, the first plotted series is below the last plotted one.
Accross multiple indicators, the first one to get processed will be in back, last in front.

Note that an indicator being required by another will always get processed before the other, and plotted before as well meaning it will be shown below the one requiring it.
Grouping all your plots in the same indicator is the only way around it ATM.

// first get the avg price ohlc
var price = avg_ohlc(bar)

// do whatever with it
line(sma(price.close, 50))
line(sma(price.close, 100))
line(sma(price.close, 200))

// then plot price
candlestick(price)

Access series API

Drawing horizontal lines

Accessing the series API let you use the priceLine feature from Lightweight Charts. You can come up with a script showing current support and resistance and use :

if (certainConditionMatch) {
  if (buyhere) {
    // remove previous line
    series[0].removePriceLine(buyhere)
  }
  buyhere = series[0].createpriceline({ 
    price: 41365, // y
    index: bar.length, // x
    title: 'right there'
  })`
}

You attach horizontal lines to an existing series, it will determine which scale the lines will use.

See price line documentation for details about horizontal lines.

Adding markers

When writing a script that give you signals, you may want use markers to print something distinctive at a specific time and price on the chart.
The marker api works a little different from the priceLines :

  • Create a persistent variable (array) in your script to store all the markers
  • When a certain condition match, add the marker to the variable
  • When you added a marker, call series[* index of the serie*].setMarkers(your variable) to update the chart with the new marker
// markers needs a serie to get attached to
line($price.close, color=transparent)

if (markers === 0) {
  // first script execution (0 is default values for all persistent variables)

  // create a var to store already drawn markers
  markers = []

  // contain the last marker that can change as the bar update
  repaintableMarker = null
}

if (repaintableMarker && repaintableMarker.time < time) {
  // bar is +1 since active marker was added, lock it in the array
  markers.push(repaintableMarker)

  // free up active marker
  repaintableMarker = null
}

var newMarker = null

// big money strategy
var bearSignal = lbuy > ema(vsell, 7) * 0.005
var bullSignal = lsell > ema(vbuy, 7) * 0.005

if (bearSignal) {
  // newMarker is a temporary variable (not included in the indicator state)
  // to avoid it being treated as persistent variable we wrap it inside parenthesis
  (newMarker) = {
    time: time,
    position: 'aboveBar',
    color: 'red',
    shape: 'arrowDown'
  }
}

if (bullSignal) {
  (newMarker) = {
    time: time,
    position: 'belowBar',
    color: 'lime',
    shape: 'arrowUp'
  }
}

if (newMarker || (repaintableMarker && !newMarker)) {
  // override persistent variable repaintable marker
  repaintableMarker = newMarker
  
  if (series[0].setMarkers) {
    series[0].setMarkers(markers.concat(repaintableMarker))
  }
}

See markers documentation for details about markers.

Defining custom options

All occurrences of 'options.youOptionName' from the script are automatically attached to a setting in the indicator window.
The value and name of the option will determine the type of input that will appear in the indicator window, according to the following rules :

  • an hex / rgb color as a value will show a color picker
  • if the name start with "show" or "toggle" the option is a checkbox
  • a default value of 14 if option name end with the word 'length'
  • a default color of #c3a87a if option name end with the word 'color'
  • otherwise it's a simple number input which reacts to mouse wheel, up/down arrow etc)

Variables

Declaring a variable

Define a variable by writing an alphanumeric string (no dash, no underscore) in front of the = sign. The declaration will not work if the variable is wrapped.

myvar=1
console.log(myvar) // 1

myvar is now accessible and stored in the indicator state.
From there, you can use it in any operations or plot :

if (time % (60 * 60) === 0) {
  myvar = 0
}
myvar += 1
line(sma(myvar, 14))

or and access its previous value

myvar=$price.close
var change = myvar - myvar[1]
histogram(sma(change, 14))

console.log(change) // output the difference
console.log(myvar) // output the last close
console.log(myvar[0]) // same
console.log(myvar[1]) // output previous close
console.log(#myvar) // prefixing the var name with a '#' will give you the full values of that var [number, number]
console.log(avg(#myvar)) // the # is perfect for avg function which require a set of values

The moment you start accessing past value of a variable, it becomes an array with length = the maximum bars that will get requested in the script.

myvar=$price.close
console.log(myvar[0], myvar[1], myvar[10])
console.log(#myvar.length) // 11 assuming the chart have more than 10 bars in it

You can force a variable to retain a certain number of values by wrapping the length in parenthesis when declaring it

myvar(10)=$price.close
console.log(avg(#myvar)) // average of last 10 bars

Declaring a temporary variable

If a variable is only temporary and has no persistance requirement it is recommended to prefix it with a var.
This way it won't be included in the indicator state, in this example myvar is always undefined at the beginning of the script.

var myvar=vbuy+vsell

And WILL improve the memory footprint of your script.

Accessing function state

Indicator's variables and functions have their own state, which is used by the chart to know what the previous values was
All periodic functions (like sma, ema, sum etc) have a "sum", "count", and "points" state as described here. Even tho you won't need it most of the time these states can be mutated from the indicator script itself.

// hourly VWAP
if (time % (60 * 60) === 0) {
  // top of the hour
  // clear both 'sum' fns states
  // bit hacky but it works 🤡
  fns[0].state.sum=fns[1].state.sum=0
  fns[0].state.count=fns[1].state.count=0
  fns[0].state.points=fns[1].state.points=[]
}
vol = vbuy + vsell
pricevol = $price.close * vol

line(sum(pricevol, options.length) / sum(vol, options.length))

Referencing sources

In aggr term a source is a market aka an association of exchange + symbol where the symbol is a association of base currency and quote currency.

COINBASE:BTC-USD

You can reference those in the script as long as the pane is listening to it.
On runtime the given source is transformed to an reference of the bar source object

renderer.sources['COINBASEBTC-USD']

Which contains the following properties

{
  open: number,
  high: number,
  low: number,
  close: number,
  vbuy: number,
  vsell: number,
  csell: number,
  csell: number,
  lbuy: number,
  lsell: number,
  pair: string,
  exchange: string,
  active: boolean,
  empty: boolean,
}

This ensure only the coinbase price is plotted

line(COINBASE:BTC-USD.close, title=BTCUSD)

Same as above but in the form of candlesticks

candlestick(COINBASE:BTC-USD.open, COINBASE:BTC-USD.high, COINBASE:BTC-USD.low, COINBASE:BTC-USD.close)

Creating custom indexes

candlestick(ohlc((COINBASE:BTC-USD.close + BITSTAMP:btcusd.close + BITFINEX:BTCUSD.close) / 3), title=CustomIndex)

Referencing other indicators / plots

Plots reference is crucial to getting a decent performance score on the chart.
For example I wouldn't call avg_ohlc more than once on the whole chart.

Indicator has a name and an id which is generated from the name (special characters are replaced with dash, always lowercase, and unique across all indicators)
Prefix the id of another indicator with a $ to get his value. Assuming you have a "Price" indicator which is enabled by default, you can create an indicator and do that :

line($price.close)

It will work because the indicator Price only have one plot, in that case l'ID of the plot become the indicator ID.
But you might have many plots in a single indicator. In that case best option is to pass an id option in the plotting function.

In indicator 1

plotcandlestick(avg_ohlc(bar), id=test)

In indicator 2

line($test.close)