I believe that is correct and not-a-bug.
Calling CONVERT() does not use the postings' price annotation to make the conversion.
It is called from here in your SQL statement:
that function:
If you inspect that function, you will find that it looks for a price annotation (the hasattr() call) to find out the currency, if present, but it uses the price map to make the conversion, without access to the price annotation from the posting.
Now, I traced the call and it indeed receives a Position instance, not a Posting.
If you're not familiar with the codebase, a Position is contained within a Posting.
A Posting is like a Position which has an account, a price annotation, and some metadata.
In fact, a number of functions accept either one of those.
(If you're familiar with OO, imagine that a Position is a (concrete) base class, and a Posting derived from it, though in this case it's not strictly true, it's all done via duck-typing, that is, Position just happens to have a strict subset of the attributes of Posting.)
If we drill down a bit, we will find that the SQL accessor for the "position" field synthesizes a Position object from the posting:
There is no corresponding accessor for Posting.
Now, it would be unwise to change the semantics of "position" here and potentially mess with everyone's numbers so I won't do that.
But I /could/ add a "posting" accessor very easily, which would allow you to choose.
(Documenting this would be no fun...)
Uh-oh... an hour later...
I started to quickly prototype this and an hour after I'm realizing it's more complicated than I though to add it, it's not a simple matter of adding accessors for posting. The behavior of the conversion in all possible cases is non-trivial and would require a lot of testing work -- I already sank an hour into this, this is probably worth a good afternoon's if not an entire day's work. Can't do it quickly right now, but I think it's doable. May need some serious revisiting of the overall semantics.)
Also, when CONVERT() converts through the cost basis (that is, units to cost to then foreign price), that's entirely done through the price database too, so potentially you're not getting the right value for cost, if you acquired multiple lots at different costs.
;; Showcasing that conversion via cost uses the price db.
plugin "beancount.plugins.auto_accounts"
plugin "beancount.plugins.sellgains"
plugin "beancount.plugins.implicit_prices"
2012-04-10 txn "My Broker"
Assets:Brokerage 5 AAPL {50.00 USD}
Assets:Brokerage:Cash -250.00 USD
2012-04-10 txn "My Broker"
Assets:Brokerage 5 AAPL {55.00 USD}
Assets:Brokerage:Cash -275.00 USD
2018-01-01 query "detail" "
SELECT
date,
account,
position,
COST(position),
CONVERT(position, 'USD', date),
CONVERT(COST(position), 'USD', date)
WHERE
date < 2018-03-01 AND
account ~ '^Assets:Brokerage$'
"
Output:
bean-query twocostssameday.beancount run detail
date account position cost_posit convert_po convert_co
---------- ---------------- ------------------ ---------- ---------- ----------
2012-04-10 Assets:Brokerage 5 AAPL {50.00 USD} 250.00 USD 275.00 USD 250.00 USD
2012-04-10 Assets:Brokerage 5 AAPL {55.00 USD} 275.00 USD 275.00 USD 275.00 USD
Notice how the middle column has the same cost basis here, because it was automatically converted via CONVERT() (you didn't ask for the cost basis when you said just CONVERT()).
Overall, there are many cases to handle and it's a fair amount of complexity to handle it all "as automatically as possible."
I think I would restrict the semantics in a reimplementation.