robertmartin8 / PyPortfolioOpt

Financial portfolio optimisation in python, including classical efficient frontier, Black-Litterman, Hierarchical Risk Parity
https://pyportfolioopt.readthedocs.io/
MIT License
4.55k stars 957 forks source link

Add constraints that ignore NaN scores #593

Open jamespanning opened 7 months ago

jamespanning commented 7 months ago

What are you trying to do? I have a table of asset class scores that I am trying to add various constraints from. However, some of these scores are only applicable to certain asset classes. For example, 'Duration' would have values for Bond sectors, but are 'NaN' for Equity sectors. I don't want to use 0s for equities, as any equity allocation would understate the calculated duration of the total bonds.

        Score   Duration
Sector      
Bond A      30  10.0
Bond B      40  5.0
Equity A    50  NaN
Equity B    60  NaN

What have you tried? I have tried the following:

  1. Add constraints based on scores https://pyportfolioopt.readthedocs.io/en/latest/FAQ.html#constraining-a-score
ef.add_constraint(lambda w: Constraint['Duration'].values @ w >= 0)
ef.add_constraint(lambda w: Constraint['Duration'].values @ w <= 7)

Error message:

ERROR in LDL_factor: Error in KKT matrix LDL factorization when computing the nonzero elements. The problem seems to be non-convex
ERROR in osqp_setup: KKT matrix factorization.
The problem seems to be non-convex.
...
SolverError: Workspace allocation error!
  1. Add custom constraints that would take a weighted average of the Duration ignoring sectors with 'NaN'. The np.nansum() functions work outside these constraint definitions when I try calculating the portfolio Duration scores given a weight vector.
def my_constraint_Duration_Min(weight_vector):
    return np.nansum(Constraint['Duration'].values * weight_vector) / np.nansum(np.where(np.isnan(Constraint['Duration'].values),0,1) * weight_vector) >= 0
def my_constraint_Duration_Max(weight_vector):
    return np.nansum(Constraint['Duration'].values * weight_vector) / np.nansum(np.where(np.isnan(Constraint['Duration'].values),0,1) * weight_vector) <= 7

ef.add_constraint(my_constraint_Duration_Min)
ef.add_constraint(my_constraint_Duration_Max)

Error message:

C:\ProgramData\anaconda3\Lib\site-packages\cvxpy\expressions\expression.py:621: UserWarning: 
This use of ``*`` has resulted in matrix multiplication.
Using ``*`` for matrix multiplication has been deprecated since CVXPY 1.1.
    Use ``*`` for matrix-scalar and vector-scalar multiplication.
    Use ``@`` for matrix-matrix and matrix-vector multiplication.
    Use ``multiply`` for elementwise multiplication.
...
Exception: Cannot evaluate the truth value of a constraint or chain constraints, e.g., 1 >= x >= 0.

What data are you using? See example below. The code is working for me with the basic 'Score' constraint, but fails when I tried adding either 'Duration' constraint from above.

from pypfopt import EfficientFrontier, plotting
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

#Sectors
Return_data = {'Sector': ['Bond A', 'Bond B', 'Equity A', 'Equity B'],
               'Return': [0.10, 0.05, 0.15, 0.20]}

Covar_data = {'Sector': ['Bond A', 'Bond B', 'Equity A', 'Equity B'],
              'Bond A': [0.0222698592925932, 0.0102208239112873, 0.00203379607967045, 0.00170610486559161],
              'Bond B': [0.0102208239112873, 0.00570949705146003, 7.05105100768718E-06, -0.000203014876458396],
              'Equity A': [0.00203379607967045, 7.05105100768718E-06, 0.013319494914958, 0.0136714010608255],
              'Equity B': [0.00170610486559161, -0.000203014876458396, 0.0136714010608255, 0.0146027220944363]}

Constraint_data = {'Sector': ['Bond A', 'Bond B', 'Equity A', 'Equity B'],
                   'Score': [30, 40, 50, 60],
                   'Duration': [10, 5, None, None]}

mu = pd.DataFrame(Return_data).set_index('Sector')
mu = mu['Return']

S = pd.DataFrame(Cov_data).set_index('Sector')
S = np.array(S, dtype=np.float64)

Constraint = pd.DataFrame(Constraint_data).set_index('Sector')

ef = EfficientFrontier(mu, S, weight_bounds=(0,1))

## Add constraints
ef.add_constraint(lambda w: Constraint['Score'].values @ w >= 0)
ef.add_constraint(lambda w: Constraint['Score'].values @ w <= 55)

# How to add constraints with NaN?

fig, ax = plt.subplots()
plotting.plot_efficient_frontier(ef, ax=ax, show_assets=True, show_tickers=True, showfig=True)