第 9 章 · 01 连续权重转整数股数


文档摘要

第 9 章 · 01 连续权重转整数股数 本节摘要:前面所有优化器输出的都是「连续权重」(如 AAPL: 0.2347),但真实交易要买「整数股」——你不能买 0.37 股 AAPL。这一步「从数学权重到实际下单」的鸿沟,正是 PyPortfolioOpt 的 类要填的。本节讲清 DiscreteAllocation 的构造、输入(权重 + 最新价格 + 总预算)和输出(整数股 + 剩余资金),让你理解为什么这一步处理不好、前面优化得再精也会失真。读完本节,你能把任意优化器的权重转成「买几股、剩多少钱」。 内容来源:源码 、文档 ,汉化并套用体系化模板。 学习目标 阅读完本节,你应当能够: 用 DiscreteAllocation 把连续权重转整数股数。

第 9 章 · 01 连续权重转整数股数

本节摘要:前面所有优化器输出的都是「连续权重」(如 AAPL: 0.2347),但真实交易要买「整数股」——你不能买 0.37 股 AAPL。这一步「从数学权重到实际下单」的鸿沟,正是 PyPortfolioOpt 的 DiscreteAllocation 类要填的。本节讲清 DiscreteAllocation 的构造、输入(权重 + 最新价格 + 总预算)和输出(整数股 + 剩余资金),让你理解为什么这一步处理不好、前面优化得再精也会失真。读完本节,你能把任意优化器的权重转成「买几股、剩多少钱」。

内容来源:源码 pypfopt/discrete_allocation.py、文档 docs/Postprocessing.rst,汉化并套用体系化模板。

学习目标

阅读完本节,你应当能够:

  1. DiscreteAllocation 把连续权重转整数股数。
  2. 说清三个核心输入:weights、latest_prices、total_portfolio_value
  3. 解释为什么连续权重必须离散化(实盘无法买分数股)。
  4. 理解输出的 (allocation, leftover) 二元组。
  5. 知道 DiscreteAllocation 提供两种算法:greedy 和 lp

一、为什么需要离散化:从权重到股数

均值-方差、BL、HRP 输出的 weights = {"AAPL": 0.2347, "MSFT": 0.1823, ...} 都是「比例」,加起来等于 1。但实盘下单时:

  • 你有总预算 T(如 $10,000)
  • AAPL 单价 $190
  • 你想买「23.47% × $10,000 / $190 ≈ 12.35 股」

12.35 股不存在。券商只接受整数股(美股),A 股还要按 100 股一手。所以必须做取舍:

如果直接「四舍五入」,有的资产会超买,资金不够;如果统一「向下取整」,会大量剩余资金,实际权重严重偏离最优。DiscreteAllocation 提供两种系统化方法处理这个矛盾。

💡 核心心法:离散化是「在整数约束下逼近连续最优」的优化问题。简单四舍五入是最差的方案——既可能超预算,又偏离权重。系统化方法(greedy / lp)能让你「资金利用率最高 + 权重偏差最小」。

二、DiscreteAllocation 的构造

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) 二元组:

  • allocation:{ticker: 整数股数} 字典,自动剔除 0 股。
  • leftover:剩余资金(USD),即 总预算 - Σ(股数 × 单价)

四、为什么 0.37 股不存在:整数约束的本质

数学上,连续权重的目标是 w ∈ ℝᴺ(实数)。离散化后变成 x ∈ ℤᴺ(整数)。整数规划的本质区别:

  • 连续:梯度下降、内点法,平滑可微。
  • 整数:组合优化,本质是 NP-hard,无梯度可用

这就是 lp_portfolio 慢的原因——整数规划求解器要枚举大量候选解。规模越大,慢得越明显(指数级增长)。

💡 官方实测:lp_portfolio 比 greedy_portfolio 慢 100~1000 倍。$10,000 / 20 个资产,greedy 几毫秒,lp 约 1 秒;但 100 个资产,lp 可能要十几秒。

五、total_portfolio_value 的语义

总预算 T 决定「绝对规模」。它和权重的关系:

某资产 USD 价值 = weight × T 某资产股数 ≈ (weight × T) / price

T 越大,可买的整股数越多,离散化误差越小。比如 AAPL 占 10%:

  • T=$1,000 → $100 → 0 股(根本买不起)
  • T=$10,000 → $1,000 → 5 股($950)
  • T=$100,000 → $10,000 → 52 股($9,880)

⚠️ 小资金的陷阱:总预算太小时,离散化误差远大于优化带来的好处。一个 $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。

本节要点回顾

  1. 离散化的必要性:连续权重是 ℝᴺ,实盘只能买整数股 ℤᴺ,中间必须做优化取舍,简单四舍五入是最差方案。
  2. DiscreteAllocation 三输入:weights(dict)、latest_prices(必须是 pd.Series)、total_portfolio_value(默认 10000)。
  3. 输出 (allocation, leftover):整数股数字典 + 剩余资金,自动剔除 0 股。
  4. 两种算法:greedy_portfolio() 快但精度中等;lp_portfolio() 慢 100~1000 倍但精度高。
  5. 整数规划是 NP-hard:lp_portfolio 的运行时间随资产数指数增长,大规模组合只能用 greedy。
  6. 小资金陷阱:总预算太小时离散误差远超优化收益;每个目标仓位至少要能买几股才有意义。高价股可能直接被跳过。

下一节,我们深入这两种算法的内部——贪婪法的两轮启发式与整数 LP 的目标函数。


发布者: 作者: 灏天文库 转发
评论区 (0)
U