Engee documentation
Notebook

Maximizing long-term investments

Introduction

This example presents the task of optimizing a long-term fixed-income investment portfolio. The goal is to maximize the final capital over a given planning horizon by reinvesting income from bonds with different maturities and interest rates. The study demonstrates the use of linear programming methods to formalize and solve this problem.

Setting the task

Let's consider an investment model with a fixed initial capital distributed among bonds with deterministic returns over a given time horizon. Each bond is characterized by a fixed interest rate with annual capitalization and the payment of a nominal value with accumulated interest income at maturity. The objective function of the model is aimed at maximizing total capital at the end of the investment period.

Additionally, the model may include a restriction on portfolio diversification, which sets the maximum share of a single investment in the total amount of capital at the time of placement.

The research methodology involves a step-by-step consideration of the problem: from a special case with a limited set of tools to a generalized formulation. The mathematical formalization of the problem is implemented within the framework of the linear programming apparatus, which makes it possible to formulate and solve the corresponding optimization problem of maximizing final well-being.

Introductory example

Let's look at a demo example with an initial capital of 1,000 rubles and an investment horizon of 5 years. The model includes four bonds with different characteristics: bond 1 is purchased in the first year with a maturity of 4 years and a yield of 2%; bond 2 is available in the fifth year with a maturity of 1 year and a yield of 4%; bonds 3 and 4 are purchased in the second year with maturities of 4 and 3 years, respectively, at a yield of 6%.

To model the non-invested funds, five one-year zero-yield bonds were introduced, which forms an equivalent model of nine investment instruments.

Add the necessary libraries and visualization function:

In [ ]:
using Plots, Random, JuMP, HiGHS
include("plotInvest.jl")

We visualize this task using horizontal rectangles that represent the available purchase periods for each bond. The numbers of the bonds with interest rates are located along the lines. The investment periods in years are shown in the columns.

In [ ]:
# Time period in years
Period = 5
# Number of bonds
Number of bonds = 4
# The initial amount of money
Summa_0 = 1000
# Total number of purchase options
All possible purchases = Number of bonds + Period
# Purchase Periods
Purchase Year = [1; 2; 3; 4; 5; 1; 5; 2; 2]
# Bond durations
Repayment period = [1; 1; 1; 1; 1; 4; 1; 4; 3]
# Bond sale periods
Year of repayment = Year of purchase .+ Repayment period .- 1
# Interest rates as a percentage
Percentages = [0; 0; 0; 0; 0; 2; 4; 6; 6]
# Yield after one year, including interest
Profitability per day = 1 .+ Percentages ./ 100
# Task visualization
p1 = plotInvest(Number of bonds, Year of purchase, Maturity, Interest)
display(p1)

Variable solutions

Let's represent the solution variables by the vector x, where — the amount in rubles invested in bond k, for k = 1, ...,9. Upon repayment, the payment for the investment equal to:

In [ ]:
# Creating an optimization model
model = Model(HiGHS.Optimizer)
# Variable solutions - investment amounts
@variable(model, x[1:All possible purchases] >= 0)
# Total return
Total return = Profitability per day . Repayment period

Target function

The purpose of optimization is to maximize the total capital generated by reinvesting income from redeemable bonds. As the graphical visualization demonstrates, the cash flows received in the interim periods are reinvested and contribute to the formation of the final well-being.

Let's create a target function and a task to maximize.

In [ ]:
# Creating an optimization task (maximizing)
@objective(model, Max, x[5] * Total Return[5] + x[7] * Total Return[7] + x[8] * Total profitability[8])

Linear constraints

The model assumes an annual allocation of available capital between investment instruments. At the initial stage, seed capital is placed, and in subsequent periods, cash flows from redeemable bonds are reinvested.

Let's make a system of equations:

In [ ]:
# Restrictions on investments
@constraint(model, restriction_investment1, x[1] + x[6] == Summa_0)
@constraint(model, restriction_investment2, x[2] + x[8] + x[9] == Profitability per day[1] * x[1])
@constraint(model, restriction_investment3, x[3] == Profitability per day[2] * x[2])
@constraint(model, restriction_investment4, x[4] == Profitability per day[3] * x[3])
@constraint(model, restriction_investment5, x[5] + x[7] == Profitability per day[4] * x[4] + Total Return[6] * x[6] + Total return[9] * x[9])

Decision

We will solve this problem without restrictions on the amount that can be invested in one bond.

In [ ]:
optimize!(model)
solution = value.(x)
value of the target function = objective_value(model)
Running HiGHS 1.11.0 (git hash: 364c83a51e): Copyright (c) 2025 HiGHS under MIT licence terms
LP   has 5 rows; 9 cols; 15 nonzeros
Coefficient ranges:
  Matrix [1e+00, 1e+00]
  Cost   [1e+00, 1e+00]
  Bound  [0e+00, 0e+00]
  RHS    [1e+03, 1e+03]
Presolving model
2 rows, 6 cols, 9 nonzeros  0s
1 rows, 4 cols, 4 nonzeros  0s
0 rows, 0 cols, 0 nonzeros  0s
Presolve : Reductions: rows 0(-5); columns 0(-9); elements 0(-15) - Reduced to empty
Solving the original LP from the solution after postsolve
Model status        : Optimal
Objective value     :  1.2624769600e+03
P-D objective error :  0.0000000000e+00
HiGHS run time      :          0.00
Out[0]:
1262.4769600000004

Visualization of the solution

Let's display the value of the return on investment.

In [ ]:
println("After $5 years, the income from the initial $(Sum_0) ₽ will be $(round(value of the target function, digits=2)) ₽")
После 5 лет, доход с начальных 1000 ₽ составит 1262.48 ₽

Visualize the solution:

In [ ]:
p2 = plotInvest(Number of bonds, Year of purchase, Maturity, Interest, solution)
display(p2)

Optimal investments with limitations

To ensure portfolio diversification, the model introduces a limit on the maximum share of investments in a single asset relative to the total capital of the current period, including proceeds from redeemable securities.

In [ ]:
# Creating a model for a problem with constraints
model_with_ constraints = Model(HiGHS.Optimizer)
# Decision variables for a constrained model
@variable(model with constraints, x2[1:All possible purchases] >= 0)
# Target function
@objective(model with constraints, Max, x2[5] * Total Return[5] + x2[7] * Total Return[7] + x2[8] * Total profitability[8])
# Basic investment restrictions
@constraint(model with constraints, investment1, x2[1] + x2[6] == Summa_0)
@constraint(model with constraints, investment2, x2[2] + x2[8] + x2[9] == Profitability per day[1] * x2[1])
@constraint(model with constraints, investment3, x2[3] ==Profitability per day[2] * x2[2])
@constraint(model with constraints, investment4, x2[4] ==Profitability per day[3] * x2[3])
@constraint(model with constraints, investment5, x2[5] + x2[7] ==Profitability per day[4] * x2[4] + Total Return[6] * x2[6] + Total return[9] * x2[9])
# The maximum interest rate for investments in any bond
Max Percentage = 0.6

Let's create a system of inequalities:

In [ ]:
# Restrictions on maximum investments
@constraint(model with constraints, limit1, x2[1] <= Max Percentage * Summa_0)
@constraint(model_with_ constraints, limit2, x2[2] <= Max Percentage * (profitability Code[1] * x2[1] + Profitability Code[6] * x2[6]))
@constraint(model with constraints, limit3, x2[3] <=Max Percentage * (Profitability Code[2] *x2[2] + Profitability Code[6]^2* x2[6]+ Profitability Code[8]* x2[8] + Profitability Code[9]*x2[9]))
@constraint(model with constraints, limit4, x2[4] <= Max Percentage * (Profitability Code[3] * x2[3] + Profitability Code[6]^3 *x2[6] + Profitability Code[8]^2*x2[8] + Profitability Code[9]^2* x2[9]))
@constraint(model with constraints, limit5, x2[5] <= Max Percentage * (Profitability Code[4] * x2[4] + Profitability Code[6]^4 *x2[6]+ Profitability Code[8]^3*x2[8] + Profitability Code[9]^3 * x2[9]))
@constraint(model with constraints, limit6, x2[6] <= Max percentage * Summa_0)
@constraint(model with constraints, limit7, x2[7] <= Max Percentage * (Profitability Code[4] * x2[4] + Profitability Code[6]^4 *x2[6]+ Profitability Code[8]^3*x2[8] + Profitability Code[9]^3 * x2[9]))
@constraint(model_with_ constraints, limit8, x2[8] <= Max Percentage * (profitability Code[1] * x2[1] + Profitability Code[6] * x2[6]))
@constraint(model_with_ constraints, limit9, x2[9] <= Max Percentage * (profitability Code[1] * x2[1] + Profitability Code[6] * x2[6]))

Solving the problem with limited diversification (maximum 60% of capital per asset) demonstrates a decrease in final profitability compared to unlimited optimization. Visualization of the resulting portfolio shows the redistribution of investment flows.

In [ ]:
# Solving a problem with constraints
optimize!(model with constraints)
solution2 = value.(x2)
goal_function_2 = objective_value(model with constraints)
Running HiGHS 1.11.0 (git hash: 364c83a51e): Copyright (c) 2025 HiGHS under MIT licence terms
LP   has 14 rows; 9 cols; 46 nonzeros
Coefficient ranges:
  Matrix [6e-01, 1e+00]
  Cost   [1e+00, 1e+00]
  Bound  [0e+00, 0e+00]
  RHS    [6e+02, 1e+03]
Presolving model
7 rows, 6 cols, 25 nonzeros  0s
Dependent equations search running on 2 equations with time limit of 1000.00s
Dependent equations search removed 0 rows and 0 nonzeros in 0.00s (limit = 1000.00s)
4 rows, 6 cols, 19 nonzeros  0s
Presolve : Reductions: rows 4(-10); columns 6(-3); elements 19(-27)
Solving the presolved LP
Using EKK dual simplex solver - serial
  Iteration        Objective     Infeasibilities num(sum)
          0    -3.3024705065e+00 Ph1: 4(3.57078); Du: 3(3.30247) 0s
          3    -1.2077779546e+03 Pr: 0(0) 0s
Solving the original LP from the solution after postsolve
Model status        : Optimal
Simplex   iterations: 3
Objective value     :  1.2077779546e+03
P-D objective error :  9.4089971014e-17
HiGHS run time      :          0.00
Out[0]:
1207.7779545600001

Let's display the income value taking into account the restrictions:

In [ ]:
println("After $5 years, the income from the initial $(Sum_0) ₽ will be $(round(value of goal_function2, digits=2)) ₽")
После 5 лет, доход с начальных 1000 ₽ составит 1207.78 ₽

Visualize the solution:

In [ ]:
# Visualization of a solution with constraints
p3 = plotInvest(Number of bonds, Year of purchase, Maturity, Interest, resolution2)
display(p3)

A custom-sized model

Let's move on to a generalized formulation of the problem, scaling the model to a 30-year investment horizon with a portfolio of 400 bonds with random returns in the range of 1-6%. This configuration forms a linear programming problem with 430 variables, demonstrating the applicability of the method to real investment problems.

In [ ]:
Random.seed!(123)
# The initial amount of money
Summer_0_ Large = 1000
# Time period in years
Long_ period = 30
# Number of bonds
The number of bonds is Large = 400
# Total number of purchase options
All possible purchases are large = The number of bonds is large + The Big_ period
# Generating random repayment durations
Repayment time_ Long = rand(1:(Period_ Large-1), all possible purchases are large)
# The bonds have a maturity period of 1 year
Repayment time_ is long[1:The period is long] .= 1
# We generate random annual interest rates for each bond
Percentages are large = RAND(1:6, all possible purchases are large)
# The bonds have an interest rate of 0 (not invested)
Percentages are large[1:Period_ Long] .= 0
# Yield after one year, including interest
Profitability is small_ = 1 .+ Percentages are large ./ 100
# Calculating the yield at the end of the maturity period for each bond
Total profitability is large = Profitability is very large . Long Repayment term
# Create random purchase years for each option
Year of purchase is Large = zeros(Int, all possible purchases are large)
# The bonds are available for purchase every year
The year of purchase is Large[1:Long_ period] = 1:The Big_ period
for i in 1:The number of bonds is large
    # Generating a random year to repay the bond before the end of the T-year period
    The year of purchase is Big[i+Long_ period] = rand(1:(Long period - Long repayment period[i+Long period] + 1))
end
# Calculating the periods when each bond reaches maturity at the end of the year
The year of repayment is large = The year of purchase is large .+ The repayment term is long .- 1

Let's form a temporary model of investment operations, where the matrices индексы_покупки_большие and индексы_продажи_большие The acceptable periods for opening and closing positions on each financial instrument are set.

In [ ]:
# The matrix of purchase indices
Purchase indexes are large = falses(All possible purchases are large, period is large)
for ii in 1:The Big_ period
    Purchase indexes are large[:, ii] = Year of purchase are large .== ii
end

# The matrix of sales indices
Sale index_sold = falses(All possible purchases are large, the period is large)
for ii in 1:The Big_ period
    The indices of the sale are large[:, ii] = The year of repayment is large .== ii
end

We set up optimization variables corresponding to bonds.

In [ ]:
model_size = Model(HiGHS.Optimizer)
# Variable solutions
@variable(model_ big, x_ big[1:All possible purchases are large] >= 0)

Let's create an optimization objective function.

In [ ]:
# The task of maximizing
@objective(model is large, Max, sum(x is large[sales indexes are large[:, Period is large]] .* Total profitability is large[sales indexes are large[:, Period is large]]))
# Limitations
@constraint(model big, initial investment big, 
    sum(x_ big[i] for i in 1:All possible purchases are large if the purchase indexes are large[i, 1]) == Sum_0_ Large)

for t in 2:The Big_ period
    @constraint(the model_ is large, 
        sum(x_ big[i] for i in 1:All possible purchases are large if the purchase indexes are large[i, t]) ==
        sum(x_ big[i] * Total profitability is large[i] for i in 1:All possible purchases are large if the sale indexes are large[i, t-1]))
end

Let's solve the problem:

In [ ]:
@time optimize!(model_ is large)
big_resolution = value.(x_ big)
The value of the target function is large = objective_value(the model is large)
Running HiGHS 1.11.0 (git hash: 364c83a51e): Copyright (c) 2025 HiGHS under MIT licence terms
LP   has 30 rows; 430 cols; 823 nonzeros
Coefficient ranges:
  Matrix [1e+00, 5e+00]
  Cost   [1e+00, 5e+00]
  Bound  [0e+00, 0e+00]
  RHS    [1e+03, 1e+03]
Presolving model
29 rows, 381 cols, 727 nonzeros  0s
24 rows, 304 cols, 545 nonzeros  0s
10 rows, 64 cols, 109 nonzeros  0s
8 rows, 47 cols, 77 nonzeros  0s
5 rows, 22 cols, 36 nonzeros  0s
4 rows, 18 cols, 28 nonzeros  0s
0 rows, 0 cols, 0 nonzeros  0s
Presolve : Reductions: rows 0(-30); columns 0(-430); elements 0(-823) - Reduced to empty
Solving the original LP from the solution after postsolve
Model status        : Optimal
Objective value     :  5.7434911729e+03
P-D objective error :  0.0000000000e+00
HiGHS run time      :          0.00
  0.006142 seconds (3.72 k allocations: 326.117 KiB)
Out[0]:
5743.491172913259

How well did the investment work?

In [ ]:
println("After $A period of many years, the income from the initial $(sum0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0")
После 30 лет, доход с начальных 1000 ₽ составит 5743.49 ₽

A solution with limited shares

Let's create an optimization problem with constraints:

In [ ]:
# Creating a model for a problem with constraints
model_ Large with constraints = Model(HiGHS.Optimizer)

@variable(model is large with constraints, x is large with[1:All possible purchases are large] >= 0)
@objective(model is large with constraints, Max, 
    sum(x_ big_c[i] * Total profitability is large[i] for i in 1:All possible purchases are large if the sale indexes are large[i, period_ Large]))

# Constraints for a model with constraints
@constraint(the model is large with constraints, the initial investment is large, 
    sum(x_ big_c[i] for i in 1:All possible purchases are large if the purchase indexes are large[i, 1]) == Sum_0_ Large)

for t in 2:The Big_ period
    @constraint(the model is large with constraints, 
        sum(x_ big_c[i] for i in 1:All possible purchases are large if the purchase indexes are large[i, t]) ==
        sum(x_ big_c[i] * Total profitability is large[i] for i in 1:All possible purchases are large if the sale indexes are large[i, t-1]))
end

The scaled model uses a diversification standard that limits the share of a single bond in the portfolio to 0.4.

In [ ]:
Max Percentage is Large = 0.4

Formalization of diversification constraints requires the construction of two data structures: a matrix of bond activity and a matrix of their current value, based on which the upper bound of the share of a single asset in the portfolio is set.

In [ ]:
active_size = falses(All possible purchases are large, Period_size)
for ii in 1:The Big_ period
    active big[:, ii] = (ii .>= Year of purchase Big) .& (ii .<= Repayment year_ Large)
end

We will set limits on maximum investments.:

In [ ]:
# Restrictions on maximum investments
for i in 1:All possible purchases are large
    for t in 1:The Big_ period
        if the purchase indexes are large[i, t]
            if t == 1
                @constraint(model is large with constraints, x is large with[i] <= maxcenter is large * Sum is0 is large)
            else
                @constraint(model is large with constraints, x is large with[i] <= maxcenter is large * 
                    sum(x_ big with[j] * (profitability is_ big[j] ^ sum(active big[j, 1:(t-1)])) 
                        for j in 1:All possible purchases are large if active are large[j, t-1]))
            end
        end
    end
end

Problem solving:

In [ ]:
@time optimize!(the model is large with constraints)
The solution is large with constraints = value.(x_ big_c)
The value of the target function is large with constraints = objective_value(the model is large with constraints)
println("After $A period of many years, the income from the initial $(sum0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0")
Running HiGHS 1.11.0 (git hash: 364c83a51e): Copyright (c) 2025 HiGHS under MIT licence terms
LP   has 460 rows; 430 cols; 72421 nonzeros
Coefficient ranges:
  Matrix [4e-01, 5e+00]
  Cost   [1e+00, 5e+00]
  Bound  [0e+00, 0e+00]
  RHS    [4e+02, 1e+03]
Presolving model
419 rows, 430 cols, 72380 nonzeros  0s
Dependent equations search running on 28 equations with time limit of 1000.00s
Dependent equations search removed 0 rows and 0 nonzeros in 0.00s (limit = 1000.00s)
417 rows, 422 cols, 70114 nonzeros  0s
Presolve : Reductions: rows 417(-43); columns 422(-8); elements 70114(-2307)
Solving the presolved LP
Using EKK dual simplex solver - serial
  Iteration        Objective     Infeasibilities num(sum)
          0    -2.3043600253e+02 Ph1: 380(10201.3); Du: 102(230.436) 0s
        117    -5.7109808455e+03 Pr: 0(0) 0s
Solving the original LP from the solution after postsolve
Model status        : Optimal
Simplex   iterations: 117
Objective value     :  5.7109808455e+03
P-D objective error :  0.0000000000e+00
HiGHS run time      :          0.05
  0.065911 seconds (10.74 k allocations: 4.741 MiB)

После 30 лет, доход с начальных 1000 ₽ составит 5710.98 ₽

Qualitative analysis of the results

To assess the effectiveness of an optimal portfolio, it is advisable to compare its profitability with the theoretical maximum — the investment of all capital in a bond with a maximum rate of 6% for the entire 30-year period. An additional metric of the analysis can be the calculation of the equivalent annual rate corresponding to the achieved level of final well-being.

In [ ]:
# Maximum possible amount
max_size = Sum_0_size * (1 + 6/100)^The Big_ period

# Ratio (percentage)
ratio = value of the target function with large limits / maximum sum * 100

# Equivalent interest rate
equivalent_set = ((value of goal_function_ large with constraints / Sum_0_ Large)^(1/Period_ Large) -1) * 100

println("The amount received will be $(round(ratio, digits=2))% of the maximum amount of $(round(max_sum, digits=2)),")
println("which could have been obtained by investing in one bond.")
println("\Your income corresponds to $(round(equivalent value, digits=2))% annual rate for $(Long)-summer period.")

# Visualize the results
p4 = plotInvest(The number of bonds is large, the year of purchase is large, the repayment period is large, the percentages are large, the resolution is large with restrictions, false)
display(p4)
Полученная сумма составит 99.43% от максимальной суммы 5743.49 ₽,
которая могла быть получена при инвестировании в одну облигацию.

Ваш доход соответствует 5.98% годовой ставке за 30-летний период.

The graph shows the time structure of the investment portfolio with restrictions:

  • Horizontal green lines - selected bonds

  • The beginning of the line is the year of purchase, the end is the year of repayment

  • Numbers in curly brackets {i} are bond identifiers

  • Top-down location — investment priority

The graph shows a diversified portfolio, where investments are distributed over different years and periods in accordance with the limitation of no more than 60% per asset.

Conclusion

The conducted research demonstrates the effectiveness of linear programming methods for optimizing long-term investment portfolios. The developed model makes it possible to determine a capital allocation strategy that maximizes final well-being over a given time horizon.

Experimental calculations confirm the existence of a compromise between diversification and profitability: the introduction of a 60% limit on the share of one asset reduces the final yield by 4.3% compared with unlimited optimization. The scalability of the approach is verified on a task with a 30-year horizon and a portfolio of 400 bonds.

The directions for future research are to take into account the uncertainty of profitability and create algorithms for dynamic portfolio optimization in changing market conditions.