"""
## GAMSSOURCE: https://www.gams.com/latest/finlib_ml/libhtml/finlib_DedicationMIP.html
## LICENSETYPE: Demo
## MODELTYPE: LP, MIP
Dedication model with tradeability constraints
DedicationMIP.gms: Dedication model with tradeability constraints.
Consiglio, Nielsen and Zenios.
PRACTICAL FINANCIAL OPTIMIZATION: A Library of GAMS Models, Section 4.3.2
Last modified: Apr 2008.
First model - Simple dedication.
"""
from __future__ import annotations
import os
import gamspy.math as gams_math
import numpy as np
import pandas as pd
from gamspy import (
Alias,
Card,
Container,
Equation,
Model,
Number,
Options,
Ord,
Parameter,
Set,
Sum,
Variable,
)
def BondDataTable():
# Bond data. Prices, coupons and maturities from the Danish market
bond_data_recs = pd.DataFrame(
np.array(
[
[112.35, 2006, 8],
[105.33, 2003, 8],
[111.25, 2007, 7],
[107.30, 2004, 7],
[107.62, 2011, 6],
[106.68, 2009, 6],
[101.93, 2002, 6],
[101.30, 2005, 5],
[101.61, 2003, 5],
[100.06, 2002, 4],
]
),
columns=["Price", "Maturity", "Coupon"],
index=[
"DS-8-06",
"DS-8-03",
"DS-7-07",
"DS-7-04",
"DS-6-11",
"DS-6-09",
"DS-6-02",
"DS-5-05",
"DS-5-03",
"DS-4-02",
],
)
bond_data_recs = bond_data_recs.reset_index().melt(
id_vars="index", var_name="Category", value_name="Value"
)
return bond_data_recs
def main():
m = Container(
system_directory=os.getenv("SYSTEM_DIRECTORY", None),
)
# SET #
Time = Set(
m,
name="Time",
records=[str(t) for t in range(2001, 2012)],
description="Time periods",
)
t = Alias(m, name="t", alias_with=Time)
# SCALARS #
Now = Parameter(m, name="Now", description="Current year")
Horizon = Parameter(m, name="Horizon", description="End of the Horizon")
Now[...] = 2001
Horizon[...] = Card(t) - 1
# PARAMETER #
tau = Parameter(m, name="tau", domain=t, description="Time in years")
# Note: time starts from 0
tau[t] = Ord(t) - 1
# SET
Bonds = Set(
m,
name="Bonds",
records=[
"DS-8-06",
"DS-8-03",
"DS-7-07",
"DS-7-04",
"DS-6-11",
"DS-6-09",
"DS-6-02",
"DS-5-05",
"DS-5-03",
"DS-4-02",
],
description="Bonds universe",
)
i = Alias(m, name="i", alias_with=Bonds)
# SCALARS #
spread = Parameter(
m,
name="spread",
description="Borrowing spread over the reinvestment rate",
)
# PARAMETERS #
Price = Parameter(m, name="Price", domain=i, description="Bond prices")
Coupon = Parameter(m, name="Coupon", domain=i, description="Coupons")
Maturity = Parameter(
m, name="Maturity", domain=i, description="Maturities"
)
Liability = Parameter(
m, name="Liability", domain=t, description="Stream of liabilities"
)
rf = Parameter(m, name="rf", domain=t, description="Reinvestment rates")
F = Parameter(m, name="F", domain=[t, i], description="Cashflows")
# Bond data. Prices, coupons and maturities from the Danish market
BondData = Parameter(
m, name="BondData", domain=[i, "*"], records=BondDataTable()
)
# Copy/transform data. Note division by 100 to get unit data, and
# subtraction of "Now" from Maturity date (so consistent with tau):
Price[i] = BondData[i, "Price"] / 100
Coupon[i] = BondData[i, "Coupon"] / 100
Maturity[i] = BondData[i, "Maturity"] - Now
# Calculate the ex-coupon cashflow of Bond i in year t:
F[t, i] = (
Number(1).where[tau[t] == Maturity[i]]
+ Coupon[i].where[(tau[t] <= Maturity[i]) & (tau[t] > 0)]
)
# For simplicity, we set the short term rate to be 0.03 in each period
rf[t] = 0.04
spread[...] = 0.02
Liability.setRecords(
np.array(
[
0,
80000,
100000,
110000,
120000,
140000,
120000,
90000,
50000,
75000,
150000,
]
)
)
# VARIABLES #
x = Variable(
m,
name="x",
type="positive",
domain=i,
description="Face value purchased",
)
surplus = Variable(
m,
name="surplus",
type="positive",
domain=t,
description="Amount of money reinvested",
)
borrow = Variable(
m,
name="borrow",
type="positive",
domain=t,
description="Amount of money borrowed",
)
v0 = Variable(m, name="v0", description="Upfront investment")
# EQUATION #
CashFlowCon = Equation(
m,
name="CashFlowCon",
type="regular",
domain=t,
description="Equations defining the cashflow balance",
)
CashFlowCon[t] = (
Sum(i, F[t, i] * x[i])
+ (v0 - Sum(i, Price[i] * x[i])).where[tau[t] == 0]
+ borrow[t].where[tau[t] < Horizon]
+ ((1 + rf[t.lag(1)]) * surplus[t.lag(1)]).where[tau[t] > 0]
== surplus[t]
+ Liability[t].where[tau[t] > 0]
+ ((1 + rf[t.lag(1)] + spread) * borrow[t.lag(1)]).where[tau[t] > 0]
)
Dedication = Model(
m,
name="Dedication",
equations=[CashFlowCon],
problem="LP",
sense="MIN",
objective=v0,
)
Dedication.solve(
options=Options(
relative_optimality_gap=0, iteration_limit=999999, time_limit=100
)
)
print("* First Model Results\n")
print("v0: ", v0.records.level.round(3).tolist(), "\n")
print("borrow: ", borrow.records.level.round(3).tolist(), "\n")
print("surplus: ", surplus.records.level.round(3).tolist(), "\n")
print("x: ", x.records.level.round(3).tolist(), "\n\n")
output_csv = "No trading constraints\n"
purchased_price = (x.records.level * Price.records.value).round(3).tolist()
for idx, ii, _ in i.records.itertuples():
output_csv += f'{round(v0.records.level[0],3)},"{ii}",{BondData.pivot().loc[ii,"Maturity"]},{Coupon.records.value[idx]},{purchased_price[idx]}\n'
for tt, _ in t.records.itertuples(index=False):
borrow_rec = borrow.records[borrow.records["t"] == tt]
borrow_rec = (
round(borrow_rec.level.array[0], 3) if not borrow_rec.empty else 0
)
surplus_rec = surplus.records[surplus.records["t"] == tt]
surplus_rec = (
round(surplus_rec.level.array[0], 3)
if not surplus_rec.empty
else 0
)
output_csv += f'"{tt}",{borrow_rec},{surplus_rec}\n'
# Second model - Dedication plus even-lot constraints.
# SCALARS #
LotSize = Parameter(
m, name="LotSize", records=1000, description="Even-Lot requirement"
)
FixedCost = Parameter(
m, name="FixedCost", records=20, description="Fixed cost per trade"
)
VarblCost = Parameter(
m, name="VarblCost", records=0.01, description="Variable cost"
)
# VARIABLE #
Y = Variable(
m,
name="Y",
type="integer",
domain=i,
description="Variable counting the number of lot purchased",
)
# EQUATION #
EvenLot = Equation(
m,
name="EvenLot",
type="regular",
domain=i,
description="Equation defining the even-lot requirements",
)
EvenLot[i] = x[i] == LotSize * Y[i]
# Some reasonable upper bounds on Y[i]
Y.up[i] = gams_math.ceil(Sum(t, Liability[t]) / Price[i] / LotSize)
DedicationMIPEvenLot = Model(
m,
name="DedicationMIPEvenLot",
equations=[CashFlowCon, EvenLot],
problem="MIP",
sense="MIN",
objective=v0,
)
DedicationMIPEvenLot.solve(
options=Options(
relative_optimality_gap=0, iteration_limit=999999, time_limit=100
)
)
print("* Second Model Results\n")
print("x: ", x.records.level.round(3).tolist(), "\n")
print("Y: ", Y.records.level.round(3).tolist(), "\n")
print("v0: ", v0.records.level.round(3).tolist(), "\n\n")
output_csv += "Even-lot constraints\n"
purchased_price = (x.records.level * Price.records.value).round(3).tolist()
for idx, ii, _ in i.records.itertuples():
output_csv += f'{round(v0.records.level[0],3)},"{ii}",{BondData.pivot().loc[ii,"Maturity"]},{Coupon.records.value[idx]},{purchased_price[idx]}\n'
for tt, _ in t.records.itertuples(index=False):
borrow_rec = borrow.records[borrow.records["t"] == tt]
borrow_rec = (
round(borrow_rec.level.array[0], 3) if not borrow_rec.empty else 0
)
surplus_rec = surplus.records[surplus.records["t"] == tt]
surplus_rec = (
round(surplus_rec.level.array[0], 3)
if not surplus_rec.empty
else 0
)
output_csv += f'"{tt}",{borrow_rec},{surplus_rec}\n'
# Third model - Dedication plus fixed and variable transaction costs
# VARIABLES #
TotalCost = Variable(
m, name="TotalCost", description="Total cost to minimize"
)
TransCosts = Variable(
m,
name="TransCosts",
description="Total transaction costs (fixed + variable)",
)
# VARIABLES #
Z = Variable(
m,
name="Z",
type="binary",
domain=i,
description="Indicator variable for assets included in the portfolio",
)
# EQUATIONS #
CostDef = Equation(
m,
name="CostDef",
type="regular",
description=(
"Equation definining the total cost including transaction costs"
),
)
TransDef = Equation(
m,
name="TransDef",
type="regular",
description="Equation the transaction costs (fixed + variable)",
)
UpBounds = Equation(
m,
name="UpBounds",
type="regular",
domain=i,
description="Upper bounds for each variable",
)
CostDef[...] = TotalCost == v0 + TransCosts
TransDef[...] = TransCosts == Sum(i, FixedCost * Z[i] + VarblCost * x[i])
UpBounds[i] = x[i] <= x.up[i] * Z[i]
DedicationMIPTrnCosts = Model(
m,
name="DedicationMIPTrnCosts",
equations=[CashFlowCon, CostDef, TransDef, UpBounds],
problem="MIP",
sense="MIN",
objective=TotalCost,
)
# Some conservative bounds on investments
x.up[i] = LotSize * Y.up[i]
DedicationMIPTrnCosts.solve(
options=Options(
relative_optimality_gap=0, iteration_limit=999999, time_limit=100
)
)
print("* Third Model Results\n")
print("x: ", x.records.level.round(3).tolist(), "\n")
print("v0: ", v0.records.level.round(3).tolist(), "\n")
print("TotalCost: ", TotalCost.records.level.round(3).tolist(), "\n")
print("TransCosts: ", TransCosts.records.level.round(3).tolist(), "\n\n")
output_csv += "Fixed and variable costs\n"
purchased_price = (x.records.level * Price.records.value).round(3).tolist()
for idx, ii, _ in i.records.itertuples():
output_csv += f'{round(v0.records.level[0],3)},"{ii}",{BondData.pivot().loc[ii,"Maturity"]},{Coupon.records.value[idx]},{purchased_price[idx]}\n'
for tt, _ in t.records.itertuples(index=False):
borrow_rec = borrow.records[borrow.records["t"] == tt]
borrow_rec = (
round(borrow_rec.level.array[0], 3) if not borrow_rec.empty else 0
)
surplus_rec = surplus.records[surplus.records["t"] == tt]
surplus_rec = (
round(surplus_rec.level.array[0], 3)
if not surplus_rec.empty
else 0
)
output_csv += f'"{tt}",{borrow_rec},{surplus_rec}\n'
# Fourth model - Dedication including even-lot restrictions and
# transaction costs.
DedicationMIPAll = Model(
m,
name="DedicationMIPAll",
equations=[CashFlowCon, EvenLot, CostDef, TransDef, UpBounds],
problem="MIP",
sense="MIN",
objective=TotalCost,
)
DedicationMIPAll.solve(
options=Options(
relative_optimality_gap=0, iteration_limit=999999, time_limit=100
)
)
print("* Fourth Model Results\n")
print("x: ", x.records.level.round(3).tolist(), "\n")
print("v0: ", v0.records.level.round(3).tolist(), "\n")
print("TotalCost: ", TotalCost.records.level.round(3).tolist(), "\n")
print("TransCosts: ", TransCosts.records.level.round(3).tolist(), "\n\n")
output_csv += "Even-lot constraints and transaction costs\n"
purchased_price = (x.records.level * Price.records.value).round(3).tolist()
for idx, ii, _ in i.records.itertuples():
output_csv += f'{round(v0.records.level[0],3)},"{ii}",{BondData.pivot().loc[ii,"Maturity"]},{Coupon.records.value[idx]},{purchased_price[idx]}\n'
for tt, _ in t.records.itertuples(index=False):
borrow_rec = borrow.records[borrow.records["t"] == tt]
borrow_rec = (
round(borrow_rec.level.array[0], 3) if not borrow_rec.empty else 0
)
surplus_rec = surplus.records[surplus.records["t"] == tt]
surplus_rec = (
round(surplus_rec.level.array[0], 3)
if not surplus_rec.empty
else 0
)
output_csv += f'"{tt}",{borrow_rec},{surplus_rec}\n'
with open(
"DedicationMIPPortfolios.csv", "w", encoding="UTF-8"
) as DedicationHandle:
DedicationHandle.write(output_csv)
if __name__ == "__main__":
main()