Conditional Value at Risk models#

CVaR.py WorldIndices.gdx

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


Conditional Value at Risk models

CVaR.gms: Conditional Value at Risk models.
Consiglio, Nielsen and Zenios.
PRACTICAL FINANCIAL OPTIMIZATION: A Library of GAMS Models, Section 5.5
Last modified: Apr 2008.
"""

from __future__ import annotations

import os
from pathlib import Path

import numpy as np

from gamspy import Card
from gamspy import Container
from gamspy import Equation
from gamspy import Model
from gamspy import Number
from gamspy import Parameter
from gamspy import Smax
from gamspy import Smin
from gamspy import Sum
from gamspy import Variable


def index_data():
    np.array([
        1.034769211,
        1.024362083,
        1.005721076,
        1.014359531,
        1.008024402,
        1.015164086,
        1.011776948,
        0.999367312,
        1.022337255,
        1.014084294,
        1.002478561,
        1.004830772,
        1.022048865,
        1.001584376,
        1.014789245,
        1.018465908,
        1.02023507,
        1.001142139,
        1.028357519,
        1.012274672,
        1.005320247,
        0.991451365,
        1.007344745,
        1.017165464,
        0.997931432,
        0.990041853,
        0.9933217,
        0.991912481,
        1.036980768,
        0.994451813,
        1.007412617,
        0.955150659,
        0.958840232,
        1.033624988,
        1.018079311,
        1.010736753,
        1.016700717,
        1.048121923,
        1.015146812,
        1.0074283,
        1.025778693,
        0.983151788,
        1.022476292,
        1.004850227,
        1.0091122,
        0.995361207,
        0.992432266,
        1.024212556,
        1.022181138,
        1.006772127,
        0.990354861,
        1.0067649,
        1.008943286,
        0.983974563,
        1.000347655,
        1.001230771,
        1.020880921,
        1.019415483,
        1.008638044,
        1.017173042,
        1.011831605,
        1.018260066,
        1.014054495,
        1.011841085,
        0.988155751,
        1.009224769,
        1.010321982,
        1.033008126,
        0.999626315,
        1.022733809,
        1.00083866,
        1.039925522,
        1.0144168,
        0.974402144,
        0.971108237,
        1.002198078,
        1.001221179,
        0.988342113,
        1.01750901,
        1.019968377,
        0.992895551,
        1.000905761,
        0.997333999,
        0.994982525,
        0.987972315,
        1.000443735,
        1.01583315,
        1.018338346,
        1.026183302,
        0.999209138,
        1.018809687,
        1.009775599,
        1.01722367,
        0.999752239,
        1.015986198,
        1.019400469,
        1.017624299,
        0.996067065,
        1.00947457,
        1.0164916,
        0.997929107,
        1.008892447,
        0.990498799,
        1.011843222,
        1.023338977,
        1.001536085,
        1.023978065,
        0.994238826,
        1.022819962,
        1.012142345,
        0.989862601,
        1.021050206,
        1.019165002,
        1.023260624,
        1.027413626,
        0.971346297,
        1.026087902,
        0.959990261,
        1.008484984,
        1.014365951,
        1.010347985,
        1.029266037,
        1.01989954,
        0.999383722,
        0.992191473,
        0.993782093,
        1.010730888,
        0.918149916,
        1.008729179,
        1.031641438,
        1.032632304,
        1.004115995,
        1.007398109,
        0.99859111,
        1.032604061,
        1.027883261,
        0.979268923,
        1.024656356,
        0.989556024,
        1.001926578,
        0.991713793,
        1.020697126,
        1.022268183,
        1.031273723,
        0.992301368,
        1.011547524,
        1.02121809,
        0.988701862,
        0.991540642,
        1.014546705,
        0.997009882,
        1.01168258,
        0.982543898,
        0.992197467,
    ])


def main():
    gdx_file = str(Path(__file__).parent.absolute()) + "/WorldIndices.gdx"
    m = Container(
        system_directory=os.getenv("SYSTEM_DIRECTORY", None),
        delayed_execution=int(os.getenv("DELAYED_EXECUTION", False)),
        load_from=gdx_file,
    )

    # SETS #
    i, l = m.getSymbols(["i", "l"])

    # SCALARS #
    Budget = Parameter(
        m, name="Budget", description="Nominal investment budget"
    )
    alpha = Parameter(m, name="alpha", description="Confidence level")
    MU_TARGET = Parameter(
        m, name="MU_TARGET", description="Target portfolio return"
    )
    MU_STEP = Parameter(m, name="MU_STEP", description="Target return step")
    MIN_MU = Parameter(
        m, name="MIN_MU", description="Minimum return in universe"
    )
    MAX_MU = Parameter(
        m, name="MAX_MU", description="Maximum return in universe"
    )
    RISK_TARGET = Parameter(
        m, name="RISK_TARGET", description="Bound on CVaR (risk)"
    )
    LossFlag = Parameter(
        m, name="LossFlag", description="Flag selecting the type of loss"
    )

    Budget[...] = 100.0
    alpha[...] = 0.99

    # PARAMETERS #
    pr = Parameter(m, name="pr", domain=l, description="Scenario probability")
    P = Parameter(m, name="P", domain=[i, l], description="Final values")
    EP = Parameter(m, name="EP", domain=i, description="Expected final values")
    AssetReturns = m.getSymbols(["AssetReturns"])[0]

    pr[l] = 1.0 / Card(l)

    P[i, l] = 1 + AssetReturns[i, l]

    EP[i] = Sum(l, pr[l] * P[i, l])

    MIN_MU[...] = Smin(i, EP[i])
    MAX_MU[...] = Smax(i, EP[i])

    # Assume we want 20 portfolios in the frontier

    MU_STEP[...] = (MAX_MU - MIN_MU) / 20

    TargetIndex = Parameter(
        m, name="TargetIndex", domain=l, description="Target index returns"
    )

    # To test the model with a market index, uncomment the following two lines.
    # Note that, this index can be used only with WorldIndexes.inc.
    # Index = Parameter(m, name="Index", domain=l, records=index_data(), description="Index returns")
    # TargetIndex[l] = Index[l]

    # VARIABLES #
    x = Variable(
        m,
        name="x",
        type="positive",
        domain=i,
        description="Holdings of assets in monetary units (not proportions)",
    )
    VaRDev = Variable(
        m,
        name="VaRDev",
        type="positive",
        domain=l,
        description="Measures of the deviations from the VaR",
    )
    VaR = Variable(m, name="VaR", description="Value-at-Risk")
    Losses = Variable(
        m, name="Losses", domain=l, description="Measures of the losses"
    )

    # EQUATIONS #
    BudgetCon = Equation(
        m,
        name="BudgetCon",
        type="regular",
        description="Equation defining the budget contraint",
    )
    ReturnCon = Equation(
        m,
        name="ReturnCon",
        type="regular",
        description="Equation defining the portfolio return constraint",
    )
    CVaRCon = Equation(
        m,
        name="CVaRCon",
        type="regular",
        description="Equation defining the CVaR allowed",
    )
    LossDef = Equation(
        m,
        name="LossDef",
        type="regular",
        domain=l,
        description="Equations defining the losses",
    )
    VaRDevCon = Equation(
        m,
        name="VaRDevCon",
        type="regular",
        domain=l,
        description="Equations defining the VaR deviation constraints",
    )

    BudgetCon[...] = Sum(i, x[i]) == Budget

    ReturnCon[...] = Sum(i, EP[i] * x[i]) >= MU_TARGET * Budget

    CVaRCon[...] = VaR + Sum(l, pr[l] * VaRDev[l]) / (1 - alpha) <= RISK_TARGET

    VaRDevCon[l] = VaRDev[l] >= Losses[l] - VaR

    LossDef[l] = (
        Losses[l]
        == (Budget - Sum(i, P[i, l] * x[i])).where[LossFlag == 1]
        + (TargetIndex[l] * Budget - Sum(i, P[i, l] * x[i])).where[
            LossFlag == Number(2)
        ]
        + (Sum(i, EP[i] * x[i]) - Sum(i, P[i, l] * x[i])).where[
            LossFlag == Number(3)
        ]
    )

    z = VaR + Sum(l, pr[l] * VaRDev[l]) / (1 - alpha)

    MinCVaR = Model(
        m,
        name="MinCVaR",
        equations=[BudgetCon, ReturnCon, LossDef, VaRDevCon],
        problem="LP",
        sense="MIN",
        objective=z,
    )

    output_csv = '"Status","VaR","CVaR","Mean",'
    i_recs = [f'"{i_rec}"' for i_rec in i.records.uni.tolist()]
    output_csv += ",".join(i_recs) + "\n"

    LossFlag[...] = 2

    # Comment the following line if you want to track the market index.
    TargetIndex[l] = 1.01

    mu_target = MIN_MU.records.value[0]
    while mu_target <= MAX_MU.records.value[0]:
        MU_TARGET[...] = mu_target

        MinCVaR.solve()

        output_csv += (
            f"{str(MinCVaR.status).split('.')[-1]},{VaR.records.level.round(3)[0]},{round(MinCVaR.objective_value, 3)},{round(MU_TARGET.records.value[0] * Budget.records.value[0],3)},"
        )
        x_recs = [str(x_rec) for x_rec in x.records.level.round(2).tolist()]
        output_csv += ",".join(x_recs) + "\n"

        mu_target += MU_STEP.records.value[0]

    FrontierHandle = open("CVaRFrontiers.csv", "w", encoding="UTF-8")
    FrontierHandle.write(output_csv)
    FrontierHandle.close()


if __name__ == "__main__":
    main()