EXPERIMENTAL: These features are experimental and should not be used in production systems.
Mathematical Optimization: The Portfolio Optimization Problem
A common example of a mixed-integer quadratic programming (MIQP) problem is the portfolio optimization problem.The problem of portfolio optimization is a classic issue in finance. The goal is to find the best way to invest a given amount of money across a set of possible investments. The challenge is to balance the expected return of the portfolio against the risk, typically represented by the variance or standard deviation of the portfolio return. In this tutorial, we’re considering a variant of the problem where we restrict the number of investments we can include in the portfolio.
Firstly, let’s define the data for our problem:
// Define the set of possible investments, their expected returns,
// the covariances between their returns, the risk aversion parameter,
// and the maximum number of investments.
module data
def investments = "Asset1" ; "Asset2" ; "Asset3" ; "Asset4"
def expected_returns = { ("Asset1", 0.1) ; ("Asset2", 0.12) ; ("Asset3", 0.09) ; ("Asset4", 0.11) }
def covariances = {
("Asset1", "Asset1", 0.05) ; ("Asset1", "Asset2", 0.01) ; ("Asset1", "Asset3", -0.01) ; ("Asset1", "Asset4", 0.02) ;
("Asset2", "Asset2", 0.06) ; ("Asset2", "Asset3", 0.01) ; ("Asset2", "Asset4", 0.02) ;
("Asset3", "Asset3", 0.04) ; ("Asset3", "Asset4", -0.01) ;
("Asset4", "Asset4", 0.05)
}
def risk_aversion_parameter = 0.5
def max_investments = 2
end
This data defines the set of possible investments (stocks in this case), the expected return of each investment, the covariances between the returns of the investments (which represent the risk), a parameter that defines our aversion to risk, and the maximum number of investments we can include in the portfolio.
Let’s now build the model that will be used to solve our portfolio optimization problem.
@inline
module model
// Bring the rel:mathopt:Solver:Solve DSL into scope.
with mathopt:Solver:Solve use sum, foreach, +, *, ≼, ≽, ∧, ∨, =, -, /
// Bring the data into scope.
with data use investments, expected_returns, covariances, risk_aversion_parameter, max_investments
// Define the model variables.
def variables = rel:mathopt:variables[
{:x, "continuous", (investments)};
{:x, "lower_bound", 0.0}; {:x, "upper_bound", 1.0};
{:y, "binary", (investments)}
]
// Bring the variables into scope.
with variables use x, y
We define two types of variables: x[i]
represents the fraction of our money
that we invest in investment i
, while y[i]
is a binary variable that indicates
whether we include investment i
in the portfolio.
Now let’s define our objective function within the model
module:
// Define the model objective.
def maximize =
sum[expected_returns[i] * x[i] for i in investments] - risk_aversion_parameter * sum[covariances[i, j] * x[i] * x[j] for i in investments, j in investments]
The objective function consists of two parts: the first part is the expected return of the portfolio, which we want to maximize, and the second part is the risk of the portfolio, which we want to minimize. The risk is represented by the variance of the portfolio return, calculated as the sum of the products of the covariances times the investment fractions. The risk_aversion_parameter allows us to control the trade-off between risk and return. If this parameter is large, we put more emphasis on minimizing risk, while if it is small, we put more emphasis on maximizing return.
Next, we define our constraints within the model
module:
// Define the model constraints
module constraints
// Budget constraint: The entire budget (// represented by 1) must be invested.
def budget_constraint = sum[x[i] for i in investments] = 1
// Maximum investments constraint: No more than 'max_investments' investments can be made.
def max_investments_constraint = sum[y[i] for i in investments] ≼ max_investments
// Linking constraints: If an investment is not included (y[i]=0), it cannot be part of the portfolio (x[i]=0).
def linking_constraints = foreach[i in investments : x[i] ≼ y[i]]
end
end
The first constraint, the budget_constraint
, is our budget constraint. It states that the sum of all
investment fractions must be equal to 1, meaning that we invest all our money.
The second constraint, max_investments_constraint
, limits the number of investments in our portfolio.
It says that the sum of the binary variables y[i]
must be less than or equal to max_investments
.
Finally, we define the linking_constraints
, which link the binary y
variables with the continuous x
variables.
These constraints state that if an investment is not selected (y[i]=0
), then we can’t allocate any money to it (x[i]=0
).
Next we set our solver to be the mixed-integer quadratic programming solver "Pavito"
, and solve the model:
// Set the solver to be "Pavito".
module config
def solver = "Pavito"
end
// Optimize the model.
def result = rel:mathopt:optimize[model, config]
// Extract the result of the optimization process.
def extracted = rel:mathopt:extract_result[result, model:variables]
And voila! The solution will give us the optimal portfolio allocation to maximize our expected return while managing our risk.
Full Code Implementation
The full code implementation of the portfolio optimization problem is shown below.
// read query
// Define the set of possible investments, their expected returns,
// the covariances between their returns, the risk aversion parameter,
// and the maximum number of investments.
module data
def investments = "Asset1" ; "Asset2" ; "Asset3" ; "Asset4"
def expected_returns = { ("Asset1", 0.1) ; ("Asset2", 0.12) ; ("Asset3", 0.09) ; ("Asset4", 0.11) }
def covariances = {
("Asset1", "Asset1", 0.05) ; ("Asset1", "Asset2", 0.01) ; ("Asset1", "Asset3", -0.01) ; ("Asset1", "Asset4", 0.02) ;
("Asset2", "Asset2", 0.06) ; ("Asset2", "Asset3", 0.01) ; ("Asset2", "Asset4", 0.02) ;
("Asset3", "Asset3", 0.04) ; ("Asset3", "Asset4", -0.01) ;
("Asset4", "Asset4", 0.05)
}
def risk_aversion_parameter = 0.5
def max_investments = 2
end
// Define the model.
module model
// Bring the rel:mathopt:Solver:Solve DSL into scope.
with mathopt:Solver:Solve use sum, foreach, +, *, ≼, ≽, ∧, ∨, =, -, /
// Bring the data into scope.
with data use investments, expected_returns, covariances, risk_aversion_parameter, max_investments
// Define the model variables.
def variables = rel:mathopt:variables[
{:x, "continuous", (investments)};
{:x, "lower_bound", 0.0}; {:x, "upper_bound", 1.0};
{:y, "binary", (investments)}
]
// Bring the variables into scope.
with variables use x, y
// Define the model objective.
def maximize =
sum[expected_returns[i] * x[i] for i in investments] - risk_aversion_parameter * sum[covariances[i, j] * x[i] * x[j] for i in investments, j in investments]
// Budget constraint: The entire budget (// represented by 1) must be invested.
def constraints:budget_constraint = sum[x[i] for i in investments] = 1
// Maximum investments constraint: No more than 'max_investments' investments can be made.
def constraints:max_investments_constraint = sum[y[i] for i in investments] ≼ max_investments
// Linking constraints: If an investment is not included (y[i]=0), it cannot be part of the portfolio (x[i]=0).
def constraints:linking_constraints = foreach[i in investments : x[i] ≼ y[i]]
end
// Set the solver to be "Pavito".
module config
def solver = "Pavito"
end
// Optimize the model.
def result = rel:mathopt:optimize[model, config]
// Extract the result of the optimization process.
def output = rel:mathopt:extract_result[result, model:variables]