imp: cost: Allow matching equity conversion equity postings to

transaction prices.

When given --infer-costs, hledger will now separately infer transaction
prices for different prices. Given a pair of adjacent conversion
postings, hledger will check if there is a single posting with a
transaction price which matches both the amounts. If so, it associates
those conversion postings to that priced post.

If it can't find any transaction price postings which match, it will
find the first non-transaction price post which matches one of the two
amounts, and will add a transaction price to that, and associate them.
This commit is contained in:
Stephen Morgan 2022-02-09 12:53:56 +11:00 committed by Simon Michael
parent 07d1b01287
commit c54fb4d514
9 changed files with 568 additions and 293 deletions

View File

@ -97,16 +97,18 @@ transactionCheckBalanced BalancingOpts{commodity_styles_} t = errs
-- check for mixed signs, detecting nonzeros at display precision -- check for mixed signs, detecting nonzeros at display precision
canonicalise = maybe id canonicaliseMixedAmount commodity_styles_ canonicalise = maybe id canonicaliseMixedAmount commodity_styles_
postingBalancingAmount p
| "_price-matched" `elem` map fst (ptags p) = mixedAmountStripPrices $ pamount p
| otherwise = mixedAmountCost $ pamount p
signsOk ps = signsOk ps =
case filter (not.mixedAmountLooksZero) $ map (canonicalise.mixedAmountCost.pamount) ps of case filter (not.mixedAmountLooksZero) $ map (canonicalise.postingBalancingAmount) ps of
nonzeros | length nonzeros >= 2 nonzeros | length nonzeros >= 2
-> length (nubSort $ mapMaybe isNegativeMixedAmount nonzeros) > 1 -> length (nubSort $ mapMaybe isNegativeMixedAmount nonzeros) > 1
_ -> True _ -> True
(rsignsok, bvsignsok) = (signsOk rps, signsOk bvps) (rsignsok, bvsignsok) = (signsOk rps, signsOk bvps)
-- check for zero sum, at display precision -- check for zero sum, at display precision
(rsum, bvsum) = (sumPostings rps, sumPostings bvps) (rsumcost, bvsumcost) = (foldMap postingBalancingAmount rps, foldMap postingBalancingAmount bvps)
(rsumcost, bvsumcost) = (mixedAmountCost rsum, mixedAmountCost bvsum)
(rsumdisplay, bvsumdisplay) = (canonicalise rsumcost, canonicalise bvsumcost) (rsumdisplay, bvsumdisplay) = (canonicalise rsumcost, canonicalise bvsumcost)
(rsumok, bvsumok) = (mixedAmountLooksZero rsumdisplay, mixedAmountLooksZero bvsumdisplay) (rsumok, bvsumok) = (mixedAmountLooksZero rsumdisplay, mixedAmountLooksZero bvsumdisplay)

View File

@ -951,8 +951,10 @@ journalAddInferredEquityPostings j = journalMapTransactions (transactionAddInfer
equityAcct = journalConversionAccount j equityAcct = journalConversionAccount j
-- | Add inferred transaction prices from equity postings. -- | Add inferred transaction prices from equity postings.
journalAddPricesFromEquity :: Journal -> Journal journalAddPricesFromEquity :: Journal -> Either String Journal
journalAddPricesFromEquity j = journalMapTransactions (transactionAddPricesFromEquity $ jaccounttypes j) j journalAddPricesFromEquity j = do
ts <- mapM (transactionAddPricesFromEquity $ jaccounttypes j) $ jtxns j
return j{jtxns=ts}
-- -- | Get this journal's unique, display-preference-canonicalised commodities, by symbol. -- -- | Get this journal's unique, display-preference-canonicalised commodities, by symbol.
-- journalCanonicalCommodities :: Journal -> M.Map String CommoditySymbol -- journalCanonicalCommodities :: Journal -> M.Map String CommoditySymbol

View File

@ -423,22 +423,23 @@ postingApplyValuation priceoracle styles periodlast today v p =
postingToCost :: M.Map CommoditySymbol AmountStyle -> ConversionOp -> Posting -> Maybe Posting postingToCost :: M.Map CommoditySymbol AmountStyle -> ConversionOp -> Posting -> Maybe Posting
postingToCost _ NoConversionOp p = Just p postingToCost _ NoConversionOp p = Just p
postingToCost styles ToCost p postingToCost styles ToCost p
| ("_matched-conversion-posting","") `elem` ptags p = Nothing -- If this is a conversion posting with a matched transaction price posting, ignore it
| otherwise = Just $ postingTransformAmount (styleMixedAmount styles . mixedAmountCost) p | "_conversion-matched" `elem` map fst (ptags p) && noCost = Nothing
| otherwise = Just $ postingTransformAmount (styleMixedAmount styles . mixedAmountCost) p
where
noCost = null . filter (isJust . aprice) . amountsRaw $ pamount p
-- | Generate inferred equity postings from a 'Posting' using transaction prices. -- | Generate inferred equity postings from a 'Posting' using transaction prices.
-- Make sure not to generate equity postings when there are already matched -- Make sure not to generate equity postings when there are already matched
-- conversion postings. -- conversion postings.
postingAddInferredEquityPostings :: Text -> Posting -> [Posting] postingAddInferredEquityPostings :: Text -> Posting -> [Posting]
postingAddInferredEquityPostings equityAcct p postingAddInferredEquityPostings equityAcct p
| ("_matched-transaction-price","") `elem` ptags p = [p] | "_price-matched" `elem` map fst (ptags p) = [p]
| otherwise = taggedPosting : concatMap conversionPostings priceAmounts | otherwise = taggedPosting : concatMap conversionPostings priceAmounts
where where
taggedPosting taggedPosting
| null priceAmounts = p | null priceAmounts = p
| otherwise = p{ pcomment = pcomment p `commentAddTag` priceTag | otherwise = p{ ptags = ("_price-matched","") : ptags p }
, ptags = priceTag : ptags p
}
conversionPostings amt = case aprice amt of conversionPostings amt = case aprice amt of
Nothing -> [] Nothing -> []
Just _ -> [ cp{ paccount = accountPrefix <> amtCommodity Just _ -> [ cp{ paccount = accountPrefix <> amtCommodity
@ -453,7 +454,7 @@ postingAddInferredEquityPostings equityAcct p
amtCommodity = commodity amt amtCommodity = commodity amt
costCommodity = commodity cost costCommodity = commodity cost
cp = p{ pcomment = pcomment p `commentAddTag` ("generated-posting","") cp = p{ pcomment = pcomment p `commentAddTag` ("generated-posting","")
, ptags = [("generated-posting", ""), ("_generated-posting", "")] , ptags = [("_conversion-matched", ""), ("generated-posting", ""), ("_generated-posting", "")]
, pbalanceassertion = Nothing , pbalanceassertion = Nothing
, poriginal = Nothing , poriginal = Nothing
} }
@ -461,7 +462,6 @@ postingAddInferredEquityPostings equityAcct p
-- Take the commodity of an amount and collapse consecutive spaces to a single space -- Take the commodity of an amount and collapse consecutive spaces to a single space
commodity = T.unwords . filter (not . T.null) . T.words . acommodity commodity = T.unwords . filter (not . T.null) . T.words . acommodity
priceTag = ("cost", T.strip . wbToText $ foldMap showAmountPrice priceAmounts)
priceAmounts = filter (isJust . aprice) . amountsRaw $ pamount p priceAmounts = filter (isJust . aprice) . amountsRaw $ pamount p
-- | Make a market price equivalent to this posting's amount's unit -- | Make a market price equivalent to this posting's amount's unit

View File

@ -7,8 +7,10 @@ tags.
-} -}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Hledger.Data.Transaction module Hledger.Data.Transaction
( -- * Transaction ( -- * Transaction
@ -44,18 +46,23 @@ module Hledger.Data.Transaction
, showTransactionOneLineAmounts , showTransactionOneLineAmounts
, showTransactionLineFirstPart , showTransactionLineFirstPart
, transactionFile , transactionFile
-- * transaction errors
, annotateErrorWithTransaction
-- * tests -- * tests
, tests_Transaction , tests_Transaction
) where ) where
import Data.Bifunctor (second) import Control.Monad.Trans.State (StateT(..), evalStateT)
import Data.Maybe (fromMaybe, mapMaybe) import Data.Bifunctor (first)
import Data.Foldable (foldrM)
import Data.Maybe (fromMaybe, isJust, mapMaybe)
import Data.Semigroup (Endo(..))
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Map as M
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB import qualified Data.Text.Lazy.Builder as TB
import Data.Time.Calendar (Day, fromGregorian) import Data.Time.Calendar (Day, fromGregorian)
import qualified Data.Map as M
import Hledger.Utils import Hledger.Utils
import Hledger.Data.Types import Hledger.Data.Types
@ -219,34 +226,120 @@ transactionAddInferredEquityPostings :: AccountName -> Transaction -> Transactio
transactionAddInferredEquityPostings equityAcct t = transactionAddInferredEquityPostings equityAcct t =
t{tpostings=concatMap (postingAddInferredEquityPostings equityAcct) $ tpostings t} t{tpostings=concatMap (postingAddInferredEquityPostings equityAcct) $ tpostings t}
-- | Add inferred transaction prices from equity postings. The transaction -- | Add inferred transaction prices from equity postings. For every adjacent
-- price will be added to the first posting whose amount is the negation of one -- pair of conversion postings, it will first search the postings with
-- of the (exactly) two conversion postings, if it exists. -- transaction prices to see if any match. If so, it will tag it as matched.
transactionAddPricesFromEquity :: M.Map AccountName AccountType -> Transaction -> Transaction -- If no postings with transaction prices match, it will then search the
transactionAddPricesFromEquity acctTypes t -- postings without transaction prices, and will match the first such posting
| [(n1, cp1), (n2, cp2)] <- conversionps -- Exactly two conversion postings with indices -- which matches one of the conversion amounts. If it finds a match, it will
, Just ca1 <- maybePostingAmount cp1, Just ca2 <- maybePostingAmount cp2 -- Each conversion posting has exactly one amount -- add a transaction price and then tag it.
, (np,pricep):_ <- mapMaybe (maybeAddPrice ca1 ca2) npostings -- Get the first posting which matches one of the conversion postings type IdxPosting = (Int, Posting)
, let subPosting (n, p) = if n == np then pricep else if n == n1 then cp1 else if n == n2 then cp2 else p transactionAddPricesFromEquity :: M.Map AccountName AccountType -> Transaction -> Either String Transaction
= t{tpostings = map subPosting npostings} transactionAddPricesFromEquity acctTypes t = first (annotateErrorWithTransaction t . T.unpack) $ do
| otherwise = t (conversionPairs, stateps) <- partitionPs npostings
f <- transformIndexedPostingsF addPricesToPostings conversionPairs stateps
return t{tpostings = map (snd . f) npostings}
where where
maybeAddPrice a1 a2 (n,p) -- Include indices for postings
| Just a <- mpamt, amountMatches (-a1) a = Just (n, markPosting p{pamount = mixedAmount a{aprice = Just $ TotalPrice a2}})
| Just a <- mpamt, amountMatches (-a2) a = Just (n, markPosting p{pamount = mixedAmount a{aprice = Just $ TotalPrice a1}})
| otherwise = Nothing
where
mpamt = maybePostingAmount p
conversionps = map (second (`postingAddTags` [("_matched-conversion-posting","")]))
$ filter (\(_,p) -> M.lookup (paccount p) acctTypes == Just Conversion) npostings
markPosting = (`postingAddTags` [("_matched-transaction-price","")])
npostings = zip [0..] $ tpostings t npostings = zip [0..] $ tpostings t
transformIndexedPostingsF f = evalStateT . fmap (appEndo . foldMap Endo) . traverse f
-- Sort postings into pairs of conversion postings, transaction price postings, and other postings
partitionPs = fmap fst . foldrM select (([], ([], [])), Nothing)
select np@(_, p) ((cs, others@(ps, os)), Nothing)
| isConversion p = Right ((cs, others), Just np)
| hasPrice p = Right ((cs, (np:ps, os)), Nothing)
| otherwise = Right ((cs, (ps, np:os)), Nothing)
select np@(_, p) ((cs, others), Just last)
| isConversion p = Right (((last, np):cs, others), Nothing)
| otherwise = Left "Conversion postings must occur in adjacent pairs"
-- Given a pair of indexed conversion postings, and a state consisting of lists of
-- priced and unpriced non-conversion postings, create a function which adds transaction
-- prices to the posting which matches the conversion postings if necessary, and tags
-- the conversion and matched postings. Then update the state by removing the matched
-- postings. If there are no matching postings or too much ambiguity, return an error
-- string annotated with the conversion postings.
addPricesToPostings :: (IdxPosting, IdxPosting)
-> StateT ([IdxPosting], [IdxPosting]) (Either Text) (IdxPosting -> IdxPosting)
addPricesToPostings ((n1, cp1), (n2, cp2)) = StateT $ \(priceps, otherps) -> do
-- Get the two conversion posting amounts, if possible
ca1 <- postingAmountNoPrice cp1
ca2 <- postingAmountNoPrice cp2
let -- The function to add transaction prices and tag postings in the indexed list of postings
transformPostingF np pricep = \(n, p) ->
(n, if | n == np -> pricep `postingAddTags` [("_price-matched","")]
| n == n1 || n == n2 -> p `postingAddTags` [("_conversion-matched","")]
| otherwise -> p)
-- All priced postings which match the conversion posting pair
matchingPricePs = mapMaybe (mapM $ pricedPostingIfMatchesBothAmounts ca1 ca2) priceps
-- All other postings which match at least one of the conversion posting pair
matchingOtherPs = mapMaybe (mapM $ addPriceIfMatchesOneAmount ca1 ca2) otherps
-- Annotate any errors with the conversion posting pair
first (annotateWithPostings [cp1, cp2]) $
if -- If a single transaction price posting matches the conversion postings,
-- delete it from the list of priced postings in the state, delete the
-- first matching unpriced posting from the list of non-priced postings
-- in the state, and return the transformation function with the new state.
| [(np, (pricep, _))] <- matchingPricePs
, Just newpriceps <- deleteIdx np priceps
-> Right (transformPostingF np pricep, (newpriceps, otherps))
-- If no transaction price postings match the conversion postings, but some
-- of the unpriced postings match, check that the first such posting has a
-- different amount from all the others, and if so add a transaction price to
-- it, then delete it from the list of non-priced postings in the state, and
-- return the transformation function with the new state.
| [] <- matchingPricePs
, (np, (pricep, amt)):nps <- matchingOtherPs
, not $ any (amountMatches amt . snd . snd) nps
, Just newotherps <- deleteIdx np otherps
-> Right (transformPostingF np pricep, (priceps, newotherps))
-- Otherwise it's too ambiguous to make a guess, so return an error.
| otherwise -> Left "There is not a unique posting which matches the conversion posting pair:"
-- If a posting with transaction price matches both the conversion amounts, return it along
-- with the matching amount which must be present in another non-conversion posting.
pricedPostingIfMatchesBothAmounts :: Amount -> Amount -> Posting -> Maybe (Posting, Amount)
pricedPostingIfMatchesBothAmounts a1 a2 p = do
a@Amount{aprice=Just _} <- postingSingleAmount p
if | amountMatches (-a1) a && amountMatches a2 (amountCost a) -> Just (p, -a2)
| amountMatches (-a2) a && amountMatches a1 (amountCost a) -> Just (p, -a1)
| otherwise -> Nothing
-- Add a transaction price to a posting if it matches (negative) one of the
-- supplied conversion amounts, adding the other amount as the price
addPriceIfMatchesOneAmount :: Amount -> Amount -> Posting -> Maybe (Posting, Amount)
addPriceIfMatchesOneAmount a1 a2 p = do
a <- postingSingleAmount p
let newp price = p{pamount = mixedAmount a{aprice = Just $ TotalPrice price}}
if | amountMatches (-a1) a -> Just (newp a2, a2)
| amountMatches (-a2) a -> Just (newp a1, a1)
| otherwise -> Nothing
hasPrice p = isJust $ aprice =<< postingSingleAmount p
postingAmountNoPrice p = case postingSingleAmount p of
Just a@Amount{aprice=Nothing} -> Right a
_ -> Left $ annotateWithPostings [p] "The posting must only have a single amount with no transaction price"
postingSingleAmount p = case amountsRaw (pamount p) of
[a] -> Just a
_ -> Nothing
maybePostingAmount p = case amountsRaw $ pamount p of
[a@Amount{aprice=Nothing}] -> Just a
_ -> Nothing
amountMatches a b = acommodity a == acommodity b && aquantity a == aquantity b amountMatches a b = acommodity a == acommodity b && aquantity a == aquantity b
isConversion p = M.lookup (paccount p) acctTypes == Just Conversion
-- Delete a posting from the indexed list of postings based on either its
-- index or its posting amount.
-- Note: traversing the whole list to delete a single match is generally not efficient,
-- but given that a transaction probably doesn't have more than four postings, it should
-- still be more efficient than using a Map or another data structure. Even monster
-- transactions with up to 10 postings, which are generally not a good
-- idea, are still too small for there to be an advantage.
deleteIdx n = deleteUniqueMatch ((n==) . fst)
deleteUniqueMatch p (x:xs) | p x = if any p xs then Nothing else Just xs
| otherwise = (x:) <$> deleteUniqueMatch p xs
deleteUniqueMatch _ [] = Nothing
annotateWithPostings xs str = T.unlines $ str : postingsAsLines False xs
-- | Apply some account aliases to all posting account names in the transaction, as described by accountNameApplyAliases. -- | Apply some account aliases to all posting account names in the transaction, as described by accountNameApplyAliases.
-- This can fail due to a bad replacement pattern in a regular expression alias. -- This can fail due to a bad replacement pattern in a regular expression alias.
@ -268,6 +361,13 @@ transactionMapPostingAmounts f = transactionMapPostings (postingTransformAmount
transactionFile :: Transaction -> FilePath transactionFile :: Transaction -> FilePath
transactionFile Transaction{tsourcepos} = sourceName $ fst tsourcepos transactionFile Transaction{tsourcepos} = sourceName $ fst tsourcepos
-- Add transaction information to an error message.
annotateErrorWithTransaction :: Transaction -> String -> String
annotateErrorWithTransaction t s =
unlines [ sourcePosPairPretty $ tsourcepos t, s
, T.unpack . T.stripEnd $ showTransaction t
]
-- tests -- tests
tests_Transaction :: TestTree tests_Transaction :: TestTree

View File

@ -320,9 +320,9 @@ journalFinalise iopts@InputOpts{..} f txt pj = do
>>= (if auto_ && not (null $ jtxnmodifiers pj) >>= (if auto_ && not (null $ jtxnmodifiers pj)
then journalAddAutoPostings _ioDay balancingopts_ -- Add auto postings if enabled, and account tags if needed then journalAddAutoPostings _ioDay balancingopts_ -- Add auto postings if enabled, and account tags if needed
else pure) else pure)
>>= journalBalanceTransactions balancingopts_ -- Balance all transactions and maybe check balance assertions. >>= (if infer_costs_ then journalAddPricesFromEquity else pure) -- Add inferred transaction prices from equity postings, if present
<$> (if infer_costs_ then journalAddPricesFromEquity else id) -- Add inferred transaction prices from equity postings, if present >>= journalBalanceTransactions balancingopts_ -- Balance all transactions and maybe check balance assertions.
<&> (if infer_equity_ then journalAddInferredEquityPostings else id) -- Add inferred equity postings, after balancing transactions and generating auto postings <&> (if infer_equity_ then journalAddInferredEquityPostings else id) -- Add inferred equity postings, after balancing and generating auto postings
<&> journalInferMarketPricesFromTransactions -- infer market prices from commodity-exchanging transactions <&> journalInferMarketPricesFromTransactions -- infer market prices from commodity-exchanging transactions
when strict_ $ do when strict_ $ do
journalCheckAccounts j -- If in strict mode, check all postings are to declared accounts journalCheckAccounts j -- If in strict mode, check all postings are to declared accounts

View File

@ -14,6 +14,7 @@ import Data.Maybe (fromMaybe)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.IO as T import qualified Data.Text.IO as T
import Data.Time.Calendar (addDays) import Data.Time.Calendar (addDays)
import Lens.Micro ((^.))
import System.Console.CmdArgs.Explicit as C import System.Console.CmdArgs.Explicit as C
import Hledger import Hledger
@ -48,7 +49,7 @@ closemode = hledgerCommandMode
-- debugger, beware: close is incredibly devious. simple rules combine to make a horrid maze. -- debugger, beware: close is incredibly devious. simple rules combine to make a horrid maze.
-- tests are in hledger/test/close.test. -- tests are in hledger/test/close.test.
close CliOpts{rawopts_=rawopts, reportspec_=rspec'} j = do close copts@CliOpts{rawopts_=rawopts, reportspec_=rspec'} j = do
let let
-- show opening entry, closing entry, or (default) both ? -- show opening entry, closing entry, or (default) both ?
(opening, closing) = (opening, closing) =
@ -101,7 +102,7 @@ close CliOpts{rawopts_=rawopts, reportspec_=rspec'} j = do
openingdate = addDays 1 closingdate openingdate = addDays 1 closingdate
-- should we show the amount(s) on the equity posting(s) ? -- should we show the amount(s) on the equity posting(s) ?
explicit = boolopt "explicit" rawopts explicit = boolopt "explicit" rawopts || copts ^. infer_costs
-- the balances to close -- the balances to close
(acctbals',_) = balanceReport rspec j (acctbals',_) = balanceReport rspec j

View File

@ -75,11 +75,13 @@ printEntries opts@CliOpts{reportspec_=rspec} j =
entriesReportAsText :: CliOpts -> EntriesReport -> TL.Text entriesReportAsText :: CliOpts -> EntriesReport -> TL.Text
entriesReportAsText opts = entriesReportAsText opts =
TB.toLazyText . foldMap (TB.fromText . showTransaction . maybeStripPrices . whichtxn) TB.toLazyText . foldMap (TB.fromText . showTransaction . whichtxn)
where where
whichtxn whichtxn
-- With -x, use the fully-inferred txn with all amounts & txn prices explicit. -- With -x, use the fully-inferred txn with all amounts & txn prices explicit.
| boolopt "explicit" (rawopts_ opts) = id | boolopt "explicit" (rawopts_ opts) = id
-- With --show-costs, make txn prices explicit.
| opts ^. infer_costs = id
-- Or also, if any of -B/-V/-X/--value are active. -- Or also, if any of -B/-V/-X/--value are active.
-- Because of #551, and because of print -V valuing only one -- Because of #551, and because of print -V valuing only one
-- posting when there's an implicit txn price. -- posting when there's an implicit txn price.
@ -87,11 +89,6 @@ entriesReportAsText opts =
| has (value . _Just) opts = id | has (value . _Just) opts = id
-- By default, use the original as-written-in-the-journal txn. -- By default, use the original as-written-in-the-journal txn.
| otherwise = originalTransaction | otherwise = originalTransaction
maybeStripPrices
-- Strip prices when inferring equity, unless the show_costs_ is set
| opts ^. infer_equity && not (opts ^. show_costs) =
transactionTransformPostings postingStripPrices
| otherwise = id
-- Replace this transaction's postings with the original postings if any, but keep the -- Replace this transaction's postings with the original postings if any, but keep the
-- current possibly rewritten account names, and the inferred values of any auto postings -- current possibly rewritten account names, and the inferred values of any auto postings

View File

@ -993,7 +993,7 @@ Pro:
Con: Con:
- Disturbs the accounting equation - Disturbs the accounting equation without the --infer-equity flag
### Equity conversion ### Equity conversion
@ -1014,16 +1014,17 @@ Pro:
- Preserves the accounting equation - Preserves the accounting equation
- keeps track of conversions and related gains/losses in one place - keeps track of conversions and related gains/losses in one place
- works in any double entry accounting system - works in any double entry accounting system
- hledger can convert this to transaction prices using the --infer-costs flag
Con: Con:
- More verbose - More verbose
- conversion rate is not clear - conversion rate is not clear
- hledger can not do cost reporting - depends on the order of postings
### Priced equity conversion ### Priced equity conversion
Another possible notation would be to record both the conversion rate and the equity postings: Another notation is to record both the conversion rate and the equity postings:
```journal ```journal
2021-01-01 2021-01-01
@ -1033,7 +1034,19 @@ Another possible notation would be to record both the conversion rate and the eq
assets:cash 120 USD assets:cash 120 USD
``` ```
hledger currently does not allow this; instead, you can record the conversion rate as a comment. Pro:
- Preserves the accounting equation
- keeps track of conversions and related gains/losses in one place
- makes the conversion rate clear
- provides some error checking
- hledger can do cost reporting
Con:
- Most verbose
- Requires --infer-costs flag
- Not compatible with ledger
## Inferring missing conversion rates ## Inferring missing conversion rates
@ -1041,23 +1054,27 @@ hledger will do this automatically for implicit conversions. Currently it can no
## Inferring missing equity postings ## Inferring missing equity postings
With the `--infer-equity` flag, hledger will add equity postings to priced and implicit conversions (and move the conversion rate into a comment). With the `--infer-equity` flag, hledger will add equity postings to priced and implicit conversions.
## Inferring missing transaction prices from equity postings
With the `--infer-costs` flag, hledger will add transaction prices from equity postings, and will be able to handle transaction prices and equity postings together.
## Cost reporting ## Cost reporting
With the `-B/--cost` flag, hledger will convert the amounts in priced and implicit conversions to their cost in the other commodity. This is useful to see a report of what you paid for things (or how much you sold things for). Currently `-B/--cost` does not work on equity conversions, and it disables `--infer-equity`. With the `-B/--cost` flag, hledger will convert the amounts in priced and implicit conversions to their cost in the other commodity. This is useful to see a report of what you paid for things (or how much you sold things for). Currently `-B/--cost` does not work on equity conversions, and it disables `--infer-equity`.
These operations are transient, only affecting reports. If you want to change the journal file permanently, you could pipe each entry through These operations are transient, only affecting reports. If you want to change the journal file permanently, you could pipe each entry through
`hledger -f- -I print [-x] [--infer-equity] [-B]` `hledger -f- -I print [-x] [--infer-equity] [--infer-costs] [-B]`
## Conversion summary ## Conversion summary
- Recording the conversion rate is good because it makes that clear and allows cost reporting. - Recording the conversion rate is good because it makes that clear and allows cost reporting.
- Recording equity postings is good because it balances the accounting equation and is correct bookkeeping. - Recording equity postings is good because it balances the accounting equation and is correct bookkeeping.
- Combining these is not yet supported, so you have to choose. For now, priced conversions are a good compromise, so that: - Combining these is possible with the --infer-costs flag, but has certain requirements for the order of postings.
- When you want to see the cost (or sale proceeds) of things, use `-B/--cost`. - When you want to see the cost (or sale proceeds) of things, use `-B/--cost`.
- When you want to see a balanced balance sheet or correct journal entries, use `--infer-equity`. - When you want to see a balanced balance sheet or correct journal entries, use `--infer-equity`.
- Combining these is not yet supported; `-B/--cost` will take precedence. - `--cost` will remove any balancing equity posts, so as not to disturb the accounting equation.
- Conversion/cost operations are performed before valuation. - Conversion/cost operations are performed before valuation.
@ -2462,6 +2479,81 @@ $ hledger bal -N --flat -B
€100 assets:euros €100 assets:euros
``` ```
### Equity conversion postings
Transaction prices can be converted to and from equity conversion postings
using the `--infer-equity` and `--infer-costs` flags.
With `--infer-equity`, hledger will add equity postings to balance out any
transaction prices.
```journal
2009/1/1
assets:euros €100 @ $1.35 ; 100 euros bought
assets:dollars -$135 ; for $135
```
```shell
$ hledger print --infer-equity
2009-01-01
assets:euros €100 @ $1.35 ; 100 euros bought
equity:conversion:$-€:€ €-100 ; 100 euros bought, generated-posting:
equity:conversion:$-€:$ $135.00 ; 100 euros bought, generated-posting:
assets:dollars $-135 ; for $135
```
The reverse is possible using `--infer-costs`, which will check any equity
conversion postings and generate a transaction price for the _first_
non-conversion posting which matches.
```journal
2009-01-01
assets:euros €100 ; 100 euros bought
equity:conversion €-100
equity:conversion $135
assets:dollars $-135 ; for $135
```
```shell
$ hledger print --infer-costs
2009-01-01
assets:euros €100 @@ $135 ; 100 euros bought
equity:conversion €-100
equity:conversion $135
assets:dollars $-135 ; for $135
```
Note that the above will assign the transaction price to the first matching
posting in the transaction.
If you want to assign it to a different posting, or if you have several
different sets of conversion postings which must match different postings, you
must manually specify the transaction price.
If you do this, equity conversion postings must occur in adjacent pairs and
must exactly match the amount of a non-conversion posting.
```journal
2009-01-01
assets:dollars $-135 ; $135 paid
equity:conversion €-100
equity:conversion $135
assets:euros €100 @@ $135 ; to buy 100 euros
```
```journal
2009-01-01
assets:euros €100 @ $1.35 ; 100 euros bought
equity:conversion €-100
equity:conversion $135
assets:pounds £80 @@ $100 ; 80 pounds bought
equity:conversion £-80
equity:conversion $100
assets:dollars $-235 ; for $235 total
```
The account names used for the conversion accounts can be changed with the
[conversion account type declaration](#account-types).
## Lot prices, lot dates ## Lot prices, lot dates
Ledger allows another kind of price, Ledger allows another kind of price,

View File

@ -1,90 +1,63 @@
# price-related tests # price-related tests
# 1. print a transaction with an explicit unit price <
hledger -f- print --explicit
<<<
2011/01/01 2011/01/01
expenses:foreign currency €100 @ $1.35 expenses:foreign currency €100 @ $1.35
assets assets
>>>
# 1. print a transaction with an explicit unit price
$ hledger -f- print --explicit
2011-01-01 2011-01-01
expenses:foreign currency €100 @ $1.35 expenses:foreign currency €100 @ $1.35
assets $-135.00 assets $-135.00
>>>=0 >=0
# 2. -B/--cost converts to the price's commodity ("cost") # 2. -B/--cost converts to the price's commodity ("cost")
hledger -f- print --explicit --cost $ hledger -f- print --explicit --cost
<<<
2011/01/01
expenses:foreign currency €100 @ $1.35
assets
>>>
2011-01-01 2011-01-01
expenses:foreign currency $135.00 expenses:foreign currency $135.00
assets $-135.00 assets $-135.00
>>>=0 >=0
<
2011/01/01
expenses:foreign currency €100 @ $1.35
assets $-135.00
# 3. --infer-equity generates conversion postings # 3. --infer-equity generates conversion postings
hledger -f- print --infer-equity $ hledger -f- print --infer-equity
<<<
2011/01/01
expenses:foreign currency €100 @ $1.35
assets
>>>
2011-01-01 2011-01-01
expenses:foreign currency €100 ; cost: @ $1.35 expenses:foreign currency €100 @ $1.35
equity:conversion:$-€:€ €-100 ; generated-posting: equity:conversion:$-€:€ €-100 ; generated-posting:
equity:conversion:$-€:$ $135.00 ; generated-posting: equity:conversion:$-€:$ $135.00 ; generated-posting:
assets assets $-135.00
>>>=0 >=0
# 4. With --infer-equity and --show-costs, the cost is still shown # 4. With --cost, --infer-equity is ignored
hledger -f- print --infer-equity --show-costs $ hledger -f- print --cost --infer-equity
<<<
2011/01/01
expenses:foreign currency €100 @ $1.35
assets
>>>
2011-01-01
expenses:foreign currency €100 @ $1.35 ; cost: @ $1.35
equity:conversion:$-€:€ €-100 ; generated-posting:
equity:conversion:$-€:$ $135.00 ; generated-posting:
assets
>>>=0
# 5. With --cost, --infer-equity is ignored
hledger -f- print --explicit --cost --infer-equity
<<<
2011/01/01
expenses:foreign currency €100 @ $1.35
assets
>>>
2011-01-01 2011-01-01
expenses:foreign currency $135.00 expenses:foreign currency $135.00
assets $-135.00 assets $-135.00
>>>=0 >=0
# 6. print a transaction with a total price <
hledger -f - print --explicit
<<<
2011/01/01 2011/01/01
expenses:foreign currency €100 @@ $135 expenses:foreign currency €100 @@ $135
assets assets
>>>
# 5. print a transaction with a total price
$ hledger -f - print --explicit
2011-01-01 2011-01-01
expenses:foreign currency €100 @@ $135 expenses:foreign currency €100 @@ $135
assets $-135 assets $-135
>>>=0 >=0
# 7. when the balance has exactly two commodities, both unpriced, infer an # 6. when the balance has exactly two commodities, both unpriced, infer an
# implicit conversion price for the first one in terms of the second. # implicit conversion price for the first one in terms of the second.
hledger -f - print --explicit <
<<<
2011/01/01 2011/01/01
expenses:foreign currency €100 expenses:foreign currency €100
misc $2.1 misc $2.1
@ -92,7 +65,8 @@ hledger -f - print --explicit
misc €1 misc €1
misc €-1 misc €-1
misc $-2.1 misc $-2.1
>>>
$ hledger -f - print --explicit
2011-01-01 2011-01-01
expenses:foreign currency €100 @ $1.35 expenses:foreign currency €100 @ $1.35
misc $2.10 misc $2.10
@ -101,325 +75,432 @@ hledger -f - print --explicit
misc €-1 @ $1.35 misc €-1 @ $1.35
misc $-2.10 misc $-2.10
>>>=0 >=0
# 8. another, from ledger tests. Just one posting to price so uses @@. # 7. another, from ledger tests. Just one posting to price so uses @@.
hledger -f - print --explicit <
<<<
2002/09/30 * 1a1a6305d06ce4b284dba0d267c23f69d70c20be 2002/09/30 * 1a1a6305d06ce4b284dba0d267c23f69d70c20be
c56a21d23a6535184e7152ee138c28974f14280c 866.231000 GGGGG c56a21d23a6535184e7152ee138c28974f14280c 866.231000 GGGGG
a35e82730cf91569c302b313780e5895f75a62b9 $-17,783.72 a35e82730cf91569c302b313780e5895f75a62b9 $-17,783.72
>>>
$ hledger -f - print --explicit
2002-09-30 * 1a1a6305d06ce4b284dba0d267c23f69d70c20be 2002-09-30 * 1a1a6305d06ce4b284dba0d267c23f69d70c20be
c56a21d23a6535184e7152ee138c28974f14280c 866.231000 GGGGG @@ $17,783.72 c56a21d23a6535184e7152ee138c28974f14280c 866.231000 GGGGG @@ $17,783.72
a35e82730cf91569c302b313780e5895f75a62b9 $-17,783.72 a35e82730cf91569c302b313780e5895f75a62b9 $-17,783.72
>>>=0 >=0
# 9. when the balance has more than two commodities, don't bother # 8. when the balance has more than two commodities, don't bother
hledger -f - print <
<<<
2011/01/01 2011/01/01
expenses:foreign currency €100 expenses:foreign currency €100
assets $-135 assets $-135
expenses:other £200 expenses:other £200
>>>= !0 $ hledger -f - print
>2 /unbalanced transaction/
>= !0
# 10. another # 9. another
hledger -f - balance -B <
<<<
2011/01/01 2011/01/01
expenses:foreign currency €99 expenses:foreign currency €99
assets $-130 assets $-130
expenses:foreign currency €1 expenses:foreign currency €1
assets $-5 assets $-5
>>>
$ hledger -f - balance -B
$-135 assets $-135 assets
$135 expenses:foreign currency $135 expenses:foreign currency
-------------------- --------------------
0 0
>>>=0 >=0
# 11. transaction in two commodities should balance out properly # 10. transaction in two commodities should balance out properly
hledger -f - balance --cost <
<<<
2011/01/01 x 2011/01/01 x
a 10£ @@ 16$ a 10£ @@ 16$
b b
>>>
$ hledger -f - balance --cost
16$ a 16$ a
-16$ b -16$ b
-------------------- --------------------
0 0
>>>=0 >=0
# 12. --value=cost,XXX is deprecated, but should still work (for now) # 11. --value=cost,XXX is deprecated, but should still work (for now)
hledger -f - balance --value=cost,XXX $ hledger -f - balance --value=cost,XXX
<<<
2011/01/01 x
a 10£ @@ 16$
b
>>>
16$ a 16$ a
-16$ b -16$ b
-------------------- --------------------
0 0
>>>=0 >=0
# 13. conversion postings should be generated when called --infer-equity # 12. conversion postings should be generated when called --infer-equity
hledger -f - balance --infer-equity $ hledger -f - balance --infer-equity
<<<
2011/01/01 x
a 10£ @@ 16$
b
>>>
10£ a 10£ a
-16$ b -16$ b
16$ equity:conversion:$-£:$ 16$ equity:conversion:$-£:$
-10£ equity:conversion:$-£:£ -10£ equity:conversion:$-£:£
-------------------- --------------------
0 0
>>>=0 >=0
# 14. transaction should be left unbalanced when called without --cost or --infer-equity # 13. transaction should be left unbalanced when called without --cost or --infer-equity
hledger -f - balance $ hledger -f - balance
<<<
2011/01/01 x
a 10£ @@ 16$
b
>>>
10£ a 10£ a
-16$ b -16$ b
-------------------- --------------------
-16$ -16$
10£ 10£
>>>=0 >=0
# 15. When commodity price is specified implicitly, transaction should # 14. When commodity price is specified implicitly, transaction should
# be considered balanced out even when first amount is negative # be considered balanced out even when first amount is negative
# (that is, price for it should be determined properly, with proper sign) # (that is, price for it should be determined properly, with proper sign)
hledger -f - balance -N <
<<<
2011/01/01 x 2011/01/01 x
a -10£ a -10£
b 16$ b 16$
>>>
$ hledger -f - balance -N
-10£ a -10£ a
16$ b 16$ b
>>>=0 >=0
# 16. Should not infer prices when --strict is specified # 15. Should not infer prices when --strict is specified
hledger -f - balance --strict $ hledger -f - balance --strict
<<< >2 /unbalanced transaction/
2011/01/01 x >=1
a -10£
b 16$
>>>
>>>=1
# 17. When commodity price is specified implicitly, transaction should <
# NOT be considered balanced out when BOTH amounts are negative
hledger -f - balance
<<<
2011/01/01 x 2011/01/01 x
a -10£ a -10£
b -16$ b -16$
>>>
>>>=1
# 18. Differently-priced lots of a commodity should be merged in balance report # 16. When commodity price is specified implicitly, transaction should
hledger -f - balance # NOT be considered balanced out when BOTH amounts are negative
<<< $ hledger -f - balance
>2 /unbalanced transaction/
>=1
# 17. Differently-priced lots of a commodity should be merged in balance report
<
2011/1/1 2011/1/1
(a) £1 @ $2 (a) £1 @ $2
2011/1/1 2011/1/1
(a) £1 @ $3 (a) £1 @ $3
>>>
$ hledger -f - balance
£2 a £2 a
-------------------- --------------------
£2 £2
>>>=0 >=0
# 19. this should balance # 18. this should balance
hledger -f - balance <
<<<
2011/1/1 2011/1/1
a 1h @ $10 a 1h @ $10
b 1h @ $20 b 1h @ $20
c $-30 c $-30
>>>= 0
# 20. these balance because of the unit prices, and should parse successfully $ hledger -f - balance --no-total
hledger -f - balance --no-total 1h a
<<< 1h b
$-30 c
>= 0
# 19. these balance because of the unit prices, and should parse successfully
<
1/1 1/1
a 1X @ 2Y a 1X @ 2Y
a -2X @ 1Y a -2X @ 1Y
>>>
$ hledger -f - balance --no-total
-1X a -1X a
>>>= 0 >= 0
# 21. # 20.
hledger -f - balance --no-total -B $ hledger -f - balance --no-total -B
<<< >= 0
1/1
a 1X @ 2Y
a -2X @ 1Y
>>>
>>>= 0
# 22. likewise with total prices. Note how the primary amount's sign is used. # 21. likewise with total prices. Note how the primary amount's sign is used.
hledger -f - balance --no-total <
<<<
1/1 1/1
a 1X @@ 1Y a 1X @@ 1Y
a -2X @@ 1Y a -2X @@ 1Y
>>>
$ hledger -f - balance --no-total
-1X a -1X a
>>>= 0 >= 0
# 23. # 22.
hledger -f - balance --no-total -B $ hledger -f - balance --no-total -B
<<< >= 0
1/1
a 1X @@ 1Y
a -2X @@ 1Y
>>>
>>>= 0
# 24. here, a's primary amount is 0, and its cost is 1Y; b is the assigned auto-balancing amount of -1Y (per issue 69) # 23. here, a's primary amount is 0, and its cost is 1Y; b is the assigned auto-balancing amount of -1Y (per issue 69)
hledger -f - balance --no-total -E <
<<<
1/1 1/1
a 1X @@ 1Y a 1X @@ 1Y
a 1X @@ 1Y a 1X @@ 1Y
a -2X @@ 1Y a -2X @@ 1Y
b b
>>>
$ hledger -f - balance --no-total -E
0 a 0 a
-1Y b -1Y b
>>>= 0 >= 0
# 25. Without -E, a should be hidden because its balance is zero, even though it has a non-zero cost. # 24. Without -E, a should be hidden because its balance is zero, even though it has a non-zero cost.
hledger -f - balance --no-total $ hledger -f - balance --no-total
<<<
1/1
a 1X @@ 1Y
a 1X @@ 1Y
a -2X @@ 1Y
b
>>>
-1Y b -1Y b
>>>= 0 >= 0
# 26. the above with -B # 25. the above with -B
hledger -f - balance --no-total -E -B $ hledger -f - balance --no-total -E -B
<<<
1/1
a 1X @@ 1Y
a 1X @@ 1Y
a -2X @@ 1Y
b
>>>
1Y a 1Y a
-1Y b -1Y b
>>>= 0 >= 0
# 27. The equity account used by --infer-equity can be customised # 26. The equity account used by --infer-equity can be customised
hledger -f- print --infer-equity <
<<<
account equity:trades ; type:V account equity:trades ; type:V
2011/01/01 2011/01/01
expenses:foreign currency €100 @ $1.35 expenses:foreign currency €100 @ $1.35
assets assets
>>>
$ hledger -f- print --infer-equity
2011-01-01 2011-01-01
expenses:foreign currency €100 ; cost: @ $1.35 expenses:foreign currency €100 @ $1.35
equity:trades:$-€:€ €-100 ; generated-posting: equity:trades:$-€:€ €-100 ; generated-posting:
equity:trades:$-€:$ $135.00 ; generated-posting: equity:trades:$-€:$ $135.00 ; generated-posting:
assets assets
>>>=0 >=0
# 28. Inferred equity postings are generated early enough to match filters # 27. Inferred equity postings are generated early enough to match filters
hledger -f- areg --infer-equity equity:conversion <
<<<
2011/01/01 2011/01/01
expenses:foreign currency €100 @ $1.35 expenses:foreign currency €100 @ $1.35
assets assets
>>>
$ hledger -f- areg --infer-equity equity:conversion
Transactions in equity:conversion and subaccounts: Transactions in equity:conversion and subaccounts:
2011-01-01 ex:foreign currenc.. $135.00 $135.00 2011-01-01 ex:foreign currenc.. $135.00 $135.00
€-100 €-100 €-100 €-100
>>>=0 >=0
# 29. Infer cost when equity postings are present # 28. Infer cost with first matching posting when equity postings are present
hledger -f- print --cost --infer-costs <
<<<
2011/01/01 2011/01/01
expenses:foreign currency €100 expenses:foreign currency €100
equity:conversion €-100 equity:conversion €-100
equity:conversion $135 equity:conversion $135
assets assets $-135
>>>
$ hledger -f- print --infer-costs
2011-01-01
expenses:foreign currency €100 @@ $135
equity:conversion €-100
equity:conversion $135
assets $-135
>=0
# 29. Infer cost and convert to cost when equity postings are present
$ hledger -f- print --infer-costs --cost
2011-01-01 2011-01-01
expenses:foreign currency $135 expenses:foreign currency $135
assets assets $-135
>>>=0 >=0
# 30. Infer cost and show it when equity postings are present # 30. Do not infer equity postings when they are specified manually
hledger -f- print --show-costs --infer-costs $ hledger -f- print --infer-equity --infer-costs
<<<
2011/01/01
expenses:foreign currency €100
equity:conversion €-100
equity:conversion $135
assets
>>>
2011-01-01 2011-01-01
expenses:foreign currency €100 @@ $135 expenses:foreign currency €100 @@ $135
equity:conversion €-100 equity:conversion €-100
equity:conversion $135 equity:conversion $135
assets assets $-135
>>>=0 >=0
# 31. Do not infer equity postings when they are specified manually # 31. Inferred equity postings with non-standard conversion account
hledger -f- print --show-costs --infer-equity --infer-costs <
<<<
2011/01/01
expenses:foreign currency €100
equity:conversion €-100
equity:conversion $135
assets
>>>
2011-01-01
expenses:foreign currency €100 @@ $135
equity:conversion €-100
equity:conversion $135
assets
>>>=0
# 32. Inferred equity postings with non-standard conversion account
hledger -f- print --cost --infer-costs
<<<
account whoopwhoop ; type:V account whoopwhoop ; type:V
2011/01/01 2011/01/01
expenses:foreign currency €100 expenses:foreign currency €100
whoopwhoop €-100 whoopwhoop €-100
whoopwhoop $135 whoopwhoop $135
assets assets $-135
>>>
2011-01-01
expenses:foreign currency $135
assets
>>>=0 $ hledger -f- print --infer-costs
2011-01-01
expenses:foreign currency €100 @@ $135
whoopwhoop €-100
whoopwhoop $135
assets $-135
>=0
# 32. Can manually make another posting match
<
2011/01/01
assets $-135
equity:conversion €-100
equity:conversion $135
expenses:foreign currency €100 @@ $135
$ hledger -f- print --infer-costs
2011-01-01
assets $-135
equity:conversion €-100
equity:conversion $135
expenses:foreign currency €100 @@ $135
>=0
# 33. Can manually match lots of different posting groups so long as there is no conflict
<
2011/01/01
expenses:food €110 @@ £80
expenses:foreign currency €100 @@ $135
equity:conversion €-100
equity:conversion $135
equity:trades €-110
equity:trades £80
assets $-135
assets £-80
$ hledger -f- print --show-costs --infer-costs
2011-01-01
expenses:food €110 @@ £80
expenses:foreign currency €100 @@ $135
equity:conversion €-100
equity:conversion $135
equity:trades €-110
equity:trades £80
assets $-135
assets £-80
>=0
# 34. And convert to cost
$ hledger -f- print --cost --infer-costs --cost
2011-01-01
expenses:food £80
expenses:foreign currency $135
assets $-135
assets £-80
>=0
# 35. Transaction posts and equity conversion postings are fine
<
2011/01/01
expenses:foreign currency €100 @ $1.35
equity:conversion €-100
equity:conversion $135
assets $-135
$ hledger -f- print --infer-costs
2011-01-01
expenses:foreign currency €100 @ $1.35
equity:conversion €-100
equity:conversion $135
assets $-135
>=0
# 36. Conversion postings should come in adjacent pairs
<
2011/01/01
expenses:foreign currency €100 @@ $135
equity:conversion €-100
assets $-135
equity:conversion $135
$ hledger -f- print --infer-costs
>2 /Conversion postings must occur in adjacent pairs/
>=1
# 37. If a conversion pair matches several postings it should throw an error
<
2011/01/01
expenses:foreign currency €100 @@ $135
expenses:foreign trades €100 @@ $135
equity:conversion €-100
equity:conversion $135
assets €-100
assets $-135
$ hledger -f- print --infer-costs
>2 /There is not a unique posting which matches the conversion posting pair/
>=1
# 38. If a conversion pair does not match it should throw an error
<
2011/01/01
expenses:foreign currency €120
equity:conversion €-100
equity:conversion $135
assets:extra $20
assets €-20
assets $-155
$ hledger -f- print --infer-costs
>2 /There is not a unique posting which matches the conversion posting pair/
>=1
# 39. Multiple conversion pairs which match a single posting should cause an
# error, and should not match both sides of the conversion
<
2011/01/01
expenses:foreign currency €100
equity:conversion €-100
equity:conversion $135
equity:conversion €-100
equity:conversion $135
assets $-270
assets €100
$ hledger -f- print --infer-costs
>2 /There is not a unique posting which matches the conversion posting pair/
>=1
# 40. We can combine other amounts into one posting, if they still match up.
<
2011-01-01
expenses:foreign currency €100 @ $1.35
expenses:foreign currency £100 @ $1.36
expenses:foreign currency ¥1000 @@ €8.00
equity:conversion €-100
equity:conversion $135
equity:conversion £-100
equity:conversion $136
equity:conversion ¥-1000
equity:conversion €8.00
assets $-271
assets €-8.00
$ hledger -f- print --infer-costs
2011-01-01
expenses:foreign currency €100.00 @ $1.35
expenses:foreign currency £100 @ $1.36
expenses:foreign currency ¥1000 @@ €8.00
equity:conversion €-100.00
equity:conversion $135
equity:conversion £-100
equity:conversion $136
equity:conversion ¥-1000
equity:conversion €8.00
assets $-271
assets €-8.00
>=0
# # when the *cost-basis* balance has exactly two commodities, both # # when the *cost-basis* balance has exactly two commodities, both
# # unpriced, infer an implicit conversion price for the first one in terms # # unpriced, infer an implicit conversion price for the first one in terms