R/trade.R

Defines functions trade

Documented in trade

#' trade
#'
#' Model deployment and reporting
#'
#' The only user-accessible function of this package, it optimizes a portfolio
#' of various convergence/divergence models across multiple asset classes and
#' timeframes, generating the appropriate number of shares to buy.  That
#' information is then relayed to TD Ameritrade via the \code{rameritrade}
#' package so that the code can be deployed.
#'
#' \strong{This package serves as a non-profitable example of a portfolio of
#' quantitative trading models.  It should not be used by anyone.}
#'
#' \strong{Please read the \emph{Notes} section in its entirety.}
#'
#' @param action character; 'cron', 'force', or 'report' (see \emph{Examples})
#' @param leverageFactors numeric; leverages for BAV, GANN, IKM, and KDA, respectively
#' @param maxLeverage numeric; the maximum number of units allowed (see \emph{Examples})
#' @param wealthScale numeric; multiplier which adjusts the amount of capital allocated (see \emph{Examples})
#' @param fullPath character; the full path to the directory containing sensitive TD credentials (see Note below)
#'
#' @return Depending on the \code{action} argument, returns nothing or the number of shares (see \emph{Examples}; also creates data files and trade reports).
#'
#' @note In the directory indicated by the argument \code{fullPath}, you should place 2 files:
#' - consumerKey.rds
#' - refreshToken.rds
#' @note See the \code{rameritrade} documentation under \emph{References} for further directions on creating these files.
#' @note If you change your password with TD Ameritrade, you will need to generate a new Refresh Token.
#'
#' @note You should make 2 cronjobs when executing this function if \code{action} is missing or set to \code{cron}:
#' - One from around 12h57-8 EST (in the event the market closes at 13h EST)
#' - One from around 15h57-8 EST (for normal market close at 16h EST)
#' @note The cronjob time is dependent upon:
#' - The connection speed and processing power of the VPS
#' - The date (on the last/first trading day of the month, for instance, parameters will be optimized)
#' - If this is your first time running the function (parameters will be optimized)
#' @note See the \emph{References} section for a link to \emph{Crontab Guru}.
#' @note Note that this function should take about 1 minute to execute.
#' @note If the system time of the VPS is different than \code{America/New_York}, an error will be thrown; edit the \code{trade} function to remove/change this feature.
#'
#' @note If you have more than one account number, only the first account specified by \code{rameritrade::td_accountData()} will be used.  Manually edit the \code{make_orders} function if you would like to change this.
#' @note This function will generate a directory named \emph{data} in your working directory.  You can ignore this directory, but do not delete it because doing so will result in this function taking longer to execute.  The directory contains no sensitive data.
#' @note Your timezone will be changed to 'America/New_York'.  If you are not using a GNU/Linux system, an error may be generated by this feature and you will need to manually edit the first 'if' statement of this function.
#'
#' @references
#' \url{https://github.com/exploringfinance/rameritrade/}
#' \cr \url{https://developer.tdameritrade.com/}
#' \cr \url{https://developer.tdameritrade.com/content/place-order-samples}
#' \cr \url{https://www.wintick.com/members/symbolGuide/tda}
#' \cr \url{https://crontab.guru/}
#' \cr \url{https://quantstrattrader.com/2019/02/27/kda-robustness-results/}
#' \cr \url{https://www.nyse.com/markets/hours-calendars}
#'
#' @importFrom magrittr '%<>%' '%T>%'
#' @export
#' @examples
#' \dontrun{
#' # ACTION
#' ## Normal usage when deployed automatically on a VPS
#' ## Intended to be triggered by a cronjob just before market close
#' ## Regardless of choice for the action argument, data files are created
#' ## in ./data which are used for internal processing; also creates
#' ## ~/fullPath/tradeReport.rds if action == 'cron', 'force', or is missing
#' trade()
#' trade('cron')
#'
#' ## Manually forces a trade regardless of time and creates
#' ## ~/fullPath/tradeReport.rds
#' trade('force')
#'
#' ## Does not trade at all, but rather just reports the shares that will be
#' ## traded
#' trade('report')
#'
#' # ACCESSING THE TRADE REPORT
#' readRDS('~/fullPath/tradeReport.rds')
#'
#' # LEVERAGE FACTORS
#' ## Adjust the leverage used for each strategy to your risk:return framework
#' ## The ordering is: BAV, GANN, IKM, KDA
#' ## It's okay if the sum of leverageFactors exceeds the leverage provided by
#' ## your broker because the number of units allocated is limited by the
#' ## maxLeverage argument
#' trade(leverageFactors = c(0.5, 0.5, 0, 1.5))
#'
#' # MAX LEVERAGE
#' ## Adjust how many units you want to buy, leaving some headroom for drawdowns
#' ## and HTB securities
#' ## In the below function call, despite 1 unit being allocated to each model,
#' ## only 2 units will be purchased at a time because models are inactivated
#' ## based on their order; if BAV and GANN are both active, for example, they
#' ## take priority, so no capital will be allocated to GANN, IKM, or KDA even
#' ## if thoses models generate a long signal at the same time
#' trade(leverageFactors = c(1, 1, 1, 1), maxLeverage = 2)
#'
#' # WEALTH SCALE
#' ## Example: only allocate half of your net liquidating value (eg, NLV, wealth)
#' trade(wealthScale = 0.5)
#'
#' ## Example: If you have a $100k account, the maximum value of this portfolio
#' ## will be $60k (you allocate 1/5 of your NLV to the portfolio which is $20k,
#' ## and you use margin to buy up to 3 units)
#' trade(maxLeverage = 3, wealthScale = 0.2)
#'
#' # FULL PATH
#' ## NB: this directory contains two sensitive files for the TD API, as well as
#' ## trade reports
#' ## Keep this data in a safe place and make GPG-encrypted backups
#' trade(fullPath = '~/my/secret/path')
#' ## or...
#' trade(fullPath = '~/my/secret/path/')
#' }

trade <- function(action,
                  leverageFactors = c(1, 1, 1, 1.5),
                  maxLeverage = 2.5,
                  wealthScale = 1,
                  fullPath = '~/td')
{
  # timezone
  if (!(Sys.timezone() == 'America/New_York')) {
    system('timedatectl set-timezone America/New_York')
    cat('\nYour system timezone has been changed to America/New_York\n\n')
  }

  # formal input fixes
  if (missing(action)) action <- 'cron'

  if (length(action) > 1)
    stop('See the action argument: length should be equal to 1')

  if (!(action %in% c('cron', 'force', 'report')))
    stop('See the action argument: please check documentation for correct inputs')

  if (length(leverageFactors) != 4)
    stop('See leverageFactors argument: length must equal 5')

  if (maxLeverage < 0) maxLeverage <- 0

  leverageFactors %<>% {.} %T>%
    {.[sapply(., function(x) x < 0)] <- 0}
  leverageFactors %<>% {.} %T>%
    {.[sapply(., function(x) x > maxLeverage)] <- maxLeverage}

  if (wealthScale > 1) wealthScale <- 1
  if (wealthScale < 0) wealthScale <- 0

  # td file handling
  if (!dir.exists(fullPath))
    stop('See the fullPath argument: directory doesn\'t exist')

  if (substring(fullPath, nchar(fullPath), nchar(fullPath)) != '/')
    fullPath <- paste0(fullPath, '/')

  fp_files <- c('consumerKey.rds', 'refreshToken.rds')
  if (!all(fp_files %in% list.files(fullPath)))
    stop('Please add necessary files to fullPath directory (see documentation)')

  # dates handling
  ## make dates if it doesn't exist
  if (!dir.exists('data')) dir.create('data')
  if (!file.exists('data/dates.rds')) make_dates()
  dates <- readRDS('data/dates.rds')

  ## if the dates rds is from last year, update
  if (lubridate::year(Sys.Date()) != lubridate::year(dates[[1]][1])) {
    make_dates()
    dates <- readRDS('data/dates.rds')
  }

  # determine if the market is open and if there is a 13h close
  market_open <- Sys.Date() %in% dates$market_open_dates
  early_close <- Sys.Date() %in% dates$early_close_dates

  get_access(fullPath)

  if (action == 'cron') {

    # confirm cronjob at 12h??, if the market is open and closes at 13h, trade
    if (lubridate::hour(Sys.time()) == 12 & market_open & early_close) {

      order(trade = TRUE,
            leverageFactors = leverageFactors,
            maxLeverage = maxLeverage,
            wealthScale = wealthScale,
            fullPath = fullPath,
            dates = dates)

    # confirm cronjob at 15h??, if the market is open and closes at 16h, trade
    } else if (lubridate::hour(Sys.time()) == 15 & market_open & !early_close) {

      order(trade = TRUE,
            leverageFactors = leverageFactors,
            maxLeverage = maxLeverage,
            wealthScale = wealthScale,
            fullPath = fullPath,
            dates = dates)

    } else {

      cat('\nAutotrade temporal conditions not met; nothing was done\n\n')

    }

  } else if (action == 'force') {

    order(trade = TRUE,
          leverageFactors = leverageFactors,
          maxLeverage = maxLeverage,
          wealthScale = wealthScale,
          fullPath = fullPath,
          dates = dates)

  } else if (action == 'report') {

    order(trade = FALSE,
          leverageFactors = leverageFactors,
          maxLeverage = maxLeverage,
          wealthScale = wealthScale,
          fullPath = fullPath,
          dates = dates)

  }
}
causality-loop/mnmt documentation built on June 17, 2022, 5:14 a.m.