How to Normalize Weights When Weights don't sum to 100%

I'm working on a take-home assignment for a company. They want me to calculate the return of a portfolio of securities over time, given the returns of the securities over time and the initial weighting of the securities. I can do this for the normal case of no shorting, i.e. no negative weights. However, for the cases of 20% net long and 0% net long, I get different answers from their test cases after the first period, i.e. after the weights diverge from their initial values. I believe this is because I am not normalizing the weights properly. For the 100% long case, I just divide each weight by the sum of all weights to ensure they sum to 100%. What's the equivalent procedure for the other cases?

Here are the security returns:

SAMPLE_RETURN_ARRAY = [
[0.02, 0.01, 0.08, 0.04, -0.05],  # MSFT
[-0.02, 0.03, 0.06, 0.01, 0.05],  # AAPL
[0.01, -0.04, -0.03, -0.05, -0.03],  # GOOG
[0.04, 0.02, 0.01, -0.02, -0.01],  # TSLA
[-0.01, -0.01, -0.06, -0.03, 0.02],  # JPM
]


And here are the initial weights:

SAMPLE_WEIGHTS = [0.2, 0.3, 0.1, 0.05, 0.35]

these are the results for the 100% long case:

correct_result = [-0.0025, 0.00440602, 0.01166979, -0.00329201, 0.00761087]
r1 = np.all(np.isclose(correct_result, result, atol=1e-5, rtol=0))


I do indeed get their results correct to 5 decimal places.

Here are there results for 20% long and 0% long (i.e. market neutral):

20%:     correct_result = [0.0005, 0.00924038, 0.05129543, 0.01710086, -0.00349387]
0%:      correct_result = [0.0015, -0.01729905, -0.05853928, -0.02974261, -0.00176708]


In the 20% case, the weights are [0.2, 0.3, 0.1, -0.05, -0.35] and in the 0% case the weights are [-0.2, -0.3, 0.1, 0.05, 0.35].

Can someone help me understand how the 0.00924038 and -0.01729905 (the second-period portfolio returns) are calculated? I don't need code (although you're welcome to provide), just the math behind the numbers. I've tried a bunch of things and get close numbers, but never exact or to 5 decimal places.

Whenever you are not fully invested, you'll need to make assumptions. Simple example: You have USD 100 and invest 20% of it. Should you compute returns with a notional of only USD 20 (the actual notional invested), or on the total USD 100? In practice, these assumptions are rarely made explicit, but neither are they contentious: Since we stress that we invested 20%, we typically compute returns on the full notional.

In your example, the assumptions seem to be: you never rebalance; and you should not do any rescaling, but assume an initial notional of 1 (i.e. 100%).

One way to compute returns in this case is as follows:

1. Create artificial prices series of your returns series, starting with 1 at time 0, and collect them in a matrix $$P$$, each stock in one column.

2. Compute the portfolio values $$v = Pw + (1 - \Sigma(w))$$, with the remaining cash (if any) included in the portfolio value.

3. Compute simple returns of $$v$$.

As a check, I do these computations in R, and compare with the results of function returns of package PMwR (which I maintain):

library("PMwR")

SAMPLE_RETURN_ARRAY <- cbind(
MSFT = c( 0.02,  0.01 , 0.08,  0.04, -0.05),
AAPL = c(-0.02,  0.03,  0.06,  0.01,  0.05),
GOOG = c( 0.01, -0.04, -0.03, -0.05, -0.03),
TSLA = c( 0.04,  0.02,  0.01, -0.02, -0.01),
JPM  = c(-0.01, -0.01, -0.06, -0.03,  0.02)
)


First, create artificial price series:

P <- apply(rbind(0, SAMPLE_RETURN_ARRAY) + 1, 2, cumprod)
##          MSFT     AAPL      GOOG     TSLA       JPM
## [1,] 1.000000 1.000000 1.0000000 1.000000 1.0000000
## [2,] 1.020000 0.980000 1.0100000 1.040000 0.9900000
## [3,] 1.030200 1.009400 0.9696000 1.060800 0.9801000
## [4,] 1.112616 1.069964 0.9405120 1.071408 0.9212940
## [5,] 1.157121 1.080664 0.8934864 1.049980 0.8936552
## [6,] 1.099265 1.134697 0.8666818 1.039480 0.9115283


Now the computations:

## invest net 100%
w <- c(0.2, 0.3, 0.1, 0.05, 0.35)
## compute manually
v <- P %*% w + (1 - sum(w))
v[-1]/v[-length(v)] - 1
## check with PMwR::returns
c(returns(P, weights = w, rebalance.when = 1))
## [1] -0.002500000  0.004406015  0.011669786 -0.003292007  0.007610873
## [1] -0.002500000  0.004406015  0.011669786 -0.003292007  0.007610873

## invest net 20%
w20 <- c(0.2, 0.3, 0.1, -0.05, -0.35)
## compute manually
v <- P %*% w20 + (1 - sum(w20))
v[-1]/v[-length(v)] - 1
## check with PMwR::returns
c(returns(P, weights = w20, rebalance.when = 1))
## [1]  0.000500000  0.009240380  0.051295426  0.017100863 -0.003493869
## [1]  0.000500000  0.009240380  0.051295426  0.017100863 -0.003493869


Finally, net-zero investment:

## invest net 0%
w0 <- c(-0.2, -0.3, 0.1, 0.05, 0.35)
## compute manually
v <- P %*% w0 + (1 - sum(w0))
v[-1]/v[-length(v)] - 1
## check with PMwR::returns
c(returns(P, weights = w0, rebalance.when = 1))
## [1]  0.00150000 -0.01729905 -0.05853928 -0.02974261 -0.00176708
## [1]  0.00150000 -0.01729905 -0.05853928 -0.02974261 -0.00176708

• this is a beautiful answer. thank you! Aug 15, 2023 at 1:40