-
Notifications
You must be signed in to change notification settings - Fork 138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add risk parity loss function & risk budgeting allocator #98
Conversation
Thanks for the PR, I will check it asap:) |
for this loss function this |
for risk budgeting allocator, it is not working yet and I am kind of stuck.
|
Ok, I read through the theory:) Let's discuss the allocator first.
I just did a sanity check and it seems to be working in the most basic setup: import cvxpy as cp
import numpy as np
def check_squared_PD(matrix):
"""Check matrix squared is positive definite."""
return np.all(np.linalg.eigvals(matrix @ matrix) > 0)
np.random.seed(98)
n_assets = 5
max_weight = 1
# Define objective and contstraints
covmat_sqrt = cp.Parameter((n_assets, n_assets))
b = cp.Parameter(n_assets, nonneg=True)
w = cp.Variable(n_assets)
term_1 = 0.5 * cp.sum_squares(covmat_sqrt @ w)
term_2 = cp.sum(b @ cp.log(w))
objective = cp.Minimize(term_1 - term_2)
constraint = [
cp.sum(w) == 1,
# cp.sum(b) == 1, # IMO this constraint does not make sense because b is a Parameter and not a Variable
w >= 0,
w <= max_weight]
# Set parameter values
some_vector = np.random.random(n_assets)
b.value = some_vector / some_vector.sum() # sums up to one and nonnegative
some_matrix = np.random.random((n_assets, n_assets))
covmat_sqrt.value = some_matrix.T @ some_matrix # Positive definite by construction
# Define Problem
prob = cp.Problem(objective, constraint)
assert prob.is_dcp()
assert prob.is_dpp() # This will guarantee that `cvxpylayers` will work = one can get gradients w.r.t. parameters
# Solve
prob.solve()
print(f"b: {b.value}\nCovmat is PD: {check_squared_PD(covmat_sqrt.value)}\nWeights: {w.value}") b: [0.25433564 0.19699964 0.10462297 0.27111695 0.17292481]
Covmat is PD: True
Weights: [0.09108105 0.73822756 0.01435531 0.05227587 0.1040602 ] So if I understand your problem correctly, you are saying that there are some covariance matrices and some b vectors for which Another question I have is why did you decide to go for this formulation? Is there any benefit? They should be all equivalent, or not? |
The way you wrote the loss function (containing |
For the allocator, I need to catch up on my maths to understand if
One reason is that it was one of the more simpler formulations of risk budgeting that is convex, see Spinu's eqn 4, also check out this video and this paper on other formulations of risk budgeting/parity, some may not be convex
If you mean compared to Sharpe Ratio, Minimum Variance and other metrics, then Risk Budgeting/Parity brings something different in the sense that it always tries to have the weight of each asset contribute equal risk to the portfolio. Refer to this blog on a comparison between different metrics vs Risk Parity. Note they used Spinu's version. If you mean compared to other Risk Budgeting/Parity formulations then I think the difference is in terms of ease of compute and whether it is convex or not, again refer to the video and paper.
Since both allocator and loss function are the same Risk Budgeting/Parity concept, seems natural to have the same formulas for both. Anyway, as you can see my theory is not that strong to be implementing these formulas effectively. I think I will leave it to you to implement the allocator and loss function of Risk Budgeting/Parity, is that okay? I think I can learn a lot from the way you would implement it and maybe next time I'll PR an actual working code 🤣. Seems like your version of the allocator is basically done? And I'll leave it to you to decide which formulations to go for in the loss function. Feel free to let me know if there's anything I can help with. |
You are totally right, the
Come on, you wrote everything!!! You did a great job:) I will finalize this PR once I have a bit of time. I think it is a great addition! The allocator is more or less done and for the loss I will just try to use the other formula without |
Hi, here is my take on this eqn without log for the risk parity/budgeting loss function. I'm not really sure if there is a proper way to reduce the last part of the eqn, 2 sums, but here is my take,
On another note, I think it would be nice if the |
Cool!! It looks correct on the first sight, however, I would suggest testing this programatically. Anyway, if you have some spare time feel free to make it a part of the actual source code and finalize this PR yourself! Especially if you need this change quickly and do not want to wait for me have some spare time to do it:) The steps to finalize the PR
As said above, if you want to do it, feel free to ask any questions and I will help! |
By the way, before continuing I just want to ask about the weights output from
you mentioned something
but I still didnt get it :/ |
Sorry for the confusion. Nonpositive weights can become an annoying problem when the loss function only works on "long only" portfolios. For example, if the loss formula contains logarithms of weights or similar (as in your initial implementation of I hope what I wrote makes sense:) And hopefully I answered your question. |
Since any allocator layers using the numerical method would run into small negative values issue, and the goal is to have the optimization algorithm converge anyway, why not just perform manual rescaling (minimum 0 weights) at the output as a standard across all numerical based allocator layers since it doesn't really affect performance. This way we can have more flexibility in the loss functions. At least this way loss functions can be design without worrying about negative weights (at least the intuition/logic is still there where weights can't be negative). Yes 0 is still an issue for log but that would be a loss function specific issue. |
I totally agree. I created an issue #102 and I suggest we address it in a totally separate PR. Does that sound good? When it comes to this PR I guess we can assume for now that the weights are "correct". |
Here is the Allocator Layer:
Here is the Loss Function: For the loss function I am a bit sceptical at my implementation, especially at option 1 vs option 2 see below code. The formula (option 1) states a dot product followed by a square and then sum, but this sum is required across the last 2 dimensions which is a bit unusual to me. On the other hand if we go for option 2 then it seems more natural to me.
|
Amazing work:) The allocator is perfect. Regarding the loss, see below my remarks
def stupid_fpass(self, w, y):
n_samples, n_assets = w.shape
covar = self.covariance_layer(y[:, self.returns_channel, ...])
var = torch.cat([(w[[i]] @ covar[i]) @ w[[i]].permute(1, 0) for i in range(n_samples)], dim=0)
vol = torch.sqrt(var)
lhs = vol / n_assets
rhs = torch.cat([(1 / vol[i]) * w[[i]] * (w[[i]] @ covar[i]) for i in range(n_samples)], dim=0)
res = torch.tensor([((lhs[i] - rhs[i]) ** 2).sum() for i in range(n_samples)])
return res Another validation I did was generating the input data in the following fashion. n_samples = 15
n_channels = 1
horizon = 1000
n_assets = 7
w = torch.ones((n_samples, n_assets)) / n_assets
normal = torch.distributions.MultivariateNormal(torch.zeros(n_assets), torch.eye(n_assets))
y = normal.sample((n_samples, 1, horizon,)) So as we increase the horizon, the sample covariance matrix should be closer and closer to the identity matrix. And Other than the remarks above, I did not notice any other problems:) Thank you very much! It is a cool addition:) Do you want to give it a shot and make it a part the source code (+ the other things I described in #98 (comment))?:) |
Hi @jankrepl thanks for your review on this. I think I will let you implement the testing part as I am in no rush. This was more of a learning experience for me although I feel bad for leaving it like this. I've recently been caught up in some other stuff so I'll have to leave it like this for now, but when I am free again I'll definitely help out where possible (although might be late). Again, thanks for your patience!! It was much appreciated!! :) |
@turmeric-blend Sure, no problem! I will do it:))))) |
Codecov Report
@@ Coverage Diff @@
## master #98 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 18 18
Lines 1733 1762 +29
=========================================
+ Hits 1733 1762 +29
Continue to review full report at Codecov.
|
added risk parity loss function based on this paper, here is a stack exchange version, Ze Vinicius's answer to make things simple. Please double check by formulas. Thanks.