Skip to Content

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)