#' @importFrom httr GET POST PATCH DELETE
#' @importFrom dplyr filter bind_rows
#' @importFrom purrr modify_depth compact map map_chr map2 pmap map_if
#' @importFrom rlang list2 abort warn
#' @importFrom jsonlite toJSON
# orders ----
# Wed Apr 22 20:23:46 2020
#' @family Orders
#' @title Get Orders
#'
#' @description The orders API allows a user to monitor, place and cancel their orders with Alpaca. Times are returned as yyyy-mm-dd hh-mm-ss POSIXct, quantity and price as numeric, and all others as a string. See [Orders GET](https://alpaca.markets/docs/api-documentation/api-v2/orders#get-a-list-of-orders)for more details.
#' @param symbol_id \code{(character)} Specify symbol, order ID, or client_order_id (must set `client_order_id = TRUE`).
#' @param status \code{(character)} Order status to be queried. \code{open}, \code{closed} or \code{all}. Defaults to open as a string.
#' @param limit \code{(integer)} The maximum number of orders in response. Defaults to 50 and max is 500.
#' @param after \code{(Date/character)} Date in YYYY-MM-DD \href{https://www.iso.org/iso-8601-date-and-time-format.html}{(ISO8601 Format)} The response will include only orders submitted \emph{after} this date exclusive as a timestamp object.
#' @param until \code{(Date/character)} Date in YYYY-MM-DD \href{https://www.iso.org/iso-8601-date-and-time-format.html}{(ISO8601 Format)} The response will include only orders submitted \emph{before} this date exclusive as a timestamp object.
#' @param direction \code{(character)} The chronological order of response based on the submission time. \code{'asc'} or \code{'desc'}. Defaults to \code{desc}.
#' @param nested \code{(logical)} If true, the result will roll up multi-leg orders under the legs field of primary order. Default `TRUE`.
#' @param client_order_id \code{(logical)} Whether `symbol_id` is a client_order_id, defaults to `NULL (FALSE)`
#' @inheritParams account
#' @return Order `(tibble)` [Order object](https://alpaca.markets/docs/api-documentation/api-v2/orders/#order-entity) or Array of Order Objects with the following information:
#' \itemize{
#' \item{\code{id}}{`(character)` order id}
#' \item{\code{client_order_id}}{`(character)` client unique order id}
#' \item{\code{created_at}}{`(POSIXct)` nullable When the order was created}
#' \item{\code{updated_at}}{`(POSIXct)` nullable When the order was updated}
#' \item{\code{submitted_at}}{`(POSIXct)` nullable When the order was submitted}
#' \item{\code{filled_at}}{`(POSIXct)` nullable When the order was filled}
#' \item{\code{expired_at}}{`(POSIXct)` nullable When the order was expired}
#' \item{\code{canceled_at}}{`(POSIXct)` nullable When the order was canceled}
#' \item{\code{failed_at}}{`(POSIXct)` nullable When the order failed}
#' \item{\code{replaced_at}}{`(POSIXct)` nullable When the order was replaced}
#' \item{\code{replaced_by}}{`(character)` id of replacement order}
#' \item{\code{replaces}}{`(character)` id of the replaced order}
#' \item{\code{asset_id}}{`(character)` asset ID}
#' \item{\code{symbol}}{`(character)` Asset symbol}
#' \item{\code{exchange}}{`(character)` Asset exchange}
#' \item{\code{asset_class}}{`(character)` Asset class}
#' \item{\code{qty}}{`(integer)` Ordered quantity}
#' \item{\code{filled_qty}}{`(integer)` Filled quantity}
#' \item{\code{filled_avg_price}}{`(numeric)` Filled average price}
#' \item{\code{order_class}}{`(character)`}
#' \item{\code{order_type}}{`(character)`}
#' \item{\code{type}}{`(character)` Valid values: market, limit, stop, stop_limit}
#' \item{\code{side}}{`(character)` Valid values: buy, sell}
#' \item{\code{time_in_force}}{`(character)` time in force}
#' \item{\code{limit_price}}{`(numeric)` Limit price}
#' \item{\code{stop_price}}{`(numeric)` Stop price}
#' \item{\code{status}}{`(character)` Status of the order}
#' \item{\code{extended_hours}}{`(logical)` If true, eligible for execution outside regular trading hours}.
#' \item{\code{legs}}{`(character)` When querying non-simple order_class orders in a nested style, an array of Order entities associated with this order. Otherwise, null.}
#' }
#' @examples
#' orders(live = FALSE)
#' orders(status = "all")
#' # For a specific symbol:
#' orders("AAPL", status = "all")
#' @export
orders <-
function(symbol_id = NULL,
status = "open",
limit = NULL,
after = NULL,
until = NULL,
direction = "desc",
client_order_id = NULL,
nested = T,
live = get_live()) {
# Reassign for cases where symbol_id needs to be changed to some new value
.s_id <- symbol_id
# Set URL & Headers
headers = get_headers(live)
# set status if abbreviated
status <- match_letters(status, o = "open", c = "closed", a = "all")
# check if id
.is_id <- is_id(symbol_id)
if (isTRUE(client_order_id)) {
# if it's a client order id
.url <- get_url(
path = "orders:by_client_order_id",
query = list(client_order_id = symbol_id),
live = live
)
} else if (isTRUE(.is_id)) {
# if it's an order_id
.url <- get_url(
path = c("orders", symbol_id),
query = list(nested = nested),
live = live
)
} else {
# if it's a ticker symbol(s)
.s_id <- toupper(.s_id)
if(length(.s_id) > 1) .s_id <- paste(.s_id, collapse = ',')
.url <- get_url(
path = "orders",
query = list(
status = status,
limit = limit,
after = after,
until = until,
direction = direction,
nested = nested,
symbols = .s_id
),
live = live
)
}
# yogat3ch: Create Query 2020-01-11 2157
if (isTRUE(get0(".dbg", envir = .GlobalEnv, mode = "logical", inherits = F))) message(paste0(.url))
# Query
out <- httr::GET(.url, headers)
# Clean
out <- order_transform(out)
return(out)
}
# order_submit ----
# Wed Apr 22 20:23:21 2020
#' @family Orders
#' @title Submit, Cancel & Replace Orders,
#' @description Places/Replaces/Cancels an order, or cancels all orders depending on argument to `action`. See parameter documentation and [Orders](https://alpaca.markets/docs/api-documentation/api-v2/orders) for details.Depending on the `action` specified, some arguments are required:
#' \itemize{
#' \item{\code{action = 'submit'}}{ All arguments can be submitted. See Arguments for which are *required*.}
#' \item{\code{action = 'replace'}}{ `qty`, `time_in_force`, `limit`, `stop`, `client_order_id` are all eligible. Only one is *required*.}
#' \item{\code{action = 'cancel'}}{ Only `symbol_id` is *required*.}
#' \item{\code{action = 'cancel_all'}}{ No arguments necessary.}
#' }
#' @param symbol_id \code{(character)} The stock symbol (*Required* when `action = "submit"`) or Order object (single row tibble) (*Required* when `action = "cancel"/"replace"`).
#' To expedite the setting of stops and limits for open positions, an Order ID from a `'buy'` order can be provided when `action = "submit"` to place a `'sell'` order with the following parameters such that they do not need to be set manually:
#' \itemize{
#' \item{\code{side = 'sell'}}
#' \item{If \code{qty} is not provided, it will be populated from the buy order}
#' \item{`symbol_id` will be set to the symbol from the buy order.}
#' \item{If `client_order_id = TRUE`, the `client_order_id` will be set to the buy Order ID provided, effectively linking the orders for your records.}
#' \item{All other parameters can be specified as usual.}
#' }
#' @param action \code{(character)} The action to take:
#' \itemize{
#' \item{\code{"submit"/"s"}}{ [Request a new order](https://alpaca.markets/docs/api-documentation/api-v2/orders/#request-a-new-order) **Default**}
#' \item{\code{"replace"/"r"}}{ [Replace an order](https://alpaca.markets/docs/api-documentation/api-v2/orders/#replace-an-order)}
#' \item{\code{"cancel"/"c"}}{ [Cancel an order](https://alpaca.markets/docs/api-documentation/api-v2/orders/#cancel-an-order)}
#' \item{\code{"cancel_all"}}{ [Cancel all orders](https://alpaca.markets/docs/api-documentation/api-v2/orders/#cancel-all-orders)}
#' }
#' @param qty \code{(integer)} The amount of shares to trade (*required* when `action = "submit"`, *optional* when `action = 'replace'`).
#' @param side \code{(character)} The side of the trade. I.E `"buy"/"b"` or `"sell"/"s"`. (*required* when `action = "submit"`). Assumed to be `"buy"` if `order_class = "bracket"`.
#' @param type \code{(character)} The type of trade order. I.E `"market"/"m"`,`"limit"/"l"`,`"stop"/"s"`,`"stop_limit"/"sl"`, `"trailing_stop"/"ts"` etc. Default `NULL`. Typically *required* except in certain situations where the value can be assumed:
#' \itemize{
#' \item{\code{stop} is set (and `type` is unset)}{ `type = "stop"/"s"`}
#' \item{\code{limit} is set}{ `type = "limit"/"l"`}
#' \item{\code{stop} & \code{limit} are set}{ `type = "stop_limit"/"sl"`}
#' \item{\code{order_class = "bracket"}}{ `type = "market"/"m"`}
#' \item{\code{order_class = "oco"}}{ `type = "limit"/"l"`}
#' \item{\code{order_class = "oto"}}{ `type = "market"/"m"`}
#' \item{\code{trail_price} is set}{ `type = "trailing_stop"`}
#' \item{\code{trail_percent} is set}{ `type = "trailing_stop"`}
#' }
#' See [Understand Orders](https://alpaca.markets/docs/trading-on-alpaca/orders/#bracket-orders) for details.
#' @param time_in_force \code{(character)} The time in force for the order. *Optional* when `action = "replace"`. Args can be `"day"`, `"gtc"`, `"opg"` etc. Default `"day"`. Please see [Understand Orders: Time in Force](https://alpaca.markets/docs/trading-on-alpaca/orders/#time-in-force) for all types and more info. Must be `"day"` or `"gtc"` for [Advanced Orders](https://alpaca.markets/docs/trading-on-alpaca/orders/#bracket-orders).
#' @param limit \code{(numeric)} limit price. *Required* if type is `"limit"` or `"stop_limit"` for `action = 'replace'/'submit'`.
#' @param stop \code{(numeric)} stop price. *Required* if type is `"stop"` or `"stop_limit"` for `action = 'replace'/'submit'`.
#' @param extended_hours \code{(logical)} Default \code{FALSE}. If \code{TRUE}, order will be eligible to execute in premarket/afterhours. Currently supported hours are: Pre-market: 9:00 - 9:30am, After-hours: 4:00 - 6:00pm ET. Only works with `type = 'limit'` and `time_in_force = 'day'` on the V2 API.
#' @param client_order_id \code{(character/logical)} <= 48 Characters. A unique identifier for the order. Automatically generated if not sent.
#' \itemize{
#' \item{\code{`action = 'replace'/'submit'`}}{ *Optional* character vector}
#' \item{\code{`action = 'submit'`}}{ If an Order object is provided to `symbol_id`, `TRUE` will set the `client_order_id` for the sell order to Order ID in `symbol_id`. Used to link buy & sell orders for your records.}
#' }
#' @param order_class \code{(character)} `'simple'`, `'bracket'`, `'oco'` or `'oto'`. *Required for advanced orders.* For details of non-simple order classes, please see [Advanced Orders](https://alpaca.markets/docs/trading-on-alpaca/orders#bracket-orders). If `order_class = 'bracket'/'oto'`, `type` can be omitted as it will always be `'market'`, this is also true with `order_class = "oco"` as `type` will always be `'limit'`. *Note* that order replacement is not supported for all advanced order types.
#' @param take_profit \code{(named list)} Additional parameters for take-profit leg of advanced orders:
#' \itemize{
#' \item{\code{'limit_price'/'l'}}{ \code{numeric} **required** for `'bracket'` & `'oco'` order classes.}
#' }
#' @param stop_loss \code{(named list)} Additional parameters for stop-loss leg of advanced orders
#' \itemize{
#' \item{\code{stop_price/s}}{ \code{numeric} **required** for bracket orders}
#' \item{\code{limit_price/l}}{ \code{numeric} The stop-loss order becomes a stop-limit order if specified. **Required** for `'bracket'` & `'oco'` order classes}
#' }
#' @param notional \code{num} dollar amount to trade. Cannot work with qty. Can only work for market order types and day for time in force.
#' @param trail_price \code{(numeric)} a dollar value away from the highest water mark (hwm). If you set this to 2.00 for a sell trailing stop, the stop price is always hwm - 2.00
#' @param trail_percent \code{(numeric)} a percent value away from the highest water mark. If you set this to 1.0 for a sell trailing stop, the stop price is always hwm * 0.99. Values less than 1 are assumed to be percentages, ie .07 = 7%, values must be less than 100.
#' @inheritParams account
#' @inherit orders return
#' @examples
#' \dontrun{
#' # most orders (except limit) must be placed during market hours or they will not be filled until the next trading day.
#' .c <- clock()
#' if (.c$is_open) {
#' #' (bo <- order_submit("AAPL", qty = 1, side = "buy", type = "market"))
#' # Or you can submit a limit order (`type` is assumed to be `"limit"` when only `limit` is set):
#' lt <- market_data("AAPL", timeframe = "lt")
#' (so <- order_submit("AAPL", q = 1, side = "s", limit = lt$p * .99))
#' # cancel an order with `action = "cancel"`. symbol_id can be either the id of the order to cancel or the order tbl object.
#' order_submit(so, a = "c")
#' # expedite a simple "sell" order by providing the id of a buy order. This can be linked to it's original buy order on the Alpaca side via the `client_order_id` by simply setting `client_order_id = T`
#' (so <- order_submit(bo, stop = lt$p * .95, client = TRUE)) # here the id is used
#' identical(so$client_order_id, bo$id)
#' # replace `"r"` parameters for simple orders (Alpaca devs are working on replacement for complex orders as of 2020-05-06)
#' order_submit(so, a = "r", stop = lt$p * .94)
#' # Complex orders
#' # Here is an example of setting a "bracket" order
#' # first retrieve the going price
#' (.lt <- market_data(timeframe = "lt", symbol_id = "AMZN"))
#' # sell if the price moves up 3% by setting `take_profit`
#' tp <- list(l = .lt$p * 1.03)
#' # hedge risk by setting a stop if it loses 5% and limit if it loses 6% with `stop_loss`
#' sl <- list(l = .lt$p * .94, s = .lt$p * .95)
#' # note that the names of these list items can be partial
#' (br_o <- order_submit("AMZN", order_class = "bracket", qty = 2, take_profit = tp, stop_loss = sl))
#' # side is assumed to be buy, and type is assumed to be market
#' # Set a trailing stop by price
#' m_o <- order_submit("AMZN", side = "buy", type = "market", qty = 1)
#' ts_o <- order_submit(m_o, trail_price = 30)
#' # Set a trailing stop by percent
#' m_o <- order_submit("AMZN", side = "buy", type = "market", qty = 1)
#' ts_o <- order_submit(m_o, trail_percent = 5)
#' }
#' # all open orders can be canceled with `action = "cancel_all"`
#' order_submit(action = "cancel_all")
#' }
#'
#' @export
order_submit <-
function(symbol_id,
action = "submit",
qty = NULL,
side = NULL,
type = NULL,
time_in_force = "day",
limit = NULL,
stop = NULL,
extended_hours = NULL,
client_order_id = NULL,
order_class = NULL,
take_profit = NULL,
stop_loss = NULL,
notional = NULL,
trail_price,
trail_percent,
live = get_live()) {
trail <- list(percent = !missing(trail_percent),
price = !missing(trail_price))
if (isTRUE(tolower(type) %in% c("trailing_stop", "ts")) || do.call(`||`, trail)) {
if (is.null(type))
type <- "trailing_stop"
val <- if (trail$percent) {
trail_percent
} else if (trail$price) {
trail_price
}
i <- which(unlist(trail))
stopifnot(length(val) == 1)
# The name of the argument that needs to be passed to the API - either trail_percent/price
stop_price <- paste0("trail_", names(i))
stop <- round(val, 2)
side = "sell"
} else {
stop_price <- "stop_price"
}
ovar <- environment()
ovar$.vn <-
list(
symbol_id = "character",
action = "character",
side = c("character", "NULL"),
type = c("character", "NULL"),
qty = c("numeric", "integer", "NULL"),
time_in_force = "character",
limit = c("numeric", "integer", "NULL"),
stop = c("numeric", "integer", "NULL"),
stop_price = c("character", "numeric"),
extended_hours = c("logical", "NULL"),
client_order_id = c("logical","character", "NULL"),
order_class = c("character", "NULL"),
take_profit = c("list", "NULL"),
notional = c("numeric", "NULL"),
stop_loss = c("list", "NULL"),
trail_price = c("numeric", "integer"),
trail_percent = c("numeric", "integer"),
live = "logical"
)
.cancel_all <- any(grepl("cancel_all", as.character(match.call()), ignore.case = T))
if (!.cancel_all) {
action <- substr(tolower(action), 0, 1)
# if the order tbl is supplied directly, extract the id
order_symbol_id(symbol_id)
} else {
action = "c"
}
rlang::env_bind(ovar, type = type, action = action)
# smart detect: type, order_class, extended_hours
# fix names for take_profit, stop_loss if partialed
# or throw errors/warnings for specific criteria
if (any(action %in% c("s", "r", "c")) && !.cancel_all) {
order_check()
}
if (!.cancel_all)
.is_id <- is_id(symbol_id)
# detect the argument provided to symbol_id
if (action == "s") {
#Create body with order details if action is submit or replace
bodyl <-
append(purrr::modify_depth(purrr::compact(
rlang::list2(
symbol = symbol_id,
qty = qty,
side = side,
type = type,
time_in_force = time_in_force,
limit_price = limit,
!!stop_price := stop,
notional = notional,
client_order_id = client_order_id,
order_class = order_class
)
),-1, .f = as.character, .ragged = TRUE),
purrr::modify_depth(purrr::compact(list(take_profit = take_profit,
stop_loss = stop_loss)), -1, .f = as.character, .ragged = TRUE))
bodyl$extended_hours <- extended_hours
bodyl <- jsonlite::toJSON(bodyl, auto_unbox = TRUE)
} else if (action == "r") {
bodyl <-
jsonlite::toJSON(
purrr::modify_depth(purrr::compact(
list(
qty = qty,
side = side,
time_in_force = time_in_force,
limit_price = limit,
stop_price = stop,
client_order_id = client_order_id)
),-2, .f = as.character),
auto_unbox = TRUE)
}
#Set URL & Headers
headers = get_headers(live)
.path <- c("orders")
if (action %in% c("r","c") && !.cancel_all) {
# if replacing or canceling, append the order ID
.path <- append(.path, symbol_id)
}
.url <- get_url(.path, live = live)
.f <- switch(action,
s = httr::POST,
r = httr::PATCH,
c = httr::DELETE)
.args <- switch(action,
s = ,
r = list(url = .url, body = bodyl, encode = "raw", headers),
c = list(url = .url, headers))
out <- rlang::exec(.f, !!!.args)
out <- order_transform(out)
return(out)
}
#' @title Transform order objects
#' @description Replaces character quantities with numeric and character dates with POSIXct
#' @param .o An order object
#' @keywords internal
o_transform <- function(.o) {
.o <- dplyr::mutate(.o, dplyr::across(dplyr::ends_with("at"), ~lubridate::ymd_hms(.x, tz = Sys.timezone())))
out <- dplyr::mutate(.o, dplyr::across(where(~is.character(.x)), toNum))
return(out)
}
#' @title Transform order responses
#'
#' @description Parses order type responses and replaces plain text quantities and dates with respective R objects
#' @param orders A dataframe returned from any orders_* endpoint
#' @return \code{(tibble)} with respective R compliant objects (numeric, POSIXct/Datetime, character)
#' @keywords internal
order_transform <- function(o) {
if (class(o) == "response") {
if (length(o$content) == 0 && grepl("^2", o$status_code)) {
message(paste0("Order canceled successfully"))
} else if (grepl("^5", o$status_code)) {
rlang::warn("Failed to cancel order.")
}
.method <- o$request$method
.code <- o$status_code
.o <- response_text_clean(o)
if (!inherits(.o, "character")) {
.message <- .o$message
} else {
.message <- .o
}
} else if (class(o) != "response") {
.method <- "DELETE"
.code <- 200
.o <- o
}
if (grepl("^4", .code)) {
rlang::warn(paste0("Code: ",.code,",\nMessage:", .message))
return(.o)
}
if ((is.list(.o) && length(.o) > 0) || ("body" %in% names(.o) && grepl("DELETE", .method, ignore.case = TRUE))) {
if (grepl("DELETE", .method, ignore.case = TRUE) && "body" %in% names(.o)) {.o <- .o$body;.q <- .o[1:2]}
.o <- tibble::as_tibble(purrr::map(.o, rlang::`%||%`, NA))
suppressMessages({
suppressWarnings({
if (is_legit(.o$legs)) {
if (inherits(.o$legs, "list")) {
.o$legs <- purrr::map(.o$legs, ~{
if (!is.null(.x)) {
.out <- o_transform(.x)
} else {
.out <- .x
}
return(.out)
})
} else {
.o$legs <- o_transform(.o$legs)
}
}
out <- o_transform(.o)
})})
} else if (length(.o) == 0 && grepl("GET", .method, ignore.case = TRUE)) {
message(paste("No orders for the selected query/filter criteria.","\nCheck `symbol_id` or set status = 'all' to see all orders."))
out <- .o
} else if (grepl("DELETE", .method, ignore.case = TRUE)) {
# case when deleting single order
out <- .o
}
if (exists(".q", inherits = FALSE)) attr(out, "query") <- .q
return(out)
}
#' @title order_check
#' @description smart detect: type, order_class, extended_hours. Fix names for take_profit, stop_loss if partialled. Throw errors/warnings for specific criteria
#' @param penv \code{environment} the parent environment, otherwise a named list of arguments from the parent environment
#' @param ... named arguments. Will automatically get arguments from enclosing environment.
#' @return \code{(list)} returns list with appropriate arguments, to be merged with parent environment via `list2env`
#' @keywords internal
order_check <- function(..., ovar = get0("ovar", mode = "environment", envir = rlang::caller_env())) {
force(ovar)
fetch_vars(ovar$.vn[!names(ovar$.vn) %in% c("trail_price", "trail_percent")], ..., evar = ovar)
# smart detect order_class ----
# Fri May 15 13:48:32 2020
if (!is.null(order_class)) {
.oc <- tolower(substr(order_class, 0, 1))
if (.oc == "b") {
order_class <- "bracket"
} else if (!order_class %in% c("oto", "oco")) {
rlang::abort(paste0(order_class, "is invalid `order_class`. See ?order_submit for help."))
}
}
# if side is partialled or missing ----
# Thu Apr 30 20:32:52 2020
if (action == "s") {
# set type if partialled and order_class is NULL ----
# Thu Apr 30 20:20:16 2020
if (!is.null(type) && is.null(order_class)){
type <- tolower(type)
if (grepl("^s", type) && grepl("(?<!i)l", type, perl = TRUE)) {
type <- "stop_limit"
} else {
type <- match_letters(type, n = 3, "trailing_stop", "stop", "limit", "market")
}
}
if (!is.null(side)) {
side <- match_letters(side, "buy", "sell", ignore.case = TRUE)
if (class(side) == "try-error") rlang::abort("Invalid value for `side`")
} else if (is.null(side)) {
if ((order_class %||% "none") %in% c("bracket", "oto")) {
side <- "buy"
message("order_class: ", order_class," requires side = 'buy', `side` set to 'buy'.")
} else if ((order_class %||% "none") == "oco") {
side <- "sell"
message("order_class: 'oco' requires side = 'sell', `side` set to 'sell'.")
} else {
rlang::abort("`side` is required for order submissions.")
}
}
# Short sell/stop buy warning
if (side == "sell") {
.pos <- suppressMessages(try(positions(symbol_id, live = live), silent = TRUE))
if (is_error(.pos) && grepl("position does not exist", attributes(.pos)$condition$message)) {
cli::cli_alert_warning(paste0("No positions exist for ",paste0(symbol_id, collapse = ", "),". This order will be a short sell."))
}
} else if (side == "buy" && (!is.null(stop))) {
.warn_msg <- switch(stop_price,
stop_price = paste0("reaches ", stop),
trail_price = paste0("decreases by ", stop),
trail_percent = paste0("decreases by ", stop, " percent"))
cli::cli_alert_warning(paste0("This stop buy order will execute when the price ", .warn_msg))
}
# if quantity is missing ----
# Thu Apr 30 20:17:38 2020
if (is.null(qty) && is.null(notional)) {
rlang::abort("`qty` or `notional` must be set.")
}
# fix names for take_profit and stop_loss
if (!is.null(take_profit)) names(take_profit) <- "limit_price"
if (!is.null(stop_loss)) {
.n <- purrr::imap_chr(stop_loss, ~{
if (grepl("^l", .y, ignore.case = T)) "limit_price" else "stop_price"
})
names(stop_loss) <- .n
}
if ((order_class %||% "none") == "bracket" && (type %||% "none") != "market") {
message("order_class: 'bracket' requires type = 'market'. `type` set to 'market'.")
type <- "market"
} else if ((order_class %||% "none") == "oco" && (type %||% "none") != "limit") {
message("order_class: 'oco' requires type = 'limit'. `type` set to 'limit'.")
type <- "limit"
} else if ((order_class %||% "none") == "oto" && (type %||% "none") != 'market') {
message("order_class: 'oto' requires type = 'market'. `type` set to 'market'.")
type <- "market"
}
if (is.null(order_class)) {
# smart detect type in the absence of order_class
if (is.null(type) && !is.null(limit) && is.null(stop)) {
# if just limit is provided
if (is.null(stop) && is.null(type)) {
type <- "limit";message("`type` set to 'limit'")
}
} else if (is.null(type) && !is.null(stop) && is.null(limit)) {
# if just stop is provided
if (is.null(limit) && is.null(type)) {
type <- "stop";message("`type` set to 'stop'")
}
} else if (!is.null(stop) && !is.null(limit)) {
if (is.null(type)) type <- "stop_limit";message("`type` set to 'stop_limit'")
} else if (is.null(type)) {
rlang::abort("`type` must be set.")
}
# throw errors if not detected or arguments don't match
if (type == "limit" && is.null(limit)){
rlang::abort(paste0("Please set limit price."))
} else if (type == "stop" && is.null(stop)) {
rlang::abort(paste0("Please set value for `stop` argument when `type = ", type,"`."))
} else if ((is.null(stop) || is.null(limit)) && type == "stop_limit") {
rlang::abort(paste0(paste0(unlist(purrr::imap(list(stop = stop, limit = limit), ~{
if (is.null(.x)) .y else NULL
})), collapse = ", "), " must be set."))
} else if (is.null(stop) && type == "trailing_stop") {
rlang::abort(paste0("Please set `trail_price` or `trail_percent` when `type = 'trailing_stop'`"))
} else if (stop_price == "trail_percent" && stop < 1) {
stop <- stop * 100
} else if (stop_price == "trail_percent" && stop > 100) {
rlang::abort("`trail_percent` must be < 100")
}
} else if (!is.null(order_class)) {
# if order class is specified, set required arguments accordingly or throw errors
# order_class Advanced orders ----
# Thu Apr 30 15:05:26 2020
if ((is.null(take_profit) && is.null(stop_loss)) && order_class == "oto") {
rlang::abort("`take_profit` or `stop_loss` must have at least one parameter set when order_class = 'oto'")
} else if ((is.null(take_profit) || is.null(stop_loss)) && order_class %in% c('oco','bracket')) {
rlang::abort("`take_profit` must be set, and `stop_loss` must have at least one parameter set when order_class = 'oco'/'bracket'")
}
# parameter parsing, error checking & warnings for advanced orders
if (order_class == "bracket") {
if (!time_in_force %in% c("day","gtc")) {
rlang::abort("time_in_force must be 'day' or 'gtc' when `order_class = 'bracket'. See documentation for details.")
}
}
}
if (isTRUE(extended_hours) && (type != "limit" || time_in_force != "day" || order_class %in% c("oco","oto", "bracket"))) rlang::abort(paste0("Extended hours only supports simple 'limit' orders and `time_in_force = 'day'`"))
} else if (action == "c") {
if (is.null(symbol_id)) rlang::abort("`symbol_id` is NULL, the order may not have been placed successfully?")
}
out <- list(
symbol_id = symbol_id,
action = action,
type = type,
qty = qty,
side = side,
time_in_force = time_in_force,
limit = limit,
stop = stop,
stop_price = stop_price,
extended_hours = extended_hours,
client_order_id = client_order_id,
order_class = order_class,
take_profit = take_profit,
stop_loss = stop_loss
)
rlang::env_bind(ovar, !!!out)
}
#' @title retrieve the order id if an order object is supplied
#' @description Retrieves the order_id from an order object if provided as `symbol_id`, or provides informative error if input order failed.
#' @inheritParams order_submit
#' @keywords internal
order_symbol_id <- function(symbol_id, ..., ovar = get0("ovar", mode = "environment", envir = rlang::caller_env())) {
fetch_vars(ovar$.vn[c("side", "action", "qty", "client_order_id")], ..., evar = ovar)
# symbol_id ----
# Fri May 01 11:15:39 2020
# Check if ticker is an id or order tbl
if (is_id(symbol_id))
symbol_id <- orders(symbol_id)
if (inherits(symbol_id, "data.frame")) {
if (is_id(symbol_id$id)) {
if (action == "s") {
if (symbol_id$side == "buy") {
# Create message update
.m <- purrr::keep(list(
side %||% "`side` set to 'sell'",
qty %||% paste0("`qty` set to ", symbol_id$qty),
paste0("`symbol_id` set to ", symbol_id$symbol),
ifelse(isTRUE(client_order_id), paste0("`client_order_id` set to ", symbol_id$id), 1)
), is.character)
if (isTRUE(client_order_id)) client_order_id <- symbol_id$id
side <- "sell"
#if symbol_id is ID, action is submit and qty is NULL, populate qty from previous order
qty <- symbol_id$qty
symbol_id <- symbol_id$symbol
message(paste0(.m, collapse = "\n"))
}
} else if (action %in% c("r","c")) {
symbol_id <- unique(symbol_id$id)
if (length(symbol_id) > 1) rlang::abort("`symbol_id` must contain a single order")
}
} else
rlang::abort(paste0("The order object provided as `symbol_id` is invalid.\norder code: ",symbol_id$code,"\nmessage: ", symbol_id$message))
} else {
symbol_id <- unique(toupper(symbol_id))
# If client_order_id is TRUE but the symbol_id is not an order, change it back to NULL
client_order_id <- purrr::when(isTRUE(client_order_id),
. ~ NULL,
~ client_order_id)
}
rlang::env_bind(ovar, symbol_id = symbol_id, side = side, action = action, qty = qty, client_order_id = client_order_id)
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.