This vignette provides a high level introduction to the
rblncr
package. You can use the functions in this package
to:
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"
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
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
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
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.