A Shiny app that calculates how to allocate new investment funds across a portfolio to bring allocations as close as possible to target weights — without selling any assets.
Based on: J. Bartroff, "Rebalance your portfolio without selling", arXiv:2305.12274. This repository implements the algorithms from that paper; the implementation is original code. See the paper for theory and proofs:
Bartroff, J. (2023). Rebalance your portfolio without selling. College Mathematics Journal. arXiv:2305.12274
Differential evolution (DE) is a stochastic numerical optimiser that was considered as an alternative approach. It would search over candidate allocations, iteratively minimising tracking error against target weights. While capable, it was rejected in favour of the closed-form solutions for the following reasons:
- Exact optimality: the ℓ₁ and ℓ₂ solutions are mathematically proven to be globally optimal. DE is heuristic and provides no such guarantee.
- Dollar amounts: this app works in dollar values, where fractional purchases are allowed (e.g. mutual funds, dollar-based brokers). The continuous closed-form solutions are directly applicable and more precise than DE, which is better suited to discrete whole-share problems.
- Transparency: each solution reduces to a single interpretable formula — a deflation factor α or threshold λ*. DE is a black-box search with no intuitive explanation of the resulting allocation.
- Speed and determinism: the analytical solutions run instantly and always produce the same result. DE requires many objective evaluations and a fixed random seed for reproducibility.
DE would have an edge if whole-share constraints or per-trade fee schedules were required. Neither applies here.
You have a portfolio of
The naive adjustments that would achieve exact targets if selling were allowed are:
Since some
The paper proves two closed-form solutions depending on how "as close as possible" is defined.
Find k*: the largest k such that sum_{i=1}^{k}(delta_i - delta_k) < y (with deltas sorted descending). Then:
lambda* = (sum_{i=1}^{k*} delta_i - y) / k*
y_i* = (delta_i - lambda*)+
Only the k* assets with the largest naive adjustments receive funds. Best when you want to close the biggest gaps first.
In the normal portfolio case y <= sum(delta_i+), a deflation factor is applied uniformly:
alpha = y / sum(delta_i+)
y_i* = alpha * delta_i+
All underweight assets receive funds proportionally to how underweight they are. When y > sum(delta_i+) (surplus), the excess is distributed evenly across all assets.
| ℓ₁ | ℓ₂ | |
|---|---|---|
| Assets receiving funds | All underweight | Only top k* underweight |
| Behaviour | Spreads funds proportionally | Concentrates on largest gaps |
| Optimises | Sum of absolute deviations | Sum of squared deviations |
- Input Data Tab: Edit assets, tickers, current values, and target percentages directly in the table. Upload a CSV file with your portfolio data.
- Buy Orders Tab: Review the calculated buy orders with a bar chart and detailed breakdown table.
- CSV Format: Columns: Asset, Ticker, Group (optional), Current_Value, Target_Percent, Chosen Weight for Buy (optional). Target percentages must sum to 100%.
- Groups: Assign assets to groups for aggregated analysis. Within each group, you can specify how new buys are split.
- New Funds: Enter the total amount of new money to invest. The app shows the minimum amount needed to avoid selling.
- Rebalancing Methods: Choose between ℓ₁ (proportional), ℓ₂ (threshold), or full rebalance (with selling) for comparison.
- Analysis Tab: View current and rebalanced allocations with pie charts and detailed tables.
Asset,Ticker,Group,Current_Value,Target_Percent,Chosen Weight for Buy
Total US Stock Market Index,VTI,US Stocks,40739.2,70,
Total International Stock Index,VXUS,International Stocks,13062.5,20,
Total Bond Market,BND,Bonds,5019,10,100
Treasury ETF,VGIT,Bonds,2000,10,0Target_Percentvalues must sum to 100.Groupis a label used to group assets (e.g. by asset class). Assets sharing aGroupandTarget_Percentcompete for the same target allocation via theChosen Weight for Buycolumn.Chosen Weight for Buyis optional. When multiple assets share a target group, this sets the relative weight for how new purchases within that group are split. Leave blank to use the default.
Restore the project environment using renv before running the app.
# 1. Install renv if you don't already have it
install.packages("renv")
# 2. Restore the project library from renv.lock
renv::restore()
# 3. Run the app
shiny::runApp()