rblncr | Computing portfolio changes
This blog post is part 3 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.
In the last post we defined a portfolio_model
, which is a specification of a desired basket of assets along with their respective target percentages.
name: port2
description: create from function
cash:
percent: 10.0
tolerance: 0.0
assets:
- symbol: AAPL
percent: 80.5
tolerance: 2.0
- symbol: GOOG
percent: 9.5
tolerance: 2.0
created_at: 2022-10-19T10:08:50
updated_at: 2022-10-19T10:08:50
Today we are going to compare our target portfolio to our actual portfolio and work out what needs to change.
Complicating Factors
In principle, you could use any stock broker that provides an API to use as a trading backend. We just need to write the code that connects to the API, gets the data we want, and makes it available in a form we can work with. But, because each broker’s API is different, there’s quite a bit of work required to make sure that data from different APIs is all presented in the same way to downstream functions.
The way we achieve this is to have two layers of functions. The backend layer functions are all broker-specific, and return data in the structure they are provided by the respective broker. In this layer we could have functions like alpaca_positions
, ib_positions
, tdameritrade_positions
etc.
The switching layer functions will present a single unified ‘smart’ interface for getting data and presnting it ins a standardized form. So, continuing the examples above, you’d have a get_positions
function which would, firstly, intelligently route your request to the right backend depending on what backend you are using, and secondly always return the same data structure so that you can reliably pass the output to downstream functions.
For now, I am only implementing an Alpaca backend. I have written the code in such a way that it should be able to accommodate arbitrary additional backends.
What we need
In order to compute the changes needed to rebalance a portfolio, we only need two things -
- The target weights
- The current weights
Target weights can be obtained by reading the yaml
off disk. Current weights is a bit more complicated - not least because these all involve querying some other service. In order to compute current weights, we need:
- Current positions (easy, but you need a backend function and a switching function)
- Current prices (what to use? last close? current bid? last trade? some sort of trade weighted average?)
Once we have the target weights and current weights, we need to write code that ‘solves’ the portfolio and tells us what needs to change.
So, six functions, roughly doing the following:
- get current positions
- get target positions
- join current and target into a single frame
- get prices for all symbols in the frame
- calculate current vlaues and percentages
- work out what needs to change to make the current holdings the same as the target holdings
Here’s how it looks in code:
trading_mode = "paper"
paper_api_key <- [REDACTED]
paper_api_secret <- [REDACTED]
# source all the functions in the R subdirectory
source_funs <- sapply(list.files("R", full.names = TRUE), source, .GlobalEnv)
# different connections because Alpaca provided different APIs for trading and data
t_conn <- alpaca_connect(trading_mode,
paper_api_key,
paper_api_secret)
d_conn <- alpaca_connect("data",
live_api_key,
live_api_secret)
# read in model
portfolio_model <- read_portfolio_model("inst/extdata/sample_portfolio.yaml")
# get current positions
portfolio_current <- get_portfolio_current(t_conn)
# load targets into positons frame
portfolio_targets <- load_portfolio_targets(portfolio_current,
portfolio_model)
# price the targets
portfolio_priced <- price_portfolio(portfolio_targets,
"close",
d_conn)
# compute the changes
portfolio_changes <- solve_portfolio_priced(portfolio_priced)
Which gives you:
> portfolio_changes
$cash
currency out_of_band
1 USD TRUE
optimal_value
1 10119.12
$assets
symbol price out_of_band
1 AAPL 143.86 TRUE
2 GME 24.54 TRUE
3 VT 80.67 TRUE
4 MSFT 236.48 TRUE
optimal_order optimal_value
1 558 80417.74
2 -1 0.00
3 1 0.00
4 40 9459.20
How it’s implemented
Ok, about 8 hours of work has yielded six high level functions, which can solve your portfolio rebalancing requirements in 7 lines of code.
Under the hood, these functions rely on about 20 functions which involve interaction with the Alpaca backend, plumbing them through the switching functions, conducting some validation, and then actually computing the required changes.
Backend switching
The key to the intelligent switching is the *_connect
functions. Each of these (I’ve only written Alpaca ones so far) is intended to yield a list, on element of which is names backend
and lets downstream functions know what backend is being used, as well as any other arbitrary elements required for the connection.
> t_conn
$backend
[1] "alpaca"
$domain
[1] "https://paper-api.alpaca.markets"
$headers
<request>
Headers:
* APCA-API-KEY-ID: [REDACTED]
* APCA-API-SECRET-KEY: [REDACTED]
When you pass the connection object to a high level switching function, it chooses which blocks of code to execute depending on what backend has been declared. Then it performes some basic validation to ensure that it’s returning a standardised data structure.
This results in a pleasing user experience. As long as you create your connection correctly, the high level functions should ‘just work’.
Note: the VT position in the example below is not an error! It’s a short position. To be honest I am not sure how to deal with short targets yet. Maybe the math works? Maybe it doesn’t.
> get_portfolio(t_conn)
$cash
currency quantity
1 USD 99908.33
$assets
symbol quantity
1 AAPL 1
2 GME 1
3 VT -1
Portfolio solving
Ok, I confess the solver could probbaly be better. Basically, I just replicated what you’d do with an Excel spreadsheet in R. I’m not sure what the effect will be if you try target a short position. It also doesn’t take into account potential trading costs, so you will always slightly overestimate how much cash you’ll have left after trading. And it uses the prices obtained for the portfolio valuation to compute how many shares you should buy.
Because of these approximations, the portfolio_changes
should be regarded as an approximation. This approximation will be worse:
- the higher your transaction costs,
- the larger your trades are relative to your portoflio value, and
- the more the current price for a stock differs from the price you used to value the stock.
These limitations can be mitigated in the order construction step, which we will implement in the next blog post.
Here’s the full portfolio solver code so you can see just how simple it is.
solve_portfolio_priced <- function(portfolio_priced) {
cash <- portfolio_priced$cash
assets <- portfolio_priced$assets
total_value <- cash$value + sum(assets$value)
assets$percent_held <- assets$value_held / total_value * 100
cash$percent_held <- cash$value_held / total_value * 100
cash$value_target <- total_value * (cash$percent_target / 100)
assets$value_target <- total_value * (assets$percent_target / 100)
assets$quantity_target <- floor(assets$value_target / assets$price)
cash$quantity_target <- floor(cash$value_target / cash$price)
assets$percent_deviation <- abs((assets$value_held - assets$value_target) / assets$value_target) * 100
cash$percent_deviation <- abs((cash$value_held - cash$value_target) / cash$value_target) * 100
assets$out_of_band <- ifelse(assets$percent_deviation > assets$tolerance, T, F)
cash$out_of_band <- ifelse(cash$percent_deviation > cash$tolerance, T, F)
assets$optimal_order <- ifelse(assets$out_of_band, assets$quantity_target - assets$quantity_held, 0)
assets$optimal_value <- (assets$quantity_held + assets$optimal_order) * assets$price
post_rebalancing_cash_balance <- total_value - sum(assets$optimal_value)
cash$optimal_value <- post_rebalancing_cash_balance
assets <- dplyr::select(assets,
symbol,
price,
out_of_band,
optimal_order,
optimal_value)
cash <- dplyr::select(cash,
currency,
out_of_band,
optimal_value)
rebalanced_frame <- list()
rebalanced_frame$cash <- cash
rebalanced_frame$assets <- assets
return(rebalanced_frame)
}