Portfolio Contribution Complexities

Author

Konilo Zio

Published

May 2, 2026

Modified

May 4, 2026

1 Introduction

When contributing to an investment portfolio that follows a strategy involving more than one asset – each with its own target allocation – while ruling out selling, computing the amount to contribute to each asset can be tricky. The study that follows explains that challenge and provides its solution. This solution is provided as a CLI tool available at github.com/Konilo/dcallocate.

2 No selling required to stay balanced – the simplest case

Let’s say we have 1 000 € to contribute and our portfolio is like so:

Table 1: Pre-contribution situation
Asset Target allocation Actual allocation Target value Actual value
Index ETF 90 % 95 % 9 000 € 9 500 €
Managed-futures ETF 10 % 5 % 1 000 € 500 €
Total portfolio 100 % 100 % 10 000 € 10 000 €

Adding 1 000 € to the portfolio would lead to the following value targets

Table 2: Post-contribution targets
Asset Target allocation Target value Pre-contribution value Delta
Index ETF 90 % 9 900 € 9 500 € + 400 €
Managed-futures ETF 10 % 1 100 € 1 000 € + 600 €
Total portfolio 100 % 11 000 € 10 000 € + 1 000 €

In that case, 400 € of the 1 000 € contribution are allocated to the index ETF and 600 € to the managed-futures ETF to reach the targets. The math is simple, no selling is required to stay balanced.

3 Selling is required to stay balanced and is allowed

Now, let’s say that just before contributing, the portfolio is like so:

Table 3: Pre-contribution situation
Asset Target allocation Actual allocation Target value Actual value
Index ETF 90 % 95 % 27 000 € 28 500 €
Managed-futures ETF 10 % 5 % 3 000 € 1 500 €
Total portfolio 100 % 100 % 30 000 € 30 000 €

Contributing 1 000 € to the portfolio would lead to the following value targets

Table 4: Post-contribution targets
Asset Target allocation Target value Pre-contribution value Delta
Index ETF 90 % 27 900 € 28 500 € - 600 €
Managed-futures ETF 10 % 3 100 € 1 500 € + 1 600 €
Total portfolio 100 % 31 000 € 30 000 € + 1 000 €

In that case we can see that, to reach the targets, we would need to:

  • sell 600 € worth of index ETF shares
  • and buy 1 600 € of managed-futures ETF shares (i.e., the 1 000 € contribution + 600 € of cash from selling the index ETF shares).

That’s still simple, mathematically.

3.1 Implementation in R

When selling is allowed, computing each asset’s adjustment (i.e., “buy x €” or “sell y €”) is straightforward: subtract its pre-contribution value from its post-contribution target value. A negative result means selling is required.

Code
allocate_with_selling <- function(v_pre, tw, C) {
    stopifnot(
        length(v_pre) == length(tw),
        all(tw > 0),
        abs(sum(tw) - 1) < 1e-9,
        C >= 0
    )
    tw * (sum(v_pre) + C) - v_pre
}

3.1.1 Demo case

Reusing the portfolio from the pre-contribution situation above:

Code
allocate_with_selling(v_pre = c(28500, 1500), tw = c(0.9, 0.1), C = 1000)
[1] -600 1600
Code
# expected: -600, 1600

4 The math problem that comes from prohibiting selling

From a practical standpoint, selling is generally avoided, if possible, in order to minimize order fees which can substantially damage the portfolio’s return if repeated (\(x\)% of the order’s amount is paid by the investor).

The resulting math problem is as follows.

The input parameters:

  • \(n\) = number of assets, indexed by \(i\) = 1, 2, …, n.
  • \(\text{tw}_i\) = target weight of asset \(i\) (e.g., 0.85). The sum of the targets is 1.
  • \(v_i^{\text{pre}}\) = pre-contribution value of asset \(i\), in €.
  • \(V^{\text{pre}} = \sum_{i=1}^{n} v_i^{\text{pre}}\) = pre-contribution total portfolio value.
  • \(C\) = contribution to invest, in €.

The variables we are solving for:

  • \(\Delta_i\) = how much of \(C\) should be allocated to asset \(i\), in €.
  • With two constraints:
    • \(\Delta_i \geq 0\) for all \(i\) i.e., no selling is allowed,
    • \(\sum_{i=1}^{n} \Delta_i = C\): the whole contribution amount is invested.

Three variables can be derived from the known input parameters:

  • \(V^{\text{post}} = V^{\text{pre}} + C\): post-contribution total.
  • \(\text{tv}_i^{\text{post}} = \text{tw}_i \times V^{\text{post}}\): target post-contribution value of asset \(i\), in €.
  • \(d_i = \text{tv}_i^{\text{post}} − v_i^{\text{pre}}\): the deficit of asset \(i\), measuring how far below its post-contribution target it currently sits. Positive means asset \(i\) is underweight; negative means it is already overweight.

And one identity: the sum of the deficits equals the contribution.

\[ \sum_{i=1}^{n} d_i = \sum_{i=1}^{n} (\text{tv}_i^{\text{post}} − v_i^{\text{pre}}) = \sum_{i=1}^{n} \text{tv}_i^{\text{post}} − \sum_{i=1}^{n} v_i^{\text{pre}} = V^{\text{post}} − V^{\text{pre}} = C \tag{1}\]

5 When selling is prohibited and all deficits \(\geq 0\)

When selling is prohibited but all deficits are positive – i.e., all assets are underweight – the resolution is simple:

  • \(\Delta_i = d_i\) for every asset.
  • By definition, both constraints are respected:
    • each \(\Delta_i\) \(\geq 0\)
    • and \(\Delta_i\) sum to \(C\) as per Equation 1.
  • By setting \(\Delta_i\) to \(d_i\), every asset’s value reaches its target: the portfolio is perfectly balanced.

6 When selling is prohibited and some deficits \(< 0\)

When some deficits are negative, a more complex method is necessary to compute the \(\Delta_i\) values.

From an investor standpoint, that happens when between contributions, the assets in the portfolio change in value at different rates: for example, the index ETF rallies while the managed-futures ETF moves sideways. The faster-growing assets become overweight relative to their target weights, and the slower ones become underweight. If the drift is small, the new contribution \(C\) is enough to (i) top up the underweight assets back to their targets and (ii) dilute the overweight ones back to their targets as well. But once an asset has drifted far enough that even diluting the portfolio with \(C\) doesn’t bring it back to target, perfect rebalancing would require selling some of that asset – which we prohibit.

From a mathematical standpoint, this shows up as \(v_i^{\text{pre}} > \text{tv}_i^{\text{post}}\) for at least one asset, which means that we have at least one negative deficit (\(d_i < 0\)). When that happens, a more complex method is required to compute the \(\Delta_i\).

6.1 The method

  1. Set \(\Delta_i = 0\) for every overweight asset: no contribution for them. Those assets are “stuck”, and the remaining ones are “non-stuck”.
  2. Compute the post-contribution total value that the non-stuck assets must collectively reach (\(V^\text{post}_N\)).
    • Define index sets: \(S\) = set of indices of stuck assets, \(N\) = set of indices of non-stuck assets.
    • Make the computation: \[ V^\text{post}_N = V^\text{post} - \sum_{i \in S} v_i^{\text{pre}} \]
  3. Compute the renormalized target weights for the non-stuck assets (\(\text{tw}^*_i\)).
    • For each \(i \in N\), divide its original target weight by the sum of the non-stuck assets’ original target weights: \[ \text{tw}^*_i = \frac{\text{tw}_i}{\sum_{j \in N} \text{tw}_j} \]
    • By construction, \(\sum_{i \in N} \text{tw}^*_i = 1\).
  4. Compute the new target values and deficits for the non-stuck assets (\(\text{tv}^{\text{post},*}_i\) and \(d^*_i\)).
    • For each \(i \in N\): \[ \text{tv}^{\text{post},*}_i = \text{tw}^*_i \times V^\text{post}_N, \qquad d^*_i = \text{tv}^{\text{post},*}_i - v_i^{\text{pre}} \]
  5. If any \(d^*_i < 0\): add those indices to \(S\) (i.e., mark them stuck), set their \(\Delta_i\) to 0, and return to step 2.
  6. Otherwise (all surviving \(d^*_i \geq 0\)): set \(\Delta_i = d^*_i\) for every \(i \in N\). Stuck assets keep \(\Delta_i = 0\). Problem solved.

6.2 Why this method is correct

Two facts justify the algorithm without the need for a measure of imbalance (a cost function whose result we should minimize).

  • Stuck assets must get \(\Delta_i = 0\). For any asset \(i\) with \(d_i < 0\) (i.e., \(v_i^\text{pre} > \text{tv}_i^\text{post}\)), allocating any cash \(\Delta_i > 0\) only grows the overshoot while depriving an underweight asset of cash that would reduce its shortfall. This strictly worsens any reasonable measure of imbalance (sum of absolute deviations, sum of squared deviations, max deviation, etc.). So the choice \(\Delta_i = 0\) for stuck assets is forced regardless of how we measure imbalance.

  • Surviving deficits still sum to \(C\) at every recursion step. The identity in Equation 1 also holds after dropping the stuck set \(S\) and renormalizing target weights across \(N\):

\[ \sum_{i \in N} d^*_i = \sum_{i \in N} \text{tw}^*_i \times V^\text{post}_N - \sum_{i \in N} v_i^\text{pre} = V^\text{post}_N - \sum_{i \in N} v_i^\text{pre} = V^\text{post} - V^\text{pre} = C \]

(where the second equality uses \(\sum_{i \in N} \text{tw}^*_i = 1\), and the third uses \(V^\text{post}_N = V^\text{post} - \sum_{i \in S} v_i^\text{pre}\).) So when the recursion terminates with all surviving \(d^*_i \geq 0\), setting \(\Delta_i = d^*_i\) for \(i \in N\) uses exactly the \(C\) € available and lands every survivor on its renormalized target value \(\text{tv}^{\text{post},*}_i\).

6.3 Implementation in R

The function below implements the algorithm from the previous section. The variable names map directly to the math notation:

  • v_pre is \(v_i^\text{pre}\),
  • tw is \(\text{tw}_i\),
  • C is \(C\),
  • V_post is \(V^\text{post}\),
  • V_post_N is \(V^\text{post}_N\),
  • tw_star is \(\text{tw}^*_i\),
  • d_star is \(d^*_i\),
  • and delta is \(\Delta_i\).
Code
allocate_no_selling <- function(v_pre, tw, C, verbose = FALSE) {
    stopifnot(
        length(v_pre) == length(tw),
        all(tw > 0),
        abs(sum(tw) - 1) < 1e-9,
        C >= 0
    )

    n <- length(v_pre)
    V_post <- sum(v_pre) + C
    stuck <- rep(FALSE, n)
    delta <- rep(0, n)

    iter <- 0
    repeat {
        iter <- iter + 1
        N <- which(!stuck)
        V_post_N <- V_post - sum(v_pre[stuck])
        tw_star <- tw[N] / sum(tw[N])
        d_star <- tw_star * V_post_N - v_pre[N]

        if (verbose) {
            S_str <- if (any(stuck)) toString(which(stuck)) else "(none)"
            cat(sprintf("\n--- Iteration %d ---\n", iter))
            cat(sprintf("  S (stuck):     %s\n", S_str))
            cat(sprintf("  N (non-stuck): %s\n", toString(N)))
            cat(sprintf("  V^post_N:      %g\n", V_post_N))
            cat(sprintf("  tw*:           %s\n", toString(round(tw_star, 4))))
            cat(sprintf("  d*:            %s\n", toString(round(d_star, 4))))
        }

        newly_stuck <- d_star < 0
        if (!any(newly_stuck)) {
            delta[N] <- d_star
            if (verbose) cat("  -> all d* >= 0; allocating d* to N. Done.\n")
            break
        }
        if (verbose) {
            cat(sprintf("  -> newly stuck: %s\n", toString(N[newly_stuck])))
        }
        stuck[N[newly_stuck]] <- TRUE
    }

    delta
}

6.3.1 Demo cases

Three demo cases, mirroring the situations described above.

  1. No recursion needed: reusing the portfolio from Section 2:
Code
allocate_no_selling(v_pre = c(9500, 500), tw = c(0.9, 0.1), C = 1000)
[1] 400 600
Code
# expected: 400, 600
  1. Single recursion step: the portfolio from Section 3, but applying the no-selling rule from Section 6 – the index ETF gets stuck and all cash goes to the managed-futures ETF:
Code
allocate_no_selling(v_pre = c(28500, 1500), tw = c(0.9, 0.1), C = 1000)
[1]    0 1000
Code
# expected: 0, 1000
  1. Two recursion steps: a 3-asset portfolio where one asset is initially overweight, and dropping it reveals a second overweight asset via renormalization (running with verbose = TRUE to trace the iterations):
Code
allocate_no_selling(v_pre = c(60, 45, 1), tw = c(0.5, 0.4, 0.1), C = 10, verbose = TRUE)

--- Iteration 1 ---
  S (stuck):     (none)
  N (non-stuck): 1, 2, 3
  V^post_N:      116
  tw*:           0.5, 0.4, 0.1
  d*:            -2, 1.4, 10.6
  -> newly stuck: 1

--- Iteration 2 ---
  S (stuck):     1
  N (non-stuck): 2, 3
  V^post_N:      56
  tw*:           0.8, 0.2
  d*:            -0.2, 10.2
  -> newly stuck: 2

--- Iteration 3 ---
  S (stuck):     1, 2
  N (non-stuck): 3
  V^post_N:      11
  tw*:           1
  d*:            10
  -> all d* >= 0; allocating d* to N. Done.
[1]  0  0 10
Code
# expected: 0, 0, 10

7 Appendix

This qmd took 0 minutes to render. It was rendered in the following environment:

R version 4.5.1 (2025-06-13)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 24.04.4 LTS

Matrix products: default
BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
LAPACK:
/usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;
LAPACK version 3.12.0

attached base packages:
[1] stats graphics grDevices datasets utils methods base

other attached packages:
[1] knitr_1.50 ggplot2_3.5.2 data.table_1.17.6

loaded via a namespace (and not attached):
[1] vctrs_0.6.5 cli_3.6.5 rlang_1.1.6 xfun_0.52
[5] renv_1.1.4 generics_0.1.4 jsonlite_2.0.0 glue_1.8.0
[9] htmltools_0.5.8.1 scales_1.4.0 rmarkdown_2.29 grid_4.5.1
[13] evaluate_1.0.4 tibble_3.3.0 fastmap_1.2.0 yaml_2.3.10
[17] lifecycle_1.0.4 compiler_4.5.1 dplyr_1.1.4 RColorBrewer_1.1-3
[21] htmlwidgets_1.6.4 pkgconfig_2.0.3 farver_2.1.2 digest_0.6.37
[25] R6_2.6.1 tidyselect_1.2.1 pillar_1.10.2 magrittr_2.0.3
[29] withr_3.0.2 tools_4.5.1 gtable_0.3.6