第 9 章 · 02 Portfolio 组合构建与优化器 ★ 本节摘要:本节是全书量化含量最高的一节,钻取 Framework 五层最复杂的一层——Portfolio 组合构建。 把 Alpha 层的"方向预测"转成"每只股票具体持仓多少股"。简单实现如 (等权,1/N);复杂实现把 Insight 的预测喂给独立的组合优化器( )求解最优权重——Black-Litterman、Markowitz 均值方差、最大夏普、最小方差、风险平价、UnconstrainedMeanVariance 等经典量化模型齐备,优化器内部用 Accord.NET / MathNet.Numerics 做二次规划求解。
本节摘要:本节是全书量化含量最高的一节,钻取 Framework 五层最复杂的一层——Portfolio 组合构建。
IPortfolioConstructionModel.CreateTargets(QCAlgorithm, Insight[])→IEnumerable<IPortfolioTarget>把 Alpha 层的"方向预测"转成"每只股票具体持仓多少股"。简单实现如EqualWeightingPortfolioConstructionModel(等权,1/N);复杂实现把 Insight 的预测喂给独立的组合优化器(IPortfolioOptimizer)求解最优权重——Black-Litterman、Markowitz 均值方差、最大夏普、最小方差、风险平价、UnconstrainedMeanVariance 等经典量化模型齐备,优化器内部用 Accord.NET / MathNet.Numerics 做二次规划求解。本节还会讲 Markowitz 的收益-风险权衡有效前沿理论,以及PortfolioOptimizerPythonWrapper怎么让用户用 Python 写优化器接入 C# 引擎。读完本节,你将真正理解 Lean 为什么被称为"专业级量化引擎"。
内容来源:原项目源码
Algorithm.Framework/Portfolio/(11 个组合模型 + 4 个优化器 + Python 包装)、Algorithm/Portfolio/IPortfolioConstructionModel.cs,精读并套用体系化模板。
⚠️ 注意:本节涉及量化金融的核心数学(协方差、二次规划、有效前沿)。读者需有"组合优化是优化问题:目标函数 + 约束 + 求解器"的基本认识。代码里大量使用 Accord.Math 的矩阵/约束 API,看不懂 Accord 没关系,关注目标函数和约束即可。
阅读完本节,你应当能够:
IPortfolioConstructionModel.CreateTargets 怎么把 Insight[] 转成 IPortfolioTarget[]。IPortfolioConstructionModel)与"组合优化器"(IPortfolioOptimizer)两个抽象层级。EqualWeighting / InsightWeighting / ConfidenceWeighted / AccumulativeInsight 四种简单权重模型的差别。PortfolioOptimizerPythonWrapper 怎么让 Python 优化器接入。Algorithm/Portfolio/IPortfolioConstructionModel.cs:24-33:
24 public interface IPortfolioConstructionModel : INotifiedSecurityChanges 25 { 32 IEnumerable<IPortfolioTarget> CreateTargets(QCAlgorithm algorithm, Insight[] insights); 33 }
输入是 Alpha 层产出的 Insight[](见上一节),输出是 IPortfolioTarget[]。IPortfolioTarget 极简——只有 Symbol 和 Quantity(目标持仓股数):
public interface IPortfolioTarget { Symbol Symbol { get; } decimal Quantity { get; } }
Portfolio 层的职责:把"方向预测"(Insight.Direction)翻译成"具体几股"(Quantity)。这一步的策略空间极大——可以是简单的"有 N 个 Up 信号就每只买总资金的 1/N",也可以是"用协方差矩阵求最大夏普权重"。
Lean 把 Portfolio 层拆成两个抽象:
IPortfolioConstructionModel(组合构建模型):决定"什么时候调仓、把哪些 Insight 送进优化器、把优化结果乘上总资金转成 Quantity"。这一层负责"框架"。IPortfolioOptimizer(组合优化器):决定"给定历史收益矩阵和预期收益,怎么算最优权重向量"。这一层负责"数学"。IPortfolioOptimizer 的接口极简:
public interface IPortfolioOptimizer { double[] Optimize(double[,] historicalReturns, double[] expectedReturns = null, double[,] covariance = null); }
输入是历史收益矩阵(K 期 × N 资产)、预期收益向量(N)、协方差矩阵(N × N),输出是权重向量(N)。优化器不关心 Symbol、不关心 Insight,只关心纯数学的"给我收益数据,返回权重"。这种数学与业务的彻底分离是 Lean 设计的精妙——研究员可以独立替换优化器,不影响策略框架。
四个简单模型用启发式权重,不调用优化器:
EqualWeightingPortfolioConstructionModel.cs:118-132 核心方法 DetermineTargetPercent:
118 protected override Dictionary<Insight, double> DetermineTargetPercent(List<Insight> activeInsights) 119 { 120 var result = new Dictionary<Insight, double>(activeInsights.Count); 123 var count = activeInsights.Count(x => x.Direction != InsightDirection.Flat && RespectPortfolioBias(x)); 124 var percent = count == 0 ? 0 : 1m / count; // 每只 1/N 125 foreach (var insight in activeInsights) 126 { 127 result[insight] = 128 (double)((int)(RespectPortfolioBias(insight) ? insight.Direction : InsightDirection.Flat) 129 * percent); // 方向 × 权重(空信号 -1/N) 130 } 131 return result; 132 }
逻辑极简:有 N 个非 Flat 信号就每只分配 1/N,Down 信号取负(insight.Direction 是 -1,乘出来是 -1/N)。基类 PortfolioConstructionModel 把百分比乘以总资金转成 IPortfolioTarget 的 Quantity,然后 CreateTargets 输出。
RespectPortfolioBias 处理多空偏好(PortfolioBias.LongShort / Long / Short)——Long 模式下 Down 信号会被改成 Flat。
| 模型 | 权重来源 |
|---|---|
InsightWeightingPortfolioConstructionModel |
直接用 Insight.Weight 字段 |
ConfidenceWeightedPortfolioConstructionModel |
按 Insight.Confidence 归一化加权 |
AccumulativeInsightPortfolioConstructionModel |
同一标的多个未过期 Insight 累积计算 |
MeanReversionPortfolioConstructionModel |
偏离均值的反转加权 |
SectorWeightingPortfolioConstructionModel |
按行业先均分再行业内等权 |
这些都不需要数学优化器,只做"信号 → 权重"的简单映射。
BlackLittermanOptimizationPortfolioConstructionModel / MeanVarianceOptimizationPortfolioConstructionModel / MaximumSharpeRatioPortfolioConstructionModel / RiskParityPortfolioConstructionModel 这四个组合模型都内部持有一个 IPortfolioOptimizer 实例。组合模型负责:
algorithm.History)算每只标的的收益矩阵和协方差矩阵。expectedReturns)。optimizer.Optimize(历史收益, 预期收益, 协方差) 得到权重向量。IPortfolioTarget[] 输出。理解优化器需要先回顾 1952 年马科维茨的均值方差理论。核心命题:给定 N 个资产的预期收益向量 μ 和协方差矩阵 Σ,求权重向量 w,使得组合在给定预期收益下风险最小(或等价地,在给定风险下收益最大)。
组合的预期收益:E[R] = μᵀw
组合的方差(风险):σ² = wᵀΣw
有效前沿:在(风险, 收益)平面上,所有"在某个风险水平下收益最大"的组合连成的曲线。前沿上的组合都是"最优"的——再找不到同风险更高收益或同收益更低风险的组合。
四个优化器对应不同目标:
| 优化器 | 优化目标 | 数学形式 |
|---|---|---|
MinimumVariancePortfolioOptimizer |
全局最小方差 | min wᵀΣw s.t. 1ᵀw=1 |
UnconstrainedMeanVarianceOptimizer |
无约束均值方差 | min wᵀΣw - λμᵀw |
MaximumSharpeRatioPortfolioOptimizer |
最大夏普比率 | max (μ-r_f)ᵀw / √(wᵀΣw) |
RiskParityPortfolioOptimizer |
风险平价(各资产风险贡献相等) | 各资产边际风险贡献相等 |
最大夏普优化器是数学最复杂的一个。MaximumSharpeRatioPortfolioOptimizer.cs:89-138 的 Optimize 方法:
89 public double[] Optimize(double[,] historicalReturns, double[] expectedReturns = null, double[,] covariance = null) 90 { 91 covariance = covariance ?? historicalReturns.Covariance(); 92 var returns = (expectedReturns ?? historicalReturns.Mean(0)).Subtract(_riskFreeRate); 94 var size = covariance.GetLength(0); 95 var equalWeights = Vector.Create(size, 1.0 / size); 99 var feasible = _lower >= 0 ? returns.Any(x => x > 0) : returns.Any(x => x != 0); 100 if (!feasible) return equalWeights; // 不可行退化为等权 105 // Charnes-Cooper 替换 y = κw (κ = 1ᵀy):最大化夏普 106 // (µ − r_f)ᵀw / √(wᵀΣw) 转化为 min yᵀΣy s.t. (µ − r_f)ᵀy = 1 109 var constraints = new List<LinearConstraint> 110 { 111 new LinearConstraint(size) 112 { CombinedAs = returns, ShouldBe = ConstraintType.EqualTo, Value = 1.0 } 116 }; 121 constraints.AddRange(GetBoundaryConditions(size)); // lw ≤ w ≤ up 124 var optfunc = new QuadraticObjectiveFunction(covariance, Vector.Create(size, 0.0)); 125 var solver = new GoldfarbIdnani(optfunc, constraints); 128 var success = solver.Minimize(Vector.Copy(equalWeights)); 129 if (!success) return equalWeights; 135 var y = solver.Solution; 136 var sum = y.Sum(); 137 return sum > 0 ? y.Divide(sum) : equalWeights; // w = y / (1ᵀy) 恢复权重 138 }
夏普比率 S = (μ-r_f)ᵀw / √(wᵀΣw) 是分式规划,直接优化难。L105-106 的 Charnes-Cooper 替换是经典技巧:令 y = κw(其中 κ = 1/(1ᵀy) > 0),把分式目标转成二次目标:
max (μ-r_f)ᵀw / √(wᵀΣw) ⟹ min yᵀΣy s.t. (μ-r_f)ᵀy = 1, lw ≤ w= y/(1ᵀy) ≤ up
求解后用 w = y / (1ᵀy) 恢复权重(L134-137)。这是把"最大夏普"包装成二次规划求解器的标准做法。
L124-128 用 Accord.NET 的 GoldfarbIdnani 算法求解带约束的二次规划——目标函数是二次(yᵀΣy),约束是线性((μ-r_f)ᵀy = 1、上下界)。Goldfarb-Idnani 是经典的对偶 active-set 方法,适合中小规模(N 通常几十到几百)的 QP。
💡 钻取要点:三个细节体现工程素养:(1) L99-103 不可行退化为等权——如果所有预期收益都 ≤ 0(纯空头市场),夏普无解,优雅退化;(2) L129 求解失败退化等权——数值算法可能不收敛,必须有兜底;(3) L137
sum > 0 ? ... : equalWeights——1ᵀy ≤ 0时恢复无意义,也退化。所有数学分支都有退化路径,这是量化代码的基本要求——任何数值异常都不能崩。
MinimumVariancePortfolioOptimizer 目标最简单——min wᵀΣw s.t. 1ᵀw=1, lw≤w≤up,直接用二次规划求组合方差最小化,不需要预期收益。
RiskParityPortfolioOptimizer 实现风险平价——每个资产对组合总风险的边际贡献相等。这意味着低波动资产配更高权重、高波动资产配更低权重,避免被少数高波动资产主导。风险平价桥水基金(Bridgewater)的全天候策略就是这套思路。
UnconstrainedMeanVarianceOptimizer 是经典 Markowitz 无约束版,目标 min wᵀΣw - λμᵀw,λ 是风险厌恶系数。
Lean 通过 Python.NET 支持 Python 写优化器。PortfolioOptimizerPythonWrapper.cs 把一个 Python 对象(实现 optimize(historical_returns, expected_returns=None, covariance=None)→numpy.array 方法)包装成 C# 的 IPortfolioOptimizer。用户可以这样:
# 自定义 Python 优化器 class MyOptimizer: def optimize(self, historical_returns, expected_returns=None, covariance=None): # 自定义权重计算,如等风险贡献、机器学习预测组合 ... return weights # numpy array # 在算法里接入 self.SetPortfolioConstruction( MeanVarianceOptimizationPortfolioConstructionModel( portfolioOptimizer=MyOptimizer() ) )
这种 C# 接口 + Python 实现 的桥接,与第 4 章的 QCAlgorithm.Python.cs、第 7 章的 Python 指标是同一套机制——Python.NET 互操作。研究员可以在熟悉的 Python 生态(numpy/scipy/cvxpy)里实现优化算法,无缝接入 C# 引擎。
QCAlgorithm.Framework.cs 提供装配入口:
public void SetPortfolioConstruction(IPortfolioConstructionModel portfolioConstruction) { PortfolioConstruction = portfolioConstruction; }
策略里典型用法:
public override void Initialize() { SetAlpha(new EmaCrossAlphaModel(12, 26)); // 信号 SetPortfolioConstruction(new BlackLittermanOptimizationPortfolioConstructionModel()); // 优化器组合 }
换优化器只改一行——从等权到 Black-Litterman,从最大夏普到风险平价,组合行为完全不同,但信号(Alpha)和执行(Execution)代码零改动。这就是"可组装策略工程化"的精髓。
IPortfolioConstructionModel.CreateTargets(insights)→IPortfolioTarget[],把方向预测转成具体持仓股数。IPortfolioOptimizer.Optimize(历史收益, 预期收益, 协方差)→权重。IPortfolioOptimizer,用历史数据算协方差,把 Insight 转预期收益。μᵀw、风险 wᵀΣw,有效前沿是"同风险收益最大"组合的曲线。optimize(...) 方法即可接入,Python.NET 桥接,cvxpy/scipy 生态可用。下一节钻取 Framework 最后两层——Execution(执行,把目标持仓拆单成真实订单:VWAP/标准差/价差驱动)与 Risk(风控过滤:最大回撤/移动止损/行业暴露),并完成 BasicTemplateFrameworkAlgorithm 五件套演示。