This vignette provides a high level introduction to the rblncr package. You can use the functions in this package to:

  • Create a model portfolio
  • Query your existing brokerage account to obtain your current holdings
  • Automatically rebalance your brokerage account so that the holdings mirror your model portfolio

Create a portfolio model

A portfolio model is an R list() that is structured in a particular way. It provides information to rblncr functions about what asset allocations you want. You can write a portfolio model list to disk with the function save_portfolio_model(). It is written in yaml format. Here is an example of a model portfolio that was written to disk.

name: sample_portfolio
description: create from function
cash:
  percent: 5
assets:
- symbol: AAPL
  percent: 30
- symbol: GOOG
  percent: 30
- symbol: VT
  percent: 30
- symbol: TSLA
  percent: 5
tolerance:
  percent: 5
cooldown:
  days: 365
created_at: 2022-10-24T11:22:01
updated_at: 2022-10-24T11:22:01

Here is how you would create a portfolio model from scratch, write it to disk, and then read it.

name <- "sample_portfolio"
description <- "create from function"
cash <- list(percent = 10)
assets <- data.frame(symbol = c("AAPL","GOOG"), percent = c(80.5,9.5))
tolerance <- list(percent = 5)
cooldown <- list(days = 365)

model <- create_portfolio_model(name = name,
                      description = description,
                      cash = cash,
                      assets = assets,
                      tolerance = tolerance,
                      cooldown = cooldown)
str(model)
#> List of 8
#>  $ name       : chr "sample_portfolio"
#>  $ description: chr "create from function"
#>  $ cash       :List of 1
#>   ..$ percent: num 10
#>  $ assets     :'data.frame': 2 obs. of  2 variables:
#>   ..$ symbol : chr [1:2] "AAPL" "GOOG"
#>   ..$ percent: num [1:2] 80.5 9.5
#>  $ tolerance  :List of 1
#>   ..$ percent: num 5
#>  $ cooldown   :List of 1
#>   ..$ days: num 365
#>  $ created_at : chr "2024-03-12T14:46:49"
#>  $ updated_at : chr "2024-03-12T14:46:49"

save_portfolio_model(model, "/tmp/model.yaml")
#> [1] "/tmp/model.yaml"

Get brokerage account holdings

In order to rebalance an existing portfolio, we need to talk to our broker to find out what our current holdings are. In order to interact with a broker we need to define a connection. A connection is just a special function that tells downstream functions how to interact with a broker.

We use the function get_portfolio_current to get the current portfolio holdings.

# alpaca trading api
t_conn <- alpaca_connect('paper',
                          Sys.getenv("ALPACA_PAPER_KEY"),
                          Sys.getenv("ALPACA_PAPER_SECRET"))

# alpaca data api
d_conn <- alpaca_connect('data',
                          Sys.getenv("ALPACA_LIVE_KEY"),
                          Sys.getenv("ALPACA_LIVE_SECRET"))

holdings <- get_portfolio_current(t_conn) 
holdings
#> $cash
#>   currency quantity_held
#> 1      USD      10029.16
#> 
#> $assets
#>   symbol quantity_held
#> 1   AAPL           346
#> 2   GOOG           215

Compute Balancing Orders

Once we have our holdings, we can load our target holdings from the model.

targets <- holdings |>
 load_portfolio_targets(model) 
targets
#> $cash
#>   currency quantity_held percent_target
#> 1      USD      10029.16             10
#> 
#> $assets
#>   symbol quantity_held percent_target
#> 1   AAPL           346           80.5
#> 2   GOOG           215            9.5
#> 
#> $tolerance
#> $tolerance$percent
#> [1] 5

Now we know what we have, and what we need. But we need to translate holdings to percentages to be able to compare them. We use the price_portfolio() function for this. It gets prices from a data connection and computes current holding values and percentages.

priced <- targets |>
 price_portfolio(connection = d_conn, price_type = 'close') 

We can now use solve_portfolio() to work out what orders we need to submit to balance our portfolio.

solution <- priced|>
 solve_portfolio(terse = TRUE) 
solution
#> $cash
#>   currency out_of_band optimal_value
#> 1      USD       FALSE      10068.84
#> 
#> $assets
#>   symbol  price out_of_band optimal_order optimal_order_value
#> 1   AAPL 172.75        TRUE           118            20384.50
#> 2   GOOG 138.94        TRUE          -147           -20424.18
#> 
#> $tolerance
#> $tolerance$percent
#> [1] 5

Trade

At this point, we have enough information to submit orders to a broker. However, it is prudent to make sure that our orders are properly sized. Orders that are too big may move the market, and orders that are too small may incure excessive fees and create unnecessary churn.

orders <- solution |>
 constrain_orders(d_conn, 
                  daily_vol_pct_limit = 0.01, 
                  min_order_size = 1000, 
                  max_order_size = 10000 )
orders
#>   symbol order    value
#> 1   AAPL    57  9846.75
#> 2   GOOG   -71 -9864.74

Now that we have safely resized our orders, we can submit them to the trader.

trader(orders, 
       trader_life = 120,
       resubmit_interval = 5,
       trading_connection = t_conn, 
       pricing_connection = d_conn)
#> 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
#> 
#> 
#> commencing trader wind-down
#>  - attempting to cancel all unfilled orders
#>  - trader wind-down cancellation success
#> 
#> 
#> NO ORDERS TO EXECUTE. EXITING.
#>             timestamp symbol order  limit filled   status
#> 1 2024-03-12 14:46:53   AAPL    57 172.62     57   filled
#> 2                <NA>   GOOG   -71     NA      0 no_limit
#> 3 2024-03-12 14:47:02   GOOG   -71 139.63    -71   filled

All in one

The above code flow is useful if you want fine grained control of the rebalancing process, or if you want to log data from the pipeline to disk. Generally speaking, it is much more convenient to just use the balance_portfolio() wrapper function.

balance_portfolio(model,
                  t_conn,
                  d_conn,
                  verbose = T)
#> 
#> # TRADE RUN
#> MAX ASSET DRIFT: 111.76%
#> 
#> ORDERS FOR THIS RUN:
#>   symbol order    value
#> 1   AAPL    57  9846.75
#> 2   GOOG   -71 -9864.74
#> 
#> 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
#> 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
#> 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
#> 
#> 
#> commencing trader wind-down
#>  - attempting to cancel all unfilled orders
#>  - trader wind-down cancellation success
#> 
#> 
#> NO ORDERS TO EXECUTE. EXITING.
#> 
#> ORDER LOG:
#>             timestamp symbol order  limit filled   status
#> 1 2024-03-12 14:47:14   AAPL    57 172.61     57   filled
#> 2                <NA>   GOOG   -71     NA      0 no_limit
#> 3                <NA>   GOOG   -71     NA      0 no_limit
#> 4                <NA>   GOOG   -71     NA      0 no_limit
#> 5 2024-03-12 14:47:42   GOOG   -71 139.08    -71   filled
#> 
#> 
#> # TALLY RUN
#> MAX ASSET DRIFT: 7.35%
#> TALLY COMPLETE.
#> $portfolio_balanced
#> [1] FALSE
#> 
#> $drift
#>   symbol drift
#> 1   AAPL  0.86
#> 2   GOOG  7.35
#> 
#> $trades
#>             timestamp symbol order  limit filled   status
#> 1 2024-03-12 14:47:14   AAPL    57 172.61     57   filled
#> 2                <NA>   GOOG   -71     NA      0 no_limit
#> 3                <NA>   GOOG   -71     NA      0 no_limit
#> 4                <NA>   GOOG   -71     NA      0 no_limit
#> 5 2024-03-12 14:47:42   GOOG   -71 139.08    -71   filled

Note that the balance_portfolio() won’t necessarily rebalance your portfolio! Because we care about constraining our order size and price to get the best deals we can, the function will exit once the trader timeout is reached. To guarantee that the portfolio is rebalanced, we need to put balance_portfolio() into a while loop, and/or relax pricing and order size constraints. See the ‘Balance or Die’ vignette for an example of how to do this.