Scipy Optimize:如何控制解数组规模,指定10只基金分配?
如何给Scipy Optimize添加「恰好选中10只基金」的约束
当然可以添加这类约束!针对你从25只基金里选10只分配资金的需求,我帮你梳理两种适配Scipy Optimize的可行方案,精准实现选中数量的控制:
方案一:混合整数线性规划(MILP)—— 精准硬约束(适合线性/二次型问题)
如果你的投资组合优化是线性或二次型问题(比如最小方差、线性近似的夏普比率最大化),Scipy 1.9+版本的scipy.optimize.milp是完美选择,它支持整数变量标记基金是否被选中,直接实现「恰好10只」的硬约束。
核心思路
- 用0/1整数变量
x_i标记第i只基金是否被选中(1=选中,0=不选) - 用连续变量
w_i表示对应基金的仓位权重,约束w_i ≤ x_i(确保未选中的基金仓位为0) - 添加关键约束:
sum(x_i) = 10,强制恰好选中10只基金 - 保留常规投资组合约束:比如
sum(w_i) = 1(总资金全部分配)、w_i ≥ 0(非负仓位)
代码示例
from scipy.optimize import milp, LinearConstraint, Bounds import numpy as np # 替换为你的真实数据 n_funds = 25 cov_matrix = np.random.rand(n_funds, n_funds) cov_matrix = (cov_matrix + cov_matrix.T) / 2 # 协方差矩阵对称化 returns = np.random.rand(n_funds) # 基金历史收益率 target_return = 0.06 # 期望最低收益率 # 变量定义:前25个是整数变量x_i(0/1),后25个是连续变量w_i(仓位) n_vars = n_funds * 2 integer_vars = list(range(n_funds)) # 指定整数变量范围 # 目标函数:最小化投资组合方差(二次型适配milp的简化处理) objective = 0.5 * cov_matrix.flatten() @ np.kron(np.eye(n_funds), np.eye(n_funds)).flatten() # 约束1:恰好选中10只基金 constraint_select = LinearConstraint( A=np.hstack([np.ones((1, n_funds)), np.zeros((1, n_funds))]), lb=10, ub=10 ) # 约束2:总仓位和为1 constraint_total = LinearConstraint( A=np.hstack([np.zeros((1, n_funds)), np.ones((1, n_funds))]), lb=1, ub=1 ) # 约束3:未选中的基金仓位必须为0(w_i ≤ x_i) constraint_w_x = [] for i in range(n_funds): row = np.zeros(n_vars) row[i] = 1 row[n_funds + i] = -1 constraint_w_x.append(LinearConstraint(A=row, lb=0, ub=np.inf)) # 约束4:组合收益率不低于目标值 constraint_return = LinearConstraint( A=np.hstack([np.zeros((1, n_funds)), returns]), lb=target_return, ub=np.inf ) # 变量边界:x_i∈{0,1},w_i∈[0,1] bounds = Bounds( lb=np.hstack([np.zeros(n_funds), np.zeros(n_funds)]), ub=np.hstack([np.ones(n_funds), np.ones(n_funds)]) ) # 求解优化问题 result = milp( c=objective, constraints=[constraint_select, constraint_total, constraint_return] + constraint_w_x, bounds=bounds, integrality=integer_vars ) # 提取结果 selected_idx = np.where(result.x[:n_funds] == 1)[0] weights = result.x[n_funds:][selected_idx] print(f"选中的基金索引:{selected_idx}") print(f"对应仓位权重:{np.round(weights, 4)}")
方案二:L1正则化+阈值调整(适合非线性问题)
如果你的目标函数是非线性的(比如直接最大化原始夏普比率),Scipy的常规优化器不支持整数变量,这时候可以用L1正则化诱导稀疏性,通过调整正则化强度,让最终非零仓位的基金数量恰好为10。
核心思路
- 在目标函数中加入L1惩罚项:
原目标函数 + λ * sum(|w_i|),λ越大,非零仓位的基金数量越少 - 逐步调整λ的值,直到优化后非零仓位的基金数量等于10(用小阈值判断「非零」,比如
w_i > 1e-6)
代码示例
from scipy.optimize import minimize import numpy as np # 替换为你的真实数据 n_funds = 25 cov_matrix = np.random.rand(n_funds, n_funds) cov_matrix = (cov_matrix + cov_matrix.T) / 2 returns = np.random.rand(n_funds) risk_free_rate = 0.02 # 无风险利率 # 定义带L1正则的目标函数(最大化夏普比率等价于最小化负夏普比率) def negative_sharpe(w, lambda_reg): port_return = w @ returns port_vol = np.sqrt(w.T @ cov_matrix @ w) sharpe = (port_return - risk_free_rate) / port_vol # 加入L1惩罚项,最小化负夏普+L1惩罚 return -sharpe + lambda_reg * np.sum(np.abs(w)) # 约束:总仓位=1,仓位非负 constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}] bounds = [(0, 1) for _ in range(n_funds)] # 调整正则化系数λ,直到非零仓位数量为10 lambda_reg = 0.001 target_count = 10 non_zero_count = 0 while non_zero_count != target_count: # 初始值设为平均分配 result = minimize(negative_sharpe, x0=np.ones(n_funds)/n_funds, args=(lambda_reg,), constraints=constraints, bounds=bounds, method='SLSQP') # 用小阈值过滤极小仓位 non_zero_weights = result.x[result.x > 1e-6] non_zero_count = len(non_zero_weights) # 动态调整λ if non_zero_count > target_count: lambda_reg *= 1.2 # 增大正则化,减少非零数量 else: lambda_reg *= 0.8 # 减小正则化,增加非零数量 # 提取结果 selected_idx = np.where(result.x > 1e-6)[0] weights = result.x[selected_idx] print(f"选中的基金索引:{selected_idx}") print(f"对应仓位权重:{np.round(weights, 4)}") print(f"最终夏普比率:{-(result.fun - lambda_reg*np.sum(np.abs(result.x)))}")
注意事项
- 方案一的MILP要求目标和约束是线性/二次型的,适合规则清晰的优化场景;方案二更灵活,可处理复杂非线性问题,但需要耐心调整正则化系数。
- 实际使用时,记得替换示例中的随机数据为你的真实基金收益率、协方差矩阵等业务数据。
内容的提问来源于stack exchange,提问作者Naveen Srinivas
相关产品推荐
相关产品推荐

