Indexation model with selective hedging#

SelectiveHedging.py SelectiveHedging.gdx

"""
## GAMSSOURCE: https://www.gams.com/latest/finlib_ml/libhtml/finlib_SelectiveHedging.html
## LICENSETYPE: Demo
## MODELTYPE: LP
## DATAFILES: SelectiveHedging.gdx


Indexation model with selective hedging

* SelectiveHedging.gms: Indexation model with selective hedging
* Consiglio, Nielsen and Zenios.
* PRACTICAL FINANCIAL OPTIMIZATION: A Library of GAMS Models, Section 7.2.3
* Last modified: Apr 2008.
"""

from __future__ import annotations

import os
from pathlib import Path

from gamspy import (
    Alias,
    Card,
    Container,
    Equation,
    Model,
    ModelStatus,
    Number,
    Parameter,
    Sense,
    Set,
    Sum,
    Variable,
)


def main(output=None):
    # Define container
    m = Container(
        system_directory=os.getenv("SYSTEM_DIRECTORY", None),
        load_from=str(Path(__file__).parent.absolute())
        + "/SelectiveHedging.gdx",
    )

    # SETS #
    BB, EE, BxE, SS = m.getSymbols(["BB2", "EE", "BxE2", "SS"])

    i = Alias(m, name="i", alias_with=BB)
    l = Alias(m, name="l", alias_with=SS)
    s = Alias(m, name="s", alias_with=SS)
    e = Alias(m, name="e", alias_with=EE)

    # PARAMETERS #
    (
        BondPrices1,
        BondPrices0,
        ExchangeRates0,
        ExchangeRates1,
        IndexReturns,
    ) = m.getSymbols(
        ["data2", "data3", "ExchangeRates0", "ExchangeRates1", "IndexReturns"]
    )

    mu = Parameter(m, name="mu", description="Target expected value")
    USDDEMForwardRate = Parameter(
        m, name="USDDEMForwardRate", description="USD-DEM forward rate"
    )
    USDCHFForwardRate = Parameter(
        m, name="USDCHFForwardRate", description="USD-CHF forward rate"
    )
    pr = Parameter(m, name="pr", domain=l, description="Scenario probability")

    USDDEMForwardRate[...] = -0.005
    USDCHFForwardRate[...] = 0.001

    BondReturns = Parameter(
        m, name="BondReturns", domain=[SS, BB], description="Bond returns"
    )
    UnhedgedBondReturns = Parameter(
        m,
        name="UnhedgedBondReturns",
        domain=[SS, BB],
        description="Unhedged bond returns",
    )
    HedgedBondReturns = Parameter(
        m,
        name="HedgedBondReturns",
        domain=[SS, BB],
        description="Hedged bond returns",
    )
    ExchangeRatesReturns = Parameter(
        m, name="ExchangeRatesReturns", domain=[SS, EE]
    )

    BondReturns[l, i] = (BondPrices1[l, i] - BondPrices0[i]) / BondPrices0[i]
    ExchangeRatesReturns[l, e] = (
        ExchangeRates1[l, e] - ExchangeRates0[e]
    ) / ExchangeRates0[e]

    # Unhedged bond returns in USD currency

    UnhedgedBondReturns[l, i].where[BxE[i, "USD"]] = BondReturns[l, i].where[
        BxE[i, "USD"]
    ]
    UnhedgedBondReturns[l, i].where[BxE[i, "DEM"]] = (
        BondReturns[l, i].where[BxE[i, "DEM"]] + ExchangeRatesReturns[l, "DEM"]
    )
    UnhedgedBondReturns[l, i].where[BxE[i, "CHF"]] = (
        BondReturns[l, i].where[BxE[i, "CHF"]] + ExchangeRatesReturns[l, "CHF"]
    )

    # Hedged bond returns

    HedgedBondReturns[l, i].where[BxE[i, "DEM"]] = (
        BondReturns[l, i].where[BxE[i, "DEM"]] + USDDEMForwardRate
    )
    HedgedBondReturns[l, i].where[BxE[i, "CHF"]] = (
        BondReturns[l, i].where[BxE[i, "CHF"]] + USDCHFForwardRate
    )

    pr[l] = 1.0 / Card(l)

    # VARIABLES #
    h = Variable(m, name="h", type="positive", domain=i)
    u = Variable(m, name="u", type="positive", domain=i)
    y = Variable(m, name="y", type="positive", domain=l)

    # EQUATIONS #
    ReturnCon = Equation(m, name="ReturnCon")
    NormalCon = Equation(m, name="NormalCon")
    yPosDef = Equation(m, name="yPosDef", domain=l)
    yNegDef = Equation(m, name="yNegDef", domain=l)

    # Objective Function
    ObjDef = Sum(l, pr[l] * y[l])

    yPosDef[l] = y[l] >= Sum(
        i, UnhedgedBondReturns[l, i] * u[i] + HedgedBondReturns[l, i] * h[i]
    ) - Sum(
        s,
        pr[s]
        * Sum(
            i,
            UnhedgedBondReturns[s, i] * u[i] + HedgedBondReturns[s, i] * h[i],
        ),
    )

    ReturnCon[...] = (
        Sum(
            l,
            pr[l]
            * Sum(
                i,
                UnhedgedBondReturns[l, i] * u[i]
                + HedgedBondReturns[l, i] * h[i],
            ),
        )
        >= mu
    )

    yNegDef[l] = y[l] >= Sum(
        s,
        pr[s]
        * Sum(
            i,
            UnhedgedBondReturns[s, i] * u[i] + HedgedBondReturns[s, i] * h[i],
        ),
    ) - Sum(
        i, UnhedgedBondReturns[l, i] * u[i] + HedgedBondReturns[l, i] * h[i]
    )

    NormalCon[...] = Sum(i, h[i] + u[i]) == 1.0

    IndexFund = Model(
        m,
        name="IndexFund",
        equations=m.getEquations(),
        problem="LP",
        sense=Sense.MIN,
        objective=ObjDef,
    )

    FrontierPoints = Set(
        m, name="FrontierPoints", records=[f"P_{i}" for i in range(1, 51)]
    )
    p = Alias(m, name="p", alias_with=FrontierPoints)

    Frontiers = Parameter(
        m, name="Frontiers", domain=[p, "*"], description="Frontiers"
    )

    # We assign to each point a return level mu

    Frontiers["P_1", "mu"] = 0.0

    for idx, pp, _ in p.records.itertuples():
        if idx == 0:
            continue
        if idx < 20:
            Frontiers[pp, "mu"] = Frontiers[f"P_{idx}", "mu"] + Number(0.0005)
        elif idx >= 20:
            Frontiers[pp, "mu"] = Frontiers[f"P_{idx}", "mu"] + Number(0.001)

    for pp, _ in p.records.itertuples(index=False):
        if IndexFund.status not in [
            ModelStatus.OptimalGlobal,
            ModelStatus.OptimalLocal,
        ]:
            continue

        mu[...] = Frontiers[pp, "mu"]

        IndexFund.solve()
        print("Objective: ", round(IndexFund.objective_value, 3))

        if IndexFund.status in [
            ModelStatus.OptimalGlobal,
            ModelStatus.OptimalLocal,
        ]:
            Frontiers[pp, "Partial Hedge"] = IndexFund.objective_value

    # Fully hedged model
    u.fx[i] = 0.0

    IndexFund.status = None
    for pp, _ in p.records.itertuples(index=False):
        if IndexFund.status not in [
            ModelStatus.OptimalGlobal,
            ModelStatus.OptimalLocal,
        ]:
            continue

        mu[...] = Frontiers[pp, "mu"]

        IndexFund.solve()
        print("Objective: ", round(IndexFund.objective_value, 3))

        if IndexFund.status in [
            ModelStatus.OptimalGlobal,
            ModelStatus.OptimalLocal,
        ]:
            Frontiers[pp, "Fully Hedged"] = IndexFund.objective_value

    # Unhedged model
    u.lo[i] = 0.0
    u.up[i] = 1.0
    h.fx[i] = 0.0

    IndexFund.status = None
    for pp, _ in p.records.itertuples(index=False):
        if IndexFund.status not in [
            ModelStatus.OptimalGlobal,
            ModelStatus.OptimalLocal,
        ]:
            continue

        mu[...] = Frontiers[pp, "mu"]

        IndexFund.solve()
        print("Objective: ", round(IndexFund.objective_value, 3))

        if IndexFund.status in [
            ModelStatus.OptimalGlobal,
            ModelStatus.OptimalLocal,
        ]:
            Frontiers[pp, "Unhedged"] = IndexFund.objective_value

    # Create an excel file with the information stored in Frontiers
    # To activate it, add the name of the output file to "main" function below
    if output is not None:
        Frontiers.pivot().to_csv(f"{output}.csv")


if __name__ == "__main__":
    main()