说实话:你的持仓早就在账本里记得清清楚楚了。真正把你困在无止境重录循环里的,是价格。
这种不对称是纯文本记账里最古老、也最恼人的苦差事。一笔买入只写一次,就永远刻在石头上:120 NWRB {41.80 USD, 2024-03-12} 永久锁定了一个数量、一个成本和一个日期,之后发生的任何事都不会改变这些事实。价格则恰恰相反——它只在某一天是对的,然后悄悄变错。更糟的是,它错得没有任何余额检查能抓住,因为一个价格过期的账本照样能完美平账。
一直以来,Beancount 的做法是用脚本抓取价格,再把输出提交进仓库。这确实可行,很多人也已经自动化了。但说到底,那仍然是一个需要你盯着的脚本、一条需要你维护的 cron 任务,以及一个需要你反复合并的文件。
不必再这样了。 对于托管在 beancount.io 上的账本,我们的引擎现在会原生完成这项解析。这篇文章讲的是刚刚发布了什么、我们有意不去碰什么,以及——同样重要的——还有什么尚未构建。
已发布的内容
一个托管的 beancount.io 账本现在可以携带一条 include 指令,指向一个 URL 而不是本地文件名:
; main.bean,位于一个托管的 beancount.io 账本中。
;
; 本页是中文,所以报价货币用 CNY。换成本站的其他语言时,请用代码块
; 下方列表中对应的货币。
option "title" "应税券商账户"
option "operating_currency" "CNY"
; 只有托管引擎会解析这种 URL 形式,而上游的 `include` 接受的是文件 glob——
; 所以这一行在这里以注释形式展示,把文件复制到自己机器上也照样能加载。
; 在托管账本里把它的注释取消掉即可生效。
; include "https://beancount.io/prices/AAPL-CNY"注意: 访问该 URL 之前需要先登录,匿名请求会被重定向到登录页。本页是中文,所以示例用人民币报价——这是中文读者最常用来记账的货币。登录之后打开 https://beancount.io/prices/AAPL-CNY,确认你看到的是标准的 price 指令,然后把那一行加进你的托管账本。
需要说明的是,AAPL-CNY 并不是目录里直接列出的交易对:它是 Databento 未经调整的 AAPL-USD 收盘价,与欧洲央行的 USD-CNY 交叉换算而来。
阅读其他语言版本时,请改用该语言通常使用的货币,且仅限目录确实提供该报价的情况:
- English:
USD,https://beancount.io/prices/AAPL-USD。这是 Databento 的直接报价,不是交叉价。 - 日本語:
JPY,https://beancount.io/prices/AAPL-JPY - 한국어:
KRW,https://beancount.io/prices/AAPL-KRW - Deutsch、Français、Español、Italiano、Nederlands、Català、Português、Slovenčina 与 Български:
EUR,https://beancount.io/prices/AAPL-EUR。保加利亚语今年新开的账簿用欧元;如果旧文件仍在用BGN,目录里也有该报价。 - فارسی、Русский 与 Українська: 目录中没有
IRR、RUB或UAH,所以没有对应的交易对可以打开,请选一个目录确实提供的报价货币。
位于 https://beancount.io/prices/ 的目录同样在这道登录门之后。其他一线股票的用法完全相同——换掉股票代码,保留你所用语言对应的报价货币。ACME-USD 会返回 404。
上游 Beancount 的 include 在规范上接受的是文件名,所以 URL include 严格来说是一种托管引擎行为,它也从不假装自己是普通的 Beancount。引擎在底层做的事情精确如下:
- 它把该数据源物化为一个只读虚拟文件。 URL 通过引擎自己的 include 解析路径来解析,正好落在本地文件会落到的位置,所以每条指令都保留一个真实的来源位置。你的原始字节永远不会被重写。你写的文件仍然是你写的文件。
- 严格的正文校验。 抓取到的正文会被校验为仅含价格。我们只允许
price指令、注释,以及四个特定的元数据键(price-source、price-kind、observed-at和provisional)。任何其他内容都会导致整个正文被拒绝。不存在部分摄取,也就是说一个失控的数据源永远无法把一笔交易偷运进你的账簿。 - 聪明的缓存。 数据源以不可变修订加一个移动指针的形式缓存。刷新由时间戳驱动而不是缓存过期驱动,确保上游故障不会抹掉你最后一个完好的修订。
- 可审计的新鲜度。 新鲜度在账本被读取时动态计算(recent、stale 或 unavailable),并与数据源自己报告的观测时间一并给出。一个你无法确定日期的价格,就是一个你无法审计的价格。
- 严格只读。 托管条目不能被编辑或删除(尝试这么做会抛出一个指明来源的错误)。此外,它们不计入你的指令上限——我们不会让数据源吃掉你账本的额度。
这一切都不会改变 price 指令在 Beancount 中的根本含义。引擎的唯一职责,是把正确的、有日期的、可归因的价格放到加载器面前。
你自己的价格永远优先
对于认真对待自己账本的人来说,这一条是底线,所以让我们把话说得非常清楚:
对于同一日期和同一商品对(包括其倒数对),你自己写下的价格永远优先于托管数据源。
不是“通常如此”,也不是“只有把 include 放在最后才如此”。这个遮蔽决策发生在价格映射被构建之前,因此它完全不依赖于 include 在你文件中的位置。放在开头、放在结尾,或者拆到三个文件里——你手写的价格永远优先。
倒数规则容易被忽略,却至关重要。AAPL-CNY 数据源给出的是 AAPL 以 CNY 计价的价格。如果你反过来手写了一个价格——同一天、CNY 以 AAPL 计价——你的那条依然胜过数据源。无论你 include 的是哪个报价货币,CNY、USD 还是 JPY,都同样适用。
为什么是这条规则? 因为你自己文件里的价格是一个有意识的决定。它可能是你正在对账的那份对账单上经纪商打印的收盘价,可能是一个交易清淡资产的同期报价,也可能是你的会计师指定要用的某个数字。数据源并不了解你的上下文。一个会悄悄覆盖人工撰写数字的系统,就不再是账本,而是一种意见。数据源是用来填补空缺的;它不纠正你。
价格只影响估值,别的什么都不影响
接下来这个保证是结构性的,而不是一项策略选择,而且它最好用真实数字来展示。看看我们的加密货币示例账本,里面有带日期的批次、质押、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 示例账本给出了这一点更尖锐的版本。
它包含一次 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。市场价值在拆股前后保持不变(两种算法都是 $7,440.00),而花括号里的取得日期也保留了下来——正是这一点,让 2026 年卖出这些股票时仍被归类为长期持有。
常见的陷阱是把拆股记成一个价格事件,转而重度依赖一个“拆股调整后”的价格序列来让数学成立。那只有在你见过的每一个价格都按完全相同的方式调整过时才行得通。一旦有一个未调整的数字进入你的账本——一份旧的成交确认、一张截图,或者一个不重述历史数据的第三方数据源——该头寸就会被按其实际价值的四倍来估值。更糟的是,账本里的股数不再与经纪商的对账单一致,于是你的年末余额断言会悄无声息地失效。
这才是依赖一个带有声明来源、声明类型和可见观测时间的价格数据源的真正理由。这不只是图方便,而是要确切知道你导入的数字究竟采用了什么计算约定。
(关于嵌入式账本的一点说明:托管账本查看器按成本渲染账户余额,目前不提供页面内的估值控制。上面这些账本是用来展示底层机制的——那些批次、那次拆股、那些卖出。两者都是公开的,你可以克隆下来在本地运行。)
这里还没有的东西
我们认为,一份会过度承诺的更新日志比没有还糟。所以下面是关于我们还没有做出来的东西的实话,不附任何时间表:
- 必须登录。 对
https://beancount.io/prices/<ALIAS>的匿名请求会把你弹回登录页。 - 本地 CLI 尚不支持。 本地的
beaCLI 从磁盘读取文件,所以 URL include 在本地会作为一个匹配不到文件的 glob 而失败。给 CLI 加上加载器支持已经在我们的路线图上。 - 没有 API 表面。 目前没有针对托管价格的 REST、GraphQL 或 MCP 字段。
- 没有 UI 仪表盘。 还没有“连接数据源”的界面,UI 里也没有新鲜度标签。(引擎计算出的新鲜度数据目前还无处显示。)
- 没有快照、导出或手动刷新端点。
我们今天发布的严格来说只是引擎层:include 解析、校验、修订缓存、优先级规则和新鲜度计算。这是其他一切都必须立足其上的基础设施,也正是我们先做它的原因。
接下来看哪里
上面提到的两个账本都属于我们的示例库——六个完整可用的模式,你可以克隆下来在本地运行。(两者都故意附带静态的、已检入的价格文件,确保两年后克隆出来的副本仍然产出与今天完全相同的报告。)我们发布的其他一切都会直接落在更新日志上。
如果你现在用自己的抓取工具保持价格最新并且很满意,那就继续用。对于严格本地的账本,那仍然是最好的答案。Beancount 自己的价格抓取文档和持续维护的 beanprice 工具是最好的起点。
让无聊的部分保持无聊
价格值得自动化,恰恰是因为它们是纯文本账本里唯一会随时间腐坏的部分。Beancount.io 的目标,是给你一套始终属于你自己的纯文本记账——可审计、受版本控制,而且绝不在你背后被改写。这就是一个托管数据源在我们愿意发布它之前必须先达到的基准。
免费开始使用,把你的账簿保存在你真正读得懂的文件里。





