rblncr | Trading
This blog post is part 4 of my follow-along-as-I-build-it series on building an R package called rblncr. You can read about the motivation behind my work in part 1 of the series.
Today we will go all the way to actually trading.
Where we currently are
Upon reviewing my last blog post, I realized I didn’t include the actual solved_portfolio
computation or object. A fair amount of code has changed in the interim anyway, so I’ll include all the code to run a worked example in this post.
Here’s the code we need to execute to get to the point where we have a solved_portfolio
object.
Also - in case the syntax confuses you. The |>
operator is the new base R pipe. Basically it performs the same as magrittr %>%
but you don’t have to load an external library.
# Load functions
source_funs <- sapply(list.files("R", full.names = TRUE), source, .GlobalEnv)
# define data and trading connections
t_conn <- alpaca_connect(trading_mode, paper_api_key, paper_api_secret)
d_conn <- alpaca_connect("data", live_api_key, live_api_secret)
# read model
portfolio_model <- read_portfolio_model("inst/extdata/sample_portfolio.yaml")
# compute holding values using last close
priced_portfolio <- get_portfolio_current(t_conn) |>
load_portfolio_targets(portfolio_model) |>
price_portfolio("close", d_conn)
# work out what needs to change
solved_portfolio <- priced_portfolio |>
solve_portfolio(terse = F)
This is what a solved portfolio object looks like when you use the arg terse = F
. It’s not very readable, but includes all the information you might need to work out the solution from first principles.
solved_portfolio
$cash
currency quantity_held percent_target price value_held percent_held
1 USD -46810.83 5 1 -46810.83 -0.05
out_of_band optimal_value
1 TRUE 48794.36
$assets
symbol quantity_held percent_target price value_held percent_held
1 AAPL 1297 20 149.35 193706.95 0.20
2 GOOG 3062 30 94.82 290338.84 0.30
3 MSFT 834 20 231.32 192920.88 0.20
4 TSLA 433 5 224.64 97269.12 0.10
5 VT 2893 20 83.65 241999.45 0.25
out_of_band optimal_order optimal_order_value optimal_value
1 FALSE 1 149.35 193856.3
2 FALSE 5 474.10 290812.9
3 FALSE 4 925.28 193846.2
4 TRUE -218 -48971.52 48297.6
5 TRUE -576 -48182.40 193817.1
$tolerance
$tolerance$percent
[1] 5
You can use this object for downstream operations, or you can use a terse = TRUE
version which is much easier on the eyes.
> solved_portfolio
$cash
currency out_of_band optimal_value
1 USD TRUE 48794.36
$assets
symbol price out_of_band optimal_order optimal_order_value
1 AAPL 144.80 FALSE 1 149.35
2 GOOG 92.60 FALSE 5 474.10
3 MSFT 226.75 FALSE 4 925.28
4 TSLA 225.09 TRUE -218 -48971.52
5 VT 83.18 TRUE -576 -48182.40
$tolerance
$tolerance$percent
[1] 5
Generating orders
You can’t just take the optimal_order
column from the assets
data frame and submit them to your broker. Well, you could, but it wouldn’t be a good idea. We need to apply some constraints to our order to make sure we don’t move the market, bite off more than we can chew, or submit such tiny orders that we get eaten by fees or generate excessive churn.
We do this by passing the solution to the constrain_orders
function. This function does quite a lot under the hood:
- Queries the data connection for the median daily volume over the last 10 days and makes sure our order stays under the
daily_vol_pct_limit
threshold. - Zeroes out any orders below the
min_order_size
. - Imposes an upper limit to any order as set by
max_order_size
It’s deceptively straightforward, but there’s a fair amount of arithmetic behind the scenes to get the values right.
orders <- solved_portfolio |>
constrain_orders(d_conn,
daily_vol_pct_limit = 0.02,
min_order_size = 1000,
max_order_size = 100000)
The constrained orders look like this. These values are actually from a different run, so don’t compare them to the data frame above. But you get the idea.
> orders
symbol order value
1 AAPL 17 2461.60
2 GOOG 21 1944.60
3 MSFT 5 1133.75
4 TSLA 0 0.00
5 VT -29 -2412.22
Submitting and filling orders
At this point, we are ready to actually trade. Originally, I was going to just throw the whole thing into a while
loop, which would keep obtaining holdings, pricing, solving, constraining and submitting orders until all the assets were balanced, but this had a few issues.
The most significant was that the prices we use to compute the holdings values (and hence whether they are in balance) might be different from the prevailing market price. Why? Well, we would like to use a stable price point, like last close, for computing portfolio values so we don’t get whipsawed by daily price swings. But we don’t want to use last close to actually set trading limits, because we might be quite far out the money. So we want to decouple the portfolio solving step from the trading steps.
What I’ve done is wrap the order submission, tracking, cancellation and resubmission in a single trader
function, which basically trades until the order requirements are satisfied, or until timeout, whichever comes first.
Because this function will keep doing stuff for a while, I’ve included a verbose
argument. Setting it to TRUE
will print out messages as the trader does its thing.
alpaca_trader(orders = orders,
trader_life = 300,
resubmit_interval = 5,
trading_connection = t_conn,
pricing_connection = d_conn,
#pricing_overrides = overrides,
verbose = TRUE)
Here is what the function printed while processing. You can see that it submitted some orders, waited a specified amount of time, then cancelled, worked out what hadn’t been filled, determined new pricing, and resubmitted the modified order. Once everyting was filled it exited.
there are 2 new orders to fill
- pricing new orders
- submitting orders
- waiting 5 seconds for orders to fill
- attempting to cancel all unfilled orders
- all open orders cancelled
- getting order statuses
- calculating remaining order amounts
there are 1 new orders to fill
- pricing new orders
- submitting orders
- waiting 5 seconds for orders to fill
- attempting to cancel all unfilled orders
- all open orders cancelled
- getting order statuses
- calculating remaining order amounts
Wind-down attempt to cancel all unfilled orders
Wind-down cancellation success
NO ORDERS TO EXECUTE. EXITING.
The trader
function returns a data frame, which is a log of each order submitted as well as the outcome. You can see that the first TSLA
order wasn’t filled, so the trader cancelled it and resubmitted at a recalculated price, which was subsequently filled.
timestamp symbol order limit filled status
1 2022-10-27 18:39:12 TSLA -218 225.04 0 new
2 2022-10-27 18:39:12 VT -576 83.33 -576 filled
3 2022-10-27 18:39:18 TSLA -218 224.48 -218 filled
Wrap up
Ok, so we are now at the point where, given just an API connection and a portfolio model, we can write a little R script to run every day or week or whatever and sleep peacefully knowing that our robot is keeping our portfolio rebalanced. At least, in theory. I need to write some tests I think, see if I can break it.
Next post will be a deep dive into the actual inner workings of the trader
.
source_funs <- sapply(list.files("R", full.names = TRUE), source, .GlobalEnv)
t_conn <- alpaca_connect("paper", paper_api_key, paper_api_secret)
d_conn <- alpaca_connect("data", live_api_key, live_api_secret)
portfolio_model <- read_portfolio_model("inst/extdata/sample_portfolio.yaml")
priced_portfolio <- get_portfolio_current(t_conn) |>
load_portfolio_targets(portfolio_model) |>
price_portfolio("close", d_conn)
solved_portfolio <- priced_portfolio |>
solve_portfolio(terse = T)
solved_portfolio
orders <- solved_portfolio |>
constrain_orders(d_conn, min_order_size = 1000, max_order_size = 100000)
alpaca_clock(t_conn)
overrides <- get_symbols_last_closing_price(solved_portfolio$assets$symbol, d_conn) %>%
dplyr::select(symbol, close) %>%
dplyr::rename(limit = close)
trades <- alpaca_trader(orders = orders,
trader_life = 30,
resubmit_interval = 5,
trading_connection = t_conn,
pricing_connection = d_conn,
pricing_overrides = overrides,
verbose = FALSE)