// (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: * */ #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()); } }