简介:这份PDF文档面向量化投资研究者、金融工程从业者及希望将大模型引入投研流程的开发者,系统讲解如何以DeepSeek大模型配合多目标优化算法,构建兼顾风险收益平衡的自动调仓策略。全文共240页、50个大章节,支持目录跳转与阅读器书签大纲定位,内容涵盖投资组合优化痛点分析、多目标优化数学模型、帕累托最优解求解、风险因子量化建模、收益目标函数构建,以及遗传算法、粒子群优化与DeepSeek融合的权重迭代方案,并延伸至Prompt工程、Embedding向量化、行业轮动语义分析、协方差矩阵预测优化与过拟合正则化等实操环节。资源包为1个PDF文件,约11.68MB,结构完整、图表清晰,已有131人学习。读者可据此掌握从数据预处理、约束条件生成到调仓决策落地的完整技术链路,适合作为量化策略研发与课程设计的参考范本。
1. 从一份 240 页的方案说起:多目标优化到底在解决什么问题
很多人第一次看到“证券投资组合优化”这几个字,脑子里浮现的是 Excel 里拉一条有效前沿曲线,或者用均值-方差模型算一组权重。但真到了要落地自动调仓这一步,你会发现单目标优化根本不够用——你既想让组合收益尽量高,又想让回撤尽量小,还想让换手率别太离谱,最好行业暴露也别太集中。这几个目标天然打架,调高收益往往意味着放大波动,压低换手又可能错过调仓窗口。多目标优化算法就是用来处理这种“既要又要还要”的局面的,它不给你一个唯一解,而是给出一组互不支配的候选方案,也就是常说的 Pareto 前沿。这份 240 页的方案本质上讲的就是:怎么把风险收益平衡这件事拆成可量化的多个目标,用多目标优化算法求解,再挂上自动调仓的执行链路。适合谁看?手里有量化选股结果、想把组合权重动态管起来的从业者,以及想把“拍脑袋调仓”换成“有约束求解”的工程师。
2. 把投资组合写成多目标优化问题:目标函数与约束怎么定
2.1 三个必须同时进目标函数的东西
单目标优化里最常见的是最大化夏普比率,或者最小化组合方差。但实盘里你会发现,只优化这两个,求解器会给你一堆极端权重——比如某只票占到 40%,或者为了压波动全塞进债券。多目标优化的价值在于把这些“隐性诉求”显式写进目标向量。
我一般会把目标拆成三类:
- 收益类:组合预期收益,或者超额收益。数据来源可以是多因子打分,也可以是分析师一致预期。
- 风险类:组合波动率、最大回撤估计、下行标准差。波动率好算,回撤估计需要模拟路径或者用历史情景。
- 交易类:换手率、交易成本。这一项在自动调仓里权重给低了,求解出来的权重根本没法执行。
写成数学形式,目标向量大致是:
minimize F(w) = [ -μᵀw + λ_cost·TC(w), wᵀΣw, Turnover(w) ] subject to Σw = 1, 0 ≤ w_i ≤ u_i, 行业暴露约束这里 μ 是预期收益向量,Σ 是协方差矩阵,TC 是交易成本函数,Turnover 是换手率。注意收益项前面加了负号,因为优化器默认是最小化。
2.2 约束条件比目标函数更容易翻车
目标函数决定解的质量,约束条件决定解能不能用。我见过太多人把约束写得太松,求解器给出一个数学上最优但交易台上根本下不出去的权重。
常见约束分四层:
| 约束类型 | 典型写法 | 作用 |
|---|---|---|
| 权重边界 | 0 ≤ w_i ≤ 0.1 | 防止单票过度集中 |
| 预算约束 | Σw = 1 | 保证满仓 |
| 行业暴露 | Σ_{i∈行业k} w_i - benchmark_k | |
| 换手约束 | Σ | w_i - w_i_old |
行业暴露约束是最容易被忽略的。如果你只做权重边界,求解器可能把 30% 仓位塞进同一个行业,回撤起来根本扛不住。换手约束则直接决定调仓频率——设得太松,每次调仓成本吃掉超额收益;设得太紧,组合跟不上信号变化。
2.3 用 Python 把问题搭起来的最小骨架
下面这段代码用pymoo搭一个多目标优化的最小可运行框架。pymoo是常用的多目标优化库,支持 NSGA-II、MOEA/D 等算法。
import numpy as np from pymoo.core.problem import Problem from pymoo.algorithms.moo.nsga2 import NSGA2 from pymoo.optimize import minimize class PortfolioProblem(Problem): def __init__(self, mu, sigma, w_old, max_weight=0.1): self.mu = mu # 预期收益向量 (n,) self.sigma = sigma # 协方差矩阵 (n, n) self.w_old = w_old # 当前权重 (n,) self.n = len(mu) super().__init__( n_var=self.n, n_obj=3, # 三个目标:收益、风险、换手 n_constr=2, # 两个约束:预算、换手上限 xl=0.0, xu=max_weight ) def _evaluate(self, X, out, *args, **kwargs): # X shape: (pop_size, n_var) ret = -X @ self.mu # 目标1:负收益(最小化) risk = np.array([x @ self.sigma @ x for x in X]) # 目标2:组合方差 turnover = np.abs(X - self.w_old).sum(axis=1) # 目标3:换手率 out["F"] = np.column_stack([ret, risk, turnover]) # 约束1:预算约束 Σw = 1,写成 g <= 0 形式 g1 = np.abs(X.sum(axis=1) - 1.0) - 1e-6 # 约束2:换手上限 0.3 g2 = turnover - 0.3 out["G"] = np.column_stack([g1, g2]) # 模拟数据:10 只标的 np.random.seed(42) n = 10 mu = np.random.uniform(0.05, 0.20, n) A = np.random.randn(n, n) sigma = A @ A.T * 0.01 w_old = np.ones(n) / n problem = PortfolioProblem(mu, sigma, w_old) algorithm = NSGA2(pop_size=100) res = minimize(problem, algorithm, ('n_gen', 200), verbose=False) print("Pareto 前沿解数量:", len(res.F)) print("第一个解的权重:", res.X[0].round(4))这段代码的逻辑说明:_evaluate是优化器每次迭代调用的函数,输入是种群矩阵X,输出是目标值F和约束违反量G。目标值三列分别对应负收益、方差、换手率。约束写成g <= 0的形式,g1是预算约束的松弛量,g2是换手率超出 0.3 的部分。
参数说明:pop_size控制种群大小,太小容易陷入局部前沿,太大会拖慢求解;n_gen是迭代代数,200 代在 10 只标的的场景下通常够用,标的数量到 50 以上建议加到 500。max_weight是单票上限,实盘里一般设 5% 到 10%,取决于组合分散度要求。
跑完之后res.F是 Pareto 前沿上的目标值矩阵,res.X是对应的权重矩阵。你需要从这几十个解里挑一个执行——挑法在下一章讲。
3. 从 Pareto 前沿到可执行权重:选解、平滑与调仓节奏
3.1 多目标解怎么挑:三种实用策略
求解器给你 50 到 100 个互不支配的解,每个都“数学上合理”,但实盘只能下一个。挑解这件事没有标准答案,但有三种常用策略。
第一种是加权求和法。给三个目标各配一个权重,算综合得分,取最高分。比如收益权重 0.5、风险权重 0.3、换手权重 0.2。缺点是权重怎么定很主观,而且不同市场环境下最优权重会漂移。
第二种是约束法。把其中两个目标转成约束,只优化剩下的一个。比如要求换手率不超过 0.15、方差不超过某个阈值,然后最大化收益。这种做法在实盘里更常见,因为约束的物理意义清晰——换手率上限对应交易成本预算,方差上限对应风控要求。
第三种是拐点法。在 Pareto 前沿上找曲率最大的点,也就是“再让一点收益需要付出很大风险”的那个位置。这个点通常被认为是性价比最高的解。实现上可以对前沿做归一化,然后算相邻点之间的斜率变化。
我一般用第二种为主、第三种为辅。先用换手和风险约束把解空间压到可执行范围,再在剩余解里找拐点。
3.2 权重平滑:为什么求解结果不能直接下单
即使你挑了一个换手率看起来不高的解,直接拿它下单仍然可能翻车。原因是多目标优化每次求解的结果会有微小抖动——今天求解器给你的权重是 [0.08, 0.07, 0.09, ...],明天可能变成 [0.07, 0.08, 0.08, ...]。这种抖动在数学上无所谓,但交易成本会累积。
常见做法是加一层指数平滑:
def smooth_weights(w_new, w_prev, alpha=0.3): """ 指数平滑权重,alpha 越大越贴近新解 w_new: 本次求解权重 w_prev: 上期实际持仓权重 """ w_smoothed = alpha * w_new + (1 - alpha) * w_prev # 重新归一化,保证满仓 w_smoothed = w_smoothed / w_smoothed.sum() return w_smoothedalpha的取值很关键。设 0.3 意味着每次只执行 30% 的调整幅度,适合信号变化慢的策略;设 0.7 以上适合高频调仓场景。但注意,平滑之后权重可能违反单票上限约束,需要再做一次截断和归一化。
3.3 调仓节奏:定期调还是触发调
自动调仓的触发方式有两种:定期调仓和条件触发。
定期调仓就是每周或每月固定时间跑一次优化,执行权重调整。优点是节奏稳定,交易成本可预期;缺点是市场剧烈变化时反应慢。
条件触发是当某个指标超过阈值时才调仓,比如组合实际波动率超过目标波动率的 1.2 倍,或者某只票权重因为价格变化偏离目标超过 2%。优点是及时,缺点是可能频繁触发,交易成本失控。
实盘里常见的是两者结合:定期调仓为主,条件触发作为补充。比如每月第一个交易日定期调仓,同时设置一个“单票偏离超过 3% 则触发临时调仓”的规则。
调仓节奏还和策略容量有关。组合规模越大,冲击成本越高,调仓频率就应该越低。一个粗略的经验:规模在千万级别,月度调仓比较合适;规模到亿级别,季度调仓加上小幅度调整更稳妥。
4. 避坑与排查:多目标组合优化里最容易翻车的五件事
4.1 协方差矩阵估计不准,求解结果全是噪声
现象:求解器给出的权重每次变化很大,Pareto 前沿上的解分布很散,换手率目标值忽高忽低。
原因:协方差矩阵用短窗口历史数据估计,样本量不够,矩阵接近奇异。优化器对协方差的微小扰动非常敏感,导致权重解不稳定。
解决:用收缩估计(Ledoit-Wolf 收缩)或者因子模型降维。常见做法是用 252 个交易日的数据算样本协方差,再向对角矩阵收缩。sklearn.covariance.LedoitWolf可以直接用。如果标的数量超过 50,建议先做因子分析,把协方差矩阵的秩降下来。
4.2 换手率约束设得太松,交易成本吃掉全部超额
现象:回测曲线好看,实盘跑三个月发现收益被交易成本磨平。
原因:优化时只约束了换手率上限,但没有把交易成本写进目标函数。求解器倾向于选那些“收益略高但换手很大”的解。
解决:把交易成本作为独立目标或者惩罚项加进目标函数。A 股场景下,单边交易成本大约在万分之五到千分之三之间,取决于佣金和冲击成本假设。换手率约束建议设在 0.15 到 0.25 之间,对应月度调仓的合理水平。
4.3 行业暴露约束写成等式,求解器直接无解
现象:求解器报 infeasible,或者返回的解全部违反约束。
原因:行业暴露约束写成了等式,比如要求每个行业权重严格等于基准权重。但组合权重是连续变量,加上单票上限和预算约束,等式约束很容易让可行域变成空集。
解决:行业暴露用不等式约束,允许一定偏离。比如 |行业权重 - 基准权重| ≤ 3%。如果确实需要严格控制行业暴露,可以把行业权重作为硬约束,但要把单票上限放宽,给求解器留出可行空间。
4.4 平滑后的权重违反约束,下单被风控拦截
现象:平滑后的权重里某只票超过 10% 上限,或者行业暴露超标。
原因:指数平滑是在无约束空间做的,平滑之后没有重新投影到可行域。
解决:平滑之后加一步“投影”操作——先截断到单票上限,再重新归一化,然后检查行业暴露,如果超标就按比例缩减超配行业的权重。这个投影过程可能需要迭代两到三次才能收敛。
4.5 用历史回测选参数,实盘表现差一大截
现象:回测时夏普 2.0,实盘跑半年夏普不到 0.5。
原因:多目标优化的参数(目标权重、约束阈值、平滑系数)在回测里被过度拟合了。尤其是 Pareto 前沿选解的策略,如果在回测里反复调,很容易过拟合。
解决:参数选择要用滚动窗口或者样本外验证。具体做法是:用前 3 年数据优化参数,后 1 年数据验证,滚动前进。另外,参数不要设得太精细——平滑系数从 0.3 调到 0.35 对结果影响很小,但过拟合风险很大。宁可粗粒度调参,也不要追求回测曲线完美。
5. 进阶技巧:用情景分析给 Pareto 前沿加一层“后悔药”
多目标优化给的是当前时点的最优解集,但市场环境会变。今天选的解,下个月可能就不合适了。一个实用的进阶做法是:在求解之前,先做情景分析,把不同市场情景下的目标函数参数分别设定,然后看 Pareto 前沿在不同情景下的位置变化。
具体操作分三步。
第一步,定义三到四个市场情景。比如“利率上行 + 成长股承压”“流动性宽松 + 小盘活跃”“震荡市 + 低波动占优”。每个情景对应一组不同的预期收益和协方差假设。预期收益可以用因子模型在不同情景下的条件期望来估计,协方差可以用历史相似区间的数据。
第二步,在每个情景下分别跑多目标优化,得到各自的 Pareto 前沿。然后对比这些前沿的位置——如果某个解在多个情景下都靠近前沿,说明它稳健性较好。
def scenario_analysis(scenarios, base_problem): """ scenarios: dict, key 是情景名, value 是 (mu, sigma) 元组 返回每个情景下的 Pareto 前沿 """ results = {} for name, (mu_s, sigma_s) in scenarios.items(): problem = PortfolioProblem(mu_s, sigma_s, base_problem.w_old) res = minimize(problem, NSGA2(pop_size=100), ('n_gen', 200), verbose=False) results[name] = { 'F': res.F, 'X': res.X } return results # 模拟两个情景 scenarios = { '利率上行': (mu * 0.8, sigma * 1.5), '流动性宽松': (mu * 1.2, sigma * 0.9) } results = scenario_analysis(scenarios, problem) for name, r in results.items(): print(f"{name} 前沿解数量: {len(r['F'])}")第三步,从多情景结果里挑“稳健解”。做法是:对每个情景的 Pareto 前沿做归一化,然后找那些在多个情景下都位于前沿附近的权重向量。具体可以用一个简单的评分:对每个候选解,计算它在每个情景下的目标值排名,取平均排名,选平均排名最高的。
这个做法相当于给 Pareto 前沿加了一层“后悔药”——你不是在赌某一个情景,而是在找一组在不同情景下都不会太差的权重。代价是单情景下的最优性会打折扣,但实盘里稳健性比最优性重要得多。
我自己的习惯是:每季度做一次情景分析,更新情景假设,然后重新跑多情景优化。如果某个解在连续两个季度的多情景分析里都排在前 20%,就把它作为核心持仓的基准权重。这个做法不复杂,但能避开很多“回测漂亮、实盘翻车”的坑。希望帮到你。
本文还有配套的精品资源,点击获取