ExLedger.Exchange (ex_ledger v0.5.6)
Currency conversion using price database.
Provides functions to convert amounts, postings, and transactions to a target currency using historical price data.
Limitations
Single-hop transitive paths: When no direct conversion exists (e.g., EUR→CHF), the module attempts conversion through one intermediate currency (EUR→USD→CHF). Multi-hop paths (EUR→USD→GBP→CHF) are not supported.
Commodity aliases: Commodity aliases parsed from declarations are not automatically resolved. Use canonical commodity symbols in transactions.
Summary
Functions
Checks if a conversion path exists between two currencies.
Converts a single amount to the target currency.
Converts a single posting to the target currency.
Converts all postings in a transaction to the target currency.
Converts a numeric value from one currency to another.
Returns all currencies that can be converted to the target currency.
Converts all transactions to a target currency.
Types
Functions
Checks if a conversion path exists between two currencies.
Returns true if there's a direct, inverse, or transitive path.
@spec convert_amount(amount(), String.t(), Date.t(), price_db()) :: {:ok, amount()} | {:error, {:no_conversion_path, String.t(), String.t()}}
Converts a single amount to the target currency.
Examples
iex> amount = %{value: 100.0, currency: "EUR", currency_position: :leading}
iex> Exchange.convert_amount(amount, "CHF", ~D[2026-01-15], price_db)
{:ok, %{value: 94.32, currency: "CHF", currency_position: :leading}}
@spec convert_posting(posting(), String.t(), Date.t(), price_db()) :: {:ok, posting()} | {:error, {:no_conversion_path, String.t(), String.t()}}
Converts a single posting to the target currency.
If the posting is already in the target currency, it's returned unchanged. If no direct conversion path exists, attempts transitive conversion.
@spec convert_transaction(transaction(), String.t(), price_db()) :: {:ok, transaction()} | {:error, {:no_conversion_path, String.t(), String.t()}}
Converts all postings in a transaction to the target currency.
Uses the transaction date for price lookup. Raises if transaction has no date.
@spec convert_value( Decimal.t() | number(), String.t(), String.t(), Date.t(), price_db() ) :: {:ok, Decimal.t()} | {:error, {:no_conversion_path, String.t(), String.t()}}
Converts a numeric value from one currency to another.
Attempts direct conversion first, then tries transitive paths.
Returns all currencies that can be converted to the target currency.
Includes currencies with direct, inverse, and transitive paths.
@spec exchange([transaction()], String.t(), price_db()) :: {:ok, [transaction()]} | {:error, {:no_conversion_path, String.t(), String.t()}}
Converts all transactions to a target currency.
This is the main entry point for currency conversion, similar to
ledger-cli's --exchange flag.
Examples
iex> prices = [
...> %{date: ~D[2026-01-15], commodity: "EUR", price: %{value: 0.9432, currency: "CHF"}}
...> ]
iex> price_db = Price.build_price_db(prices)
iex> transactions = [%{date: ~D[2026-01-15], postings: [...]}]
iex> Exchange.exchange(transactions, "CHF", price_db)
{:ok, converted_transactions}