第 9 章 · 03 get_latest_prices 与输出


文档摘要

第 9 章 · 03 getlatestprices 与输出 本节摘要:DiscreteAllocation 的三输入之一是「最新价 latestprices」,它必须是 pd.Series。怎么从你已有的价格 DataFrame 里取出每资产的最新价?这就是 工具函数的职责。本节讲清这个小工具的细节(ffill + iloc[-1])、它如何与 DiscreteAllocation 串联,以及离散分配最终输出的 怎么解读和落地。读完本节,你能完成端到端流水线的最后一步:权重 → 整数股数 → 实际下单建议。 内容来源:源码 、文档 ,汉化并套用体系化模板。 学习目标 阅读完本节,你应当能够: 用 getlatestprices 从价格 DataFrame 取最新价。

第 9 章 · 03 get_latest_prices 与输出

本节摘要:DiscreteAllocation 的三输入之一是「最新价 latest_prices」,它必须是 pd.Series。怎么从你已有的价格 DataFrame 里取出每资产的最新价?这就是 get_latest_prices 工具函数的职责。本节讲清这个小工具的细节(ffill + iloc[-1])、它如何与 DiscreteAllocation 串联,以及离散分配最终输出的 (allocation, leftover) 怎么解读和落地。读完本节,你能完成端到端流水线的最后一步:权重 → 整数股数 → 实际下单建议。

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

学习目标

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

  1. get_latest_prices 从价格 DataFrame 取最新价。
  2. 解释 ffill 在缺失数据中的作用。
  3. 说清 allocationleftover 的语义。
  4. 把离散分配结果端到端落地到下单流程。
  5. 警惕停牌、退市、缺失值对 latest_prices 的影响。

一、get_latest_prices:一个小而关键的工具

源码极简:

def get_latest_prices(prices): """ A helper tool which retrieves the most recent asset prices from a dataframe of asset prices, required in order to generate a discrete allocation. """ if not isinstance(prices, pd.DataFrame): raise TypeError("prices not in a dataframe") return prices.ffill().iloc[-1]

只两步:

  1. ffill():forward fill,用前一个有效值填充 NaN。
  2. iloc[-1]:取最后一行(最新一天)。

返回 pd.Series,索引是 ticker,值是最新价。

💡 为什么 ffill:金融数据常有缺失——某资产某天停牌、数据源漏报。ffill() 用「上一个有效收盘价」填充,避免 NaN 污染。如果不 ffill 直接 iloc[-1],只要最后一天某资产没数据就会报错。

二、用法示例

from pypfopt.discrete_allocation import get_latest_prices from pypfopt import DiscreteAllocation # prices 是从 yfinance 拉的价格 DataFrame,日期 × ticker latest_prices = get_latest_prices(prices) print(latest_prices) # AAPL 189.45 # MSFT 372.30 # AMZN 3051.20 # ... # 离散分配 da = DiscreteAllocation( weights=weights, latest_prices=latest_prices, total_portfolio_value=20000, ) allocation, leftover = da.greedy_portfolio()

三、为何必须是 pd.Series

DiscreteAllocation 的构造函数有严格校验:

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")

为什么必须 pd.Series 而不接受 dict?

  • 索引对齐:DA 内部要按 weights 的 ticker 顺序访问价格,pd.Series 的索引保证对齐不出错。
  • NaN 校验:pd.Series 能用 any(np.isnan(...)) 直接判断;dict 不行。

⚠️ 传 dict 会报错:如果你手动构造一个 {ticker: price} 字典传给 DiscreteAllocation,会得到 TypeError: latest_prices should be a pd.Series with no NaNs。正确做法:pd.Series({ticker: price}) 或直接用 get_latest_prices(prices_df)

四、输出 allocation 的解读

greedy_portfolio()lp_portfolio() 返回 (allocation, leftover) 二元组:

allocation, leftover = da.greedy_portfolio() print(allocation) # OrderedDict([('MA', 24), ('FB', 21), ('PFE', 95), ('BABA', 8), # ('AAPL', 9), ('BBY', 5), ('SBUX', 32), ('GOOG', 1)]) print(leftover) # 12.15

allocation 的特征:

  • OrderedDict,键是 ticker、值是整数股数
  • 自动剔除 0 股:源码 _remove_zero_positions:
@staticmethod def _remove_zero_positions(allocation): return {k: v for k, v in allocation.items() if v != 0}
  • 做空时,空头股数为负:
short_alloc = {t: -w for t, w in short_alloc.items()} # 翻成负数

五、输出 leftover 的解读

leftover 是「未投出的剩余资金」:

leftover = total_portfolio_value - Σ(allocation[ticker] × latest_prices[ticker])

它的物理含义:

  • 优化的副产物——离散化必然买不光所有钱。
  • 一般情况下,贪婪法 leftover 在总预算的 0.1%~1% 之间。
  • leftover 过大(>5%)说明:① 某些高价股买不起,② 总预算太小,③ 算法选错。

💡 leftover 的处理建议:实盘时,这点零头可以:① 留作现金缓冲(推荐);② 加到最大权重资产再凑整;③ 改用 lp_portfolio 让 leftover 最小化。留作现金最简单也最稳健。

六、verbose 输出:逐 ticker 对账

allocation, leftover = da.greedy_portfolio(verbose=True)
Funds remaining: $12.15 MA: allocated 0.242, desired 0.246 FB: allocated 0.200, desired 0.199 PFE: allocated 0.183, desired 0.184 BABA: allocated 0.088, desired 0.096 AAPL: allocated 0.086, desired 0.092 AMZN: allocated 0.000, desired 0.072 BBY: allocated 0.064, desired 0.061 SBUX: allocated 0.036, desired 0.038 GOOG: allocated 0.102, desired 0.013 Allocation has RMSE: 0.038

每行含义:allocated(实际权重,= 股数 × 单价 / 总投入)vs desired(目标权重)。差距大说明该资产离散化失败。最后 RMSE 是全体偏差的均方根。

⚠️ AMZN allocated 0 的警示:这种「目标 7.2% 但实际 0%」是最严重的离散化失败——AMZN 单价太高,1 股都买不起。处理方案:① 增大 total_portfolio_value,② 移除 AMZN 改用 ETF 替代,③ 接受偏差并在重新平衡时补齐。

七、端到端:从价格到下单

把第 4-8 章的优化器和本章后处理串起来:

import pandas as pd from pypfopt import ( EfficientFrontier, expected_returns, risk_models, DiscreteAllocation, ) from pypfopt.discrete_allocation import get_latest_prices # 1. 数据 prices = ... # 价格 DataFrame # 2. 优化 mu = expected_returns.mean_historical_return(prices) S = risk_models.CovarianceShrinkage(prices).ledoit_wolf() ef = EfficientFrontier(mu, S) ef.max_sharpe() weights = ef.clean_weights() # 3. 离散分配 latest_prices = get_latest_prices(prices) da = DiscreteAllocation(weights, latest_prices, total_portfolio_value=20000) allocation, leftover = da.lp_portfolio() # 4. 输出下单建议 print("=== 下单建议 ===") for ticker, shares in allocation.items(): print(f" {ticker}: 买入 {shares} 股 @ ${latest_prices[ticker]:.2f}" f" = ${shares * latest_prices[ticker]:.2f}") print(f"剩余资金: ${leftover:.2f}") print(f"实际投入: ${20000 - leftover:.2f} / $20000")

输出:

=== 下单建议 === MA: 买入 24 股 @ $246.10 = $5906.40 FB: 买入 21 股 @ $190.30 = $3996.30 ... 剩余资金: $0.42 实际投入: $19999.58 / $20000

八、警惕:停牌与退市

get_latest_pricesffill 填充缺失——如果某资产最近停牌多日,它的「最新价」其实是停牌前的旧价,可能严重失真。

⚠️ 检查项:跑 da 之前,先看一眼 prices.isna().sum() 和最后 5 行,确认没有大段缺失。停牌资产最好剔除或用其他数据源补全。

本节要点回顾

  1. get_latest_prices:prices.ffill().iloc[-1],从 DataFrame 取最新价 pd.Series;ffill 用前值填缺失,避免停牌 NaN。
  2. 必须 pd.Series:DiscreteAllocation 强制要求 pd.Series + 无 NaN,传 dict 会报错。
  3. allocation 输出:OrderedDict,整数股数,自动剔除 0 股,做空为负数。
  4. leftover 输出:未投出的剩余资金 = 总预算 - Σ(股数×单价),贪婪法常在 0.1%~1% 量级。
  5. verbose 对账:逐 ticker 打印 allocated vs desired,RMSE 是全体偏差,>0.05 要警惕。
  6. 实盘警惕:停牌/退市资产 ffill 后是旧价,先检查 NaN 再喂 DA;高价股可能整股都买不起,需增大预算或换 ETF。

至此第 9 章讲完。下一章是收尾——把权重与分配可视化、自定义优化器、参与上游贡献。


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