跳转到主要内容

托管账本现在会解析托管价格包含文件

发布日期 阅读需 2 分钟Mike ThriftMike Thrift
托管账本现在会解析托管价格包含文件
本页总览

你的持仓已经在账本里了。它们的价格才是你一直在反复重录的那部分。

这种不对称是纯文本记账里最古老的苦差事。一笔买入只写一次,就永远为真:120 NWRB {41.80 USD, 2024-03-12} 记录了一个数量、一个成本和一个日期,之后发生的任何事都不会改变这三者。价格则恰恰相反——它在某一天是对的,然后悄悄变错,而且错得没有任何余额检查能抓住,因为一个过期的价格照样能完美平账。Beancount 自己的答案一直是用工具抓取价格,然后把输出提交进去,这确实可行,很多人也已经自动化了。但它仍然是你自己拥有的一个脚本、你自己维护的一条 cron 任务,以及你自己要合并的一个文件。

beancount.io 的账本引擎现在会自己完成这个解析,适用于托管在我们这里的账本。这篇文章讲的是实际发布了什么、它有意不去碰什么,以及——同样重要的是——还有什么尚未构建。

已发布的内容

一个托管的 beancount.io 账本可以携带一个 include,其目标是一个 URL 而不是文件名:

; main.bean, in a hosted beancount.io ledger.
;
; The managed line is shown commented out on purpose: upstream `include` takes a
; file glob, so this file still loads if you copy it to your own machine. Only
; the hosted engine resolves the URL form.
 
option "title" "Taxable brokerage"
option "operating_currency" "USD"
 
include "accounts.bean"
; include "https://beancount.io/prices/ACME-USD"
 
include "transactions/purchases.bean"
include "transactions/sales.bean"

上游 Beancount 的 include 接受一个文件名——“指定的路径可以是绝对文件名或相对文件名”就是规范的全部内容——所以 URL 包含不是普通的 Beancount,也从不假装自己是。它是一种托管引擎行为,而引擎对它的处理精确如下:

  • 它把该数据源物化为一个只读虚拟文件。 该 URL 通过引擎自己的包含解析路径来解析,正好落在本地文件会落到的位置,所以每条指令都保留一个真实的来源位置。你的字节永远不会被重写。 你写的文件仍然是你写的文件。
  • 抓取到的正文会被校验为仅含 price。 price 指令、注释,以及四个允许的元数据键——price-sourceprice-kindobserved-atprovisional——除此以外什么都没有。任何超出这些的内容都会拒绝整个正文。不存在部分摄取,所以数据源永远无法把一笔交易偷运进你的账簿。
  • 数据源以不可变修订加一个移动指针的形式缓存。 刷新由时间戳驱动,而不是由缓存过期驱动,这意味着上游故障无法把你最后一个完好的修订一起带走。一次失败的刷新永远不会用一个空值替换掉一个完好的修订。
  • 新鲜度在账本被读取时计算,而不是被存储:recentstaleunavailable,连同数据源本身报告的观测时间一起给出。一个你无法确定日期的价格,就是一个你无法审计的价格。
  • 托管条目是只读的。 编辑或删除其中一个会被拒绝,并报出一个指明托管来源的错误,而且它们不计入指令上限——数据源不允许消耗你账本的额度。

那个列表里的一切都在托管账本服务内部运行。它们都不会改变 price 指令的含义:它仍然建立一种基础商品与一种报价商品之间的兑换比率,完全如语言参考所定义的那样。引擎的职责只是把正确的、有日期的、可归因的价格放到加载器面前。

你自己的价格永远优先

这一部分决定了一个数据源是否能被认真对待自己账本的人所使用,所以它要被准确地陈述。

对于同一日期和同一商品对——以及对于倒数对——你自己写的一个价格优先于托管数据源,无论包含的顺序如何。

不是“通常如此”,也不是“如果你把 include 放在最后”。遮蔽决策是在价格映射被构建之前做出的,所以它不依赖于该包含在文件中的位置。把它移到顶部,把它移到底部,把它拆到三个文件里:答案都一样。

; include "https://beancount.io/prices/ACME-USD"   ; hosted-engine form, again shown commented
 
; A price you wrote yourself, for the same date and pair.
; This one wins — above the include or below it, it makes no difference.
2026-09-16 price ACME 93.40 USD

ACME 是下方示例账本里那个虚构的发行方;这个数字是编造的,不是市场观测值。)

为什么是那条规则而不是另一条:你自己文件里的一个价格是一个决定。它可能是你的经纪商在你正在对账的那份对账单上打印的收盘价,可能是一个交易清淡头寸的同期报价,也可能是你的会计师要求你使用的一个数字。数据源对这些一无所知,而一个会悄悄覆盖人工撰写数字的系统,已经不再是一个账本,而开始变成一种意见。数据源填补空缺;它不纠正你。

价格只影响估值,别的什么都不影响

第二个保证是结构性的,而不是一个策略选择,而且它值得用真实数字来展示而不是空口断言。这是那个加密货币示例账本——有日期的批次、质押、挖矿、DeFi 头寸、空投:

在新标签页中打开 加密货币记账示例 — 带日期的批次、质押、DeFi 和空投

从里面拿一条分录。一笔治理代币空投到账,并在落地当天按公允价值记为收入:

2024-03-20 * "Uniswap" "Receive UNI governance token airdrop"
  Assets:Crypto:Wallet:MetaMask:UNI          50.00 UNI {12.50 USD, 2024-03-20}
  Income:Crypto:Airdrops                      -625.00 USD

那 $625.00 的收入,以及附着在该批次上每单位 $12.50 的基准,现在是关于 2024-03-20 的事实。账本里的每一条 price 指令——托管的、手写的,或者完全缺失的——都不去碰这两者。价格改变的是市场价值;它们从不改变数量、成本基准、现金流、费用或已实现收益。这正是为什么一个价格数据源是一件接受帮助很安全的事:一个错误价格最坏能做的是误报某个头寸今天值多少,而它永远无法破坏你将填进纳税申报表的那个数字。

错误的定价模型实际上会在哪里误导你

那个股票和 ETF 示例账本给出了同一点的更尖锐版本:

在新标签页中打开 股票与 ETF 成本基础示例 — 可识别批次、4 拆 1 拆股、指定批次卖出

它有一次 4 比 1 的拆股,而这次拆股是以正确的方式记录的——作为一次保持总基准不变、不碰任何收入账户的数量变化:

2025-07-15 * "Broker" "NWRB 4-for-1 share split — quantity change, not income"
  Assets:Brokerage:NWRB                   -120 NWRB {41.80 USD, 2024-03-12}
  Assets:Brokerage:NWRB                    480 NWRB {10.45 USD, 2024-03-12}

两边都是 $5,016.00。市场价值在拆股前后保持不变——前一天 120 股每股 $62.00,后一天 480 股每股 $15.50,无论哪种都是 $7,440.00——而且花括号里的取得日期保留了下来,正是这一点让 2026 年卖掉这些股票时算作长期。

常见的错误是把拆股记成一个价格事件,然后依靠一个“拆股调整后”的序列来让估值算出正确结果。这只在你见过的每一个价格都按同样方式调整过时才成立。一旦一个未调整的数字出现——一份旧确认单、一张截图、一个什么都不重述的第三方序列——该头寸就被按它价值四倍来估值,而账本里的股数不再与经纪商的对账单匹配,于是那个本可以抓住它的年末断言无法触发。

这才是支持一个带有声明来源、声明类型和可见观测时间的价格数据源的真正论据:不是便利,而是知道刚刚导入的那个数字是在什么约定下计算出来的。示例账本自己的价格文件是故意未调整的,并且明说了这一点,而它那两条跨越拆股的指令被写成你可以用眼睛验证的检查。

关于点进任一个嵌入式账本的一个诚实说明:托管账本查看器按成本渲染账户余额,页面上不提供估值控制。上面的账本在那里是为了向你展示那些账本——那些批次、那次拆股、那些指定批次卖出——而不是查看器目前不绘制的市场估值。两者都是公开的,都可以克隆并在本地运行。

这里还没有的东西

一份更新日志条目如果让你相信一件不真实的事,那它就比毫无价值还糟,所以这里是另一半,直白地说,并且不给其中任何一项附带日期。

  • 价格端点不是公开的。https://beancount.io/prices/<ALIAS> 的匿名请求会被重定向到登录页。没有公开的别名目录。
  • 所以这不是你今天就能粘贴进自己文件里的东西。 引擎解析该包含;它解析的路由尚未开放。等它开放时,那会是它自己的更新日志条目。
  • 本地 bea CLI 不解析 URL 包含。 它从磁盘读取文件,所以一个 URL 包含在本地会作为一个匹配不到任何文件的文件 glob 而失败。CLI 中的加载器支持是一个已明确的后续工作。
  • 没有 API 表面。 没有针对托管价格的 REST、GraphQL 或 MCP 字段。
  • 没有仪表盘表面。 没有连接数据源的界面,UI 里也没有新鲜度标签;引擎计算出的新鲜度目前无处可显示。
  • 快照和导出尚未构建,工具目录或手动刷新端点也尚未构建。

已发布的是引擎层:包含解析、校验、修订缓存、优先级规则和新鲜度计算。那是其他一切必须立足的部分,也是日后最难改动的部分,这就是为什么它先上。

接下来看哪里

上面两个账本都是示例库的一部分,六个你可以克隆并在本地运行的完整模式——这两个都故意附带静态的、已检入的价格文件,所以两年后克隆出来的东西仍然会产出它今天产出的那份报告。我们发布的其他一切都会落在更新日志上。

如果你仍在用自己写的抓取工具保持价格最新,对于一个本地账本来说那仍然是正确答案,而 Beancount 自己的价格抓取文档加上持续维护的 beanprice 工具就是入手之处。

让无聊的部分保持无聊

价格值得自动化,是因为它们是纯文本账本中唯一会自行衰减的部分。Beancount.io 给你的是始终属于你的纯文本记账——可审计、受版本控制,而且从不在你背后被重写,这正是一个托管数据源在我们愿意发布一个之前必须达到的标准。免费开始,把你的账簿保存在你能读的文件里。

分享这篇文章

关注这个话题

来源:https://beancount.io/zh/blog/2026/09/17/managed-price-includes-hosted-ledgers

发布日期: 2026年9月17日