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 the 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:
import Pkg
Pkg.add(["JuMP", "HiGHS"])
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.
# Time period in years
Период = 5
# Number of bonds
КоличествоОблигаций = 4
# The initial amount of money
Сумма_0 = 1000
# Total number of purchase options
ВсегоВозможностейПокупки = КоличествоОблигаций + Период
# Purchase Periods
ГодыПокупки = [1; 2; 3; 4; 5; 1; 5; 2; 2]
# Bond durations
СрокПогашения = [1; 1; 1; 1; 1; 4; 1; 4; 3]
# Bond sale periods
ГодыПогашения = ГодыПокупки .+ СрокПогашения .- 1
# Interest rates as a percentage
Проценты = [0; 0; 0; 0; 0; 2; 4; 6; 6]
# Yield after one year, including interest
ДоходностьЗаГод = 1 .+ Проценты ./ 100
# Task visualization
p1 = plotInvest(КоличествоОблигаций, ГодыПокупки, СрокПогашения, Проценты)
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:
# Creating an optimization model
модель = Model(HiGHS.Optimizer)
# Variable solutions - investment amounts
@variable(модель, x[1:ВсегоВозможностейПокупки] >= 0)
# Total return
ОбщаяДоходность = ДоходностьЗаГод .^ СрокПогашения
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.
# Creating an optimization task (maximizing)
@objective(модель, Max, x[5] * ОбщаяДоходность[5] + x[7] * ОбщаяДоходность[7] + x[8] * ОбщаяДоходность[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:
# Restrictions on investments
@constraint(модель, ограничение_инвестиции1, x[1] + x[6] == Сумма_0)
@constraint(модель, ограничение_инвестиции2, x[2] + x[8] + x[9] == ДоходностьЗаГод[1] * x[1])
@constraint(модель, ограничение_инвестиции3, x[3] == ДоходностьЗаГод[2] * x[2])
@constraint(модель, ограничение_инвестиции4, x[4] == ДоходностьЗаГод[3] * x[3])
@constraint(модель, ограничение_инвестиции5, x[5] + x[7] == ДоходностьЗаГод[4] * x[4] + ОбщаяДоходность[6] * x[6] + ОбщаяДоходность[9] * x[9])
Decision
We will solve this problem without restrictions on the amount that can be invested in one bond.
optimize!(модель)
решение = value.(x)
значение_целевой_функции = objective_value(модель)
Visualization of the solution
Let's display the value of the return on investment.
println("После $Период лет, доход с начальных $(Сумма_0) ₽ составит $(round(значение_целевой_функции, digits=2)) ₽")
Visualize the solution:
p2 = plotInvest(КоличествоОблигаций, ГодыПокупки, СрокПогашения, Проценты, решение)
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.
# Creating a model for a problem with constraints
модель_с_ограничениями = Model(HiGHS.Optimizer)
# Decision variables for a constrained model
@variable(модель_с_ограничениями, x2[1:ВсегоВозможностейПокупки] >= 0)
# Target function
@objective(модель_с_ограничениями, Max, x2[5] * ОбщаяДоходность[5] + x2[7] * ОбщаяДоходность[7] + x2[8] * ОбщаяДоходность[8])
# Basic investment restrictions
@constraint(модель_с_ограничениями, инвестиции1, x2[1] + x2[6] == Сумма_0)
@constraint(модель_с_ограничениями, инвестиции2, x2[2] + x2[8] + x2[9] == ДоходностьЗаГод[1] * x2[1])
@constraint(модель_с_ограничениями, инвестиции3, x2[3] == ДоходностьЗаГод[2] * x2[2])
@constraint(модель_с_ограничениями, инвестиции4, x2[4] == ДоходностьЗаГод[3] * x2[3])
@constraint(модель_с_ограничениями, инвестиции5, x2[5] + x2[7] == ДоходностьЗаГод[4] * x2[4] + ОбщаяДоходность[6] * x2[6] + ОбщаяДоходность[9] * x2[9])
# The maximum interest rate for investments in any bond
МаксПроцент = 0.6
Let's create a system of inequalities:
# Restrictions on maximum investments
@constraint(модель_с_ограничениями, лимит1, x2[1] <= МаксПроцент * Сумма_0)
@constraint(модель_с_ограничениями, лимит2, x2[2] <= МаксПроцент * (ДоходностьЗаГод[1] * x2[1] + ДоходностьЗаГод[6] * x2[6]))
@constraint(модель_с_ограничениями, лимит3, x2[3] <= МаксПроцент * (ДоходностьЗаГод[2] * x2[2] + ДоходностьЗаГод[6]^2 * x2[6] + ДоходностьЗаГод[8] * x2[8] + ДоходностьЗаГод[9] * x2[9]))
@constraint(модель_с_ограничениями, лимит4, x2[4] <= МаксПроцент * (ДоходностьЗаГод[3] * x2[3] + ДоходностьЗаГод[6]^3 * x2[6] + ДоходностьЗаГод[8]^2 * x2[8] + ДоходностьЗаГод[9]^2 * x2[9]))
@constraint(модель_с_ограничениями, лимит5, x2[5] <= МаксПроцент * (ДоходностьЗаГод[4] * x2[4] + ДоходностьЗаГод[6]^4 * x2[6] + ДоходностьЗаГод[8]^3 * x2[8] + ДоходностьЗаГод[9]^3 * x2[9]))
@constraint(модель_с_ограничениями, лимит6, x2[6] <= МаксПроцент * Сумма_0)
@constraint(модель_с_ограничениями, лимит7, x2[7] <= МаксПроцент * (ДоходностьЗаГод[4] * x2[4] + ДоходностьЗаГод[6]^4 * x2[6] + ДоходностьЗаГод[8]^3 * x2[8] + ДоходностьЗаГод[9]^3 * x2[9]))
@constraint(модель_с_ограничениями, лимит8, x2[8] <= МаксПроцент * (ДоходностьЗаГод[1] * x2[1] + ДоходностьЗаГод[6] * x2[6]))
@constraint(модель_с_ограничениями, лимит9, x2[9] <= МаксПроцент * (ДоходностьЗаГод[1] * x2[1] + ДоходностьЗаГод[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.
# Solving a problem with constraints
optimize!(модель_с_ограничениями)
решение2 = value.(x2)
значение_целевой_функции2 = objective_value(модель_с_ограничениями)
Let's display the income value taking into account the restrictions:
println("После $Период лет, доход с начальных $(Сумма_0) ₽ составит $(round(значение_целевой_функции2, digits=2)) ₽")
Visualize the solution:
# Visualization of a solution with constraints
p3 = plotInvest(КоличествоОблигаций, ГодыПокупки, СрокПогашения, Проценты, решение2)
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.
Random.seed!(123)
# The initial amount of money
Сумма_0_большая = 1000
# Time period in years
Период_большой = 30
# Number of bonds
КоличествоОблигаций_большое = 400
# Total number of purchase options
ВсегоВозможностейПокупки_большое = КоличествоОблигаций_большое + Период_большой
# Generating random repayment durations
СрокПогашения_большой = rand(1:(Период_большой-1), ВсегоВозможностейПокупки_большое)
# The bonds have a maturity period of 1 year
СрокПогашения_большой[1:Период_большой] .= 1
# We generate random annual interest rates for each bond
Проценты_большие = rand(1:6, ВсегоВозможностейПокупки_большое)
# The bonds have an interest rate of 0 (not invested)
Проценты_большие[1:Период_большой] .= 0
# Yield after one year, including interest
ДоходностьЗаГод_большая = 1 .+ Проценты_большие ./ 100
# Calculating the yield at the end of the maturity period for each bond
ОбщаяДоходность_большая = ДоходностьЗаГод_большая .^ СрокПогашения_большой
# Create random purchase years for each option
ГодыПокупки_большие = zeros(Int, ВсегоВозможностейПокупки_большое)
# The bonds are available for purchase every year
ГодыПокупки_большие[1:Период_большой] = 1:Период_большой
for i in 1:КоличествоОблигаций_большое
# Generating a random year to repay the bond before the end of the T-year period
ГодыПокупки_большие[i+Период_большой] = rand(1:(Период_большой - СрокПогашения_большой[i+Период_большой] + 1))
end
# Calculating the periods when each bond reaches maturity at the end of the year
ГодыПогашения_большие = ГодыПокупки_большие .+ СрокПогашения_большой .- 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.
# The matrix of purchase indices
индексы_покупки_большие = falses(ВсегоВозможностейПокупки_большое, Период_большой)
for ii in 1:Период_большой
индексы_покупки_большие[:, ii] = ГодыПокупки_большие .== ii
end
# The matrix of sales indices
индексы_продажи_большие = falses(ВсегоВозможностейПокупки_большое, Период_большой)
for ii in 1:Период_большой
индексы_продажи_большие[:, ii] = ГодыПогашения_большие .== ii
end
We set up optimization variables corresponding to bonds.
модель_большая = Model(HiGHS.Optimizer)
# Variable solutions
@variable(модель_большая, x_большой[1:ВсегоВозможностейПокупки_большое] >= 0)
Let's create an optimization objective function.
# The task of maximizing
@objective(модель_большая, Max, sum(x_большой[индексы_продажи_большие[:, Период_большой]] .* ОбщаяДоходность_большая[индексы_продажи_большие[:, Период_большой]]))
# Limitations
@constraint(модель_большая, НачальнаяИнвестицияБольшая,
sum(x_большой[i] for i in 1:ВсегоВозможностейПокупки_большое if индексы_покупки_большие[i, 1]) == Сумма_0_большая)
for t in 2:Период_большой
@constraint(модель_большая,
sum(x_большой[i] for i in 1:ВсегоВозможностейПокупки_большое if индексы_покупки_большие[i, t]) ==
sum(x_большой[i] * ОбщаяДоходность_большая[i] for i in 1:ВсегоВозможностейПокупки_большое if индексы_продажи_большие[i, t-1]))
end
Let's solve the problem:
@time optimize!(модель_большая)
решение_большое = value.(x_большой)
значение_целевой_функции_большое = objective_value(модель_большая)
How well did the investment work?
println("После $Период_большой лет, доход с начальных $(Сумма_0_большая) ₽ составит $(round(значение_целевой_функции_большое, digits=2)) ₽")
A solution with limited shares
Let's create an optimization problem with constraints:
# Creating a model for a problem with constraints
модель_большая_с_ограничениями = Model(HiGHS.Optimizer)
@variable(модель_большая_с_ограничениями, x_большой_с[1:ВсегоВозможностейПокупки_большое] >= 0)
@objective(модель_большая_с_ограничениями, Max,
sum(x_большой_с[i] * ОбщаяДоходность_большая[i] for i in 1:ВсегоВозможностейПокупки_большое if индексы_продажи_большие[i, Период_большой]))
# Constraints for a model with constraints
@constraint(модель_большая_с_ограничениями, НачальнаяИнвестицияБольшаяС,
sum(x_большой_с[i] for i in 1:ВсегоВозможностейПокупки_большое if индексы_покупки_большие[i, 1]) == Сумма_0_большая)
for t in 2:Период_большой
@constraint(модель_большая_с_ограничениями,
sum(x_большой_с[i] for i in 1:ВсегоВозможностейПокупки_большое if индексы_покупки_большие[i, t]) ==
sum(x_большой_с[i] * ОбщаяДоходность_большая[i] for i in 1:ВсегоВозможностейПокупки_большое if индексы_продажи_большие[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.
МаксПроцент_большой = 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.
активные_большие = falses(ВсегоВозможностейПокупки_большое, Период_большой)
for ii in 1:Период_большой
активные_большие[:, ii] = (ii .>= ГодыПокупки_большие) .& (ii .<= ГодыПогашения_большие)
end
We will set limits on maximum investments.:
# Restrictions on maximum investments
for i in 1:ВсегоВозможностейПокупки_большое
for t in 1:Период_большой
if индексы_покупки_большие[i, t]
if t == 1
@constraint(модель_большая_с_ограничениями, x_большой_с[i] <= МаксПроцент_большой * Сумма_0_большая)
else
@constraint(модель_большая_с_ограничениями, x_большой_с[i] <= МаксПроцент_большой *
sum(x_большой_с[j] * (ДоходностьЗаГод_большая[j] ^ sum(активные_большие[j, 1:(t-1)]))
for j in 1:ВсегоВозможностейПокупки_большое if активные_большие[j, t-1]))
end
end
end
end
Problem solving:
@time optimize!(модель_большая_с_ограничениями)
решение_большое_с_ограничениями = value.(x_большой_с)
значение_целевой_функции_большое_с_ограничениями = objective_value(модель_большая_с_ограничениями)
println("\nПосле $Период_большой лет, доход с начальных $(Сумма_0_большая) ₽ составит $(round(значение_целевой_функции_большое_с_ограничениями, digits=2)) ₽")
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.
# Maximum possible amount
максимальная_сумма = Сумма_0_большая * (1 + 6/100)^Период_большой
# Ratio (as a percentage)
отношение = значение_целевой_функции_большое_с_ограничениями / максимальная_сумма * 100
# Equivalent interest rate
эквивалентная_ставка = ((значение_целевой_функции_большое_с_ограничениями / Сумма_0_большая)^(1/Период_большой) - 1) * 100
println("\nПолученная сумма составит $(round(отношение, digits=2))% от максимальной суммы $(round(максимальная_сумма, digits=2)) ₽,")
println("which could have been obtained by investing in one bond.")
println("\nВаш доход соответствует $(round(эквивалентная_ставка, digits=2))% годовой ставке за $(Период_большой)-летний период.")
# Visualize the results
p4 = plotInvest(КоличествоОблигаций_большое, ГодыПокупки_большие, СрокПогашения_большой, Проценты_большие, решение_большое_с_ограничениями, false)
display(p4)
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 a limit 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.



