Dedication model with tradeability constraints#

DedicationMip.py

"""
## 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()