R/helper.R

Defines functions check_indicator_call normalize as_rgb bar chart_theme chart_layout pkg_header pkg_information default_dates convert_date movement_color flatten coerce_date check_internet_connection annotations vline pull assert is.date infer_interval find_mode to_title build indicator

Documented in assert chart_layout convert_date default_dates flatten is.date

# script: helpr
# date: 2023-09-22
# author: Serkan Korkmaz, serkor1@duck.com
# objective: A class of helper
# function
# script start;
indicator <- function(
    x,
    columns = NULL,
    .f = NULL,
    ...) {

  x <- tryCatch(
    expr = {

      xts::as.xts(
        x
      )

    },
    error = function(error) {

      assert(
        FALSE,
        error_message = c(
          "x" = "Could not coerce to {.cls xts}",
          "i" = error$message
        )
      )

    }
  )

  x <- do.call(
    cbind,
    # Set  names here
    # and remove from pull
    stats::setNames(
      lapply(
        X = if (is.null(columns)) names(x) else columns,
        FUN = pull,
        from = x
      ),
      nm = if (is.null(columns))  names(x) else columns
    )
  )

  # 1) get the indicator function
  # for each
  if (!is.null(.f)) {

    x <- .f(
      x,
      ...
    )

  }

  names(x) <- tolower(names(x))

  zoo::fortify.zoo(
    x,
    names = "index"
  )

}

# var_ly <- function(
    #     variable) {
#
#   # 0) extract variable
#   # from the source
#   variable <- grep(
#     pattern     = variable,
#     x           = names(get("data",envir = parent.frame())),
#     ignore.case = TRUE,
#     value       = TRUE
#   )
#
#   # 1) assert variable
#   # existance
#   assert(
#     !identical(
#       variable,
#       character(0)
#     ) & length(variable) == 1,
#     error_message = c(
#       "x" = "Error in {.val variable}"
#     )
#   )
#
#   # 2) return as formula
#   as.formula(
#     paste(
#       '~', variable
#     )
#   )
#
# }




build <- function(
    plot,
    layers,
    ...
) {

  # 0) generate function
  apply_layer <- function(
    plot,
    layer
  ) {

    # Generalize function calling based on the 'type' attribute

    # The Plotly function to call, e.g., "add_lines", "add_ribbons"
    fun_name <- layer$type

    # Ensure the plot is passed as the first argument
    layer$params$p <- plot

    if (!"data" %in% names(layer$params)) {
      # If 'data' is not explicitly provided,
      #  assume the plot's original data should be used
      layer$params$data <- plot$x$data
    }

    # Dynamically call the Plotly function with
    #  the parameters specified in 'layer$params'
    do.call(
      get(
        fun_name,
        envir = asNamespace('plotly')
      ), args = layer$params
    )

  }

  plotly::layout(
    p =  Reduce(
      f = apply_layer,
      x = layers,
      init = plot
    ),
    ...
  )


}




# converting Quotes to and from data.frames; ####

to_title <- function(
    x) {

  gsub("\\b(.)", "\\U\\1", tolower(x), perl = TRUE)
}




find_mode <- function(
    x) {

  # 1) create a trable
  # of values
  frequency_table <- table(
    x
  )

  # 2) find the mode
  # and return as character
  value <- as.character(
    names(frequency_table)[which.max(frequency_table)]
  )

  value

}


infer_interval <- function(
    x) {


  # 0) extract
  # index
  index <- zoo::index(
    utils::head(
      x = x,
      # n should be the minimum
      # of available rows and 7. 7
      # was chosen randomly, but its important
      # that its odd numbered so consensus can be
      # reached. This application is 20x faster
      # than using the entire dataset
      # and reaches the same conclusion
      n = min(nrow(x), 7)
    )
  )

  # 1) calculate
  # differences
  x <- as.numeric(
    difftime(
      time1 = index[-1],
      time2 = index[-length(index)],
      units = "secs"
    )
  )

  x <- find_mode(x)


  switch(
    x,
    "1" = "1s",
    "60" = "1m",
    "180" = "3m",
    "300" = "5m",
    "900" = "15m",
    "1800" = "30m",
    "3600" = "1h",
    "7200" = "2h",
    "14400" = "4h",
    "21600" = "6h",
    "28800" = "8h",
    "43200" = "12h",
    "86400" = "1d",
    "259200" = "3d",
    "604800" = "1w",
    "1209600" = "2w",
    "1296000" = "2w",
    "2678400" = "1M",
    "2592000" = "1M",
    "2419200" = "1M",
    "2505600" = "1M",
    NULL
  )



}


#' Check if values are valid dates
#'
#' @description
#' This function check is equivalent to [is.numeric()], [is.logical()],
#' and checks for the date type classes POSIXct, POSIXt and Date.
#' And wether the character vector can be formatted to dates.
#'
#' @param x object to be tested
#'
#'
#' @family development tools
#' @keywords internal
#' @returns [TRUE] if its either POSIXct, POSIXt or Date. [FALSE] otherwise.
is.date <- function(x){

  # check if its
  # a date
  indicator <- inherits(
    x = x,
    what = c("Date","POSIXct","POSIXt")
  )


  if (!indicator & is.character(x)){


    indicator <- tryCatch(
      expr = {
        # Either of these have to be
        # non-NA to work
        !is.na(as.POSIXct(x)) | !is.na(
          as.POSIXct(
            x,
            format = "%Y-%m-%d %H:%M:%S")
        )
      },
      error = function(error){

        FALSE

      }
    )

  }

  indicator

}

#' Assert truthfulness of conditions before evaluation
#'
#'
#' @description
#' This function is a wrapper of [stopifnot()], [tryCatch()] and
#' [cli::cli_abort()] and asserts the truthfulness of the passed expression(s).
#' @param ... expressions >= 1. If named the names are used
#' as error messages, otherwise R's internal error-messages are thrown
#'
#' @param error_message character. An error message, supports [cli::cli]-formatting.
#' @example man/examples/scr_assert.R
#' @seealso [stopifnot()], [cli::cli_abort()], [tryCatch()]
#' @keywords internal
#' @returns [NULL] if all statements in ... are [TRUE]
assert <- function(..., error_message = NULL) {

  # 1) count number of expressions
  # in the ellipsis - this
  # is the basis for the error-handling
  number_expressions <- ...length()
  named_expressions  <- ...names()


  # 2) if there is more than
  # one expression the condtions
  # will either be stored in an list
  # or pased directly into the tryCatch/stopifnot
  if (number_expressions != 1 & !is.null(named_expressions)){

    # 2.1) store all conditions
    # in a list alongside its
    # names
    conditions <- c(...)

    # 2.2) if !is.null(condition_names) the
    # above condition never gets evaluated and
    # stopped otherwise, if there is errors
    #
    # The condition is the names(list()), and is
    # the error messages written on lhs of the the assert
    # function
    if (all(conditions)) {

      # Stop the funciton
      # here if all conditions
      # are [TRUE]
      return(NULL)

    } else {

      cli::cli_abort(
        message = c(
          "x" = named_expressions[which.min(conditions)]
        ),
        call = sys.call(
          1 - length(sys.calls())
        )
      )

    }

  }

  # 3) if there length(...) == 1 then
  # above will not run, and stopped if anything

  tryCatch(
    expr = {
      eval.parent(
        substitute(
          stopifnot(exprs = ...)
        )
      )
    },
    error = function(error){

      # each error message
      # has a message and call
      #
      # the call will reference the caller
      # by default, so we need the second
      # topmost caller

      cli::cli_abort(
        # 3.1) if the length of expressions
        # is >1, then then the error message
        # is forced to be the internal otherwise
        # the assert function will throw the same error-message
        # for any error.
        message = if (is.null(error_message) || number_expressions != 1)
          error$message else
            error_message,
        call    = sys.call(
          1 - length(sys.calls())
        )
      )

    }
  )

}


pull <- function(
    from,
    what = "Open") {


  # 0) identify column
  # by name
  column <- grep(
    pattern = what,
    x       = colnames(from),
    ignore.case = TRUE,
    value = TRUE
  )

  assert(
    !identical(character(0), column),
    error_message = c(
      "x" = sprintf(
        fmt = "Could not find column {.val %s}",
        what
      )
    )
  )


  stats::setNames(
    do.call(
      what = `$`,
      args = list(
        from,
        column
      )
    ),
    nm = tolower(what)
  )

}


# Plotly parameters; ####
vline <- function(
    x = 0,
    col = 'steelblue'
) {

  list(
    type = "line",
    y0 = 0,
    y1 = 1,
    yref = "paper",
    x0 = x,
    x1 = x,
    line = list(
      color = col,
      dash="dot"
    )
  )

}

annotations <- function(
    x = 0,
    text = 'text'
) {

  list(
    x = x,
    y = 1,
    text = text,
    showarrow = FALSE,
    #xref = 'paper',
    yref = 'paper',
    xanchor = 'right',
    yanchor = 'auto',
    xshift = 0,
    textangle = -90,
    yshift = 0,
    font = list(
      size = 15,
      # color = "black",
      angle = '90'
    )
  )


}



check_internet_connection <- function() {

  # 0) check internet connection
  # before anything
  assert(
    curl::has_internet(),
    error_message = c(
      "x" = "You are currently not connected to the internet."
    )
  )

}


coerce_date <- function(x){

  if (!is.null(x)) {

    as.POSIXct(
      x = x,
      tz = Sys.timezone(),
      origin = "1970-01-01"
    )
  } else {

    NULL

  }


}

# general helpers; ####
#' flatten nested lists
#'
#' @description
#' `r lifecycle::badge("stable")`
#'
#' Flatten a nested [list], regardless of its level of nesting.
#'
#' @param x A [list]
#'
#' @example man/examples/devtools_flatten.R
#'
#' @family development tools
#'
#' @returns An unnested [list]
#'
#' @keywords internal
flatten <- function(x) {

  if (!inherits(x, "list"))
    list(x)
  else
    unlist(c(lapply(x, flatten)), recursive = FALSE)
}



# base-chart colors; ####
movement_color <- function(
    deficiency = FALSE) {

  palette  <- c("#d3ba68","#d5695d","#5d8ca8","#65a479")
  location <- if (deficiency) c(3,1) else c(4,2)

  list(
    bullish = palette[location[1]],
    bearish = palette[location[2]]
  )

}



#' Convert dates passed to UNIX
#'
#' @description
#' This function converts dates to UNIX time if passed to the API, and converts
#' it to [POSIXct] from API
#'
#'
#' @param x a [numeric] vector, or [date]-type object
#' @param multiplier [numeric]
#'
#' @details
#' If x is numeric, then the function assumes
#' that its a return value
#'
#' @family development tools
#'
#' @keywords internal
#' @returns A vector of same length as x.
convert_date <- function(
    x,
    multiplier) {


  # NOTE: If its numeric its a return
  # value from the API
  is_numeric <- is.numeric(x)

  # calculate scale
  # factor
  #
  # If the values are numeric
  # it is returned from the
  # API
  scale_factor <- multiplier ** if (is_numeric) -1 else 1

  if (is_numeric) {

    # NOTE: Only this part
    # needs to be in try

    x <- tryCatch(
      expr = {
        as.POSIXct(
          x = x * scale_factor,
          tz = Sys.timezone(),
          origin = "1970-01-01"
        )
      },
      error = function(error){
        assert(
          FALSE,
          error_message = sprintf(
            fmt = "
            Unexpected error. Contact the package maintainer or submit a %s.
            ",
            cli::style_hyperlink(
              text = cli::col_br_red("bug report"),
              url = "https://github.com/serkor1/cryptoQuotes"
            )
          )
        )
      }
    )

  } else {

    # NOTE: All dates passed into
    # this function from
    # the get_*-function are already
    # validated and checked so we
    # can just go ahead and make the numeric values
    x <- as.numeric(x) * scale_factor


  }

  x

}


#' Get the minimum and maximum date range
#'
#'
#' @description
#' `r lifecycle::badge("experimental")`
#'
#' All [available_exchanges()] have different limitations on the
#' returned data this function is adaptive in nature so enforces
#' compliance on the limits
#'
#' @inheritParams get_quote
#' @param length a [numeric]-value of [length] 1.
#' The desired distance between `from` and `to`.
#'
#'
#' @returns
#'
#' A vector of minimum and maximum dates.
#'
#'
#' @family development tools
#' @keywords internal
#' @author Serkan Korkmaz
#'
default_dates <-function(
    interval,
    from   = NULL,
    to     = NULL,
    length = 200,
    limit  = NULL) {


  # 1) Determine parameters
  # passed futher;
  current_time <- Sys.time()
  origin_date <- '1970-01-01'

  # 1.1) check if from date is
  # provided
  is_from_provided <- !is.null(from)

  # 1.2) if a from date is provided
  # the operation is always
  # adding values
  operation <- if (is_from_provided) "+" else "-"


  # 1.3) determine starting point
  # if from is no provided
  # use Sys.time. Truncate to nearest
  # 15 minutes
  starting_point_time <- if (is_from_provided) from else current_time

  starting_point <- as.POSIXct(
    trunc(as.double(starting_point_time)/(15*60))*(15*60),
    tz = Sys.timezone(),
    origin = origin_date
  )

  # 2) Construct the returning
  # intervals based on granularity
  # and units

  # 2.1) Extract the granularity
  # by removing the numbers
  # of the intervals; this is a
  # reverse operation
  #
  # Returns either s, m, h, d, w, M
  # based on supplied intervals
  granularity <-  gsub(
    pattern = "([0-9]*)",
    x = interval,
    replacement = ""
  )

  # 2.2) Extract the numerical
  # value of the interval -
  # all intervals is 1m, 2w, etc
  # which has to be supplied
  # to sequence
  value <- as.integer(
    gsub("([a-zA-Z]+)", "", interval)
  )

  # 2.3) translate the granularity
  # so it can passed into seq
  # accordingly using switc;
  #
  # This change gains 10-15% in speed
  # over using multiple if-statements
  granularity <- switch(
    EXPR = granularity,
    s = "secs",
    m = "mins",
    h = "hours",
    w = "weeks",
    d = "days",
    M = "months"
  )

  # 2.4) construct interval
  # from starting point and return
  # 100 (or 100 if daily) values
  interval_length <- length + (granularity == "days")

  interval_seq <- seq(
    from = starting_point,
    by = paste0(operation, value, " ", granularity),
    length.out = interval_length
  )

  if (!is.null(limit)) interval_seq <-  utils::head(interval_seq, limit)

  # 3) construct the interval
  # by extracing the min date (from)
  # and the max date in the constructed
  # interval; this has to be limited
  # by sys.date to avoid calling values
  # that havent been realized yet... for obvious reasons...
  interval <- list(
    from = min(interval_seq),
    to   = min(
      max(interval_seq),
      as.POSIXct(
        current_time,
        tz = Sys.timezone(),
        origin = origin_date
      )
    )
  )

  # 4) return statement
  # as interval
  interval

}


# pkg-startup; ####

# This information contains
# links to the development blog, github source code
# and guides
pkg_information <- function(){

  # 1) wrattep in format
  # inline to ensure that the
  # message be supressed at startup
  cli::format_inline(
    c(
      paste(
        cli::col_br_red(cli::symbol$heart),
        cli::style_hyperlink(
          text = cli::col_br_blue('release notes'),
          url = 'https://serkor1.github.io/cryptoQuotes/news/index.html'
        ),
        "\n"
      ),
      # Source code  link
      paste(
        cli::col_br_yellow(cli::symbol$star),
        "Browse the",
        cli::style_hyperlink(
          text = cli::col_br_blue("source code"),
          url = "https://github.com/serkor1/cryptoQuotes/"
        ),
        "\n"
      ),

      # link to pkgdown website
      paste(
        cli::col_br_yellow(cli::symbol$star),
        "Read the",
        cli::style_hyperlink(
          text = cli::col_br_blue("documentation"),
          url = 'https://serkor1.github.io/cryptoQuotes/'
        ),
        "\n"
      ),

      paste(
        cli::col_br_yellow(cli::symbol$star),
        "Join the",
        cli::style_hyperlink(
          text = cli::col_br_blue("discussion"),
          url = 'https://github.com/serkor1/cryptoQuotes/discussions'
        )
      )
    )
  )

}


# This is the cryptoQuotes
# header that prints the line
# with pkgname and version
pkg_header <- function(
    pkgname) {


  cli::cli(
    cli::cli_h1(
      text =  paste(
        pkgname,
        utils::packageVersion(
          pkgname
        )
      )
    )

  )

}



#' Create a list of layout elements on subcharts
#'
#' @param x [integer]-vector of [length] 1.
#' @param layout_element [character]-vector of [length] 1.
#' [plotly::layout] elements. See example.
#' @param layout_attribute [character]-vector of [length] 1.
#' [plotly::layout] element value. See example.
#'
#' @examples
#' \dontrun{
#' chart_layout(
#'   x = 1:plot_list_length,
#'   layout_element = "yaxis",
#'   layout_attribute = list(
#'   gridcolor = if (dark) "#40454c" else  '#D3D3D3' # Was CCCCCC
#'     )
#' )
#' }
#'
#'
#' @return A [list] of layout elements.
#' @keywords internal
#' @family development tools
chart_layout <- function(
    x,
    layout_element,
    layout_attribute) {

  stats::setNames(
    lapply(
      0:x,
      function(i){

        layout_attribute

      }


    ),
    nm = paste0(layout_element, c("", 1:x))
  )

}


chart_theme <- function(
    dark) {

  if (dark) {
    list(
      paper_bgcolor = '#2b3139',
      plot_bgcolor  = '#2b3139',
      font_color    = '#848e9c',
      grid_color    = '#40454c'
    )
  } else {
    list(
      paper_bgcolor = '#E3E3E3',
      plot_bgcolor  = '#E3E3E3',
      font_color    = '#A3A3A3',
      grid_color    = '#D3D3D3'
    )
  }
}


bar <- function(
    dark,
    plot,
    name,
    market,
    date_range,
    modebar,
    scale,
    ...) {

  # 0) chart theme
  theme <- chart_theme(dark = dark)

  title_text <- if (!is.null(market))
    sprintf(
      "<b>Ticker:</b> %s <b>Market:</b> %s<br><sub><b>Period:</b> %s</sub>",
      name,
      market,
      date_range
    )
  else
    sprintf(
      "<b>Ticker:</b> %s<br><sub><b>Period:</b> %s</sub>",
      name,
      date_range
    )

  plot <- plotly::layout(
    p = plot,
    margin = list(l = 5, r = 5, b = 5, t = if(modebar) 85 else 55),
    paper_bgcolor = theme$paper_bgcolor,
    plot_bgcolor  = theme$plot_bgcolor,
    font = list(
      size = 14 * scale,
      color = theme$font_color
    ),
    showlegend = TRUE,
    legend = list(
      orientation = 'h',
      x = 0,
      y = 100,
      yref="container",
      title = list(
        text = "<b>Indicators:</b>",
        font = list(
          size = 16 * scale
        )
      )
    ),
    title = list(
      text = title_text,
      font = list(
        size = 20 * scale
      ),
      x = 1,
      xref = "paper",
      xanchor = "right"
    )

  )


  do.call(
    what = plotly::layout,
    args = c(
      list(plot),
      chart_layout(
        x = length(plot),
        layout_element = "yaxis",
        layout_attribute = list(
          gridcolor = theme$grid_color # Was CCCCCC
        )
      ),
      chart_layout(
        x = length(plot),
        layout_element = "xaxis",
        layout_attribute = list(
          gridcolor = theme$grid_color# was C3
        )
      )
    )
  )

}


as_rgb <- function(
    hex_color,
    alpha = NULL) {

  # Remove the '#' if present and convert to RGB values
  rgb_values <- grDevices::col2rgb(hex_color)

  # Format RGB values
  rgb_string <- sprintf(
    fmt = "rgb(%d, %d, %d)",
    rgb_values[1, ],
    rgb_values[2, ],
    rgb_values[3, ]
  )

  # Check if alpha is provided
  if (!is.null(alpha)) {

    assert(
      alpha >= 0 & alpha <= 1,
      error_message = c(
        "x" = sprintf(
          fmt = "{.arg alpha} has to be in ]0, 1[-range. Got {.val %s}.",
          alpha
        )
      )
    )

    # Append alpha for rgba() format
    rgb_string <- sprintf(
      "rgba(%d, %d, %d, %.2f)",
      rgb_values[1, ],
      rgb_values[2, ],
      rgb_values[3, ],
      alpha
    )
  }

  rgb_string
}

normalize <- function(
    x,
    range,
    value
) {

  # 0) get the minimum/maximum
  # of the vector
  min_x <- min(value, na.rm = TRUE)
  max_x <- max(value, na.rm = TRUE)

  # 1) scale x within
  # the range
  scaled_x <- abs(
    (x - min_x) / (max_x - min_x)
  )

  # factor
  factor_x <- (max(range) - min(range))

  # 3) create range
  # and return
  pmin(
    pmax(
      ceiling(
        scaled_x * factor_x
      ),
      1
    ),
    30
  )

}


check_indicator_call <- function(
    system_calls = sys.calls(),
    caller       = match.call(envir = parent.frame())) {

  # 0) get the entire call stack
  # to determine the calling function
  call_stack <- as.character(
    lapply(system_calls, `[[`, 1)
  )

  # 1) get the calling calling
  # function, ie. SMA, EMA etc
  calling_function <- sys.call(-1)

  calling_function <- as.character(
    calling_function[[1]]
    )[length(calling_function)]

  # 2) check the location
  # of chart
  location_chart <- which(call_stack == "chart")
  location_indicator <- which(call_stack == calling_function)


  # 3) assert that the indicator
  # is being called from the charting
  # function, or some wrapper around
  # chart
  assert(
    any(call_stack == "chart") & location_chart < location_indicator,
    error_message = c(
      "x" = sprintf(
        "The {.fn %s}-function is called outside {.fn chart}",
        call_stack
      ),

      "i" = paste(
        "Run",
        cli::code_highlight(
          code = "cryptoQuotes::chart(...)",
          code_theme = "Chaos"
        ),
        "to build charts."
      )
    )
  )

}

# script end;

Try the cryptoQuotes package in your browser

Any scripts or data that you put into this service are public.

cryptoQuotes documentation built on April 4, 2025, 2:33 a.m.