Engine
wealthbraid.engine.money
¶
Monetary value types: :class:Commodity and :class:Amount.
All monetary quantities are :class:decimal.Decimal. Floats are rejected at
construction so floating-point error can never enter the accounting engine. An
:class:Amount pairs an exact quantity with a commodity; arithmetic between
amounts of different commodities is an error rather than a silent coercion.
Amount
dataclass
¶
An exact quantity of a single commodity.
The quantity is always a :class:~decimal.Decimal; passing a float is an
error. The Decimal's own exponent carries the amount's precision, which the
balancing rules use to infer tolerances.
Attributes:
| Name | Type | Description |
|---|---|---|
quantity |
Decimal
|
The exact signed quantity. |
commodity |
Commodity
|
The commodity the quantity is denominated in. |
fractional_digits
property
¶
Return the number of digits after the decimal point.
Returns:
| Type | Description |
|---|---|
int
|
The count of fractional digits (0 for integers). |
__add__(other)
¶
__mul__(factor)
¶
Scale the amount by a scalar.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
factor
|
Decimal | int | str
|
The scalar multiplier (Decimal, int, or decimal string). |
required |
Returns:
| Type | Description |
|---|---|
Amount
|
The scaled amount as a new :class: |
Amount
|
floats, booleans, and non-numeric factors, so Python raises |
Amount
|
|
__neg__()
¶
Return the negated amount.
Returns:
| Type | Description |
|---|---|
Amount
|
A new :class: |
__post_init__()
¶
Coerce and validate the quantity.
int and str quantities are coerced to :class:~decimal.Decimal.
float (and bool) are rejected to prevent floating-point error
from entering the engine.
Raises:
| Type | Description |
|---|---|
InvalidAmountError
|
If the quantity is a float/bool or otherwise not convertible to a finite Decimal. |
__str__()
¶
Render the amount as <quantity> <commodity>.
Returns:
| Type | Description |
|---|---|
str
|
A human-readable string such as |
__sub__(other)
¶
is_zero(tolerance=None)
¶
of(quantity, code)
classmethod
¶
wealthbraid.engine.account
¶
Accounts: the five account types and the :class:Account value type.
Account names are hierarchical, colon-delimited paths whose first component is one of the five canonical roots (Assets, Liabilities, Equity, Income, Expenses). Names are validated against a strict grammar so that typos become errors rather than silently created accounts.
Account
dataclass
¶
A declared account in the chart of accounts.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The full colon-delimited account name. |
type |
AccountType
|
The account's root type, derived from |
metadata |
Mapping[str, str]
|
Arbitrary key/value metadata. |
aliases |
tuple[str, ...]
|
Alternative names that resolve to this account. |
tags |
frozenset[str]
|
Free-form tags attached to the account. |
AccountType
¶
Bases: Enum
The five roots of a double-entry chart of accounts.
The enum value is the canonical root name used as the first component of an account path.
is_debit_normal
property
¶
Report whether this account type increases on the debit side.
Assets and Expenses are debit-normal; Liabilities, Equity, and Income are credit-normal. This underpins report sign conventions.
Returns:
| Type | Description |
|---|---|
bool
|
|
from_root(root)
classmethod
¶
Return the account type for a root component name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
str
|
The first component of an account path (e.g. |
required |
Returns:
| Type | Description |
|---|---|
AccountType
|
The matching :class: |
Raises:
| Type | Description |
|---|---|
InvalidAccountNameError
|
If |
parse_account_name(name)
¶
Validate an account name and split it into its type and components.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The full colon-delimited account name. |
required |
Returns:
| Type | Description |
|---|---|
AccountType
|
A tuple of the account's :class: |
tuple[str, ...]
|
(including the root). |
Raises:
| Type | Description |
|---|---|
InvalidAccountNameError
|
If the name is empty, has an unknown root, or contains a malformed component. |
wealthbraid.engine.transaction
¶
Transactions and postings.
A :class:Transaction is a dated, balanced set of :class:Posting legs plus
metadata (payee, description, tags, links, attachments). Postings may leave the
amount elided (None); the balancing rules fill in at most one such amount.
Both types are immutable so that transformations produce new values and history
stays auditable.
Posting
dataclass
¶
A single leg of a transaction: an account and an optional amount.
Attributes:
| Name | Type | Description |
|---|---|---|
account |
str
|
The account name this leg posts to. |
amount |
Amount | None
|
The signed amount, or |
metadata |
Mapping[str, str]
|
Arbitrary key/value metadata for the leg. |
Transaction
dataclass
¶
A dated, balanced double-entry transaction.
Attributes:
| Name | Type | Description |
|---|---|---|
date |
date
|
The transaction date. |
postings |
tuple[Posting, ...]
|
The transaction's legs; at least one is required. |
payee |
str | None
|
The counterparty, if any. |
description |
str | None
|
A human-readable description of the transaction. |
tags |
frozenset[str]
|
Free-form tags. |
links |
frozenset[str]
|
Identifiers linking related transactions. |
attachments |
tuple[str, ...]
|
Paths to supporting documents. |
metadata |
Mapping[str, str]
|
Arbitrary key/value metadata (memo, notes, external ids). |
id |
str | None
|
An optional stable identifier assigned by higher layers. |
elided_postings
property
¶
__post_init__()
¶
Validate structural invariants and normalise the postings container.
Raises:
| Type | Description |
|---|---|
InvalidTransactionError
|
If there are no postings. |
with_postings(postings)
¶
Return a copy of this transaction with replaced postings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
postings
|
tuple[Posting, ...]
|
The new postings. |
required |
Returns:
| Type | Description |
|---|---|
Transaction
|
A new :class: |
wealthbraid.engine.balancing
¶
Transaction balancing: the central accounting invariant.
A transaction must sum to zero within each commodity independently. This module infers per-commodity tolerances from the precision of the amounts involved, fills in at most one elided posting amount, and verifies that every commodity nets to zero within tolerance.
With the default multiplier, balancing is exact. The inferred tolerance is
half of the last decimal place (0.5 * 10**-d for a commodity whose most
precise amount has d fractional digits), but every amount is a multiple of
10**-d, so any non-zero residual is at least 10**-d and always exceeds it.
Callers that want real slack (for example when importing rounded foreign
amounts) can pass a larger multiplier.
balance_transaction(transaction, *, multiplier=_DEFAULT_TOLERANCE_MULTIPLIER)
¶
Return a balanced copy of transaction, inferring any elided amount.
At most one posting may leave its amount elided; that posting absorbs the single-commodity residual so the transaction nets to zero. After filling, every commodity is verified to balance within the inferred tolerance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transaction
|
Transaction
|
The transaction to balance. |
required |
multiplier
|
Decimal
|
Tolerance multiplier passed to :func: |
_DEFAULT_TOLERANCE_MULTIPLIER
|
Returns:
| Type | Description |
|---|---|
Transaction
|
A new :class: |
Transaction
|
sum to zero within tolerance. |
Raises:
| Type | Description |
|---|---|
AmbiguousBalanceError
|
If more than one posting has an elided amount. |
UnresolvedElidedAmountError
|
If an elided amount cannot be inferred because the residual is empty or spans multiple commodities. |
BalanceError
|
If the transaction does not balance within tolerance. |
infer_tolerances(postings, *, multiplier=_DEFAULT_TOLERANCE_MULTIPLIER)
¶
Infer a per-commodity balancing tolerance from posting precision.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
postings
|
Iterable[Posting]
|
The postings to inspect (elided postings are ignored). |
required |
multiplier
|
Decimal
|
Fraction of the last significant place to allow; defaults to one half. |
_DEFAULT_TOLERANCE_MULTIPLIER
|
Returns:
| Type | Description |
|---|---|
dict[Commodity, Decimal]
|
A mapping of commodity to tolerance. Commodities whose amounts are all |
dict[Commodity, Decimal]
|
integers map to a tolerance of exactly zero. |
is_balanced(transaction, *, multiplier=_DEFAULT_TOLERANCE_MULTIPLIER)
¶
Report whether a transaction balances (allowing elided inference).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transaction
|
Transaction
|
The transaction to check. |
required |
multiplier
|
Decimal
|
Tolerance multiplier passed to :func: |
_DEFAULT_TOLERANCE_MULTIPLIER
|
Returns:
| Type | Description |
|---|---|
bool
|
|
residual(postings)
¶
wealthbraid.engine.inventory
¶
Multi-commodity balances: the :class:Inventory value type.
An inventory is a set of amounts keyed by commodity — the natural result of
summing postings that may span several currencies. It is immutable: combining
inventories yields new inventories. Zero balances are dropped so that a fully
offset commodity does not clutter reports, and :meth:amounts returns a
deterministic, commodity-sorted view.
Inventory
¶
An immutable collection of amounts, at most one per commodity.
Commodities whose net quantity is zero are not stored, so an empty inventory represents a fully balanced position.
__eq__(other)
¶
__hash__()
¶
Return a hash consistent with equality.
Returns:
| Type | Description |
|---|---|
int
|
A hash over the stored (commodity, quantity) balances. |
__init__(balances=None)
¶
__repr__()
¶
Return a debug representation listing the held amounts.
Returns:
| Type | Description |
|---|---|
str
|
A string such as |
add_amount(amount)
¶
amounts()
¶
from_amounts(amounts)
classmethod
¶
get(commodity)
¶
wealthbraid.engine.ledger
¶
The :class:Ledger aggregate: accounts, transactions, and balances.
The ledger is the in-memory root of the accounting model. It enforces the two structural invariants at the point of mutation: every posted account must have been declared (strict accounts), and every stored transaction must balance. It also computes account balances, including hierarchical roll-ups. It performs no I/O; persistence lives behind the storage ports.
Ledger
¶
A collection of declared accounts, balanced transactions, and prices.
__init__()
¶
Initialise an empty ledger.
account(name)
¶
account_balances()
¶
accounts()
¶
add_price(price)
¶
Record an exchange rate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price
|
Price
|
The price to add. |
required |
add_transaction(transaction)
¶
Balance, validate, and store a transaction.
The transaction is balanced (inferring at most one elided amount), every referenced account is checked against the chart of accounts, and the balanced transaction is appended to the ledger.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transaction
|
Transaction
|
The transaction to add. |
required |
Returns:
| Type | Description |
|---|---|
Transaction
|
The stored, balanced :class: |
Raises:
| Type | Description |
|---|---|
UndeclaredAccountError
|
If any posting references an undeclared account. |
AmbiguousBalanceError
|
If more than one posting elides its amount. |
UnresolvedElidedAmountError
|
If an elided amount cannot be inferred. |
BalanceError
|
If the transaction does not balance within tolerance. |
balance(name, *, include_subaccounts=True)
¶
Return the balance of an account, optionally rolling up descendants.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The account name to total. |
required |
include_subaccounts
|
bool
|
If true, include all descendant accounts. |
True
|
Returns:
| Type | Description |
|---|---|
Inventory
|
The combined :class: |
declare_account(account)
¶
Register an account in the chart of accounts.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
account
|
Account
|
The account to declare. |
required |
Raises:
| Type | Description |
|---|---|
DuplicateAccountError
|
If an account with the same name already exists. |
is_declared(name)
¶
price_db()
¶
Return a price database over the ledger's prices.
Returns:
| Name | Type | Description |
|---|---|---|
A |
PriceDB
|
class: |
prices()
¶
transactions()
¶
Return the stored transactions in insertion order.
Returns:
| Type | Description |
|---|---|
list[Transaction]
|
A shallow copy of the transaction list. |
wealthbraid.engine.prices
¶
Prices and cross-commodity valuation.
A :class:Price records an exchange rate for a commodity as of a date, for
example 1 EUR = 1.10 USD. A :class:PriceDB answers rate queries using the
most recent price on or before a date, considering both the direct rate and its
inverse. Valuation converts amounts and inventories into a target commodity;
commodities without a usable rate are left unconverted rather than silently
dropped, keeping conversions explicit.
Price
dataclass
¶
PriceDB
¶
A queryable collection of price records.
__init__(prices=None)
¶
add(price)
¶
Add a price record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
price
|
Price
|
The price to add. |
required |
latest(on)
¶
path(base, quote, on)
¶
Return the conversion steps from base to quote as of a date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base
|
Commodity
|
The commodity to convert from. |
required |
quote
|
Commodity
|
The commodity to convert to. |
required |
on
|
date
|
The as-of date. |
required |
Returns:
| Type | Description |
|---|---|
list[RateStep] | None
|
The steps in order (empty when |
prices()
¶
rate(base, quote, on)
¶
Return the rate to convert base into quote as of a date.
Rates chain transitively: if there is no direct (or inverse) price, the
shortest path through intermediate commodities is used (for example
VWCG → EUR → USD). Each edge uses the most recent price on or before
on. When several equal-length paths exist, neighbours are visited in
commodity-code order so the result is deterministic.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base
|
Commodity
|
The commodity to convert from. |
required |
quote
|
Commodity
|
The commodity to convert to. |
required |
on
|
date
|
The as-of date. |
required |
Returns:
| Type | Description |
|---|---|
Decimal | None
|
The conversion rate, or None if no path of prices connects them. |
RateStep
dataclass
¶
value_amount(amount, target, prices, on)
¶
Convert an amount into the target commodity as of a date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
amount
|
Amount
|
The amount to convert. |
required |
target
|
Commodity
|
The target commodity. |
required |
prices
|
PriceDB
|
The price database. |
required |
on
|
date
|
The as-of date. |
required |
Returns:
| Type | Description |
|---|---|
Amount | None
|
The converted amount, or None if no usable rate exists. |
value_inventory(inventory, target, prices, on)
¶
Convert an inventory into the target commodity where possible.
Amounts that cannot be converted (no usable rate) are retained in their original commodity, so the result is explicit about what was valued.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inventory
|
Inventory
|
The inventory to value. |
required |
target
|
Commodity
|
The target commodity. |
required |
prices
|
PriceDB
|
The price database. |
required |
on
|
date
|
The as-of date. |
required |
Returns:
| Type | Description |
|---|---|
Inventory
|
A new :class: |
Inventory
|
|