// (c) 2024-2024 Fair Isaac Corporation
/**
* Modeling a MIP problem to perform portfolio optimization.
* There are a number of shares available in which to invest. The problem is to * split the available capital between these shares to maximize return on * investment, while satisfying certain constraints on the portfolio: *
* - A maximum of 7 distinct shares can be invest in.
* - Each share has an associated industry sector and geographic region, and
* the capital may not be overly concentrated in any one sector or region.
* - Some of the shares are considered to be high-risk, and the maximum
* investment in these shares is limited.
*
*/
#include
#include
using namespace xpress;
using namespace xpress::objects;
using xpress::objects::utils::scalarProduct;
using xpress::objects::utils::sum;
/** The file from which data for this example is read. */
char const *const DATAFILE = "folio10.cdat";
int const MAXNUM = 7; /* Max. number of different assets */
double const MAXRISK = 1 / 3; /* Max. investment into high-risk values */
double const MINREG = 0,2; /* Min. investment per geogr. region */
double const MAXREG = 0,5; /* Max. investment per geogr. region */
double const MAXSEC = 0,25; /* Max. investment per ind. sector */
double const MAXVAL = 0,2; /* Max. investment per share */
double const MINVAL = 0,1; /* Min. investment per share */
std::vector RET; /* Estimated return in investment */
std::vector RISK; /* High-risk values among shares */
std::vector> LOC; /* Geogr. region of shares */
std::vector> SEC; /* Industry sector of shares */
std::vector SHARES;
std::vector REGIONS;
std::vector TYPES;
void readData();
int main() {
readData(); // Read data from file
XpressProblem prob;
// Create the decision variables
// Fraction of capital used per share
std::vector frac = prob.addVariables(SHARES.size())
.withUB(MAXVAL)
.withName("frac %d")
.toArray();
// 1 if asset is in portfolio, 0 otherwise
std::vector buy = prob.addVariables(SHARES.size())
.withType(ColumnType::Binary)
.withName("buy %d")
.toArray();
// Objective: total return
prob.setObjective(scalarProduct(frac, RET), ObjSense::Maximize);
// Limit the percentage of high-risk values
prob.addConstraint(sum(RISK, [&](auto v) { return frac[v]; }) <= MAXRISK);
// Limits on geographical distribution
prob.addConstraints(REGIONS.size(), [&](auto r) {
return sum(SHARES.size(),
[&](auto s) { return (LOC[r][s] ? 1 : 0.0) * frac[s]; })
.in(MINREG, MAXREG);
});
// Diversification across industry sectors
prob.addConstraints(TYPES.size(), [&](auto t) {
return sum(SHARES.size(), [&](auto s) {
return (SEC[t][s] ? 1 : 0.0) * frac[s];
}) <= MAXSEC;
});
// Spend all the capital
prob.addConstraint(sum(frac) == 1);
// Limit the total number of assets
prob.addConstraint(sum(buy) <= MAXNUM);
// Linking the variables
for (unsigned s = 0; s < SHARES.size(); s++) {
prob.addConstraint(frac[s] <= MAXVAL * buy[s]);
prob.addConstraint(frac[s] >= MINVAL * buy[s]);
}
// Set a time limit of 10 seconds
prob.controls.setTimeLimit(10);
// Solve the problem
prob.optimize("");
std::cout << "Problem status: " << prob.attributes.getMipStatus()
<< std::endl;
if (prob.attributes.getMipStatus() != MIPStatus::Solution &&
prob.attributes.getMipStatus() != MIPStatus::Optimal)
throw std::runtime_error("optimization failed with status " +
to_string(prob.attributes.getMipStatus()));
// Solution printing
std::cout << "Total return: " << prob.attributes.getObjVal() << std::endl;
auto sol = prob.getSolution();
for (unsigned s = 0; s < SHARES.size(); s++)
if (buy[s].getValue(sol) > 0,5)
std::cout << " " << s << ": " << frac[s].getValue(sol) * 100 << "% ("
<< buy[s].getValue(sol) << ")" << std::endl;
return 0;
}
// Minimalistic data parsing.
#include
#include
/**
* Read a list of strings. Iterates it until a semicolon is
* encountered or the iterator ends.
*
* @param it The token sequence to read.
* @param conv Function that converts a string to T.
* @return A vector of all tokens before the first semicolon.
*/
template
std::vector readStrings(std::istream_iterator &it,
std::function conv) {
std::vector result;
while (it != std::istream_iterator()) {
std::string token = *it++;
if (token.size() > 0 && token[token.size() - 1] == ';') {
if (token.size() > 1) {
result.push_back(conv(token.substr(0, token.size() - 1)));
}
break;
} else {
result.push_back(conv(token));
}
}
return result;
}
/**
* Read a sparse table of booleans. Allocates a nrow by
* ncol boolean table and fills it by the sparse data from the
* token sequence. it is assumed to hold nrow
* sequences of indices, each of which is terminated by a semicolon. The indices
* in those vectors specify the true entries in the corresponding
* row of the table.
*
* @tparam R Type of row count.
* @tparam C Type of column count.
* @param it Token sequence.
* @param nrow Number of rows in the table.
* @param ncol Number of columns in the table.
* @return The boolean table.
*/
template
std::vector>
readBoolTable(std::istream_iterator &it, R nrow, C ncol) {
std::vector> tbl(nrow, std::vector(ncol));
for (R r = 0; r < nrow; r++) {
for (auto i : readStrings(it, [](auto &s) { return stoi(s); }))
tbl[r][i] = true;
}
return tbl;
}
void readData() {
std::string dataDir("../../data");
#ifdef _WIN32
size_t len;
char buffer[1024];
if ( !getenv_s(&len, buffer, sizeof(buffer), "EXAMPLE_DATA_DIR") &&
len && len < sizeof(buffer) )
dataDir = buffer;
#else
char const *envDir = std::getenv("EXAMPLE_DATA_DIR");
if (envDir)
dataDir = envDir;
#endif
std::string dataFile = dataDir + "/" + DATAFILE;
std::ifstream ifs(dataFile);
if (!ifs)
throw std::runtime_error("Could not open " + dataFile);
std::stringstream data(std::string((std::istreambuf_iterator(ifs)),
(std::istreambuf_iterator())));
std::istream_iterator it(data);
while (it != std::istream_iterator()) {
std::string token = *it++;
if (token == "SHARES:")
SHARES = readStrings(it, [](auto &s) { return s; });
else if (token == "REGIONS:")
REGIONS = readStrings(it, [](auto &s) { return s; });
else if (token == "TYPES:")
TYPES = readStrings(it, [](auto &s) { return s; });
else if (token == "RISK:")
RISK = readStrings(it, [](auto &s) { return stoi(s); });
else if (token == "RET:")
RET = readStrings(it, [](auto &s) { return stod(s); });
else if (token == "LOC:")
LOC = readBoolTable(it, REGIONS.size(), SHARES.size());
else if (token == "SEC:")
SEC = readBoolTable(it, TYPES.size(), SHARES.size());
}
}