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,
    Container,
    Equation,
    Model,
    Number,
    Parameter,
    Smax,
    Smin,
    Sum,
    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),
        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]

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


if __name__ == "__main__":
    main()