R package for the Charles Schwab trade API, facilitating authentication, trading, price requests, account balances, positions, order history, option chains, and more. A user will need a Schwab Brokerage account and Schwab developer app. See instructions below.
Please note: the Schwab Developer App takes a few days to be fully approved so start that process ASAP. You will not be able to login until the App is in “Ready to use” State.
There is another and similar R CRAN package: charlesschwabapi. It seems similar in terms of functionality to this package. Schwabr
was built because most of the functionality already existed in the deprecated TD Ameritrade R API called rameritrade.
Charles Schwab is one of many trading platforms that offer a trade API. They ported it over from TD Ameritrade, so users of that platform should be familiar with much of the components here. This package was built off of the CRAN package schwabr. Other platforms like Alpaca, TastyTrade, TradeStation, etc. may have more modern APIs, but they do not have the full breath and depth as a broker dealer like Schwab. This API does have a few limitations, obstacles:
All that said, it should give a retail user all the basic functionality needed to programatically manage their accounts and trading.
This software is in no way affiliated, endorsed, or approved by Charles Schwab or any of its affiliates. It comes with absolutely no warranty and should not be used in actual trading unless the user can read and understand the source code. The functions within this package have been tested under basic scenarios. There may be bugs or issues that could prevent a user from executing trades or canceling trades. It is also possible trades could be submitted in error. The user will use this package at their own risk.
Please heed the following warning for the schwab_placeOrder
function. WARNING: TRADES THAT ARE SUCCESSFULLY ENTERED WILL BE SUBMITTED IMMEDIATELY THERE IS NO REVIEW PROCESS. THE schwab_placeOrder
FUNCTION HAS HUNDREDS OF POTENTIAL COMBINATIONS AND ONLY A HANDFUL HAVE BEEN TESTED. SCHWAB HAS THEIR OWN ERROR HANDLING BUT IF A SUCCESSFUL COMBINATION IS ENTERED IT COULD BE EXECUTED IMMEDIATELY. DOUBLE CHECK ALL ENTRIES BEFORE SUBMITTING. IT IS STRONGLY RECOMMENDED TO TEST THE DESIRED ORDER FIRST. THREE POTENTIAL OPTIONS ARE:
FOR OPTIONS 1 AND 2, BE SURE TO CANCEL ORDERS USING schwab_cancelOrder
OR THROUGH THE TD WEBSITE
You can install schwabr using:
# NOT YET Available on CRAN
# install.packages("schwabr")
# Install development version - fixes for cronR and price history
# install.packages("devtools")
devtools::install_github("altanalytics/schwabr")
Initial authorization to Schwab requires a 3 step authentication process. Once initial authorization is achieved, a refresh_token can by used to grant access for 7 days. After that, a manual login must be completed following steps 1-3. Below is a detailed summary of the entire process followed by code demonstrating the 3 step process. More can be found at the Schwab site or the Authentication Guide. Details are also provided within the functions.
https://127.0.0.1
).schwab_auth1_loginURL
to generate a URL specific to the app for user log in.https://127.0.0.1?code=AUTHORIZATIONCODE
).schwab_auth2_refreshToken
to get a Refresh Token.schwab_auth3_accessToken
which gives account access for 30 minutes.schwab_auth1_loginURL
.The schwab_auth1_loginURL
is used to gain initial access to the API. Once a Refresh Token is generated using an authorization code, manual log in will not be required until the Refresh Token expires in 7 days.
# --------- Step 1 -----------
# Register an App with Schwab Developer, create a Callback URL, and get an App Key and App Secret.
# The callback URL can be any valid URL (for example: https://127.0.0.1).
# Use the schwab_auth1_loginURL to generate an app specific URL. See the Schwab documentation for issues.
callbackURL = 'https://127.0.0.1'
AppKey = 'APPKEY'
schwab::schwab_auth1_loginURL(AppKey, callbackURL)
# "https://api.schwabapi.com/v1/oauth/authorize?client_id=APPKEY&redirect_uri=https://127.0.0.1"
# Visit the URL above to see a Schwab login screen. Log in with your Schwab account details to grant the app access.
# --------- Step 2 -----------
# A successful log in to the URL from Step 1 will result in a blank page once "Done" is clicked.
# The URL of this blank page is the Authorization Code.
# The blank page may indicate "This site can't be reached". The URL is still a valid Authorization Code.
# Feed the Authorization Code URL into schwab_auth2_refreshToken to get a Refresh Token.
authCode = 'https://127.0.0.1?code=CO.AUTHORIZATIONCODE&session=ABCD' # This could be very long
refreshToken = schwabr::schwab_auth2_refreshToken(AppKey, AppSecret, callbackURL, authCode)
# "Successful Refresh Token Generated"
# Save the Refresh Token to a safe location so it can be retrieved as needed. It will be valid for 7 days.
saveRDS(refreshToken$refresh_token,'/secure/location/')
# --------- Step 3 -----------
# Use the Refresh Token to get an Access Token
# The function will return an Access Token and also store it for use as a default token in Options
refreshToken = readRDS('/secure/location/')
accessTokenList = schwab::schwab_auth_accessToken(consumerKey, refreshToken)
accessTokenList = schwab_auth3_accessToken(appKey, appSecret, refreshToken)
# "SSuccessful Login. Access Token has been stored and will be valid for 30 minutes"
# Authentication has been completed. Other functions can now be used.
# --------- Automation -----------
# You can use Python and Selenium to partially automate this process
Use the schwabr_accountData
to get current account data that includes balances, positions, and current day orders.
library(schwabr)
refreshToken = readRDS('/secure/location/')
appKey = 'APP_KEY'
appSecret = 'APP_SECRET'
accessTokenList = schwabr::schwab_auth3_accessToken(appKey, appSecretm refreshToken)
actDF = schwab_accountData()
str(actDF)
# List of 3
# $ balances : tibble [1 × 33] (S3: tbl_df/tbl/data.frame)
# ..$ account_number : chr "1234"
# ..$ type : chr "MARGIN"
# ..$ roundTrips : int 0
# ..$ isDayTrader : logi TRUE
# ..$ isClosingOnlyRestricted : logi FALSE
# ..$ accruedInterest : num 0
# ..$ cashBalance : num 0
# ..$ cashReceipts : num 0
# ..$ longOptionMarketValue : num 0
# ..$ liquidationValue : num 0
actList = schwab_accountData('list')
str(actList)
# List of 3
# $ balances :List of 1
# ..$ :List of 2
# .. ..$ securitiesAccount:List of 9
# .. .. ..$ type : chr "MARGIN"
# .. .. ..$ account_number : chr "1234"
# .. .. ..$ roundTrips : int 0
# .. .. ..$ isDayTrader : logi TRUE
# .. .. ..$ isClosingOnlyRestricted: logi FALSE
Use the price
functions to get quotes or historical pricing. Quotes will be real-time if the account has access to real-time quotes.
library(schwabr)
refreshToken = readRDS('/secure/location/')
appKey = 'APP_KEY'
appSecret = 'APP_SECRET'
accessTokenList = schwabr::schwab_auth3_accessToken(appKey, appSecretm refreshToken)
### Quote data
SP500Qt = schwabr::schwab_priceQuote(c('SPY', 'IVV', 'VOO'))
str(SP500Qt)
# tibble [3 × 71] (S3: tbl_df/tbl/data.frame)
# $ assetMainType : chr [1:3] "EQUITY" "EQUITY" "EQUITY"
# $ assetSubType : chr [1:3] "ETF" "ETF" "ETF"
# $ quoteType : chr [1:3] "NBBO" "NBBO" "NBBO"
# $ realtime : logi [1:3] TRUE TRUE TRUE
# $ ssid : int [1:3] 1281357639 354190790 2003991281
# $ symbol : chr [1:3] "SPY" "IVV" "VOO"
# Historical Data
SP500H = schwabr::schwab_priceHistory(c(c('SPY','IVV','VOO')))
head(SP500H)
# ticker date date_time open high low close volume
# <chr> <date> <dttm> <dbl> <dbl> <dbl> <dbl> <int>
# 1 SPY 2024-12-26 2024-12-26 01:00:00 600. 602. 598. 601. 41338891
# 2 SPY 2024-12-27 2024-12-27 01:00:00 598. 598. 591. 595. 64969310
# 3 SPY 2024-12-30 2024-12-30 01:00:00 588. 592. 584. 588. 56578757
# 4 SPY 2024-12-31 2024-12-31 01:00:00 590. 591. 584. 586. 57052654
# 5 SPY 2025-01-02 2025-01-02 01:00:00 589. 591. 580. 585. 50203975
# 6 SPY 2025-01-03 2025-01-03 01:00:00 588. 593. 586. 592. 37888459
# Time series data
# History is only available back to a certain time depending on frequency
schwabr::schwab_priceHistory('AAPL', startDate = Sys.Date()-1, freq='5min')
# A tibble: 78 × 8
# ticker date date_time open high low close volume
# <chr> <date> <dttm> <dbl> <dbl> <dbl> <dbl> <int>
# 1 AAPL 2025-01-23 2025-01-23 09:30:00 225. 226. 224. 225. 2734142
# 2 AAPL 2025-01-23 2025-01-23 09:35:00 225. 226. 225. 226. 905901
# 3 AAPL 2025-01-23 2025-01-23 09:40:00 226. 226. 225. 225. 739369
# 4 AAPL 2025-01-23 2025-01-23 09:45:00 225. 225. 224. 225. 677993
# 5 AAPL 2025-01-23 2025-01-23 09:50:00 225. 226. 225. 225. 554527
# 6 AAPL 2025-01-23 2025-01-23 09:55:00 225. 226. 225. 225. 619021
# 7 AAPL 2025-01-23 2025-01-23 10:00:00 225. 226. 225. 226. 689537
# 8 AAPL 2025-01-23 2025-01-23 10:05:00 226. 226. 225. 226. 993476
# 9 AAPL 2025-01-23 2025-01-23 10:10:00 226. 226. 226. 226. 589547
# 10 AAPL 2025-01-23 2025-01-23 10:15:00 226. 227. 226. 227. 861674
Order entry offers hundreds of potential combinations. It is strongly recommended to submit trades outside market hours first to test the trade entries. You can confirm proper entry on the Schwab website before entering. See the order sample guide for more examples. Please note, schwab_placeOrder
only allows for single order entry and will not support some of the complex examples in the guide.
library(schwabr)
# Set Access Token using a valid Refresh Token
refreshToken = readRDS('/secure/location/')
appKey = 'APP_KEY'
appSecret = 'APP_SECRET'
accessTokenList = schwabr::schwab_auth3_accessToken(appKey, appSecretm refreshToken)
account_number = 1234567890
# Market Order
Ord0 = schwabr::schwab_placeOrder(account_number,
ticker = 'PSLV',
quantity = 1,
instruction = 'BUY')
schwabr::schwab_cancelOrder(Ord0$orderId, account_number)
# [1] "Order Cancelled"
# Good till cancelled stop limit INCORRECT ENTRY
Ordr1 = schwabr::schwab_placeOrder(account_number = account_number,
ticker = 'SCHB',
quantity = 1,
instruction = 'buy',
duration = 'good_till_cancel',
orderType = 'stop_limit',
limitPrice = 50,
stopPrice = 49)
# Error: 400 - The stop price must be above the current ask for buy stop orders
# and below the bid for sell stop orders.
# Good till Cancelled Stop Limit Order correct entry
Ordr1 = schwabr::schwab_placeOrder(account_number = account_number,
ticker = 'SCHB',
quantity = 1,
instruction = 'buy',
duration = 'good_till_cancel',
orderType = 'stop_limit',
limitPrice = 86,
stopPrice = 85)
schwabr::schwab_cancelOrder(Ordr1$orderId, account_number)
# [1] "Order Cancelled"
# Trailing Stop Order
Ordr2 = schwabr::schwab_placeOrder(account_number = account_number,
ticker = 'SPY',
quantity = 1,
instruction = 'sell',
orderType = 'trailing_stop',
stopPriceBasis = 'BID',
stopPriceType = 'percent',
stopPriceOffset = 10)
schwabr::schwab_cancelOrder(Ordr2$orderId,account_number)
# [1] "Order Cancelled"
# Option Order
Ord3 = schwabr::schwab_placeOrder(account_number = account_number,
ticker = 'SLV_091820P24.5',
quantity = 1,
instruction = 'BUY_TO_OPEN',
duration = 'Day',
orderType = 'LIMIT',
limitPrice = .02,
assetType = 'OPTION')
schwabr::schwab_cancelOrder(Ord3$orderId, account_number)
# [1] "Order Cancelled"
You can pull entire option chains for individual securities.
# Pull all SPY chains for 6 months with 12 strikes above and below current market
SPY = schwab_optionChain('SPY',
strikes = 12,
endDate = Sys.Date() + 180)
# This returns a list of two data frames
str(SPY$underlying)
# tibble [1 × 23] (S3: tbl_df/tbl/data.frame)
# $ symbol : chr "SPY"
# $ description : chr "SPDR S&P 500"
# $ change : num 2.52
# $ percentChange : num 0.76
# $ close : num 332
# $ quoteTime : num 1.6e+12
# $ tradeTime : num 1.6e+12
# $ bid : num 334
# $ ask : num 334
# $ last : num 335
# $ mark : num 334
# $ markChange : num 1.53
# $ markPercentChange: num 0.46
# $ bidSize : int 300
# $ askSize : int 100
# $ highPrice : num 338
# $ lowPrice : num 333
# $ openPrice : num 333
# $ totalVolume : int 101506148
# $ exchangeName : chr "PAC"
# $ fiftyTwoWeekHigh : num 359
# $ fiftyTwoWeekLow : num 218
# $ delayed : logi TRUE
str(SPY$fullChain)
# $ putCall : chr [1:552] "PUT" "PUT" "PUT" "PUT" ...
# $ symbol : chr [1:552] "SPY_093020P329" "SPY_093020P330" "SPY_093020P331" "SPY_093020P332" ...
# $ description : chr [1:552] "SPY Sep 30 2020 329 Put (Quarterly)" "SPY Sep 30 2020 330 Put (Quarterly)" "SPY Sep 30 2020 331 Put (Quarterly)" "SPY Sep 30 2020 332 Put (Quarterly)" ...
# $ exchangeName : chr [1:552] "OPR" "OPR" "OPR" "OPR" ...
# $ bid : num [1:552] 0 0 0.01 0.01 0.04 0.3 1.15 2.02 3.14 4.1 ...
# $ ask : num [1:552] 0.01 0.01 0.02 0.02 0.05 0.38 1.25 2.33 3.24 4.61 ...
# $ last : num [1:552] 0.01 0.02 0.01 0.02 0.05 0.32 1.22 2.35 3.02 4.3 ...
# $ mark : num [1:552] 0.01 0.01 0.02 0.02 0.05 0.34 1.2 2.17 3.19 4.36 ...
# $ bidSize : int [1:552] 0 0 6739 1457 390 40 10 10 10 15 ...
# $ askSize : int [1:552] 4927 3498 6062 3177 10 15 10 141 10 150 ...
# $ bidAskSize : chr [1:552] "0X4927" "0X3498" "6739X6062" "1457X3177" ...
# $ lastSize : int [1:552] 0 0 0 0 0 0 0 0 0 0 ...
# $ highPrice : num [1:552] 0.33 0.49 0.67 1 1.4 1.93 2.58 3.2 4.1 5 ...
# $ lowPrice : num [1:552] 0.01 0.01 0.01 0.01 0.01 0.02 0.07 0.21 0.36 0.66 ...
# $ openPrice : num [1:552] 0 0 0 0 0 0 0 0 0 0 ...
# $ closePrice : num [1:552] 0.61 0.81 1.08 1.4 1.8 2.28 2.85 3.51 4.25 5.06 ...
# $ totalVolume : int [1:552] 29439 60708 55127 95477 127601 162990 158762 130057 61796 36514 ...
# $ tradeDate : logi [1:552] NA NA NA NA NA NA ...
# $ tradeTimeInLong : num [1:552] 1.6e+12 1.6e+12 1.6e+12 1.6e+12 1.6e+12 ...
# $ quoteTimeInLong : num [1:552] 1.6e+12 1.6e+12 1.6e+12 1.6e+12 1.6e+12 ...
# $ netChange : num [1:552] -0.6 -0.8 -1.07 -1.38 -1.75 -1.96 -1.63 -1.16 -1.23 -0.76 ...
# $ volatility : num [1:552] 11.52 9.39 8.48 5.92 NaN ...
# $ delta : num [1:552] -0.008 -0.009 -0.027 -0.036 NaN NaN -0.889 -0.952 -0.949 -0.88 ...
# $ gamma : num [1:552] 0.01 0.015 0.041 0.077 NaN NaN 0.202 0.077 0.055 0.057 ...
# $ theta : num [1:552] -0.021 -0.02 -0.046 -0.042 NaN NaN -0.102 -0.078 -0.113 -0.362 ...
# $ vega : num [1:552] 0.004 0.004 0.011 0.014 0.045 0.07 0.033 0.017 0.018 0.035 ...
# $ rho : num [1:552] 0 0 0 0 NaN NaN -0.008 -0.009 -0.009 -0.008 ...
# $ openInterest : int [1:552] 19356 25285 12418 10482 12659 7611 12022 3251 2734 2637 ...
# $ timeValue : num [1:552] 0.01 0.02 0.01 0.02 0.05 0.32 1.11 1.24 0.91 1.19 ...
# $ theoreticalOptionValue: num [1:552] 0.005 0.005 0.015 0.015 NaN ...
# $ theoreticalVolatility : num [1:552] 29 29 29 29 29 29 29 29 29 29 ...
# $ optionDeliverablesList: logi [1:552] NA NA NA NA NA NA ...
# $ strikePrice : num [1:552] 329 330 331 332 333 334 335 336 337 338 ...
# $ expirationDate : num [1:552] 1.6e+12 1.6e+12 1.6e+12 1.6e+12 1.6e+12 ...
# $ daysToExpiration : int [1:552] 0 0 0 0 0 0 0 0 0 0 ...
# $ expirationType : chr [1:552] "Q" "Q" "Q" "Q" ...
# $ lastTradingDay : num [1:552] 1.6e+12 1.6e+12 1.6e+12 1.6e+12 1.6e+12 ...
# $ multiplier : num [1:552] 100 100 100 100 100 100 100 100 100 100 ...
# $ settlementType : chr [1:552] " " " " " " " " ...
# $ deliverableNote : chr [1:552] "" "" "" "" ...
# $ isIndexOption : logi [1:552] NA NA NA NA NA NA ...
# $ percentChange : num [1:552] -98.4 -97.5 -99.1 -98.6 -97.2 ...
# $ markChange : num [1:552] -0.61 -0.81 -1.07 -1.38 -1.75 -1.94 -1.65 -1.34 -1.06 -0.7 ...
# $ markPercentChange : num [1:552] -99.2 -99.4 -98.6 -98.9 -97.5 ...
# $ nonStandard : logi [1:552] FALSE FALSE FALSE FALSE FALSE FALSE ...
# $ inTheMoney : logi [1:552] FALSE FALSE FALSE FALSE FALSE FALSE ...
# $ mini : logi [1:552] FALSE FALSE FALSE FALSE FALSE FALSE ...
# $ expireDate : Date[1:552], format: "2020-09-30" "2020-09-30" "2020-09-30" "2020-09-30" ...