[{"data":1,"prerenderedAt":1148},["ShallowReactive",2],{"dev-\u002Fdeveloper\u002Fexamples\u002Fcardinality-portfolio":3},{"id":4,"title":5,"body":6,"description":1127,"extension":1128,"faq":1129,"meta":1142,"navigation":809,"noindex":1143,"path":1144,"seo":1145,"stem":1146,"__hash__":1147},"content\u002Fdeveloper\u002Fexamples\u002Fcardinality-portfolio.md","Cardinality-constrained portfolio optimization in Python (MIQP)",{"type":7,"value":8,"toc":1121},"minimark",[9,13,38,43,65,82,86,128,964,969,973,999,1027,1045,1063,1073,1079,1083,1107,1117],[10,11,5],"h1",{"id":12},"cardinality-constrained-portfolio-optimization-in-python-miqp",[14,15,16,17,21,22,25,26,29,30,33,34,37],"p",{},"Classic minimum-variance portfolio optimization gives every asset a weight. In\npractice you often want to hold ",[18,19,20],"strong",{},"only a handful"," of positions — fewer tickets,\nlower transaction and monitoring cost. Adding \"hold ",[18,23,24],{},"at most K"," assets\" turns\nthe smooth quadratic problem into a ",[18,27,28],{},"mixed-integer quadratic program (MIQP)",":\nbinary hold\u002Fskip decisions on top of a variance objective. With ",[18,31,32],{},"quicopt"," it is\na short Pyomo model, solved to a ",[18,35,36],{},"proven"," optimum.",[39,40,42],"h2",{"id":41},"the-naive-approach","The naive approach",[14,44,45,46,51,52,56,57,61,62,64],{},"The tempting shortcut is to solve the ordinary ",[47,48,50],"a",{"href":49},"\u002Fdeveloper\u002Fexamples\u002Fportfolio-optimization","minimum-variance QP",",\nthen keep the ",[53,54,55],"code",{},"K"," largest weights and renormalize. That is only a heuristic:\ndropping the small positions changes which weights are optimal for the ones that\nremain, so the truncated portfolio is usually ",[58,59,60],"em",{},"not"," the best ",[53,63,55],{},"-asset portfolio.",[14,66,67,68,70,71,74,75,78,79,81],{},"The exhaustive fix — enumerate every allowed subset of assets and solve a QP for\neach — is correct but blows up: choosing at most ",[53,69,55],{}," of ",[53,72,73],{},"N"," assets is\n",[53,76,77],{},"C(N,1) + … + C(N,K)"," subsets, hopeless the moment ",[53,80,73],{}," is realistic. Let the\nsolver make the discrete choice and the continuous one together.",[39,83,85],{"id":84},"model-it-as-an-miqp","Model it as an MIQP",[14,87,88,89,92,93,96,97,100,101,104,105,108,109,111,112,115,116,119,120,123,124,127],{},"A continuous weight ",[53,90,91],{},"w[i]"," per asset and a binary ",[53,94,95],{},"d[i]"," that is ",[53,98,99],{},"1"," only if the\nasset is held. Fully invested (",[53,102,103],{},"sum(w) == 1","), a weight can be positive only when\nits asset is held (",[53,106,107],{},"w[i] \u003C= d[i]","), and at most ",[53,110,55],{}," assets are held\n(",[53,113,114],{},"sum(d) \u003C= K","). Minimize the portfolio variance ",[53,117,118],{},"wᵀΣw",". Modeled in ",[18,121,122],{},"Pyomo","\n(",[53,125,126],{},"pip install \"quicopt[pyomo]\"","):",[129,130,136],"pre",{"className":131,"code":132,"filename":133,"language":134,"meta":135,"style":135},"language-python shiki shiki-themes github-dark","import pyomo.environ as pyo\nfrom quicopt import Client\nSigma = [\n    [ 0.12, 0.02, 0.05, 0.00,-0.01, 0.02, 0.03,-0.01],\n    [ 0.02, 0.12, 0.00,-0.03, 0.01, 0.02,-0.02, 0.00],\n    [ 0.05, 0.00, 0.24, 0.11, 0.02,-0.02, 0.00,-0.04],\n    [ 0.00,-0.03, 0.11, 0.30,-0.02, 0.02,-0.03,-0.05],\n    [-0.01, 0.01, 0.02,-0.02, 0.13,-0.05, 0.02, 0.02],\n    [ 0.02, 0.02,-0.02, 0.02,-0.05, 0.22,-0.03,-0.01],\n    [ 0.03,-0.02, 0.00,-0.03, 0.02,-0.03, 0.18, 0.03],\n    [-0.01, 0.00,-0.04,-0.05, 0.02,-0.01, 0.03, 0.22]]\nN, K = 8, 3\nm = pyo.ConcreteModel()\nm.w = pyo.Var(range(N), bounds=(0, 1))\nm.d = pyo.Var(range(N), domain=pyo.Binary)\nm.budget = pyo.Constraint(expr=sum(m.w[i] for i in range(N)) == 1)\nm.link = pyo.Constraint(range(N), rule=lambda m, i: m.w[i] \u003C= m.d[i])\nm.card = pyo.Constraint(expr=sum(m.d[i] for i in range(N)) \u003C= K)\nm.obj = pyo.Objective(expr=sum(Sigma[i][j]*m.w[i]*m.w[j] for i in range(N) for j in range(N)), sense=pyo.minimize)\n\nresult = Client().solve(m)\nprint(result.display)\n# The weights w[0..N-1] are the model's first N variables, reported as x1..xN.\nprint(\"class:\", result.model_class, \"| held:\", [\n    f\"asset {i+1} = {result.solution[f'x{i+1}']:.4f}\"\n    for i in range(N) if result.solution[f\"x{i+1}\"] > 1e-6])\n","cardinality_portfolio.py","python","",[53,137,138,157,171,183,236,277,321,367,412,458,502,548,564,575,611,633,679,708,742,804,811,822,831,838,858,916],{"__ignoreMap":135},[139,140,143,147,151,154],"span",{"class":141,"line":142},"line",1,[139,144,146],{"class":145},"snl16","import",[139,148,150],{"class":149},"s95oV"," pyomo.environ ",[139,152,153],{"class":145},"as",[139,155,156],{"class":149}," pyo\n",[139,158,160,163,166,168],{"class":141,"line":159},2,[139,161,162],{"class":145},"from",[139,164,165],{"class":149}," quicopt ",[139,167,146],{"class":145},[139,169,170],{"class":149}," Client\n",[139,172,174,177,180],{"class":141,"line":173},3,[139,175,176],{"class":149},"Sigma ",[139,178,179],{"class":145},"=",[139,181,182],{"class":149}," [\n",[139,184,186,189,193,196,199,201,204,206,209,212,215,218,220,222,224,227,229,231,233],{"class":141,"line":185},4,[139,187,188],{"class":149},"    [ ",[139,190,192],{"class":191},"sDLfK","0.12",[139,194,195],{"class":149},", ",[139,197,198],{"class":191},"0.02",[139,200,195],{"class":149},[139,202,203],{"class":191},"0.05",[139,205,195],{"class":149},[139,207,208],{"class":191},"0.00",[139,210,211],{"class":149},",",[139,213,214],{"class":145},"-",[139,216,217],{"class":191},"0.01",[139,219,195],{"class":149},[139,221,198],{"class":191},[139,223,195],{"class":149},[139,225,226],{"class":191},"0.03",[139,228,211],{"class":149},[139,230,214],{"class":145},[139,232,217],{"class":191},[139,234,235],{"class":149},"],\n",[139,237,239,241,243,245,247,249,251,253,255,257,259,261,263,265,267,269,271,273,275],{"class":141,"line":238},5,[139,240,188],{"class":149},[139,242,198],{"class":191},[139,244,195],{"class":149},[139,246,192],{"class":191},[139,248,195],{"class":149},[139,250,208],{"class":191},[139,252,211],{"class":149},[139,254,214],{"class":145},[139,256,226],{"class":191},[139,258,195],{"class":149},[139,260,217],{"class":191},[139,262,195],{"class":149},[139,264,198],{"class":191},[139,266,211],{"class":149},[139,268,214],{"class":145},[139,270,198],{"class":191},[139,272,195],{"class":149},[139,274,208],{"class":191},[139,276,235],{"class":149},[139,278,280,282,284,286,288,290,293,295,298,300,302,304,306,308,310,312,314,316,319],{"class":141,"line":279},6,[139,281,188],{"class":149},[139,283,203],{"class":191},[139,285,195],{"class":149},[139,287,208],{"class":191},[139,289,195],{"class":149},[139,291,292],{"class":191},"0.24",[139,294,195],{"class":149},[139,296,297],{"class":191},"0.11",[139,299,195],{"class":149},[139,301,198],{"class":191},[139,303,211],{"class":149},[139,305,214],{"class":145},[139,307,198],{"class":191},[139,309,195],{"class":149},[139,311,208],{"class":191},[139,313,211],{"class":149},[139,315,214],{"class":145},[139,317,318],{"class":191},"0.04",[139,320,235],{"class":149},[139,322,324,326,328,330,332,334,336,338,340,343,345,347,349,351,353,355,357,359,361,363,365],{"class":141,"line":323},7,[139,325,188],{"class":149},[139,327,208],{"class":191},[139,329,211],{"class":149},[139,331,214],{"class":145},[139,333,226],{"class":191},[139,335,195],{"class":149},[139,337,297],{"class":191},[139,339,195],{"class":149},[139,341,342],{"class":191},"0.30",[139,344,211],{"class":149},[139,346,214],{"class":145},[139,348,198],{"class":191},[139,350,195],{"class":149},[139,352,198],{"class":191},[139,354,211],{"class":149},[139,356,214],{"class":145},[139,358,226],{"class":191},[139,360,211],{"class":149},[139,362,214],{"class":145},[139,364,203],{"class":191},[139,366,235],{"class":149},[139,368,370,373,375,377,379,381,383,385,387,389,391,393,396,398,400,402,404,406,408,410],{"class":141,"line":369},8,[139,371,372],{"class":149},"    [",[139,374,214],{"class":145},[139,376,217],{"class":191},[139,378,195],{"class":149},[139,380,217],{"class":191},[139,382,195],{"class":149},[139,384,198],{"class":191},[139,386,211],{"class":149},[139,388,214],{"class":145},[139,390,198],{"class":191},[139,392,195],{"class":149},[139,394,395],{"class":191},"0.13",[139,397,211],{"class":149},[139,399,214],{"class":145},[139,401,203],{"class":191},[139,403,195],{"class":149},[139,405,198],{"class":191},[139,407,195],{"class":149},[139,409,198],{"class":191},[139,411,235],{"class":149},[139,413,415,417,419,421,423,425,427,429,431,433,435,437,439,441,444,446,448,450,452,454,456],{"class":141,"line":414},9,[139,416,188],{"class":149},[139,418,198],{"class":191},[139,420,195],{"class":149},[139,422,198],{"class":191},[139,424,211],{"class":149},[139,426,214],{"class":145},[139,428,198],{"class":191},[139,430,195],{"class":149},[139,432,198],{"class":191},[139,434,211],{"class":149},[139,436,214],{"class":145},[139,438,203],{"class":191},[139,440,195],{"class":149},[139,442,443],{"class":191},"0.22",[139,445,211],{"class":149},[139,447,214],{"class":145},[139,449,226],{"class":191},[139,451,211],{"class":149},[139,453,214],{"class":145},[139,455,217],{"class":191},[139,457,235],{"class":149},[139,459,461,463,465,467,469,471,473,475,477,479,481,483,485,487,489,491,493,496,498,500],{"class":141,"line":460},10,[139,462,188],{"class":149},[139,464,226],{"class":191},[139,466,211],{"class":149},[139,468,214],{"class":145},[139,470,198],{"class":191},[139,472,195],{"class":149},[139,474,208],{"class":191},[139,476,211],{"class":149},[139,478,214],{"class":145},[139,480,226],{"class":191},[139,482,195],{"class":149},[139,484,198],{"class":191},[139,486,211],{"class":149},[139,488,214],{"class":145},[139,490,226],{"class":191},[139,492,195],{"class":149},[139,494,495],{"class":191},"0.18",[139,497,195],{"class":149},[139,499,226],{"class":191},[139,501,235],{"class":149},[139,503,505,507,509,511,513,515,517,519,521,523,525,527,529,531,533,535,537,539,541,543,545],{"class":141,"line":504},11,[139,506,372],{"class":149},[139,508,214],{"class":145},[139,510,217],{"class":191},[139,512,195],{"class":149},[139,514,208],{"class":191},[139,516,211],{"class":149},[139,518,214],{"class":145},[139,520,318],{"class":191},[139,522,211],{"class":149},[139,524,214],{"class":145},[139,526,203],{"class":191},[139,528,195],{"class":149},[139,530,198],{"class":191},[139,532,211],{"class":149},[139,534,214],{"class":145},[139,536,217],{"class":191},[139,538,195],{"class":149},[139,540,226],{"class":191},[139,542,195],{"class":149},[139,544,443],{"class":191},[139,546,547],{"class":149},"]]\n",[139,549,551,554,556,559,561],{"class":141,"line":550},12,[139,552,553],{"class":149},"N, K ",[139,555,179],{"class":145},[139,557,558],{"class":191}," 8",[139,560,195],{"class":149},[139,562,563],{"class":191},"3\n",[139,565,567,570,572],{"class":141,"line":566},13,[139,568,569],{"class":149},"m ",[139,571,179],{"class":145},[139,573,574],{"class":149}," pyo.ConcreteModel()\n",[139,576,578,581,583,586,589,592,596,598,601,604,606,608],{"class":141,"line":577},14,[139,579,580],{"class":149},"m.w ",[139,582,179],{"class":145},[139,584,585],{"class":149}," pyo.Var(",[139,587,588],{"class":191},"range",[139,590,591],{"class":149},"(N), ",[139,593,595],{"class":594},"s9osk","bounds",[139,597,179],{"class":145},[139,599,600],{"class":149},"(",[139,602,603],{"class":191},"0",[139,605,195],{"class":149},[139,607,99],{"class":191},[139,609,610],{"class":149},"))\n",[139,612,614,617,619,621,623,625,628,630],{"class":141,"line":613},15,[139,615,616],{"class":149},"m.d ",[139,618,179],{"class":145},[139,620,585],{"class":149},[139,622,588],{"class":191},[139,624,591],{"class":149},[139,626,627],{"class":594},"domain",[139,629,179],{"class":145},[139,631,632],{"class":149},"pyo.Binary)\n",[139,634,636,639,641,644,647,649,652,655,658,661,664,667,670,673,676],{"class":141,"line":635},16,[139,637,638],{"class":149},"m.budget ",[139,640,179],{"class":145},[139,642,643],{"class":149}," pyo.Constraint(",[139,645,646],{"class":594},"expr",[139,648,179],{"class":145},[139,650,651],{"class":191},"sum",[139,653,654],{"class":149},"(m.w[i] ",[139,656,657],{"class":145},"for",[139,659,660],{"class":149}," i ",[139,662,663],{"class":145},"in",[139,665,666],{"class":191}," range",[139,668,669],{"class":149},"(N)) ",[139,671,672],{"class":145},"==",[139,674,675],{"class":191}," 1",[139,677,678],{"class":149},")\n",[139,680,682,685,687,689,691,693,696,699,702,705],{"class":141,"line":681},17,[139,683,684],{"class":149},"m.link ",[139,686,179],{"class":145},[139,688,643],{"class":149},[139,690,588],{"class":191},[139,692,591],{"class":149},[139,694,695],{"class":594},"rule",[139,697,698],{"class":145},"=lambda",[139,700,701],{"class":149}," m, i: m.w[i] ",[139,703,704],{"class":145},"\u003C=",[139,706,707],{"class":149}," m.d[i])\n",[139,709,711,714,716,718,720,722,724,727,729,731,733,735,737,739],{"class":141,"line":710},18,[139,712,713],{"class":149},"m.card ",[139,715,179],{"class":145},[139,717,643],{"class":149},[139,719,646],{"class":594},[139,721,179],{"class":145},[139,723,651],{"class":191},[139,725,726],{"class":149},"(m.d[i] ",[139,728,657],{"class":145},[139,730,660],{"class":149},[139,732,663],{"class":145},[139,734,666],{"class":191},[139,736,669],{"class":149},[139,738,704],{"class":145},[139,740,741],{"class":149}," K)\n",[139,743,745,748,750,753,755,757,759,762,765,768,770,773,775,777,779,781,784,786,789,791,793,796,799,801],{"class":141,"line":744},19,[139,746,747],{"class":149},"m.obj ",[139,749,179],{"class":145},[139,751,752],{"class":149}," pyo.Objective(",[139,754,646],{"class":594},[139,756,179],{"class":145},[139,758,651],{"class":191},[139,760,761],{"class":149},"(Sigma[i][j]",[139,763,764],{"class":145},"*",[139,766,767],{"class":149},"m.w[i]",[139,769,764],{"class":145},[139,771,772],{"class":149},"m.w[j] ",[139,774,657],{"class":145},[139,776,660],{"class":149},[139,778,663],{"class":145},[139,780,666],{"class":191},[139,782,783],{"class":149},"(N) ",[139,785,657],{"class":145},[139,787,788],{"class":149}," j ",[139,790,663],{"class":145},[139,792,666],{"class":191},[139,794,795],{"class":149},"(N)), ",[139,797,798],{"class":594},"sense",[139,800,179],{"class":145},[139,802,803],{"class":149},"pyo.minimize)\n",[139,805,807],{"class":141,"line":806},20,[139,808,810],{"emptyLinePlaceholder":809},true,"\n",[139,812,814,817,819],{"class":141,"line":813},21,[139,815,816],{"class":149},"result ",[139,818,179],{"class":145},[139,820,821],{"class":149}," Client().solve(m)\n",[139,823,825,828],{"class":141,"line":824},22,[139,826,827],{"class":191},"print",[139,829,830],{"class":149},"(result.display)\n",[139,832,834],{"class":141,"line":833},23,[139,835,837],{"class":836},"sAwPA","# The weights w[0..N-1] are the model's first N variables, reported as x1..xN.\n",[139,839,841,843,845,849,852,855],{"class":141,"line":840},24,[139,842,827],{"class":191},[139,844,600],{"class":149},[139,846,848],{"class":847},"sU2Wk","\"class:\"",[139,850,851],{"class":149},", result.model_class, ",[139,853,854],{"class":847},"\"| held:\"",[139,856,857],{"class":149},", [\n",[139,859,861,864,867,870,873,876,879,882,884,887,890,893,895,897,899,901,904,907,910,913],{"class":141,"line":860},25,[139,862,863],{"class":145},"    f",[139,865,866],{"class":847},"\"asset ",[139,868,869],{"class":191},"{",[139,871,872],{"class":149},"i",[139,874,875],{"class":145},"+",[139,877,878],{"class":191},"1}",[139,880,881],{"class":847}," = ",[139,883,869],{"class":191},[139,885,886],{"class":149},"result.solution[",[139,888,889],{"class":145},"f",[139,891,892],{"class":847},"'x",[139,894,869],{"class":191},[139,896,872],{"class":149},[139,898,875],{"class":145},[139,900,878],{"class":191},[139,902,903],{"class":847},"'",[139,905,906],{"class":149},"]",[139,908,909],{"class":145},":.4f",[139,911,912],{"class":191},"}",[139,914,915],{"class":847},"\"\n",[139,917,919,922,924,926,928,930,933,936,938,941,943,945,947,949,952,955,958,961],{"class":141,"line":918},26,[139,920,921],{"class":145},"    for",[139,923,660],{"class":149},[139,925,663],{"class":145},[139,927,666],{"class":191},[139,929,783],{"class":149},[139,931,932],{"class":145},"if",[139,934,935],{"class":149}," result.solution[",[139,937,889],{"class":145},[139,939,940],{"class":847},"\"x",[139,942,869],{"class":191},[139,944,872],{"class":149},[139,946,875],{"class":145},[139,948,878],{"class":191},[139,950,951],{"class":847},"\"",[139,953,954],{"class":149},"] ",[139,956,957],{"class":145},">",[139,959,960],{"class":191}," 1e-6",[139,962,963],{"class":149},"])\n",[965,966],"term-result",{":rows":967,"cmd":968},"[\"├── shots\",\"│   ├── 1 · primal-dual · global   0.0398   0.48s  ◀ best\",\"│   ├── 2 · primal                 0.0398   2.09s\",\"│   └── 3 · primal                 0.0398   2.09s\",\"├── status:     optimal\",\"├── feasible:   true\",\"├── objective:  0.03976761723715457\",\"├── x:          x1=0.3248, x10=0, x11=0, x12=0, x13=1, x14=1, …  (16 variables)\",\"└── solve_time: 2.0916 s\",\"class: miqcp | held: ['asset 1 = 0.3248', 'asset 5 = 0.4272', 'asset 6 = 0.2481']\"]","$ python cardinality_portfolio.py",[39,970,972],{"id":971},"what-you-get","What you get",[14,974,975,976,979,980,195,983,986,987,990,991,994,995,998],{},"The least-risk three-asset portfolio holds assets ",[18,977,978],{},"1, 5 and 6",", at weights\n",[53,981,982],{},"0.32",[53,984,985],{},"0.43"," and ",[53,988,989],{},"0.25",", for a variance of ",[53,992,993],{},"0.0398",". With sixteen variables the\n",[53,996,997],{},"x:"," line truncates, which is why the script prints the held positions itself.",[14,1000,1001,1002,1004,1005,195,1007,195,1009,1011,1012,1015,1016,1018,1019,1022,1023,1026],{},"Look at which assets it did ",[58,1003,60],{}," pick. The three least volatile assets are 1, 2\nand 5 (variances ",[53,1006,192],{},[53,1008,192],{},[53,1010,395],{},"), so the obvious portfolio is those three.\nThe model drops asset 2 and takes asset ",[18,1013,1014],{},"6"," instead, whose variance is ",[53,1017,443],{}," —\nnearly double. It is worth holding because it moves ",[58,1020,1021],{},"against"," asset 5\n(",[53,1024,1025],{},"Sigma[4][5] = -0.05","), and that cancellation buys more than the extra volatility\ncosts. Ranking the assets individually cannot find this; only pricing the subset\nas a whole can.",[14,1028,1029,1032,1033,1036,1037,1040,1041,1044],{},[53,1030,1031],{},"status: optimal"," means quicopt ",[18,1034,1035],{},"proved"," it — over all ",[53,1038,1039],{},"C(8,1)+C(8,2)+C(8,3)","\nways to choose the held assets ",[58,1042,1043],{},"and"," every continuous weighting of each, no\nportfolio has lower variance. It picks the discrete subset and the weights in one\nsolve, because the binary hold\u002Fskip decisions and the quadratic variance are\nhandled together rather than as a relaxation.",[14,1046,1047,1048,1051,1052,1054,1055,1058,1059,1062],{},"The ",[53,1049,1050],{},"shots"," block is where that proof shows up. quicopt runs several solvers on\nthe model and reports what each one's answer is worth. All three found ",[53,1053,993],{},"\nhere, but three solvers agreeing is not a proof. Only the first one settles the\nquestion: it came back ",[53,1056,1057],{},"primal-dual · global",", so it carries a dual bound, and\nthat bound is what rules out every portfolio you never looked at. The other two\nreturned a ",[53,1060,1061],{},"primal"," point only — the same weights, with nothing attached that\nsays no better weights exist.",[14,1064,1065,1066,1069,1070,1072],{},"The winning shot finished well within a second, yet ",[53,1067,1068],{},"solve_time"," reads about\ntwo seconds, because\n",[53,1071,1068],{}," is the wall clock for the whole set: quicopt waits for the slower\nshots even after the answer is already proved.",[14,1074,1075,1076,1078],{},"The same model scales from eight assets to hundreds — change the covariance matrix\nand ",[53,1077,55],{},", not the method. (This is an illustrative optimization example, not\nfinancial advice.)",[39,1080,1082],{"id":1081},"next","Next",[1084,1085,1086,1094,1100],"ul",{},[1087,1088,1089,1090],"li",{},"The problem class behind this: ",[47,1091,1093],{"href":1092},"\u002Fproblems\u002Fmiqp","Mixed-integer quadratic (MIQP)",[1087,1095,1096,1097],{},"The unconstrained cousin: ",[47,1098,1099],{"href":49},"Portfolio optimization (QP)",[1087,1101,1102,1103],{},"A runnable model for every supported class: ",[47,1104,1106],{"href":1105},"\u002Fdeveloper\u002Fexamples\u002Fby-class","Examples",[14,1108,1109,1112,1113,1116],{},[18,1110,1111],{},"Reference:"," T.-J. Chang, N. Meade, J. E. Beasley, Y. M. Sharaiha,\n",[58,1114,1115],{},"Heuristics for cardinality constrained portfolio optimisation",", Computers &\nOperations Research, 2000.",[1118,1119,1120],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":135,"searchDepth":159,"depth":159,"links":1122},[1123,1124,1125,1126],{"id":41,"depth":159,"text":42},{"id":84,"depth":159,"text":85},{"id":971,"depth":159,"text":972},{"id":1081,"depth":159,"text":1082},"Classic minimum-variance portfolio optimization gives every asset a weight. In\npractice you often want to hold only a handful of positions — fewer tickets,\nlower transaction and monitoring cost. Adding \"hold at most K assets\" turns\nthe smooth quadratic problem into a mixed-integer quadratic program (MIQP):\nbinary hold\u002Fskip decisions on top of a variance objective. With quicopt it is\na short Pyomo model, solved to a proven optimum.","md",[1130,1133,1136,1139],{"q":1131,"a":1132},"Why can't I just solve the plain QP and keep the largest weights?","Truncating a continuous minimum-variance portfolio to its top K weights and renormalizing is a heuristic — the result is usually not the best K-asset portfolio, because dropping assets changes which weights are optimal. The mixed-integer model picks the K assets and their weights together, and proves it.",{"q":1134,"a":1135},"What kind of problem is a cardinality constraint?","A hold-or-skip decision per asset is binary, and the variance objective is quadratic, so the whole thing is a mixed-integer quadratic program (MIQP). The constraints stay linear, so this is not a general MINLP — the server classifies it as `miqcp` and solves it to a proved optimum.",{"q":1137,"a":1138},"Can I add a minimum position size or sector limits?","Yes. A minimum weight when held (w_i >= min * d_i), a maximum weight, group\u002Fsector caps and a target return are all linear constraints on the same binary and continuous variables.",{"q":1140,"a":1141},"Is quicopt free to use?","Yes, to start: sign up for a free account, install the client with pip install \"quicopt[pyomo]\" and set your API key. The free account solves models with up to 50 variables and has a limited compute allowance; no license is needed.",{},false,"\u002Fdeveloper\u002Fexamples\u002Fcardinality-portfolio",{"title":5,"description":1127},"developer\u002Fexamples\u002Fcardinality-portfolio","BLdRs3GTwt5md1u3z3ubwrdeGckY_t-HJ4VaMgbzW0Q",1794337679439]