A Guide on Custom Indicators

How to build and chart it

library(cryptoQuotes)

Trading indicators comes in various forms. From the alignment of the moon relative to the sun, to sophisticated trading rules based on neural networks which incorporates classified features. It is not possible to cover them all in an R package.

In this vignette an introduction to the construction of charting indicators are given, and is recommended for those who would want to chart indicators not otherwise found in the cryptoQuotes-package.

Note: Feel free to make a PR with your indicators that you wish to share with the rest of the community.

Charting indicators

Below is a chart, with the indicators macd() and bollinger_bands(). Each indicator is created using the TTR-package.

chart(
  ticker    = BTC,
  main      = kline(),
  sub       = list(
    macd()
    ),
  indicator = list(
    bollinger_bands()
  ) 
)

The anatomy of indicators

Each indicator is either a main chart- or subchart-indicator, lets call them classes for consistency. The source code for each class of indicator is given below,

Main chart indicator (Bollinger Bands)
#> function (n = 20, sd = 2, maType = "SMA", color = "#4682b4", 
#>     ...) 
#> {
#>     check_indicator_call()
#>     structure(.Data = {
#>         args <- list(...)
#>         linewidth <- 0.9
#>         data <- indicator(x = args$data, columns = c("high", 
#>             "low", "close"), .f = TTR::BBands, n = n, sd = sd, 
#>             maType = maType)
#>         layers <- list(list(type = "add_lines", params = list(showlegend = FALSE, 
#>             legendgroup = "bb", name = "Lower", inherit = FALSE, 
#>             data = data, x = ~index, y = ~dn, line = list(color = color, 
#>                 width = linewidth))), list(type = "add_lines", 
#>             params = list(showlegend = FALSE, legendgroup = "bb", 
#>                 name = "Upper", inherit = FALSE, data = data, 
#>                 x = ~index, y = ~up, line = list(color = color, 
#>                   width = linewidth))), list(type = "add_lines", 
#>             params = list(showlegend = FALSE, legendgroup = "bb", 
#>                 name = paste(maType), inherit = FALSE, data = data, 
#>                 x = ~index, y = ~mavg, line = list(color = color, 
#>                   dash = "dot", width = linewidth))))
#>         plot <- plotly::add_ribbons(showlegend = TRUE, legendgroup = "bb", 
#>             p = args$plot, inherit = FALSE, x = ~index, ymin = ~dn, 
#>             ymax = ~up, data = data, fillcolor = as_rgb(alpha = 0.1, 
#>                 hex_color = color), line = list(color = "transparent"), 
#>             name = paste0("BB(", paste(c(n, sd, maType), collapse = ", "), 
#>                 ")"))
#>         plot <- build(plot = plot, layers = layers)
#>         invisible(plot)
#>     }, class = c("indicator", "plotly", "htmlwidget"))
#> }
#> <bytecode: 0x610985007230>
#> <environment: namespace:cryptoQuotes>
Subchart indicator (MACD)
#> function (nFast = 12, nSlow = 26, nSig = 9, maType = "SMA", percent = TRUE, 
#>     ...) 
#> {
#>     check_indicator_call()
#>     structure(.Data = {
#>         args <- list(...)
#>         linewidth <- 0.9
#>         data <- indicator(x = args$data, columns = "close", .f = TTR::MACD, 
#>             nFast = nFast, nSlow = nSlow, nSig = nSig, maType = maType, 
#>             percent = percent)
#>         data$direction <- as.logical(data$signal >= data$macd)
#>         p <- plotly::plot_ly(data = data, showlegend = FALSE, 
#>             name = "MACD", x = ~index, y = ~(macd - signal), 
#>             color = ~direction, colors = c(args$candle_color$bullish, 
#>                 args$candle_color$bearish), type = "bar", marker = list(line = list(color = "black", 
#>                 width = 0.5)))
#>         layers <- list(list(type = "add_lines", params = list(name = "MACD: Signal", 
#>             data = data, showlegend = FALSE, x = ~index, y = ~signal, 
#>             inherit = FALSE, line = list(width = linewidth))), 
#>             list(type = "add_lines", params = list(name = "MACD: MACD", 
#>                 data = data, showlegend = FALSE, x = ~index, 
#>                 y = ~macd, inherit = FALSE, line = list(width = linewidth))))
#>         p <- build(plot = p, layers = layers, annotations = list(list(text = paste0("MACD(", 
#>             paste(c(nFast, nSlow, nSig), collapse = ", "), ")"), 
#>             x = 0, y = 1, font = list(size = 16), xref = "paper", 
#>             yref = "paper", showarrow = FALSE)), yaxis = list(title = NA))
#>         p
#>     }, class = c("subchart", "plotly", "htmlwidget"))
#> }
#> <bytecode: 0x6109828d68a0>
#> <environment: namespace:cryptoQuotes>

Common for both indicator classes is that they are wrapped in structure, with class = c("plotly", "htmlwidget"),

structure(
  .Data = {
  
  # Indicator Logic
  
  },
  class = c(
    yourclass,
    "plotly", 
    "htmlwidget"
  )
)

What differentiates the two classes of indicators, is the addition of indicator or subchart in the yourclass-placeholder.

The indicator logic is important for the correct charting of your custom indicator. As the cryptoQuotes-package uses plotly as backend for charting, your class of indicator has to be consistent with the use of plotly-functions. More specifically; subchart-indicators uses plotly::plot_ly()-functions, while main chart indicator uses add_*-functions.

When creating the custom indicators there is a couple of additional steps needed which will be covered in the examples.

Donchian Channels (Example)

Assume a trading strategy based on Donchian Channels (TTR::DonchianChannel()) is needed to optimize your profits. This indicator is a main chart indicator, similar to that of TTR::BBands(),

tail(
  TTR::DonchianChannel(
    HL = BTC[,c("high", "low")]
  )
)
#>                      high      mid     low
#> 2024-05-31 17:00:00 69006 68133.80 67261.6
#> 2024-05-31 18:00:00 69006 67849.15 66692.3
#> 2024-05-31 19:00:00 69006 67849.15 66692.3
#> 2024-05-31 20:00:00 69006 67849.15 66692.3
#> 2024-05-31 21:00:00 69006 67849.15 66692.3
#> 2024-05-31 22:00:00 69006 67849.15 66692.3

This indicator has three features; high, mid and low. To chart this indicator, we would need to call the plotly::add_lines()-function three times to chart it properly. Each of these features are defined as layers in the cryptoQuotes-package. All layers get built with the cryptoQuotes:::build()-function.

## define custom TA
## donchian_channel
donchian_channel <- function(
    ## these arguments are the
    ## available arguments in the TTR::DonchianChannel
    ## function
    n = 10,
    include.lag = FALSE,
    ## the ellipsis
    ## is needed to interact with
    ## the chart-function
    ...
) {
  
  structure(
    .Data = {
      
      ## 1) define args
      ## as a list from the ellipsis
      ## which is how the chart-function
      ## communicates with the indicators
      args <- list(
        ...
      )
      
      ## 2) define the data, which in this
      ## case is the indicator. The indicator
      ## function streamlines the data so it works
      ## with plotly
      data <- cryptoQuotes:::indicator(
        ## this is just the ticker
        ## that is passed into the chart-function
        x = args$data,
        
        ## columns are the columns of the ohlc
        ## which the indicator is calculated on
        columns = c("high", "low"),
        
        ## the function itself 
        ## can be a custom function
        ## too.
        .f = TTR::DonchianChannel,
        
        ## all other arguments
        ## passed into .f
        n = n,
        include.lag = FALSE
      )
      
      ## each layer represents
      ## each output from the indicator
      ## in this case we have
      ## high, mid and low.
      ## 
      ## The lists represents a plotly-function
      ## and its associated parameters.
      layers <- list(
        ## high
        list(
          type = "add_lines",
          params = list(
            showlegend = FALSE,
            legendgroup = "DC",
            name = "high",
            inherit = FALSE,
            data = data,
            x    = ~index,
            y    = ~high,
            line = list(
              color = "#d38b68",
              width = 0.9
            )
          )
        ),
        
        ## mid
        list(
          type = "add_lines",
          params = list(
            showlegend = FALSE,
            legendgroup = "DC",
            name = "mid",
            inherit = FALSE,
            data = data,
            x    = ~index,
            y    = ~mid,
            line = list(
              color = "#d38b68",
              dash ='dot',
              width = 0.9
            )
          )
        ),
        
        ## low
        list(
          type = "add_lines",
          params = list(
            showlegend = FALSE,
            legendgroup = "DC",
            name = "low",
            inherit = FALSE,
            data = data,
            x    = ~index,
            y    = ~low,
            line = list(
              color = "#d38b68",
              width = 0.9
            )
          )
        )
      )
      
      ## we can add ribbons
      ## to the main plot to give
      ## it a more structured look.
      plot <- plotly::add_ribbons(
        showlegend = TRUE,
        legendgroup = 'DC',
        p = args$plot,
        inherit = FALSE,
        x = ~index,
        ymin = ~low,
        ymax = ~high,
        data = data,
        fillcolor = cryptoQuotes:::as_rgb(alpha = 0.1, hex_color = "#d38b68"),
        line = list(
          color = "transparent"
        ),
        name = paste0("DC(", paste(c(n), collapse = ", "), ")")
      )
      
      ## the plot has to be build
      ## using the cryptoQuotes::build-function
      invisible(
        cryptoQuotes:::build(
          plot,
          layers = layers
        )
      )
      
    }
  )
  
}

The indicator function can be passed into the appropriate argument in the chart()-function, which will handle everything else,

chart(
  ticker = BTC,
  main   = kline(),
  sub    = list(
    volume()
  ),
  indicator = list(
    bollinger_bands(),
    donchian_channel()
  )
)

Commodity Channel Index (Example)

Assume a trading strategy based on Commodity Channel Indices (TTR::CCI()) is needed to optimize your profits. This indicator is subchart indicator similar to that of TTR::RSI(),

tail(
  TTR::CCI(
    HLC = BTC[,c("high", "low", "close")]
  )
)
#>                            cci
#> 2024-05-31 17:00:00 -290.27510
#> 2024-05-31 18:00:00 -266.68182
#> 2024-05-31 19:00:00 -160.89230
#> 2024-05-31 20:00:00 -127.22393
#> 2024-05-31 21:00:00  -66.20187
#> 2024-05-31 22:00:00  -50.50047

This indicator has a single feature; cci. As this indicator is a subchart indicator with a single feature, we only need a single layer built with plot_ly(),

## define custom TA
## Commodity Channel Index (CCI)
cc_index <- function(
    ## these arguments are the
    ## available arguments in the TTR::CCI
    ## function
    n = 20,
    maType,
    c = 0.015,
    ## the ellipsis
    ## is needed to interact with
    ## the chart-function
    ...
) {
  
  structure(
    .Data = {
      
      ## 1) define args
      ## as a list from the ellipsis
      ## which is how the chart-function
      ## communicates with the indicators
      args <- list(
        ...
      )
      
      ## 2) define the data, which in this
      ## case is the indicator. The indicator
      ## function streamlines the data so it works
      ## with plotly
      data <- cryptoQuotes:::indicator(
        ## this is just the ticker
        ## that is passed into the chart-function
        x = args$data,
        
        ## columns are the columns of the ohlc
        ## which the indicator is calculated on
        columns = c("high", "low", "close"),
        
        ## the function itself 
        ## can be a custom function
        ## too.
        .f = TTR::CCI,
        
        ## all other arguments
        ## passed into .f
        n = n,
        maType = maType,
        c      = c
      )
      
      
      layer <- list(
        list(
          type = "plot_ly",
          params = list(
            name = paste0("CCI(", n,")"),
            data = data,
            showlegend = TRUE,
            x = ~index,
            y = ~cci,
            type = "scatter",
            mode = "lines",
            line = list(
              color = cryptoQuotes:::as_rgb(alpha = 1, hex_color = "#d38b68"),
              width = 0.9
            )
          )
          
        )
      )
      
      cryptoQuotes:::build(
        plot = args$plot,
        layers = layer,
        annotations = list(
          list(
            text = "Commodity Channel Index",
            x = 0,
            y = 1,
            font = list(
              size = 18
            ),
            xref = 'paper',
            yref = 'paper',
            showarrow = FALSE
          )
        )
      )
      
      
      
    }
  )
  
}
chart(
  ticker = BTC,
  main   = kline(),
  sub    = list(
    volume(),
    cc_index()
  ),
  indicator = list(
    bollinger_bands(),
    donchian_channel()
  )
)

Summary

Creating custom indicators for the chart()-functions can be daunting. Two examples of how these are developed in th e cryptoQuotes-packages have been covered.

Note: A full pipeline of charting indicators, custom and built-in, will be released sometime in the future.

To summarise the example,

  1. Define the indicator-function (e.g TTR::CCI())
  2. Define the chart-function for the indicator (e.g cc_index())
    1. Wrap the indicator-function in cryptoQuotes:::indicator()
    2. Define the layers according to features, and wether its a subchart or main chart indicator
    3. Build the chart using cryptoQuotes:::build()
  3. Add the chart-function for the indicator in the appropriate argument in the chart()-function