"""
## GAMSSOURCE: https://www.gams.com/latest/finlib_ml/libhtml/finlib_MeanVarMip.html
## LICENSETYPE: Demo
## MODELTYPE: MINLP
## DATAFILES: MeanVarMip.gdx
Mean-variance model with diversification constraints
MeanVarMip.gms: Mean-variance model with diversification constraints.
Consiglio, Nielsen and Zenios.
PRACTICAL FINANCIAL OPTIMIZATION: A Library of GAMS Models, Section 3.4
Last modified: Apr 2008.
"""
from __future__ import annotations
import os
from pathlib import Path
from gamspy import (
Alias,
Container,
Equation,
Model,
Options,
Parameter,
Set,
Sum,
Variable,
)
def main():
m = Container(
system_directory=os.getenv("SYSTEM_DIRECTORY", None),
)
# Read from MeanVarMip.gdx the data needed to run the mean-variance model
m.read(
str(Path(__file__).parent.absolute()) + "/MeanVarMip.gdx",
[
"assets",
"SUBSET",
"s1",
"s2",
"MeanRiskFreeReturn",
"VarCov",
"ExpectedReturns",
],
)
# SETS #
Assets = m.getSymbols(["subset"])[0]
# ALIASES #
i = Alias(m, name="i", alias_with=Assets)
j = Alias(m, name="j", alias_with=Assets)
# PARAMETERS #
ExpectedReturns, VarCov = m.getSymbols(["ExpectedReturns", "VarCov"])
# Risk attitude: 0 is risk-neutral, 1 is very risk-averse.
StockMax = Parameter(
m, name="StockMax", records=3, description="Maximum number of stocks"
)
lamda = Parameter(m, name="lamda", description="Risk attitude")
xlow = Parameter(
m,
name="xlow",
domain=i,
description="lower bound for active variables",
)
# VARIABLES #
x = Variable(m, name="x", domain=i, description="Holdings of assets")
Y = Variable(
m,
name="Y",
type="binary",
domain=i,
description="Indicator variable for assets included in the portfolio",
)
PortVariance = Variable(
m, name="PortVariance", description="Portfolio variance"
)
PortReturn = Variable(m, name="PortReturn", description="Portfolio return")
# In case short sales are allowed these bounds must be set properly.
xlow[i] = 0.0
x.up[i] = 1.0
# EQUATIONS #
ReturnDef = Equation(
m,
name="ReturnDef",
type="regular",
description="Equation defining the portfolio return",
)
VarDef = Equation(
m,
name="VarDef",
type="regular",
description="Equation defining the portfolio variance",
)
NormalCon = Equation(
m,
name="NormalCon",
type="regular",
description="Equation defining the normalization contraint",
)
LimitCon = Equation(
m,
name="LimitCon",
type="regular",
description="Constraint defining the maximum number of assets allowed",
)
UpBounds = Equation(
m,
name="UpBounds",
type="regular",
domain=i,
description="Upper bounds for each variable",
)
LoBounds = Equation(
m,
name="LoBounds",
type="regular",
domain=i,
description="Lower bounds for each variable",
)
ReturnDef[...] = PortReturn == Sum(i, ExpectedReturns[i] * x[i])
VarDef[...] = PortVariance == Sum([i, j], x[i] * VarCov[i, j] * x[j])
LimitCon[...] = Sum(i, Y[i]) <= StockMax
UpBounds[i] = x[i] <= x.up[i] * Y[i]
LoBounds[i] = x[i] >= xlow[i] * Y[i]
NormalCon[...] = Sum(i, x[i]) == 1
# Objective Function
ObjDef = (1 - lamda) * PortReturn - lamda * PortVariance
MeanVarMip = Model(
m,
name="MeanVarMip",
equations=[
ReturnDef,
VarDef,
LimitCon,
UpBounds,
LoBounds,
NormalCon,
],
problem="MINLP",
sense="MAX",
objective=ObjDef,
)
MeanVarianceMIP = '"Lambda","z","Variance","ExpReturn",'
i_recs = [f'"{i_rec}"' for i_rec in i.records.ASSETS.tolist()]
MeanVarianceMIP += ",".join(i_recs)
MeanVarianceMIP += "\n"
lamda_loop = 0
while True:
if lamda_loop > 1:
break
lamda[...] = lamda_loop
MeanVarMip.solve(
options=Options(minlp="SBB", relative_optimality_gap=0)
)
MeanVarianceMIP += f"{round(lamda_loop,1)},{round(MeanVarMip.objective_value,4)},{round(PortVariance.records.level[0],4)},{round(PortReturn.records.level[0],4)},"
x_recs = [str(round(x_rec, 4)) for x_rec in x.records.level.tolist()]
MeanVarianceMIP += ",".join(x_recs)
MeanVarianceMIP += "\n"
lamda_loop += 0.1
with open("MeanVarianceMIP.csv", "w", encoding="UTF-8") as FrontierHandle:
FrontierHandle.write(MeanVarianceMIP)
# ***** Transaction Cost *****
# In this section the MeanVar.gms model is modified by imposing transaction
# costs. We consider a more realistic setting with fixed and proportional costs.
FlatCost = Parameter(
m, name="FlatCost", records=0.001, description="Flat transaction cost"
)
PropCost = Parameter(
m,
name="PropCost",
records=0.005,
description="Proportional transaction cost",
)
x_0 = Variable(
m,
name="x_0",
type="positive",
domain=i,
description="Holdings for the flat cost regime",
)
x_1 = Variable(
m,
name="x_1",
type="positive",
domain=i,
description="Holdings for the linear cost regime",
)
# Amount at which is possible to make transactions at the flat fee.
x_0.up[i] = 0.1
HoldingCon = Equation(
m,
name="HoldingCon",
type="regular",
domain=i,
description="Constraint defining the holdings",
)
ReturnDefWithCost = Equation(
m,
name="ReturnDefWithCost",
type="regular",
description="Equation defining the portfolio return with cost",
)
FlatCostBounds = Equation(
m,
name="FlatCostBounds",
type="regular",
domain=i,
description="Upper bounds for flat transaction fee",
)
LinCostBounds = Equation(
m,
name="LinCostBounds",
type="regular",
domain=i,
description="Upper bonds for linear transaction fee",
)
HoldingCon[i] = x[i] == x_0[i] + x_1[i]
ReturnDefWithCost[...] = PortReturn == Sum(
i, (ExpectedReturns[i] * x_0[i] - FlatCost * Y[i])
) + Sum(i, (ExpectedReturns[i] - PropCost) * x_1[i])
FlatCostBounds[i] = x_0[i] <= x_0.up[i] * Y[i]
LinCostBounds[i] = x_1[i] <= Y[i]
MeanVarWithCost = Model(
m,
name="MeanVarWithCost",
equations=[
ReturnDefWithCost,
VarDef,
HoldingCon,
NormalCon,
FlatCostBounds,
LinCostBounds,
],
problem="MINLP",
sense="MAX",
objective=ObjDef,
)
MeanVarianceWithCost = '"Lambda","z","Variance","ExpReturn",'
MeanVarianceWithCost += ",".join(i_recs) + ","
MeanVarianceWithCost += ",".join(i_recs)
MeanVarianceWithCost += "\n"
lamda_loop = 0
while True:
if lamda_loop > 1:
break
lamda[...] = lamda_loop
MeanVarWithCost.solve(
options=Options(minlp="SBB", relative_optimality_gap=0)
)
MeanVarianceWithCost += f"{round(lamda_loop,1)},{round(MeanVarWithCost.objective_value,4)},{round(PortVariance.records.level[0],4)},{round(PortReturn.records.level[0],4)},"
x0_recs = [
str(round(x_rec, 4)) for x_rec in x_0.records.level.tolist()
]
x1_recs = [
str(round(x_rec, 4)) for x_rec in x_1.records.level.tolist()
]
MeanVarianceWithCost += ",".join(x0_recs) + ","
MeanVarianceWithCost += ",".join(x1_recs) + "\n"
lamda_loop += 0.1
with open(
"MeanVarianceWithCost.csv", "w", encoding="UTF-8"
) as FrontierHandleTwo:
FrontierHandleTwo.write(MeanVarianceWithCost)
# ***** Portfolio Revision *****
# In this section the MeanVar.gms model is modified by imposing zero-or-range
# variable to cope with portfolio revision.
Bound = Set(m, name="Bound", records=["Lower", "Upper"])
BuyLimits = Parameter(m, name="BuyLimits", domain=[Bound, i])
SellLimits = Parameter(m, name="SellLimits", domain=[Bound, i])
InitHold = Parameter(
m, name="InitHold", domain=i, description="Current holdings"
)
# We set the curret holding to the optimal unconstrained mean-variance portfolio
# with lamda = 0.5
InitHold["CASH_EU"] = 0.3686
InitHold["YRS_1_3"] = 0.3597
InitHold["EMU"] = 0.0
InitHold["EU_EX"] = 0.0
InitHold["PACIFIC"] = 0.0
InitHold["EMERGT"] = 0.0591
InitHold["NOR_AM"] = 0.2126
InitHold["ITMHIST"] = 0.0
BuyLimits["Lower", i] = InitHold[i] * 0.9
BuyLimits["Upper", i] = InitHold[i] * 1.10
SellLimits["Lower", i] = InitHold[i] * 0.75
SellLimits["Upper", i] = InitHold[i] * 1.25
buy = Variable(
m,
name="buy",
type="positive",
domain=i,
description="Amount to be purchased",
)
sell = Variable(
m,
name="sell",
type="positive",
domain=i,
description="Amount to be sold",
)
Yb = Variable(
m,
name="Yb",
type="binary",
domain=i,
description="Indicator variable for assets to be purchased",
)
Ys = Variable(
m,
name="Ys",
type="binary",
domain=i,
description="Indicator variable for assets to be sold",
)
BuyTurnover = Equation(m, name="BuyTurnover", type="regular")
LoBuyLimits = Equation(m, name="LoBuyLimits", type="regular", domain=i)
UpBuyLimits = Equation(m, name="UpBuyLimits", type="regular", domain=i)
UpSellLimits = Equation(m, name="UpSellLimits", type="regular", domain=i)
LoSellLimits = Equation(m, name="LoSellLimits", type="regular", domain=i)
BinBuyLimits = Equation(m, name="BinBuyLimits", type="regular", domain=i)
BinSellLimits = Equation(m, name="BinSellLimits", type="regular", domain=i)
InventoryCon = Equation(
m,
name="InventoryCon",
type="regular",
domain=i,
description="Inventory constraints",
)
InventoryCon[i] = x[i] - buy[i] + sell[i] == InitHold[i]
UpBuyLimits[i] = InitHold[i] + buy[i] <= BuyLimits["Upper", i]
LoBuyLimits[i] = InitHold[i] + buy[i] >= BuyLimits["Lower", i]
UpSellLimits[i] = InitHold[i] - sell[i] <= SellLimits["Upper", i]
LoSellLimits[i] = InitHold[i] - sell[i] >= SellLimits["Lower", i]
BinBuyLimits[i] = buy[i] <= Yb[i]
BinSellLimits[i] = sell[i] <= Ys[i]
BuyTurnover[...] = Sum(i, buy[i]) <= 0.05
MeanVarRevision = Model(
m,
name="MeanVarRevision",
equations=[
NormalCon,
HoldingCon,
InventoryCon,
ReturnDef,
UpBuyLimits,
LoBuyLimits,
UpSellLimits,
LoSellLimits,
BinBuyLimits,
BinSellLimits,
BuyTurnover,
VarDef,
],
problem="MINLP",
sense="MAX",
objective=ObjDef,
)
MeanVarianceRevision = (
'"Model status","Lambda","z","Variance","ExpReturn",'
)
MeanVarianceRevision += ",".join(i_recs) + ","
MeanVarianceRevision += ",".join(i_recs) + ","
MeanVarianceRevision += ",".join(i_recs) + "\n"
lamda_loop = 0
while True:
if lamda_loop > 1:
break
lamda[...] = lamda_loop
MeanVarRevision.solve(
options=Options(minlp="SBB", relative_optimality_gap=0)
)
MeanVarianceRevision += f"{MeanVarRevision.status},{round(lamda_loop,1)},{round(MeanVarRevision.objective_value,4)},{round(PortVariance.records.level[0],4)},{round(PortReturn.records.level[0],4)},"
x_recs = [str(round(x_rec, 4)) for x_rec in x.records.level.tolist()]
buy_recs = [
str(round(x_rec, 4)) for x_rec in buy.records.level.tolist()
]
sell_recs = [
str(round(x_rec, 4)) for x_rec in sell.records.level.tolist()
]
MeanVarianceRevision += ",".join(x_recs) + ","
MeanVarianceRevision += ",".join(buy_recs) + ","
MeanVarianceRevision += ",".join(sell_recs) + "\n"
lamda_loop += 0.1
with open(
"MeanVarianceRevision.csv", "w", encoding="UTF-8"
) as FrontierHandleThree:
FrontierHandleThree.write(MeanVarianceRevision)
if __name__ == "__main__":
main()