第 9 章 · 01 连续权重转整数股数 本节摘要:前面所有优化器输出的都是「连续权重」(如 AAPL: 0.2347),但真实交易要买「整数股」——你不能买 0.37 股 AAPL。这一步「从数学权重到实际下单」的鸿沟,正是 PyPortfolioOpt 的 类要填的。本节讲清 DiscreteAllocation 的构造、输入(权重 + 最新价格 + 总预算)和输出(整数股 + 剩余资金),让你理解为什么这一步处理不好、前面优化得再精也会失真。读完本节,你能把任意优化器的权重转成「买几股、剩多少钱」。 内容来源:源码 、文档 ,汉化并套用体系化模板。 学习目标 阅读完本节,你应当能够: 用 DiscreteAllocation 把连续权重转整数股数。
本节摘要:前面所有优化器输出的都是「连续权重」(如 AAPL: 0.2347),但真实交易要买「整数股」——你不能买 0.37 股 AAPL。这一步「从数学权重到实际下单」的鸿沟,正是 PyPortfolioOpt 的
DiscreteAllocation类要填的。本节讲清 DiscreteAllocation 的构造、输入(权重 + 最新价格 + 总预算)和输出(整数股 + 剩余资金),让你理解为什么这一步处理不好、前面优化得再精也会失真。读完本节,你能把任意优化器的权重转成「买几股、剩多少钱」。
内容来源:源码
pypfopt/discrete_allocation.py、文档docs/Postprocessing.rst,汉化并套用体系化模板。
阅读完本节,你应当能够:
均值-方差、BL、HRP 输出的 weights = {"AAPL": 0.2347, "MSFT": 0.1823, ...} 都是「比例」,加起来等于 1。但实盘下单时:
12.35 股不存在。券商只接受整数股(美股),A 股还要按 100 股一手。所以必须做取舍:
如果直接「四舍五入」,有的资产会超买,资金不够;如果统一「向下取整」,会大量剩余资金,实际权重严重偏离最优。DiscreteAllocation 提供两种系统化方法处理这个矛盾。
💡 核心心法:离散化是「在整数约束下逼近连续最优」的优化问题。简单四舍五入是最差的方案——既可能超预算,又偏离权重。系统化方法(greedy / lp)能让你「资金利用率最高 + 权重偏差最小」。
from pypfopt import DiscreteAllocation da = DiscreteAllocation( weights=weights, # {ticker: weight} 字典 latest_prices=latest_prices, # pd.Series,{ticker: price} total_portfolio_value=10000, # 总预算(USD),默认 10000 short_ratio=None, # 做空比例,默认 None(自动从权重推) )
源码 __init__ 做了几件事:
def __init__(self, weights, latest_prices, total_portfolio_value=10000, short_ratio=None): if not isinstance(weights, dict): raise TypeError("weights should be a dictionary of {ticker: weight}") if any(np.isnan(val) for val in weights.values()): raise ValueError("weights should have no NaNs") if (not isinstance(latest_prices, pd.Series)) or any(np.isnan(latest_prices)): raise TypeError("latest_prices should be a pd.Series with no NaNs") if total_portfolio_value <= 0: raise ValueError("total_portfolio_value must be greater than zero") if short_ratio is not None and short_ratio < 0: raise ValueError("short_ratio must be non-negative") self.weights = list(weights.items()) # 转成 list[tuple],保留顺序 self.latest_prices = latest_prices self.total_portfolio_value = total_portfolio_value if short_ratio is None: self.short_ratio = sum((-x[1] for x in self.weights if x[1] < 0)) else: self.short_ratio = short_ratio
输入校验:
| 参数 | 类型 | 约束 |
|---|---|---|
weights |
dict | 不能有 NaN |
latest_prices |
pd.Series | 不能有 NaN,索引对齐 weights 的 ticker |
total_portfolio_value |
int/float | 必须 > 0 |
short_ratio |
float | ≥ 0,仅当有权重为负时使用 |
⚠️ latest_prices 必须是 pd.Series:如果你传 dict 会报
TypeError。可以用get_latest_prices(prices_df)帮你转(09-03 详讲)。
DiscreteAllocation 提供两个方法:
| 方法 | 算法 | 速度 | 精度 |
|---|---|---|---|
greedy_portfolio() |
贪婪两轮启发式 | 快(毫秒级) | 中等 |
lp_portfolio() |
整数线性规划 | 慢(100~1000 倍) | 高 |
最简调用:
# 贪婪 allocation, leftover = da.greedy_portfolio() print(allocation) # {"AAPL": 12, "MSFT": 8, ...} print(leftover) # 67.42 # 整数 LP allocation, leftover = da.lp_portfolio()
输出统一是 (dict, float) 二元组:
{ticker: 整数股数} 字典,自动剔除 0 股。总预算 - Σ(股数 × 单价)。数学上,连续权重的目标是 w ∈ ℝᴺ(实数)。离散化后变成 x ∈ ℤᴺ(整数)。整数规划的本质区别:
这就是 lp_portfolio 慢的原因——整数规划求解器要枚举大量候选解。规模越大,慢得越明显(指数级增长)。
💡 官方实测:lp_portfolio 比 greedy_portfolio 慢 100~1000 倍。$10,000 / 20 个资产,greedy 几毫秒,lp 约 1 秒;但 100 个资产,lp 可能要十几秒。
总预算 T 决定「绝对规模」。它和权重的关系:
某资产 USD 价值 = weight × T 某资产股数 ≈ (weight × T) / price
T 越大,可买的整股数越多,离散化误差越小。比如 AAPL 占 10%:
⚠️ 小资金的陷阱:总预算太小时,离散化误差远大于优化带来的好处。一个 $1,000 的组合做均值-方差优化毫无意义——优化算出的细微权重差异,在「能否多买 1 股」面前完全失真。离散化要求 T 足够大,使每个目标仓位至少能买几股。
from pypfopt import EfficientFrontier, DiscreteAllocation from pypfopt.discrete_allocation import get_latest_prices # 假设已优化得到 weights ef = EfficientFrontier(mu, S) ef.max_sharpe() weights = ef.clean_weights() # 离散分配 latest_prices = get_latest_prices(prices) # 09-03 详讲 da = DiscreteAllocation( weights, latest_prices, total_portfolio_value=20000 ) allocation, leftover = da.greedy_portfolio(verbose=True) print(f"Discrete allocation: {allocation}") print(f"Funds remaining: ${leftover:.2f}")
verbose=True 会打印每个 ticker 的实际权重 vs 目标权重,以及 RMSE 偏差:
MA: allocated 0.242, desired 0.246 FB: allocated 0.200, desired 0.199 PFE: allocated 0.183, desired 0.184 ... AMZN: allocated 0.000, desired 0.072 # AMZN 太贵,1 股都买不起! Allocation has RMSE: 0.038 Funds remaining: $12.15
⚠️ 高价股的盲点:像 AMZN(BRWK 时代一股 $3000+)这种高价股,小资金可能「1 股都买不起」,在贪婪法里直接被跳过。这是离散化不可避免的硬约束,不是 bug。
ℝᴺ,实盘只能买整数股 ℤᴺ,中间必须做优化取舍,简单四舍五入是最差方案。weights(dict)、latest_prices(必须是 pd.Series)、total_portfolio_value(默认 10000)。(allocation, leftover):整数股数字典 + 剩余资金,自动剔除 0 股。greedy_portfolio() 快但精度中等;lp_portfolio() 慢 100~1000 倍但精度高。下一节,我们深入这两种算法的内部——贪婪法的两轮启发式与整数 LP 的目标函数。