diff --git a/hledger-lib/Hledger/Data/Amount.hs b/hledger-lib/Hledger/Data/Amount.hs index c8d1df312..639deeb7d 100644 --- a/hledger-lib/Hledger/Data/Amount.hs +++ b/hledger-lib/Hledger/Data/Amount.hs @@ -67,6 +67,7 @@ module Hledger.Data.Amount ( amountLooksZero, divideAmount, multiplyAmount, + invertAmount, -- ** styles amountstyle, canonicaliseAmount, @@ -89,6 +90,7 @@ module Hledger.Data.Amount ( showAmountDebug, showAmountWithoutPrice, amountSetPrecision, + amountSetPrecisionMin, withPrecision, amountSetFullPrecision, amountSetFullPrecisionUpTo, @@ -335,6 +337,10 @@ divideAmount n = transformAmount (/n) multiplyAmount :: Quantity -> Amount -> Amount multiplyAmount n = transformAmount (*n) +-- | Invert an amount (replace its quantity q with 1/q). +invertAmount :: Amount -> Amount +invertAmount a@Amount{aquantity=q} = a{aquantity=1/q} + -- | Is this amount negative ? The price is ignored. isNegativeAmount :: Amount -> Bool isNegativeAmount Amount{aquantity=q} = q < 0 @@ -375,6 +381,12 @@ withPrecision = flip amountSetPrecision amountSetPrecision :: AmountPrecision -> Amount -> Amount amountSetPrecision p a@Amount{astyle=s} = a{astyle=s{asprecision=p}} +-- | Ensure an amount's display precision is at least the given minimum precision. +-- Always sets an explicit Precision. +amountSetPrecisionMin :: Word8 -> Amount -> Amount +amountSetPrecisionMin minp a = amountSetPrecision p a + where p = Precision $ max minp (amountDisplayPrecision a) + -- | Increase an amount's display precision, if needed, to enough decimal places -- to show it exactly (showing all significant decimal digits, without trailing zeros). -- If the amount's display precision is unset, it will be treated as precision 0. @@ -784,14 +796,12 @@ isNegativeMixedAmount m = _ -> Nothing -- multiple amounts with different signs -- | Does this mixed amount appear to be zero when rendered with its display precision? --- i.e. does it have zero quantity with no price, zero quantity with a total price (which is also zero), --- and zero quantity for each unit price? +-- See amountLooksZero. mixedAmountLooksZero :: MixedAmount -> Bool mixedAmountLooksZero (Mixed ma) = all amountLooksZero ma -- | Is this mixed amount exactly zero, ignoring its display precision? --- i.e. does it have zero quantity with no price, zero quantity with a total price (which is also zero), --- and zero quantity for each unit price? +-- See amountIsZero. mixedAmountIsZero :: MixedAmount -> Bool mixedAmountIsZero (Mixed ma) = all amountIsZero ma diff --git a/hledger-lib/Hledger/Data/Valuation.hs b/hledger-lib/Hledger/Data/Valuation.hs index a93f6500b..1bde2d257 100644 --- a/hledger-lib/Hledger/Data/Valuation.hs +++ b/hledger-lib/Hledger/Data/Valuation.hs @@ -203,8 +203,12 @@ amountValueAtDate priceoracle styles mto d a = -- The display precision will be that of nullamt (0). -- Now apply the standard display style for comm & styleAmounts styles - -- and set the display precision to rate's (internal) precision + -- and set the display precision to rate's internal precision + -- XXX (unnormalised - don't strip trailing zeros) ? + -- XXX valuation.test:8 why is it showing precision 1 ? & amountSetPrecision (Precision $ decimalPlaces $ normalizeDecimal rate) + -- or at least 1, ensuring we show at least one decimal place. + -- & amountSetPrecision (Precision $ max 1 (decimalPlaces $ normalizeDecimal rate)) -- see also print-styles.test, valuation2.test -- | Calculate the gain of each component amount, that is the difference @@ -447,6 +451,8 @@ makePriceGraph alldeclaredprices allinferredprices d = ,pgDefaultValuationCommodities=defaultdests } where + -- XXX logic duplicated in Hledger.Cli.Commands.Prices.prices, keep synced + -- prices in effect on date d, either declared or inferred visibledeclaredprices = dbg9 "visibledeclaredprices" $ filter ((<=d).mpdate) alldeclaredprices visibleinferredprices = dbg9 "visibleinferredprices" $ filter ((<=d).mpdate) allinferredprices diff --git a/hledger/Hledger/Cli/Commands/Prices.hs b/hledger/Hledger/Cli/Commands/Prices.hs index 2735d4b2f..b8509c953 100644 --- a/hledger/Hledger/Cli/Commands/Prices.hs +++ b/hledger/Hledger/Cli/Commands/Prices.hs @@ -1,5 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE NamedFieldPuns #-} module Hledger.Cli.Commands.Prices ( pricesmode @@ -7,78 +8,97 @@ module Hledger.Cli.Commands.Prices ( ) where -import qualified Data.Map as M import Data.List import qualified Data.Text as T import qualified Data.Text.IO as T import Hledger import Hledger.Cli.CliOptions import System.Console.CmdArgs.Explicit +import Data.Maybe (mapMaybe) pricesmode = hledgerCommandMode $(embedFileRelative "Hledger/Cli/Commands/Prices.txt") - [flagNone ["infer-reverse-prices"] (setboolopt "infer-reverse-prices") "also show prices obtained by inverting transaction prices" + [flagNone ["show-reverse"] (setboolopt "show-reverse") + "also show the prices inferred by reversing known prices" ] [generalflagsgroup1] (hiddenflags ++ - [flagNone ["costs"] (setboolopt "infer-market-prices") "deprecated, use --infer-market-prices instead" - ,flagNone ["inverted-costs"] (setboolopt "infer-reverse-prices") "deprecated, use --infer-reverse-prices instead" + [flagNone ["costs"] (setboolopt "infer-market-prices") "deprecated, use --infer-market-prices instead" + ,flagNone ["inverted-costs"] (setboolopt "show-reverse") "deprecated, use --show-reverse instead" + ,flagNone ["infer-reverse-prices"] (setboolopt "show-reverse") "deprecated, use --show-reverse instead" ]) ([], Just $ argsFlag "[QUERY]") --- XXX the original hledger-prices script always ignored assertions +instance HasAmounts PriceDirective where + styleAmounts styles pd = pd{pdamount=styleAmounts styles $ pdamount pd} + +-- List market prices. prices opts j = do let - styles = journalCommodityStyles j - q = _rsQuery $ reportspec_ opts - ps = filter (matchesPosting q) $ allPostings j - mprices = jpricedirectives j - cprices = - map (stylePriceDirectiveExceptPrecision styles) $ - concatMap postingPriceDirectivesFromCost ps - rcprices = - map (stylePriceDirectiveExceptPrecision styles) $ - concatMap (postingPriceDirectivesFromCost . postingTransformAmount (mapMixedAmount invertPrice)) - ps - allprices = - mprices - ++ ifBoolOpt "infer-market-prices" cprices - ++ ifBoolOpt "infer-reverse-prices" rcprices -- TODO: shouldn't this show reversed P prices also ? valuation will use them + styles = journalCommodityStyles j + q = _rsQuery $ reportspec_ opts - mapM_ (T.putStrLn . showPriceDirective) $ - sortOn pddate $ - filter (matchesPriceDirective q) $ - allprices + -- XXX duplicates logic in Hledger.Data.Valuation.makePriceGraph, keep synced + + declaredprices = + -- dbg0 "declaredprices" $ + jpricedirectives j + + pricesfromcosts = + -- dbg0 "pricesfromcosts" $ + concatMap postingPriceDirectivesFromCost $ + journalPostings j + + forwardprices = + -- dbg0 "forwardprices" $ + if boolopt "infer-market-prices" (rawopts_ opts) + then declaredprices `mergePriceDirectives` pricesfromcosts + else declaredprices + + reverseprices = + -- dbg0 "reverseprices" $ + mapMaybe reversePriceDirective forwardprices + + allprices = + -- dbg0 "allprices" $ + if boolopt "show-reverse" (rawopts_ opts) + then forwardprices `mergePriceDirectives` reverseprices + else forwardprices + + filteredprices = + -- dbg0 "filtered unsorted" $ + filter (matchesPriceDirective q) allprices + + mapM_ (T.putStrLn . showPriceDirective . styleAmounts styles) $ + sortOn pddate filteredprices + +-- XXX performance +-- | Append any new price directives (with different from commodity, +-- to commodity, or date) from the second list to the first. +-- (Does not remove redundant prices from the first; just avoids adding more.) +mergePriceDirectives :: [PriceDirective] -> [PriceDirective] -> [PriceDirective] +mergePriceDirectives pds1 pds2 = + pds1 ++ [ pd | pd <- pds2 , pdid pd `notElem` pds1ids ] where - ifBoolOpt opt | boolopt opt $ rawopts_ opts = id - | otherwise = const [] + pds1ids = map pdid pds1 + pdid PriceDirective{pddate,pdcommodity,pdamount} = (pddate, pdcommodity, acommodity pdamount) showPriceDirective :: PriceDirective -> T.Text -showPriceDirective mp = T.unwords ["P", T.pack . show $ pddate mp, quoteCommoditySymbolIfNeeded $ pdcommodity mp, wbToText . showAmountB noColour{displayZeroCommodity=True} $ pdamount mp] +showPriceDirective mp = T.unwords [ + "P", + T.pack . show $ pddate mp, + quoteCommoditySymbolIfNeeded $ pdcommodity mp, + wbToText . showAmountB noColour{displayZeroCommodity=True} $ pdamount mp + ] --- XXX - --- | Invert an amount's price for --invert-cost, somehow ? Unclear. -invertPrice :: Amount -> Amount -invertPrice a = - case aprice a of - Nothing -> a - Just (UnitPrice pa) -> invertPrice - -- normalize to TotalPrice - a { aprice = Just $ TotalPrice pa' } where - pa' = ((1 / aquantity a) `divideAmount` pa) { aprice = Nothing } - Just (TotalPrice pa) -> - a { aquantity = aquantity pa * nonZeroSignum (aquantity a), acommodity = acommodity pa, aprice = Just $ TotalPrice pa' } where - pa' = pa { aquantity = abs $ aquantity a, acommodity = acommodity a, aprice = Nothing, astyle = astyle a } - where - nonZeroSignum x = if x < 0 then -1 else 1 - --- | Given a map of standard amount display styles, apply the --- appropriate one, if any, to this price directive's amount. --- But keep the number of decimal places unchanged. -stylePriceDirectiveExceptPrecision :: M.Map CommoditySymbol AmountStyle -> PriceDirective -> PriceDirective -stylePriceDirectiveExceptPrecision styles pd@PriceDirective{pdamount=a} = - pd{pdamount = styleAmounts styles a} - -allPostings :: Journal -> [Posting] -allPostings = concatMap tpostings . jtxns +-- | Convert a market price directive to a corresponding one in the +-- opposite direction, if possible. (A price directive specifying zero +-- as the price can't be reversed.) +-- The display precision is set to show all significant decimal digits, +-- up to a maximum of 8 (this is visible eg in the prices command's output). +reversePriceDirective :: PriceDirective -> Maybe PriceDirective +reversePriceDirective pd@PriceDirective{pdcommodity=c, pdamount=a} + | amountIsZero a = Nothing + | otherwise = + Just pd{pdcommodity=acommodity a, pdamount=setprec $ invertAmount a{acommodity=c}} + where setprec = amountSetFullPrecisionUpTo 8 diff --git a/hledger/Hledger/Cli/Commands/Prices.md b/hledger/Hledger/Cli/Commands/Prices.md index ca09306fc..666a95cd5 100644 --- a/hledger/Hledger/Cli/Commands/Prices.md +++ b/hledger/Hledger/Cli/Commands/Prices.md @@ -1,9 +1,17 @@ ## prices -Print [market price directives](https://hledger.org/hledger.html#market-prices) from the journal. -With --infer-market-prices, generate additional market prices from [costs](https://hledger.org/hledger.html#costs). -With --infer-reverse-prices, also generate market prices by inverting known prices. -Prices can be filtered by a query. -Price amounts are displayed with their full precision. +Print the [market prices](hledger.md#p-directive) declared with P directives. +With --infer-market-prices, also show any additional prices inferred from [costs](hledger.md#costs). +With --show-reverse, also show additional prices inferred by reversing known prices. + +Price amounts are always displayed with their full precision, +except for reverse prices which are limited to 8 decimal digits. + +Prices can be filtered by a date:, cur: or amt: query. + +Generally if you run this command with --infer-market-prices --show-reverse, +it will show the same prices used internally to calculate value reports. +But if in doubt, you can inspect those directly by running the value report +with --debug=2. _FLAGS diff --git a/hledger/hledger.m4.md b/hledger/hledger.m4.md index e09577666..5e7e81c2e 100644 --- a/hledger/hledger.m4.md +++ b/hledger/hledger.m4.md @@ -5231,9 +5231,7 @@ the `V` key always resets it to "end".) To convert a commodity A to its market value in another commodity B, hledger looks for a suitable market price (exchange rate) as follows, -in this order of preference - -: +in this order of preference: 1. A *declared market price* or *inferred market price*: A's latest market price in B on or before the valuation date diff --git a/hledger/test/prices.test b/hledger/test/prices.test index 6deb1f034..5610de8f6 100644 --- a/hledger/test/prices.test +++ b/hledger/test/prices.test @@ -1,92 +1,119 @@ # * prices command -# ** 1. by default only market prices are reported +# ** 1. By default it lists market prices declared with P directives. +# It shows them with their original precisions, ignoring commodity display styles. +# Redundant P directives on the same date are shown (though only the last is used in value reports). < -P 2016/1/1 EUR $1.06 -P 2016/2/1 EUR $1.05 +P 2023-01-01 B A10 +P 2023-01-01 B A100 +P 2023-01-01 A B100 -2016/1/1 paycheck - income:remuneration $-100 - income:donations $-15 - assets:bank +2023-01-02 + (a) A2 @ B1 -2016/1/2 spend - expenses 20 EUR @ $1.07 - assets:bank -$ hledger prices -f- -P 2016-01-01 EUR $1.06 -P 2016-02-01 EUR $1.05 +2023-01-02 + (a) B1 @ A200 -# ** 2. costs from postings can be included also -< -P 2016/1/1 EUR $1.06 -P 2016/2/1 EUR $1.05 +2023-01-03 + a B1 + b A-3 -2016/1/1 paycheck - income:remuneration $-100 - income:donations $-15 - assets:bank +2023-01-04 + (a) B1 @@ A4 -2016/1/2 spend - expenses 20 EUR @ $1.07 - assets:bank +2023-01-05 + (a) B-1 @@ A5 -2016/1/3 spend - expenses 20 EUR @@ $21.45 - assets:bank +2023-01-06 + a B1 + b + +2023-01-07 + (a) B1 @ A7 -2016/1/4 spend - expenses -20 EUR @@ $21.45 - assets:bank +P 2023-01-07 B A70 + +$ hledger prices -f- -c A1.00 +P 2023-01-01 B A10 +P 2023-01-01 B A100 +P 2023-01-01 A B100 +P 2023-01-07 B A70 + +# ** 2. With --show-reverse it also lists any new prices inferred by inverting known prices. +# Reverse prices are shown with all of their decimal digits up to a maximum of 8. +# Redundant reverse prices are discarded. +$ hledger prices -f- --show-reverse +P 2023-01-01 B A10 +P 2023-01-01 B A100 +P 2023-01-01 A B100 +P 2023-01-07 B A70 +P 2023-01-07 A B0.01428571 + +# ** 3. With --infer-market-prices it also lists prices inferred from costs +# (explicit or inferred, unit or total, positive or negative amounts). +# Redundant prices inferred from costs are discarded. +# ** XXX why precision 1 for 3/4/5 ? $ hledger prices -f- --infer-market-prices -P 2016-01-01 EUR $1.06 -P 2016-01-02 EUR $1.07 -P 2016-01-03 EUR $1.0725 -P 2016-01-04 EUR $1.0725 -P 2016-02-01 EUR $1.05 +P 2023-01-01 B A10 +P 2023-01-01 B A100 +P 2023-01-01 A B100 +P 2023-01-02 A B1 +P 2023-01-02 B A200 +P 2023-01-03 B A3.0 +P 2023-01-04 B A4.0 +P 2023-01-05 B A5.0 +P 2023-01-07 B A70 -# ** 3. inverted prices can be calculated +# ** 4. --infer-market-prices and --show-reverse combine. +$ hledger prices -f- --infer-market-prices --show-reverse +P 2023-01-01 B A10 +P 2023-01-01 B A100 +P 2023-01-01 A B100 +P 2023-01-02 A B1 +P 2023-01-02 B A200 +P 2023-01-03 B A3.0 +P 2023-01-03 A B0.33333333 +P 2023-01-04 B A4.0 +P 2023-01-04 A B0.25 +P 2023-01-05 B A5.0 +P 2023-01-05 A B0.2 +P 2023-01-07 B A70 +P 2023-01-07 A B0.01428571 + +# ** 5. Prices can be filtered by a date: query. +$ hledger prices -f- --infer-market-prices --show-reverse date:2023-01-01 +P 2023-01-01 B A10 +P 2023-01-01 B A100 +P 2023-01-01 A B100 + +# ** 6. Prices can be filtered by a cur: query. +$ hledger prices -f- --infer-market-prices --show-reverse cur:A +P 2023-01-01 A B100 +P 2023-01-02 A B1 +P 2023-01-03 A B0.33333333 +P 2023-01-04 A B0.25 +P 2023-01-05 A B0.2 +P 2023-01-07 A B0.01428571 + +# ** 7. Prices can be filtered by a amt: query. +$ hledger prices -f- --infer-market-prices --show-reverse amt:100 +P 2023-01-01 B A100 +P 2023-01-01 A B100 + +# ** 8. Zero prices, which can't be reversed, are not reversed. < -P 2016/1/1 EUR $1.06 -P 2016/2/1 EUR $1.05 +P 2021-10-16 B 0.0 A +$ hledger -f- prices --show-reverse +P 2021-10-16 B 0.0 A -2016/1/1 paycheck - income:remuneration $-100 - income:donations $-15 - assets:bank - -2016/1/3 spend - expenses $21.45 @@ 20.00 EUR - assets:bank -$ hledger prices -f- --infer-reverse-prices -P 2016-01-01 EUR $1.06 -P 2016-01-03 EUR $1.0725 -P 2016-02-01 EUR $1.05 - -# +# ** 9. Zero postings, which can't determine a price, are ignored. < -commodity 1.000,00 A +2021-10-16 + (b) 0 A @@ 1 B -P 2019-01-01 X A1000,123 -P 2019-01-02 X A1000,1 - -2019-02-01 - (a) X1 @ A1000,2345 -2019-02-02 - (a) X1 @ A1000,2 - -# ** 4. Commodity styles are applied, but precision is left unchanged. -$ hledger -f- prices -P 2019-01-01 X 1.000,123 A -P 2019-01-02 X 1.000,1 A - -# ** 5. Commodity styles aren't yet applied to prices inferred from transaction prices. $ hledger -f- prices --infer-market-prices -P 2019-01-01 X 1.000,123 A -P 2019-01-02 X 1.000,1 A -P 2019-02-01 X 1.000,2345 A -P 2019-02-02 X 1.000,2 A +# ** 10. Inferring prices should play well with balance assertions mixing prices and no prices. (#1736) < ;; Total asset value should be 400 USD + 1000 USD = 1400 USD 2021-10-15 Broker initial balance (equity ABC) @@ -97,22 +124,47 @@ P 2019-02-02 X 1.000,2 A Assets:Broker = 1000 USD Equity:Opening Balances -# ** 6. Inferring prices should play well with balance assertions involving mixing -# of prices and no prices. (#1736) $ hledger -f- prices --infer-market-prices P 2021-10-15 ABC 100.0 USD +# ** 11. Commodity styles are applied to all price amounts, but their precision is left unchanged. < -2021-10-15 - (a) 1 A @@ 0 B +commodity 1.000,00 A -2021-10-16 - (b) 0 A @@ 1 B +P 2019-01-01 X A1000,123 +P 2019-01-02 X A1000,1 -# ** 7. Gracefully ignore any postings which would result in an infinite price. -$ hledger -f- prices --infer-market-prices -P 2021-10-15 A 0.0 B +2019-02-01 + (a) X1 @ A1000,2345 + +2019-02-02 + (a) X1 @ A1000,2 + +2019-03-01 + (a) 20 EUR @ $1.07 + +$ hledger -f- prices --infer-market-prices --show-reverse +P 2019-01-01 X 1.000,123 A +P 2019-01-01 A X0.00099988 +P 2019-01-02 X 1.000,1 A +P 2019-01-02 A X0.00099990 +P 2019-02-01 X 1.000,2345 A +P 2019-02-01 A X0.00099977 +P 2019-02-02 X 1.000,2 A +P 2019-02-02 A X0.00099980 +P 2019-03-01 EUR $1.07 +P 2019-03-01 $ 0.93457944 EUR + +# ** 12. Reverse market prices are shown with all decimal digits, up to a maximum of 8. +# ** XXX does this prevent displaying more precise prices ? +< +P 2023-01-01 B 3A + +2023-01-01 + a 1A + b + +$ hledger -f - prices --show-reverse +P 2023-01-01 B 3A +P 2023-01-01 A 0.33333333B -# ** 8. Same for reverse prices -$ hledger -f- prices --infer-reverse-prices -P 2021-10-16 B 0.0 A