"""
## 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 numpy as np
import pandas as pd

import gamspy.math as gams_math
from gamspy import Alias
from gamspy import Card
from gamspy import Container
from gamspy import Equation
from gamspy import Model
from gamspy import Number
from gamspy import Options
from gamspy import Ord
from gamspy import Parameter
from gamspy import Set
from gamspy import Sum
from gamspy import 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),
        delayed_execution=int(os.getenv("DELAYED_EXECUTION", False)),
    )

    # 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'

    DedicationHandle = open(
        "DedicationMIPPortfolios.csv", "w", encoding="UTF-8"
    )
    DedicationHandle.write(output_csv)


if __name__ == "__main__":
    main()
