A Guide to Precision & Tolerances in Beancount
Managing numerical precision is a cornerstone of double-entry accounting. In digital bookkeeping, especially when dealing with multiple currencies, stock prices, and fractional shares, small rounding discrepancies can quickly lead to frustrating balancing errors. Beancount provides a sophisticated but intuitive system for handling precision and setting acceptable tolerances. This guide will walk you through how it works. ⚙️
Core Precision Concepts
Beancount's primary goal is to ensure that every transaction balances to zero. However, calculations involving prices or costs often produce results with more decimal places than are practical to record. The tolerance system allows for small, acceptable imbalances.
Automatic Tolerance Inference
By default, Beancount infers the required tolerance for each transaction automatically. This inference is handled individually for every transaction and calculated separately for each currency involved.
The rule is simple: the tolerance is half of the last significant digit of the numbers present in the transaction's postings.
For example, consider this purchase:
2013-04-03 * "Buy Fund"
Assets:Fund 10.22626 FUND {37.61 USD}
Assets:Cash -384.61 USD
Beancount infers the tolerances as follows:
- For the
FUND
commodity, the number10.22626
has 5 decimal places. The tolerance is half of the last digit, soFUND
. - For the
USD
commodity, the number-384.61
has 2 decimal places. The tolerance is half of the last digit, soUSD
.
Transaction Weight Rules
When checking if a transaction balances, Beancount calculates the "weight" of each posting. The rules for this calculation are:
- Simple Amount: If a posting has only an amount (e.g.,
Assets:Cash -100.00 USD
), its weight is that exact amount. - Price Posting: If a posting has a per-unit price (e.g.,
10 FUND @ 38.46 USD
), its weight is theamount × price
. - Cost Posting: If a posting has a total cost (e.g.,
10 FUND {384.61 USD}
), its weight is the total cost amount. - Cost and Price: If a posting has both a total cost and a per-unit price (e.g.,
10 FUND {384.61 USD} @ 38.46 USD
), only the total cost is used for balancing. The per-unit price is treated as a comment or memo.
Precision Inference Rules
The automatic inference system follows a few specific rules:
- Number Format
- Integer amounts (e.g.,
10 USD
) do not contribute to precision inference. - The maximum tolerance that can be automatically inferred is
0.05
units (e.g., from a number like10.1 USD
). If you need a larger tolerance, you must specify it manually. - Costs and prices (e.g.,
{37.61 USD}
) are excluded from tolerance inference. Only the primary amounts of the postings are used. - If postings for the same currency have different precisions (e.g.,
-10.10 USD
and5.123 USD
), Beancount uses the coarsest (largest) tolerance. In this case, it would be based on-10.10 USD
, yielding a tolerance ofUSD
.
-
Default Handling You can set a global or currency-specific default tolerance if a transaction has no numbers with decimal places from which to infer it.
; Sets a default tolerance for all currencies without explicit rules
option "inferred_tolerance_default" "*:0.001"
; Sets a specific default tolerance for USD
option "inferred_tolerance_default" "USD:0.003" -
Tolerance Multiplier You can globally increase all inferred tolerances by a fixed multiplier. This is useful for loosening checks across your entire file without changing every transaction. A multiplier of
1.2
increases all inferred tolerances by 20%.option "inferred_tolerance_multiplier" "1.2"
-
Cost-Based Inference While costs are normally ignored for tolerance inference, you can instruct Beancount to use them. This is helpful when the final amount (e.g., a cash withdrawal) is the most precise number in a transaction.
option "infer_tolerance_from_cost" "TRUE"
Balance Assertions
Balance assertions (balance
) are used to verify that your account's balance matches a known value on a specific date. They also have an associated tolerance.
Basic Format
Similar to transactions, the tolerance for a balance
assertion is inferred from the number of decimal places in the amount.
; Asserts the balance is 4.271 RGAGX with a tolerance of ±0.0005
2015-05-08 balance Assets:Fund 4.271 RGAGX
; Asserts the balance is 4.27 RGAGX with a tolerance of ±0.005
2015-05-08 balance Assets:Fund 4.27 RGAGX
The calculated balance must fall within this range. For the second example, any balance between and would pass the check.
Explicit Tolerances
If the inferred tolerance is not suitable, you can specify one explicitly using the tilde (~
) character.
; Asserts the balance is 4.271 RGAGX with a custom tolerance of ±0.01 RGAGX
2015-05-08 balance Assets:Fund 4.271 ~ 0.01 RGAGX
Here, the assertion will pass if the calculated balance is between and RGAGX
.
Rounding Management
For cases where small residuals from calculations are expected and acceptable, Beancount provides tools to manage them systematically.
Rounding Error Tracking
You can designate a special account to automatically collect rounding errors. This keeps your transactions perfectly balanced by sweeping tiny leftover amounts into one place.
First, enable the option and open the account:
option "account_rounding" "Equity:RoundingError"
2000-01-01 open Equity:RoundingError
Now, Beancount will automatically add a third leg to any transaction that doesn't balance within its tolerance, posting the difference to Equity:RoundingError
.
2013-02-23 * "Purchase"
Assets:Invest 1.245 RGAGX {43.23 USD}
Assets:Cash -53.82 USD
In this transaction, . The transaction is imbalanced by USD. With the rounding option enabled, Beancount internally treats it as:
2013-02-23 * "Purchase"
Assets:Invest 1.245 RGAGX {43.23 USD}
Assets:Cash -53.82 USD
Equity:RoundingError -0.00135 USD ; Automatically added
Inferred Number Precision
Beancount can also use tolerance settings to automatically round numbers before they are even inserted into the book's data structures.
-
No Tolerance Specified: If no tolerance is defined, numbers are used at their full precision. No rounding occurs.
-
With Default Tolerance: If you set a default tolerance, numbers will be quantized to that level.
option "default_tolerance" "USD:0.001"
With this setting, a number like
53.82135 USD
would be rounded and stored as53.821 USD
. -
With Rounding Account: If both a default tolerance and a rounding account are active, Beancount quantizes the number and captures the residual.
option "default_tolerance" "USD:0.01"
option "account_rounding" "Equity:RoundingError"A number like
53.82135 USD
would be stored as53.82 USD
, and the-0.00135 USD
residual would be posted toEquity:RoundingError
.
Implementation Details
A few technical points clarify how Beancount achieves this reliability.
-
Number Representation: Beancount uses Python's
decimal
module, not floating-point numbers. This allows for up to 28 decimal places of precision and avoids the binary representation errors common to floats. -
DisplayContext Class: This internal class handles all number formatting for display purposes. It respects currency-specific precision settings and can format output with aligned columns and commas.
-
Precision vs. Tolerance: It's crucial to distinguish these two concepts:
- Precision relates to the display format of a number (how many decimal places are shown).
- Tolerance is the allowance for imbalance used during verification checks.
Best Practices ✨
Here are some practical recommendations for managing precision in your ledger.
Initial Setup
For most new ledgers, this is a robust starting configuration:
; A reasonable default for most currencies (e.g., USD, EUR)
option "inferred_tolerance_default" "*:0.005"
; A 10% buffer on all inferred tolerances
option "inferred_tolerance_multiplier" "1.1"
; An account to catch all rounding dust
option "account_rounding" "Equity:RoundingError"
2000-01-01 open Equity:RoundingError
Troubleshooting Tips
If you encounter balancing errors:
- Add decimal digits to a posting's amount to create a tighter, more accurate local tolerance inference.
- Use explicit tolerances (
~
) onbalance
assertions that fail due to predictable discrepancies. - Track rounding errors in a dedicated account to see where and how often they occur.
- Consider setting currency-specific defaults if you frequently deal with currencies that have different conventions (e.g., JPY has no decimals).
Migration Strategy
When applying these concepts to an existing, messy ledger:
- Start with a generous global tolerance (e.g.,
*:0.05
) and a high multiplier to get the file to validate. - Gradually tighten the tolerances and fix the errors that appear.
- Add explicit digits to amounts in problematic transactions to let inference do its job.
- Monitor the rounding account's balance. A large or rapidly growing balance may signal a systemic issue that needs investigation.