Markowitz Efficient Frontier - The Details

First consider the sets section:

SETS:

  ASSET: RATE, UB, X;

  COVMAT( ASSET, ASSET): V;

  POINTS: XRET, YVAR;

ENDSETS

It defines three sets: ASSET, COVMAT and POINTS.

The ASSET set will contain the set of assets available for investment.  Each asset has an expected rate of return (RETURN), an upper bound on the amount of the asset we’ll allow in the portfolio (UB), and the fraction of the portfolio devoted to the asset (X).  Note that the fraction of the portfolio devoted to each asset, X, constitutes our decision variables.

The COVMAT set is a cross of the ASSET set on itself.  We create this set for the attribute V, which will store the covariance matrix for all the assets.

The POINTS set is used to represent the points on the efficient frontier that we will be generating.  For each point, we will determine its x-coordinate, XRET, and its y-coordinate, YVAR.  Note that the x-coordinate will represent risk, while the y-coordinate will represent return.  What we intend to do is to solve a portfolio model once for each member of the POINTS set to get a new point on the efficient frontier.  These points will be stored in XRET and YVAR.  Once the loop is completed, the list of points in XRET and YVAR will give us a glimpse of what the efficient frontier looks like.

Next we have the data section:

DATA:

! Number of points on the

  efficient frontier graph;

 NPOINTS = 10;

 POINTS = 1..NPOINTS;

! The stocks;

 ASSET = GOOGLE, YAHOO, CISCO;

! Expected growth rate of each asset;

 RATE = 1.3   1.2  1.08;

! Upper bound on investment in each;

 UB   = .75   .75   .75;

! Covariance matrix;

 V    =   3     1   -.5

          1     2   -.4

        -.5   -.4     1;

ENDDATA

In this data section we are setting the number of points that we will generate along the efficient frontier (NPOINTS) to 10.  Once we have the number of points established, we dynamically create the POINTS set.  Next, we input the set of assets, their expected rate of return, their upper bounds, and their covariance matrix.

The next section of the model:

! Below are the three objectives we'll use;

SUBMODEL SUB_RET_MAX:

  [OBJ_RET_MAX] MAX = RETURN;

ENDSUBMODEL

 

SUBMODEL SUB_RET_MIN:

  [OBJ_RET_MIN] MIN = RETURN;

ENDSUBMODEL

 

SUBMODEL SUB_MIN_VAR:

  [OBJ_MIN_VAR] MIN =

   @SUM( COVMAT( I, J): V( I, J) * X( I) * X( J));

ENDSUBMODEL

makes use of the SUBMODEL and ENDSUBMODEL statements to establish three different objectives.  The first two objectives respectively maximize and minimize portfolio return, which are used later on in the model to determine that maximum and minimum possible returns that can be generated with out basket of available stocks.  The third objective, SUB_MIN_VAR, minimizes portfolio risk as measured by its variance.

Following the three submodels containing our various objectives, we have another submodel that contains our three constraints:

!and the constraints;

SUBMODEL SUB_CONSTRAINTS:

  ! Compute return;

  RETURN = @SUM( ASSET: RATE * X);

  ! Must be fully invested;

  @SUM( ASSET: X) = 1;

  ! Upper bounds on each;

  @FOR( ASSET: @BND( 0, X, UB));

  ! Must achieve target return;

  RETURN >= RET_LIM;

ENDSUBMODEL

The first constraint of our constraint section computes portfolio return.  The second constraint says that we must invest 100 percent of our capital.  The third constraint puts an upper bound on the percentage of the portfolio invested in each asset.  Finally, the fourth constraint forces total portfolio return to achieve some desired level.

The next section, the calc section, is of primary interest.  It contains the logic to solve the model multiple times to generate and store the points on the efficient frontier.  First, we make use of @SET to set some parameter values:

! Set some parameters;

  ! Reset all params to their defaults;

  @SET( 'DEFAULT');

  ! Minimize output;

  @SET( 'TERSEO', 1);

  ! Suppress status window;

  @SET( 'STAWIN', 0);

The first call to @SET restores all parameters to their default values, the second call minimizes the level of LINGO’s output to improve performance, while the third call suppresses the status window that would normally pop up when solving a model.  We suppress the status window so it does not obscure the custom report we are creating at the end of the run.

Next, we use @DIVERT to route LINGO’s output to a log file:

  ! Capture spurious output;

  @DIVERT( 'LINGO.LOG');

We do this to capture the output LINGO would normally generate while it’s solving the various portfolio models.  We capture this output so it doesn’t distract from the custom report we will display at the end of the run.

Our next task is to find out the possible range of portfolio returns.  We want this information so that we don’t try to run our model for returns lying outside this range, which would result in an infeasible model.  This requires two runs: one where we maximize return and one where we minimize return.   Here is the code that performs the runs:

! Solve to get maximum return;

  RET_LIM = 0;

  @SOLVE( SUB_RET_MAX, SUB_CONSTRAINTS);

 

! Save maximum return;

  RET_MAX = OBJ_RET_MAX;

 

! Solve to get minimum return;

  @SOLVE( SUB_RET_MIN, SUB_CONSTRAINTS);

 

! Save minimum return;

  RET_MIN = OBJ_RET_MIN;

This is our first example of the the @SOLVE command. @SOLVE takes one or more submodel names as arguments.  It then combines the submodels into a single model and solves them.  In this case, we first solve submodel SUB_RET_MAX along with submodel SUB_CONSTRAINTS to get the maximum return possible subject to our constraint set.  We then do a similar solve to find the minimal return subject to the constraints.  Other items of interest are that we zero out RET_LIM, given that the constraint on return is temporarily not required, and we store the two extreme objective values in RET_MAX and RET_MIN for later use.

Our end goal is to solve the model for 10 values of portfolio return, with these values being equally spaced from RET_MIN to RET_MAX.  The next step in the model computes the distance, or interval, between these points:

! Interval between return points;

  INTERVAL =

   ( RET_MAX - RET_MIN) / ( NPOINTS-1);

Our next code segment is an @FOR loop that loops over each of the 10 return values and minimizes portfolio variance subject to attaining the desired level of return:

! Loop over range of possible returns

 minimizing variance;

  RET_LIM = RET_MIN;

  @FOR( POINTS( I):

     @SOLVE( SUB_MIN_VAR, SUB_CONSTRAINTS);

     XRET( I) = RET_LIM;

     YVAR( I) = OBJ_MIN_VAR;

     RET_LIM = RET_LIM + INTERVAL;

  );

We start by setting the desired level of return to RET_MIN and then increment this level of return each pass through the loop by the interval amount.  Note that the solve command now uses the submodel containing the objective that minimizes return, SUB_MIN_VAR.  Also, after each solve command, we store the coordinates of the point on the efficient frontier in XRET and YRET.

Our next section of code creates a custom report:

! Close log file;

  @DIVERT();

 

! Display the results;

  @WRITE( '     Return     Variance', @NEWLINE( 1));

  @FOR( POINTS: @WRITE( @FORMAT( XRET, '#12.6G'),

   @FORMAT( YVAR, '#12.6G'), @NEWLINE( 1))

  );

Once again, we make use of @DIVERT, but this time we do not pass an argument.  This results in output once again going to the report window, which will allow us to view our report on screen.  After restoring terminal output with @DIVERT, we use the @WRITE command inside an @FOR loop to write out the 10 points on the efficient frontier.  If you run the model, you should see the following report:

Return     Variance

1.11000    0.417375

1.12833    0.417375

1.14667    0.418054

1.16500    0.462381

1.18333    0.575957

1.20167    0.758782

1.22000     1.01086

1.23833     1.33218

1.25667     1.72275

1.27500     2.18750

In summary, returns range from 11% to a high of 27.5%, with portfolio variance ranging from .417 to 2.18.

One final note, if you load this model from your Lingo samples folder you will find additional Lingo code at the end devoted to graphing the frontier.  We will not go into the details of that code at this point; however, the generated graph is pictured below:

  Risk

    ^

2.2 |                                                    *      

    |                                                          

    |                                                          

    |                                                          

    |                                                          

    |                                              *            

    |                                                          

    |                                                          

    |                                                          

    |                                         *                

    |                                                          

    |                                                          

    |                                                          

    |                                   *                      

    |                                                          

    |                                                          

    |                              *                            

    |                                                          

    |                        *                                  

0.40 |  *     *    *     *                                      

    --------------------------------------------------------->

   1.1                                                     1.3

Model: LOOPPORT – Graph of Efficient Frontier