2
$\begingroup$

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.

Let me know if I can provide more info.

Many thanks in advance!

$\endgroup$

1 Answer 1

2
$\begingroup$

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
$\endgroup$
1
  • $\begingroup$ this is a beautiful answer. thank you! $\endgroup$ Aug 15, 2023 at 1:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.