Skip to main content

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

precision

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 number 10.22626 has 5 decimal places. The tolerance is half of the last digit, so 0.00001div2=0.0000050.00001 \\div 2 = 0.000005 FUND.
  • For the USD commodity, the number -384.61 has 2 decimal places. The tolerance is half of the last digit, so 0.01div2=0.0050.01 \\div 2 = 0.005 USD.

Transaction Weight Rules

When checking if a transaction balances, Beancount calculates the "weight" of each posting. The rules for this calculation are:

  1. Simple Amount: If a posting has only an amount (e.g., Assets:Cash -100.00 USD), its weight is that exact amount.
  2. Price Posting: If a posting has a per-unit price (e.g., 10 FUND @ 38.46 USD), its weight is the amount × price.
  3. Cost Posting: If a posting has a total cost (e.g., 10 FUND {384.61 USD}), its weight is the total cost amount.
  4. 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:

  1. 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 like 10.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 and 5.123 USD), Beancount uses the coarsest (largest) tolerance. In this case, it would be based on -10.10 USD, yielding a tolerance of 0.0050.005 USD.
  1. 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"
  2. 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"
  3. 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 4.2654.265 and 4.2754.275 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 4.2614.261 and 4.2814.281 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, 1.245times43.23=53.821351.245 \\times 43.23 = 53.82135. The transaction is imbalanced by 0.00135-0.00135 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.

  1. No Tolerance Specified: If no tolerance is defined, numbers are used at their full precision. No rounding occurs.

  2. 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 as 53.821 USD.

  3. 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 as 53.82 USD, and the -0.00135 USD residual would be posted to Equity:RoundingError.

Implementation Details

A few technical points clarify how Beancount achieves this reliability.

  1. 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.

  2. 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.

  3. 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 (~) on balance 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:

  1. Start with a generous global tolerance (e.g., *:0.05) and a high multiplier to get the file to validate.
  2. Gradually tighten the tolerances and fix the errors that appear.
  3. Add explicit digits to amounts in problematic transactions to let inference do its job.
  4. Monitor the rounding account's balance. A large or rapidly growing balance may signal a systemic issue that needs investigation.