© Carlos Oliveira 2021
C. OliveiraPractical C++20 Financial Programminghttps://doi.org/10.1007/978-1-4842-6834-6_12

12. Optimization

Carlos Oliveira1  
(1)
Seattle, WA, USA
 

Optimization is a wide area that covers a large set of techniques used to find the minimum or maximum of a function over a predefined group of conditions. Optimization strategies are frequently employed in several areas of financial engineering such as portfolio optimization and as such should be part of the basic skill set of financial developers.

In this chapter, we discuss programming examples that explore a few of the implement aspects of optimization algorithms. We start with a concise explanation of some techniques and how they are typically implemented in C++. Topics covered in this chapter include the following:
  • Optimization concepts: Basic concepts on optimization and how it is used as a common step of algorithms for financial applications.

  • Linear programming models: The basics of linear optimization models, with common assumptions and how the results can be interpreted. You will also learn how to create linear programming models for common problems.

  • Solving linear models: You will learn about techniques and algorithms commonly used to solve linear programming models. In particular, you will learn how to employ a popular open source library to solve linear programming problems.

  • Solving mixed-integer programming models: A common extension of linear programming is to require that one or more decision variables assume only integer values. This type of problem, called an integer programming problem, is frequently used whenever mutually exclusive choices are part of a linear model. You will also learn how to extend the linear programming class to solve such mixed-integer programming models.

Interfacing with a Linear Programming Solver

In this section, we create a generic class to solve a linear programming problem, given the objective function and constraints in matrix form.

Solution

Optimization is a mathematical technique used to find the maximum or minimum of a given function over a set of constraints. The methods currently used in optimization have started as a set of simple results from calculus, where a single function is subject to minimization or maximization. Nowadays, these techniques include complex models involving multiple linear and nonlinear components.

In financial engineering and economics, optimization is a tool used for purposes such as designing an optimal asset portfolio allocation or more widely to determine the best investment decision from a large set of asset classes. Due to its origins in the analysis of scarce resources and their optimum use, linear programming has been a favorite tool for economists and financial analysts—which shows why optimization is such a common technique in financial programming. Effectively, every time we need to make a decision on asset allocation given a large number of scenarios, optimization becomes a useful tool to help select the best decision.

In the code example in the section “Using LP Solver Libraries,” we consider how to interface with existing libraries that can be used to solve a large class of optimization problems. To keep the discussion well contained, I employ an open source library called GLPK (Gnu Linear Programming Kit) , which will be also used as a basis for future examples. GLPK is simple to use, but it is a C-based library only. This means that it provides no direct support for C++ high-level concepts such as classes, templates, and containers. Therefore, as part of the discussion, I will show you how to create a class that provides a basic C++ interface to GLPK and other solvers.

First, however, I give you some preliminary information about the kinds of problems that can be solved with an optimization engine, starting with linear programming. Then, I present some code that can be used to translate simple linear models into calls to the solver application programming interface (API).

Linear Programming Concepts

The first case of optimization that you will learn about is characterized by an objective represented as a linear function. This objective is then optimized over a set of linear functions, also called constraints . Such optimization problems are called linear programming (LP) problems, and they constitute an important class of mathematical models that have been widely used in disciplines such as financial analysis and economics.

Using a more formal (mathematical) definition, LP is the area of optimization that deals with the determination of the minimum or maximum value of a linear function over a set of linear constraints. Each constraint is of the form
$$ sum limits_{j=1}^n{a}_j{x}_j={a}_1+dots +{a}_nle {b}_i. $$
Similarly, the function that you want to optimize over (also known as objective function ) is a linear function. This results in a problem that can be denoted in the following way:
$$ mathit{operatorname{minimize}}sum limits_{j=1}^n{c}_j{x}_j $$
$$ subject tosum limits_{j=1}^n{a}_{ij}{x}_jle {b}_ikern2.5em for iin left{1dots m
ight} $$
$$ {x}_jge 0kern1.25em forkern0.5em jin left{1dots n
ight} $$

In these equations, xj is a variable, and ai j , bi, and cj are constant values. These parameters are frequently provided as matrix A and two vectors, b and c. Due to its generic characteristics, this type of problem can assume several forms, depending on the exact value for the given coefficients, as well as if they are zero or nonzero. Also, variations of the problem involve the change of the ≤ relation to ≥ or = in one or more equations. Finally, the problem can require the maximization, rather than the minimization, of the objective function. All these variants can be easily shown to be equivalent to each other, in the sense that it is possible to convert them to a particular form and use the same algorithm in their solution.

Solving an LP problem can be done with the help of a method called simplex algorithm . The basic approach of the simplex algorithm is to consider the geometric region defined by the constraints in a multidimensional space and start to visit the corners of this object in a well-defined way—until an optimal solution is found.

In essence, the mechanics of solving an LP problem are not very different from solving a sequence of linear systems, and a few strategies have been devised using this general strategy. The simplex algorithm, which is still the most common technique to solve LP problems, proceeds by defining a sequence of modified linear systems that are shown to be equivalent to the original while at the same time improving the value of the objective function. One of the advantages of the simplex algorithm is that its properties are well known—mathematical analyses of the simplex algorithm throughout the years have considered several important questions such as its convergence and performance.

While the operation of the simplex algorithm is not difficult to describe, the implementation of such an algorithm contains a lot of intricate edge cases. To avoid such issues, most frequently, you will be using an LP solver library, which has been especially designed to hide the complexities of the implementation. Essentially, a solver provides just a simple API so that users can call the algorithms, provide necessary data, and retrieve the results.

Using LP Solver Libraries

There are several commercial and free libraries that implement the simplex algorithm (and even a few more efficient algorithms for this problem). In this section, to give a flavor of how the process of modeling and LP works, we use a simple but well-maintained open source library called GLPK. With GLPK, it is possible to solve from medium- to relatively large-size LP problems (as well as a few other model variants such as mixed-integer programs).

To start using GLPK from C++, the first step is to download and compile the source code. You will find a version of this software in the Gnu website (at the time I checked, the URL was www.gnu.org/software/glpk). Unlike many math open source libraries, GLPK is very easy to compile and install. You need to decompress the file and build the library using the configure and make commands (these instructions work on UNIX systems, but you can download software such as Cygwin that will allow you to perform the same commands in Windows).

Once GLPK is installed, you can link to its library, libglpk.a, and make use of the functions that are exported by its API. On Windows systems, you can use the precompiled binary dll and lib files available on the GLPK website. You can also use the MingWin compiler for gcc on Windows. I present a class called LPSolver that is able to interface with the GLPK API. The following is the public part of the class declaration:
class LPSolver {
public:
    LPSolver(Matrix &m, const std::vector<double> &b, const std::vector<double> &c);
    LPSolver(const LPSolver &p);
    ~LPSolver();
    LPSolver &operator=(const LPSolver &p);
    enum ResultType {
        INFEASIBLE,
        FEASIBLE,
        ERROR
    };
    void setName(const std::string &s);
    bool isValid();
    void setMaximization();
    void setMinimization();
    ResultType solve(std::vector<double> &result, double &objValue);
// ...
};

First, an object of LPSolver type can be created if you pass a matrix A, a vector b, and a vector c to the constructor. These parameters are interpreted as the coefficients of the objective function as well as the constraints of the LP.

You can also give a descriptive name to the problem using the setName member function. Its implementation shows how a simple function in the GLPK looks.
void LPSolver::setName(const std::string &s)
{
    glp_set_prob_name(m_lp, s.c_str());
}

The API function is called glp_set_prob_name. The first parameter, as for most other functions in GLPK, is a pointer to the LP data structure. The second parameter, a string, is unique for this API call.

The isValid member function checks if the object has been properly initialized. The setMaximization and setMinimization member functions can be used to define the direction of the optimization.

Finally, the solve member function performs the optimization algorithm. This is done with a call to GLPK, where the glp_simplex function is used to do the hard work. After the optimization is finished, the algorithm collects the result of the objective function and the value of each variable for this optimal solution.
LPSolver::ResultType LPSolver::solve(std::vector<double> &result, double &objValue)
{
    glp_simplex(m_lp, NULL);
    result.resize(m_M, 0);
    objValue = glp_get_obj_val(m_lp);
    for (int i=0; i<m_M; ++i)
    {
        result[i] = glp_get_col_prim(m_lp, i+1);
    }
    return LPSolver::FEASIBLE;
}

Finally, an example LP is used to test the LPSolver class. In this example, I provided objective function coefficients equal to 10, 6, and 4. The right-hand side of the constraints is also provided as a vector. Finally, the constraints of the problem are given in the form of Matrix object A.

Complete Code

Listing 12-1 displays the complete listing for the LP solver described in the previous section. An example of the class LPSolver is given in the main function at the end of the listing.
//
// LPSolver.h
#ifndef __FinancialSamples__Glpk__
#define __FinancialSamples__Glpk__
#include <vector>
#include <string>
#include "Matrix.h"
struct glp_prob;
class LPSolver {
public:
    LPSolver(Matrix &A, const std::vector<double> &b,
             const std::vector<double> &c);
    LPSolver(Matrix &A, const std::vector<double> &b,
             const std::vector<double> &c,
             const std::string &probname);
    LPSolver(const LPSolver &p);
    ~LPSolver();
    LPSolver &operator=(const LPSolver &p);
    enum ResultType {
        INFEASIBLE,
        FEASIBLE,
        ERROR
    };
    virtual ResultType solve(std::vector<double> &result, double &objValue);
    void setName(const std::string &s);
    bool isValid();
    void setMaximization();
    void setMinimization();
private:
    size_t m_M;
    size_t m_N;
    std::vector<double> m_c;
    std::vector<double> m_b;
    Matrix m_A;
    glp_prob *m_lp;
    void initProblem(size_t M, size_t N);
    void setRowBounds();
    void setColumnCoefs();
protected:
    glp_prob *getLP();
    int getNumCols();
    int getNumRows();
};
#endif /* defined(__FinancialSamples__Glpk__) */
//
//  LPSolver.cpp
#include "LPSolver.h"
#include <glpk.h>
#include <iostream>
using std::vector;
using std::string;
using std::cout;
using std::endl;
LPSolver::LPSolver(Matrix &m, const vector<double> &b, const vector<double> &c)
: m_M(m.numRows()),
  m_N(m[0].size()),
  m_c(c),
  m_b(b),
  m_A(m),
  m_lp(glp_create_prob())
{
    initProblem(m_M, m_N);
}
LPSolver::LPSolver(Matrix &m, const std::vector<double> &b,
             const std::vector<double> &c,
             const std::string &probname)
: m_M(m.numRows()),
  m_N(m[0].size()),
  m_c(c),
  m_b(b),
  m_A(m),
  m_lp(glp_create_prob())
{
    initProblem(m_M, m_N);
    glp_set_prob_name(m_lp, probname.c_str());
}
LPSolver::LPSolver(const LPSolver &p)
: m_M(p.m_M),
  m_N(p.m_N),
  m_c(p.m_c),
  m_b(p.m_b),
  m_A(p.m_A),
  m_lp(glp_create_prob())
{
    initProblem(m_M, m_N);
}
// performs necessary initialization of the given values
void LPSolver::initProblem(size_t M, size_t N)
{
    if (!m_lp) return;
    setRowBounds();
    setColumnCoefs();
    vector<int> I, J;
    vector<double> V;
    // indices in GLPK start on 1
    I.push_back(0);
    J.push_back(0);
    V.push_back(0);
    for (int i=0; i<M; ++i)
    {
        for (int j=0; j<N; ++j)
        {
            I.push_back(i+1);
            J.push_back(j+1);
            V.push_back(m_A[i][j]);
        }
    }
    glp_load_matrix(m_lp, (int)(m_M * m_N), &I[0], &J[0], &V[0]);
}
LPSolver::~LPSolver()
{
    glp_delete_prob(m_lp);
}
LPSolver &LPSolver::operator=(const LPSolver &p)
{
    if (this != &p)
    {
        m_M = p.m_M;
        m_N = p.m_N;
        m_c = p.m_c;
        m_b = p.m_b;
        m_A = p.m_A;
        m_lp = glp_create_prob();
        initProblem(m_M, m_N);
    }
    return *this;
}
void LPSolver::setName(const std::string &s)
{
    glp_set_prob_name(m_lp, s.c_str());
}
bool LPSolver::isValid()
{
    return m_lp != NULL;
}
void LPSolver::setMaximization()
{
    glp_set_obj_dir(m_lp, GLP_MAX);
}
void LPSolver::setMinimization()
{
    glp_set_obj_dir(m_lp, GLP_MIN);
}
void LPSolver::setRowBounds()
{
    glp_add_rows(m_lp, (int)m_M);
    for (int i=0; i<m_M; ++i)
    {
        glp_set_row_bnds(m_lp, i+1, GLP_UP, 0.0, m_b[i]);
    }
}
void LPSolver::setColumnCoefs()
{
    glp_add_cols(m_lp, (int)m_N);
    for (int j=0; j<m_N; ++j)
    {
        glp_set_col_bnds(m_lp, j+1, GLP_LO, 0.0, 0.0);
        glp_set_obj_coef(m_lp, j+1, m_c[j]);
    }
}
LPSolver::ResultType LPSolver::solve(std::vector<double> &result, double &objValue)
{
    glp_simplex(m_lp, NULL);
    result.resize(m_N, 0);
    objValue = glp_get_obj_val(m_lp);
    for (int j=0; j<m_N; ++j)
    {
        result[j] = glp_get_col_prim(m_lp, j+1);
    }
    return LPSolver::FEASIBLE;
}
glp_prob *LPSolver::getLP()
{
    return m_lp;
}
int LPSolver::getNumCols()
{
    return (int)m_N;
}
int LPSolver::getNumRows()
{
    return (int)m_M;
}
int main_lps()
{
    Matrix A(3);
    A[0][0] = 1;  A[0][1] = 1; A[0][2] = 1;
    A[1][0] = 10; A[1][1] = 2; A[1][2] = 4;
    A[2][0] = 2;  A[2][1] = 5; A[2][2] = 6;
    vector<double> c = { 10, 6, 4 };
    vector<double> b = { 100, 600, 300 };
    LPSolver solver(A, b, c);
    solver.setMaximization();
    vector<double> results;
    double objVal;
    solver.solve(results, objVal);
    for (int i=0; i<results.size(); ++i)
    {
        cout << " x" << i << ": " << results[i];
    }
    cout << " max: " << objVal << endl;
    return 0;
}
Listing 12-1

Class LPSolver Header and Implementation

Running the Code

The code presented in Listing 12-1 can be compiled with a standards-compliant compiler, such as gcc or Visual Studio. Remember to add the GLPK library to the link step (in gcc, this is done with the -L and –l switches). The result of the program execution should be similar to the following:
./lpSolver
GLPK Simplex Optimizer, v4.54
3 rows, 3 columns, 9 non-zeros
*     0: obj =   0.000000000e+00  infeas =  0.000e+00 (0)
*     2: obj =   7.565217391e+02  infeas =  0.000e+00 (0)
OPTIMAL LP SOLUTION FOUND
 x0: 52.1739 x1: 39.1304 x2: 0 max: 756.522

Here, you see the first output of GLPK. By default, GLPK displays the best solutions and the number of iterations it has taken to achieve the results. You can see that after two iterations of the simplex algorithm, GLPK found a solution with an objective value equal to 756.

Solving Two-Dimensional Investment Problems

In this section, we use LP techniques to model and solve a financial product allocation decision problem for two investments with known returns.

Solution

One of the main uses of optimization techniques is in the support of investment decisions. In this respect, there are several concepts that can be optimized based on the known properties of investment classes. For a few types of investments, such as bonds, it is easier to determine the returns of the investment, as well as some basic information about the risk for that class of investments. This knowledge translates into more accurate models, particularly the ones that can be employed to optimize profits for the investor.

This section presents a very simple version of a decision support system modeled using linear programming. This problem shows the basic geometric process that is used to solve LPs (even the most complicated ones).

Consider the process of introducing two new financial products to the market in a large bank. The process is usually defined by a number of practical constraints on resources necessary for these investments. Suppose that the bank wants to add two classes of new products to the market: new bond-based products and new mortgage-backed derivatives products. The question is how many hours should be spent on the development of these new products. Let’s call these two variables x and y. Since the bank unit receives payment from its clients based on the number of hours spent on these tasks, the goal is to maximize the amount paid per hours. For bonds, the cost is $5.3K per hour spent on that activity, while the price is $7.1K for derivatives.

In terms of constraints, the bank unit has to consider research expenditures costs. The cost of working with bonds is negative because its costs can be offset by other activities in that area. For derivatives, the full cost of research is considered. The maximum cost spent on marketing for these financial products is prorated by working hours too. Thus, it depends on the values of x and y. It is known that there is a constant of $3K for working hours on bond-related products, while the multiplier for derivatives work is $1K.

Finally, there is a limit on the amount of human resources available for these two tasks. While there are only six units of human resources allocated to these tasks, each hour of bond-related work is eight times more demanding than for derivatives. Notice also that the two variables in this example are clearly non-negative.

The result of these assumptions can be readily translated into the following LP model, which tries to maximize the expected return (profit) for the considered unit of the investment bank.
  • max 5.3x + 7.1y (maximize department results)

  • –2.1x + y ≤ 3.4 (maximum research expenditure)

  • 3.1x + y ≤ 4.3 (maximum marketing expenditure)

  • 7.9x + y ≤ 6 (maximum number of employees needed)

  • x, y ≥ 0 (working hours are always positive)

The model described previously has only two unknowns, x and y, and therefore can be readily plotted as seen in Figure 12-1. Being an inequality, each constraint results in a half-space that is defined by the equality line. For example, 3x + y ≤ 4 is the half-space defined by all points under the line 3x + y = 4.
../images/323908_2_En_12_Chapter/323908_2_En_12_Fig1_HTML.jpg
Figure 12-1

Feasible set for the LP defined by inequalities shown earlier

To find a solution for a two-dimensional LP model like the one described previously, you can concentrate on the intersections of all half-spaces defined by the constraints. The intersection is, by definition of the problem, contained in the first quadrant of the plot because it is known that x ≥ 0 and y ≥ 0. It is possible then to recognize the area contained in the intersection of all other half-spaces. The result is a polygonal area, whose border is defined by a set of lines derived from the given constraints.

To find a solution for such an LP, you just need to calculate the value of the objective function at each of the corners of the area defined by the constraints. The corner that gives the best value of the objective function is, by definition of the linear objective function, the best that can be found for the problem.

While the process described previously is easy to perform for two-dimensional problems, it becomes quite difficult to accomplish for higher dimensions. As the number of dimensions and constraints increases, the number of corners grows exponentially. It takes a more sophisticated algorithm (such as the simplex algorithm) to find the best corner of the multidimensional space that defines the optimum solution for the given LP.

To demonstrate how the problem is solved in practice, I show you the C++ implementation of the proposed two-dimensional LP. The class, called TwoDimensionalLPSolver , is a blueprint of how such a problem can be implemented using the LPSolver described in the previous section.

First, you need to create the model, which is described using a matrix A, a vector b (the right-hand side of the constraints), and a vector c (the cost vector). The necessary data is provided in the main function. Once the data is available, it can be used to create an object of class LPSolver. The solve() function in the LPSolver class will then perform any necessary data conversion and call the GLPK library to find the optimum solution.

Complete Code

Listing 12-2 gives the complete implementation for the two-dimensional LP solver. Function main() presents a sample use of the class TwoDimensionalLPSolver.
//
//  TwoDimensionalLPSolver.h
#ifndef __FinancialSamples__TwoDimensionalLPSolver__
#define __FinancialSamples__TwoDimensionalLPSolver__
#include <vector>
class TwoDimensionalLPSolver {
public:
    using Vector = std::vector<double>;
    TwoDimensionalLPSolver(const Vector &c, const Vector &A1, const Vector &A2, const Vector &b);
    TwoDimensionalLPSolver(const TwoDimensionalLPSolver &p);
    ~TwoDimensionalLPSolver();
    TwoDimensionalLPSolver &operator=(const TwoDimensionalLPSolver &p);
    bool solveProblem(Vector &results, double &objVal);
private:
    std::vector<double> m_c;
    std::vector<double> m_A1;
    std::vector<double> m_A2;
    std::vector<double> m_b;
};
#endif /* defined(__FinancialSamples__TwoDimensionalLPSolver__) */
//
//  TwoDimensionalLPSolver.cpp
#include "TwoDimensionalLPSolver.h"
#include "Matrix.h"
#include "LPSolver.h"
#include <iostream>
using std::vector;
using std::cout;
using std::endl;
TwoDimensionalLPSolver::TwoDimensionalLPSolver(const Vector &c, const Vector &A1,
                                               const Vector &A2, const Vector &b)
: m_c(c),
  m_A1(A1),
  m_A2(A2),
  m_b(b)
{
}
TwoDimensionalLPSolver::TwoDimensionalLPSolver(const TwoDimensionalLPSolver &p)
: m_c(p.m_c),
  m_A1(p.m_A1),
  m_A2(p.m_A2),
  m_b(p.m_b)
{
}
TwoDimensionalLPSolver::~TwoDimensionalLPSolver()
{
}
TwoDimensionalLPSolver &TwoDimensionalLPSolver::operator=(const TwoDimensionalLPSolver &p)
{
    if (this != &p)
    {
        m_c = p.m_c;
        m_A1 = p.m_A1;
        m_A2 = p.m_A2;
        m_b = p.m_b;
    }
    return *this;
}
bool TwoDimensionalLPSolver::solveProblem(Vector &res, double &objVal)
{
    int size = m_b.size();
    Matrix A(size, 2);
    for (int j=0; j<size; ++j)
    {
        A[j][0] = m_A1[j];
        A[j][1] = m_A2[j];
    }
    LPSolver solver(A, m_b, m_c);
    solver.setMaximization();
    return solver.solve(res, objVal) == LPSolver::ResultType::FEASIBLE;
}
int main()
{
    vector<double> A1 = { -2.1, 3.1, 7.9};
    vector<double> A2 = { 1, 1, 1 };
    vector<double> c = { 5.3, 7.1 };
    vector<double> b = { 3.4, 4.3, 6 };
    TwoDimensionalLPSolver solver(c, A1, A2, b);
    vector<double> results;
    double objVal;
    solver.solveProblem(results, objVal);
    cout << "objVal : " << objVal << endl;
    for (int i=0; i<results.size(); ++i)
    {
        cout << " x" << i << ": " << results[i];
    }
    cout << endl;
    return 0;
}
Listing 12-2

Header File and Implementation for the Class TwoDimensionalLPSolver

Running the Code

You can compile and run the provided code with your preferred standards-compliant compiler. I tested the code using gcc and GLPK optimizer version 4.54. The results are as follows:
./twoDimSolver
GLPK Simplex Optimizer, v4.54
3 rows, 2 columns, 6 non-zeros
*     0: obj = 0.000000000e+00 infeas = 0.000e+00 (0)
*     2: obj = 2.763788462e+01 infeas = 0.000e+00 (0)
OPTIMAL LP SOLUTION FOUND
objVal : 27.6379
 x0: 0.173077 x1: 3.76346
Program ended with exit code: 0

From the output listed in the example, you can see that the optimal solution was achieved at the vertex (0.173, 3.763), which corresponds to the intersection of equations –2.1x + y = 3.4 and 3.1x + y ≤ 4. At that point, the objective function has a value of 27.63, which can be interpreted as the profit achieved by the department in spending the given number of hours in the two financial products that were discussed earlier.

Creating Mixed-Integer Programming Models

Extend the LPSolver class so that it can deal with mixed-integer programming (MIP) problems, that is, LP problems where one or more variables are restricted to be integers.

Solution

After continuous LP problems, MIP problems are probably the most common type of optimization problem that practitioners need to deal with. In terms of modeling, the biggest difference between LP and MIP is that such problems have one or more decision variables that are required to be integer numbers—unlike LP problems, where all decision variables are continuous (normally real numbers).

Integer variables are ideal for cases where you need to make decisions that are exclusive within a small- to medium-size set. Moreover, these decision variables may be applicable to resources that are not divisible. For example, you can use such variables to decide on the number of local branches for a commercial bank or on the number of different stocks included in a portfolio. These are common examples of resources that can only be used in integer quantities.

A special type of integer variable is a binary decision variable, also called a 0–1 decision variable. These are variables that can assume only a 0 or 1 (all-or-nothing) value. They are the purest form of integer variable, because they allow one to decide between only two alternative choices. As you can expect, many MIP problems make use of binary variables as their primary way to reach an optimal decision.

In terms of techniques for problem solving, MIP problems are a lot more complicated than LP problems. While there are very efficient algorithms available to solve LP formulations, not all MIP problems are readily solvable by current computer algorithms. As a short explanation, this occurs because when a decision variable is an integer, it creates “jumps” in the objective function that make it much harder to search for the optimum solution. So, unlike LP problems where the optimal vertex of the set of feasible solutions can be quickly determined, MIP solvers need to spend much more time generating possible solutions and testing if they are optimal. This exponential explosion of options is the main reason why MIP problems are much more difficult to solve than LP problems.

Most LP libraries have been extended to deal with at least some forms of MIP. GLPK implements a generic algorithm for MIP solving, called branch-and-cut. With this algorithm, it is possible to solve small- to moderate-size MIPs to optimality. More complicated MIP problems, however, may not be solvable using this technique, depending on the structure of the required problem.

In this coding example, you will see how to extend the LPSolver class to deal with MIP problems, in addition to classical LP problems. In the next section, you will see an example of how to use the LPSolver class to model and solve a MIP problem.

The main reason I decided to inherit from LPSolver, instead of creating a new unrelated class, is that in terms of modeling, MIP problems are very close to LP problems. The only additional thing you need to do in the latter case is to tell which variables are integer or binary and call the right version of the function that solves and retrieves values found by GLPK.

In the MIPSolver class, this is implemented in the following way. First, there are two new member functions called setColBinary and setColInteger. These member functions can be used to tell GLPK that the variable in a given column is either integer or binary, respectively. Their implementations are straightforward and simply call the related C function in GLPK. For example:
void MIPSolver::setColBinary(int colNum)
{
    glp_set_col_kind(getLP(), colNum+1, GLP_BV);
}

The other part of the puzzle is to implement a new version of the solve member function. The new version supersedes the original version in LPSolver and calls specific functions for MIP, such as glp_mip_obj_val. One of the differences is that, for MIP problems, you are required to solve the corresponding LP problem first, as a way to create an initial feasible solution for the continuous problem. After that, you can call the MIP solver, which will create the solution-search algorithm based on a tree of possible integer values.

Complete Code

Listing 12-3 displays the complete code for the MIP solver described in the previous section. You can test the MIPSolver class using the sample code in the main function at the end of Listing 12-3.
//
//  MIPSolver.h
#ifndef __FinancialSamples__MIPSolver__
#define __FinancialSamples__MIPSolver__
#include "LPSolver.h"
class MIPSolver : public LPSolver {
public:
    MIPSolver(Matrix &A, const std::vector<double> &b, const std::vector<double> &c);
    MIPSolver(const MIPSolver &p);
    ~MIPSolver();
    MIPSolver &operator=(const MIPSolver &p);
    void setColInteger(int colNum);
    void setColBinary(int colNum);
    virtual ResultType solve(std::vector<double> &result, double &objValue);
};
#endif /* defined(__FinancialSamples__MIPSolver__) */
//
//  MIPSolver.cpp
#include "MIPSolver.h"
#include "Matrix.h"
#include <glpk.h>
#include <iostream>
using std::vector;
using std::cout;
using std::endl;
MIPSolver::MIPSolver(Matrix &A, const std::vector<double> &b, const std::vector<double> &c)
: LPSolver(A, b, c)
{
}
MIPSolver::MIPSolver(const MIPSolver &p)
: LPSolver(p)
{
}
MIPSolver::~MIPSolver()
{
}
MIPSolver &MIPSolver::operator=(const MIPSolver &p)
{
    return *this;
}
void MIPSolver::setColInteger(int colNum)
{
    glp_set_col_kind(getLP(), colNum+1, GLP_IV);
}
void MIPSolver::setColBinary(int colNum)
{
    glp_set_col_kind(getLP(), colNum+1, GLP_BV);
}
LPSolver::ResultType MIPSolver::solve(vector<double> &result, double &objValue)
{
    glp_simplex(getLP(), NULL);
    int res = glp_intopt(getLP(), NULL);
    if (res != 0)
    {
        cout << "res = " << res << " ";
    }
    result.resize(getNumCols(), 0);
    objValue = glp_mip_obj_val(getLP());
    for (int i=0; i<getNumCols(); ++i)
    {
        result[i] = glp_mip_col_val(getLP(), i+1);
    }
    return LPSolver::FEASIBLE;
}
int main()
{
    Matrix A(2,2);
    vector<double> b = { 2, 3 };
    vector<double> c = { 1, 1 };
    A[0][0] = 1;
    A[0][1] = 2;
    A[1][0] = 3;
    A[1][1] = 4;
    MIPSolver solver(A, b, c);
    solver.setMaximization();
    solver.setColInteger(0);
    vector<double> result;
    double objVal;
    solver.solve(result, objVal);
    cout << "optimum: " << objVal << endl;
    cout << " x0: " << result[0] << " x1: " << result[1] << endl;
    return 0;
}
Listing 12-3

MIPSolver Class

Running the Code

After compiling the code in Listing 12-3 with your favorite compiler, you should have a binary program, which I will call mipSolver. The following is the output of the application after it is executed:
./mipSolver
GLPK Simplex Optimizer, v4.54
2 rows, 2 columns, 4 non-zeros
*     0: obj = 0.000000000e+00 infeas = 0.000e+00 (0)
*     1: obj = 1.000000000e+00 infeas = 0.000e+00 (0)
OPTIMAL LP SOLUTION FOUND
GLPK Integer Optimizer, v4.54
2 rows, 2 columns, 4 non-zeros
1 integer variable, none of which are binary
Integer optimization begins...
+     1: mip =     not found yet <=              +inf        (1; 0)
+     1: >>>>>   1.000000000e+00 <=   1.000000000e+00   0.0% (1; 0)
+     1: mip =   1.000000000e+00 <=     tree is empty   0.0% (0; 1)
INTEGER OPTIMAL SOLUTION FOUND
optimum: 1
 x0: 1 x1: 0

Notice that the main function has a very simple MIP model as part of the test code. The previous output just shows how the data is printed by default by GLPK. At each step of the process, it shows the current solution found and its objective cost.

Conclusion

Optimization is a set of mathematical techniques that can be used to find the maximum or minimum of a function under certain conditions. Since many problems in finance can be described as maximizing certain outcomes (such as investment return), optimization tools play an important role in the analysis of investments.

You have learned in this chapter how to use C++ to model and solve common optimization problems. In the first section, I showed how to use GLPK, a popular optimization library that is used to solve a large class of LP models. While other libraries such as cplex and gurobi use more sophisticated algorithms, GLPK is able to solve a surprisingly large number of models for LP and MIP. The example presented in the first section shows how to create a C++ interface to interact with the C-based API supported by GLPK. You have learned the main components of an LP problem and how the solver uses these components to determine the optimal solution. The example also provided some test code, which shows how the LPSolver class can be used to solve a simple LP model.

The next section provided a substantial example of LP model, targeted at finding the best resource allocation in a big investment bank. The model tries to find the best way to develop two financial products while maximizing the department profits. The constraints considered in the model have to do with resource limitations at the bank unit. Solving this LP problem using the LPSolver class shows how to model such problems in C++ and interpret the results of the optimization process returned by GLPK.

MIP is another important class of linear models that can be solved using optimization techniques. In an MIP, some or all decision variables are restricted to contain only integer values. This makes it possible to model situations where it is necessary to decide between two or more scenarios in a mutually exclusive way. While MIP models may be much harder to solve than LP models, the mechanics of setting up the problem and using a solver are very similar. You learned how to interface with an MIP solver by inheriting from the LPSolver class. The new class MIPSolver uses most of the modeling mechanisms provided by LPSolver, but it adds the ability to define integer and binary decision variables, as well as to solve the problem and retrieve the optimal integer values. Finally, you have seen a small example of how such models work. Chapter 13 provides more examples.

You have so far learned some of the basic concepts of optimization and mathematical programming models. In the next chapter, you will explore how these LP and MIP optimization models can be applied to investment management problems. Since one of the main applications of optimization theory is in the area of finance, many common problems such as portfolio management have well-known optimization models. You will learn how such modeling concepts work, as well as how these models can be implemented with the C++ programming language.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.218.230