[{"data":1,"prerenderedAt":674},["ShallowReactive",2],{"dev-\u002Fdeveloper\u002Fguides\u002Fcardinality-portfolio":3},{"id":4,"title":5,"body":6,"description":653,"extension":654,"faq":655,"meta":668,"navigation":669,"noindex":669,"path":670,"seo":671,"stem":672,"__hash__":673},"content\u002Fdeveloper\u002Fguides\u002Fcardinality-portfolio.md","Cardinality-constrained portfolio optimization in Python (MINLP)",{"type":7,"value":8,"toc":647},"minimark",[9,13,38,43,65,82,86,128,533,538,542,576,594,600,604,628,638,643],[10,11,5],"h1",{"id":12},"cardinality-constrained-portfolio-optimization-in-python-minlp",[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 (MINLP)",":\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\u002Fguides\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-minlp","Model it as an MINLP",[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 = [[0.10,0.02,0.04,0.00],[0.02,0.08,0.01,0.02],[0.04,0.01,0.12,0.03],[0.00,0.02,0.03,0.06]]\nN, K = 4, 2\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)\nprint(Client(\"https:\u002F\u002Ftry.quicoptapi.pgi.fz-juelich.de\").solve(m).display)\n","cardinality_portfolio.py","python","",[53,137,138,157,171,260,277,288,324,346,392,421,455,517],{"__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,183,187,190,193,195,198,200,203,206,208,210,213,215,218,220,222,224,226,228,230,232,235,237,240,242,244,246,248,250,252,254,257],{"class":141,"line":173},3,[139,175,176],{"class":149},"Sigma ",[139,178,179],{"class":145},"=",[139,181,182],{"class":149}," [[",[139,184,186],{"class":185},"sDLfK","0.10",[139,188,189],{"class":149},",",[139,191,192],{"class":185},"0.02",[139,194,189],{"class":149},[139,196,197],{"class":185},"0.04",[139,199,189],{"class":149},[139,201,202],{"class":185},"0.00",[139,204,205],{"class":149},"],[",[139,207,192],{"class":185},[139,209,189],{"class":149},[139,211,212],{"class":185},"0.08",[139,214,189],{"class":149},[139,216,217],{"class":185},"0.01",[139,219,189],{"class":149},[139,221,192],{"class":185},[139,223,205],{"class":149},[139,225,197],{"class":185},[139,227,189],{"class":149},[139,229,217],{"class":185},[139,231,189],{"class":149},[139,233,234],{"class":185},"0.12",[139,236,189],{"class":149},[139,238,239],{"class":185},"0.03",[139,241,205],{"class":149},[139,243,202],{"class":185},[139,245,189],{"class":149},[139,247,192],{"class":185},[139,249,189],{"class":149},[139,251,239],{"class":185},[139,253,189],{"class":149},[139,255,256],{"class":185},"0.06",[139,258,259],{"class":149},"]]\n",[139,261,263,266,268,271,274],{"class":141,"line":262},4,[139,264,265],{"class":149},"N, K ",[139,267,179],{"class":145},[139,269,270],{"class":185}," 4",[139,272,273],{"class":149},", ",[139,275,276],{"class":185},"2\n",[139,278,280,283,285],{"class":141,"line":279},5,[139,281,282],{"class":149},"m ",[139,284,179],{"class":145},[139,286,287],{"class":149}," pyo.ConcreteModel()\n",[139,289,291,294,296,299,302,305,309,311,314,317,319,321],{"class":141,"line":290},6,[139,292,293],{"class":149},"m.w ",[139,295,179],{"class":145},[139,297,298],{"class":149}," pyo.Var(",[139,300,301],{"class":185},"range",[139,303,304],{"class":149},"(N), ",[139,306,308],{"class":307},"s9osk","bounds",[139,310,179],{"class":145},[139,312,313],{"class":149},"(",[139,315,316],{"class":185},"0",[139,318,273],{"class":149},[139,320,99],{"class":185},[139,322,323],{"class":149},"))\n",[139,325,327,330,332,334,336,338,341,343],{"class":141,"line":326},7,[139,328,329],{"class":149},"m.d ",[139,331,179],{"class":145},[139,333,298],{"class":149},[139,335,301],{"class":185},[139,337,304],{"class":149},[139,339,340],{"class":307},"domain",[139,342,179],{"class":145},[139,344,345],{"class":149},"pyo.Binary)\n",[139,347,349,352,354,357,360,362,365,368,371,374,377,380,383,386,389],{"class":141,"line":348},8,[139,350,351],{"class":149},"m.budget ",[139,353,179],{"class":145},[139,355,356],{"class":149}," pyo.Constraint(",[139,358,359],{"class":307},"expr",[139,361,179],{"class":145},[139,363,364],{"class":185},"sum",[139,366,367],{"class":149},"(m.w[i] ",[139,369,370],{"class":145},"for",[139,372,373],{"class":149}," i ",[139,375,376],{"class":145},"in",[139,378,379],{"class":185}," range",[139,381,382],{"class":149},"(N)) ",[139,384,385],{"class":145},"==",[139,387,388],{"class":185}," 1",[139,390,391],{"class":149},")\n",[139,393,395,398,400,402,404,406,409,412,415,418],{"class":141,"line":394},9,[139,396,397],{"class":149},"m.link ",[139,399,179],{"class":145},[139,401,356],{"class":149},[139,403,301],{"class":185},[139,405,304],{"class":149},[139,407,408],{"class":307},"rule",[139,410,411],{"class":145},"=lambda",[139,413,414],{"class":149}," m, i: m.w[i] ",[139,416,417],{"class":145},"\u003C=",[139,419,420],{"class":149}," m.d[i])\n",[139,422,424,427,429,431,433,435,437,440,442,444,446,448,450,452],{"class":141,"line":423},10,[139,425,426],{"class":149},"m.card ",[139,428,179],{"class":145},[139,430,356],{"class":149},[139,432,359],{"class":307},[139,434,179],{"class":145},[139,436,364],{"class":185},[139,438,439],{"class":149},"(m.d[i] ",[139,441,370],{"class":145},[139,443,373],{"class":149},[139,445,376],{"class":145},[139,447,379],{"class":185},[139,449,382],{"class":149},[139,451,417],{"class":145},[139,453,454],{"class":149}," K)\n",[139,456,458,461,463,466,468,470,472,475,478,481,483,486,488,490,492,494,497,499,502,504,506,509,512,514],{"class":141,"line":457},11,[139,459,460],{"class":149},"m.obj ",[139,462,179],{"class":145},[139,464,465],{"class":149}," pyo.Objective(",[139,467,359],{"class":307},[139,469,179],{"class":145},[139,471,364],{"class":185},[139,473,474],{"class":149},"(Sigma[i][j]",[139,476,477],{"class":145},"*",[139,479,480],{"class":149},"m.w[i]",[139,482,477],{"class":145},[139,484,485],{"class":149},"m.w[j] ",[139,487,370],{"class":145},[139,489,373],{"class":149},[139,491,376],{"class":145},[139,493,379],{"class":185},[139,495,496],{"class":149},"(N) ",[139,498,370],{"class":145},[139,500,501],{"class":149}," j ",[139,503,376],{"class":145},[139,505,379],{"class":185},[139,507,508],{"class":149},"(N)), ",[139,510,511],{"class":307},"sense",[139,513,179],{"class":145},[139,515,516],{"class":149},"pyo.minimize)\n",[139,518,520,523,526,530],{"class":141,"line":519},12,[139,521,522],{"class":185},"print",[139,524,525],{"class":149},"(Client(",[139,527,529],{"class":528},"sU2Wk","\"https:\u002F\u002Ftry.quicoptapi.pgi.fz-juelich.de\"",[139,531,532],{"class":149},").solve(m).display)\n",[534,535],"term-result",{":rows":536,"cmd":537},"[\"├── status:     optimal\",\"├── feasible:   true\",\"├── objective:  0.037500218891769396\",\"├── x:          x1=0.3738, x2=0, x3=0, x4=0.6262, x5=1, x6=0, …  (8 variables)\",\"└── solve_time: 0.1888 s\"]","$ python cardinality_portfolio.py",[39,539,541],{"id":540},"what-you-get","What you get",[14,543,544,545,548,549,552,553,556,557,273,560,563,564,567,568,571,572,575],{},"The least-risk two-asset portfolio holds assets ",[18,546,547],{},"1 and 4"," — weights ",[53,550,551],{},"0.37"," and\n",[53,554,555],{},"0.63"," (",[53,558,559],{},"x1",[53,561,562],{},"x4","), with the two hold flags on (",[53,565,566],{},"x5=1",", and the flag for asset 4\nset) and assets 2 and 3 dropped. The variance is ",[53,569,570],{},"0.0375"," (the ",[53,573,574],{},"…218"," tail is\nfloating point). Even though only two assets are held, this beats every other\npair and every single-asset portfolio.",[14,577,578,581,582,585,586,589,590,593],{},[53,579,580],{},"status: optimal"," means quicopt ",[18,583,584],{},"proved"," it — over all ",[53,587,588],{},"C(4,1)+C(4,2)"," ways to\nchoose the held assets ",[58,591,592],{},"and"," every continuous weighting of each, no portfolio has\nlower variance. It picks the discrete subset and the weights in one solve,\nbecause the binary hold\u002Fskip decisions and the quadratic variance are handled\ntogether rather than as a relaxation.",[14,595,596,597,599],{},"The same model scales from four assets to hundreds — change the covariance matrix\nand ",[53,598,55],{},", not the method. (This is an illustrative optimization example, not\nfinancial advice.)",[39,601,603],{"id":602},"next","Next",[605,606,607,615,621],"ul",{},[608,609,610,611],"li",{},"The problem class behind this: ",[47,612,614],{"href":613},"\u002Fproblems\u002Fminlp","Mixed-integer nonlinear (MINLP)",[608,616,617,618],{},"The unconstrained cousin: ",[47,619,620],{"href":49},"Portfolio optimization (QP)",[608,622,623,624],{},"A runnable model for every supported class: ",[47,625,627],{"href":626},"\u002Fdeveloper\u002Fexamples","Examples",[14,629,630,633,634,637],{},[18,631,632],{},"Reference:"," T.-J. Chang, N. Meade, J. E. Beasley, Y. M. Sharaiha,\n",[58,635,636],{},"Heuristics for cardinality constrained portfolio optimisation",", Computers &\nOperations Research, 2000.",[639,640],"contact-cta",{"sub":641,"title":642},"Tell us what you're optimizing. We'll help you model it and point you at the right approach.","A bigger portfolio — or a different problem?",[644,645,646],"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 .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":648},[649,650,651,652],{"id":41,"depth":159,"text":42},{"id":84,"depth":159,"text":85},{"id":540,"depth":159,"text":541},{"id":602,"depth":159,"text":603},"Choose minimum-variance asset weights while holding at most K assets in Python — cardinality-constrained portfolio optimization as a mixed-integer quadratic program (MINLP), solved to proven optimality with quicopt instead of enumerating every subset.","md",[656,659,662,665],{"q":657,"a":658},"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":660,"a":661},"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 — a quadratic MINLP. quicopt solves it directly, no convex relaxation.",{"q":663,"a":664},"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":666,"a":667},"Is quicopt free to use?","Yes — pip install quicopt and your first call sets up a free key, no license.",{},true,"\u002Fdeveloper\u002Fguides\u002Fcardinality-portfolio",{"title":5,"description":653},"developer\u002Fguides\u002Fcardinality-portfolio","MlEvSm3aEa15ocm2HU7pt1xCSVuhVnhvphqljgZltc0",1784110685996]