Decide the booking method for cost-basis accounts (Assets:Lightning:Balance) #45

Open
opened 2026-06-15 18:39:23 +00:00 by padreug · 0 comments
Owner

Background

Libra holds sats at a fiat cost basis — e.g. "200000 SATS {100.00 EUR}". Each acquisition creates a lot (a quantity of sats tagged with the EUR cost it was acquired at). When sats are later spent/reduced, Beancount has to decide which lots are drawn down. That decision is the booking method, set per-account as an optional quoted string on the open directive:

2024-01-01 open Assets:Lightning:Balance SATS "FIFO"

Today we write no booking method, so cost-basis accounts inherit the global booking_method option, which defaults to STRICT. This is an implicit accounting-policy choice we're currently making by omission. This issue is to make it deliberate.

Verified against the Beancount source (beancount/core/data.py Booking enum; parser/grammar.y opt_booking).

Why it matters

The chosen method changes the realized EUR cost of a spend, and therefore any realized-gain/loss reporting, whenever sats were acquired at different rates and the price has since moved. Across an exchange-rate swing the same spend can book a very different cost depending on the method. It has no effect on the current sats balance — only on which fiat lots get consumed.

It only affects accounts that hold commodities at cost ({...} annotations). Pure-SATS or pure-fiat operational accounts are unaffected.

Options (pros / cons)

STRICT (current default by omission)

Requires the reduction to unambiguously identify a single lot; errors on ambiguity.

  • Pro: No silent/arbitrary lot selection — forces explicitness, surfaces modeling gaps as parse errors rather than quietly picking a lot.
  • Con: Operationally painful for a Lightning balance that commingles many same-currency lots — ordinary spends would error unless every posting names its exact cost lot. Almost certainly too strict for how Libra actually records sats.

STRICT_WITH_SIZE

Like STRICT, but if a lot matches the reduction size exactly, it's accepted.

  • Pro: Slightly more forgiving than STRICT while keeping its discipline.
  • Con: The "exact size match" escape hatch rarely fires for a fungible sats balance; in practice behaves like STRICT and inherits the same friction.

NONE

Disables lot matching; allows mixed inventories (positions at different costs coexist without reduction logic).

  • Pro: Never errors; simplest to operate.
  • Con: Effectively abandons cost-basis tracking on reductions — defeats the reason we record {cost} at all. Realized cost/gain becomes meaningless. Only sensible if we decide we don't actually want lot-level cost tracking.

AVERAGE

Merges all matching lots into a single average-cost position before/after each reduction.

  • Pro: Intuitive for a commingled pooled balance (one blended acquisition cost); stable, no lot-ordering surprises; matches how many people mentally model "our sats cost us ~X on average." Some jurisdictions accept/require average cost.
  • Con: Loses per-lot granularity; can't later reclassify specific lots. Average-cost recomputation has its own edge cases. Not permitted under some tax regimes that mandate FIFO.

FIFO (first-in, first-out)

Oldest lots consumed first.

  • Pro: Widely accepted/expected default for inventory and crypto in many jurisdictions; deterministic; intuitive audit trail. Likely the best fit if we want real lot-level cost basis with minimal surprise.
  • Con: In a rising-sats market, books the lowest-cost (oldest, cheapest) lots first → larger realized gains earlier (tax timing). Just a consequence of the policy, not a bug.

LIFO (last-in, first-out)

Newest lots consumed first.

  • Pro: In a rising market, books higher-cost lots first → smaller near-term realized gains (tax deferral where permitted).
  • Con: Disallowed in many jurisdictions; less intuitive; can leave very old cheap lots lingering indefinitely.

HIFO (highest-in, first-out)

Highest-cost lots consumed first.

  • Pro: Minimizes realized gains aggressively (consumes most expensive basis first) — tax-efficient where allowed.
  • Con: Least intuitive; acceptability is jurisdiction-dependent; can produce a counterintuitive remaining-lot picture.

Recommendation (for discussion)

For a collective's pooled Lightning balance, the realistic contenders are AVERAGE (simplest mental model — one blended cost for the pool) or FIFO (real lot-level basis, broad jurisdictional acceptance). STRICT/STRICT_WITH_SIZE are likely too operationally strict for commingled sats; NONE throws away cost tracking; LIFO/HIFO are tax-optimization plays with narrower acceptance.

The choice should be driven by the reporting/tax jurisdiction(s) the deploying collectives operate under — worth confirming that before locking it in.

Scope of the change

  • Set the booking method explicitly on cost-basis account open directives (at minimum Assets:Lightning:Balance; review whether any other {cost}-bearing accounts need it).
  • Decide whether to set it per-account on the open directive or globally via the booking_method option (per-account is more explicit and self-documenting).
  • Note: changing the method later re-interprets historical reductions — pin it before meaningful cost-basis history accrues.

Follow-up from the add-account UI work (PR feat/add-account-ui).

## Background Libra holds sats at a fiat cost basis — e.g. `"200000 SATS {100.00 EUR}"`. Each acquisition creates a *lot* (a quantity of sats tagged with the EUR cost it was acquired at). When sats are later **spent/reduced**, Beancount has to decide *which lots* are drawn down. That decision is the **booking method**, set per-account as an optional quoted string on the `open` directive: ``` 2024-01-01 open Assets:Lightning:Balance SATS "FIFO" ``` Today we write **no** booking method, so cost-basis accounts inherit the global `booking_method` option, which defaults to **`STRICT`**. This is an implicit accounting-policy choice we're currently making by omission. This issue is to make it deliberate. Verified against the Beancount source (`beancount/core/data.py` `Booking` enum; `parser/grammar.y` `opt_booking`). ## Why it matters The chosen method changes the **realized EUR cost** of a spend, and therefore any realized-gain/loss reporting, whenever sats were acquired at different rates and the price has since moved. Across an exchange-rate swing the same spend can book a very different cost depending on the method. It has **no effect** on the current sats balance — only on which fiat lots get consumed. It only affects accounts that hold commodities **at cost** (`{...}` annotations). Pure-SATS or pure-fiat operational accounts are unaffected. ## Options (pros / cons) ### `STRICT` (current default by omission) Requires the reduction to unambiguously identify a single lot; errors on ambiguity. - **Pro:** No silent/arbitrary lot selection — forces explicitness, surfaces modeling gaps as parse errors rather than quietly picking a lot. - **Con:** Operationally painful for a Lightning balance that commingles many same-currency lots — ordinary spends would error unless every posting names its exact cost lot. Almost certainly too strict for how Libra actually records sats. ### `STRICT_WITH_SIZE` Like `STRICT`, but if a lot matches the reduction size exactly, it's accepted. - **Pro:** Slightly more forgiving than `STRICT` while keeping its discipline. - **Con:** The "exact size match" escape hatch rarely fires for a fungible sats balance; in practice behaves like `STRICT` and inherits the same friction. ### `NONE` Disables lot matching; allows mixed inventories (positions at different costs coexist without reduction logic). - **Pro:** Never errors; simplest to operate. - **Con:** Effectively abandons cost-basis tracking on reductions — defeats the reason we record `{cost}` at all. Realized cost/gain becomes meaningless. Only sensible if we decide we don't actually want lot-level cost tracking. ### `AVERAGE` Merges all matching lots into a single average-cost position before/after each reduction. - **Pro:** Intuitive for a commingled pooled balance (one blended acquisition cost); stable, no lot-ordering surprises; matches how many people mentally model "our sats cost us ~X on average." Some jurisdictions accept/require average cost. - **Con:** Loses per-lot granularity; can't later reclassify specific lots. Average-cost recomputation has its own edge cases. Not permitted under some tax regimes that mandate FIFO. ### `FIFO` (first-in, first-out) Oldest lots consumed first. - **Pro:** Widely accepted/expected default for inventory and crypto in many jurisdictions; deterministic; intuitive audit trail. Likely the best fit if we want real lot-level cost basis with minimal surprise. - **Con:** In a rising-sats market, books the *lowest*-cost (oldest, cheapest) lots first → larger realized gains earlier (tax timing). Just a consequence of the policy, not a bug. ### `LIFO` (last-in, first-out) Newest lots consumed first. - **Pro:** In a rising market, books higher-cost lots first → smaller near-term realized gains (tax deferral where permitted). - **Con:** Disallowed in many jurisdictions; less intuitive; can leave very old cheap lots lingering indefinitely. ### `HIFO` (highest-in, first-out) Highest-cost lots consumed first. - **Pro:** Minimizes realized gains aggressively (consumes most expensive basis first) — tax-efficient where allowed. - **Con:** Least intuitive; acceptability is jurisdiction-dependent; can produce a counterintuitive remaining-lot picture. ## Recommendation (for discussion) For a collective's pooled Lightning balance, the realistic contenders are **`AVERAGE`** (simplest mental model — one blended cost for the pool) or **`FIFO`** (real lot-level basis, broad jurisdictional acceptance). `STRICT`/`STRICT_WITH_SIZE` are likely too operationally strict for commingled sats; `NONE` throws away cost tracking; `LIFO`/`HIFO` are tax-optimization plays with narrower acceptance. The choice should be driven by the **reporting/tax jurisdiction(s)** the deploying collectives operate under — worth confirming that before locking it in. ## Scope of the change - Set the booking method explicitly on cost-basis account `open` directives (at minimum `Assets:Lightning:Balance`; review whether any other `{cost}`-bearing accounts need it). - Decide whether to set it per-account on the `open` directive or globally via the `booking_method` option (per-account is more explicit and self-documenting). - Note: changing the method later re-interprets historical reductions — pin it before meaningful cost-basis history accrues. Follow-up from the add-account UI work (PR `feat/add-account-ui`).
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/libra#45
No description provided.