beancount / beanprice

Daily price quotes fetching library for plain-text accounting
GNU General Public License v2.0
66 stars 37 forks source link

Support non-unit currency price #72

Open upsuper opened 1 year ago

upsuper commented 1 year ago

For some of the commodities, quotes may not be on 1 unit of a currency. For example, this ETF on Yahoo is quoted in 0.01 GBP. If I put price: "GBP:yahoo/XSDR.L" in beancount file, the price fetched would be 100x larger than the actual price.

I've also considered to introduce a separate unit for it, e.g. CGBP, and assume it is 0.01 GBP, but the problem then becomes that there is no easy way to fetch the exchange rate for this artificial currency.

It would probably be better to extend the syntax and support non-unit currency price, for example, price: "0.01GBP:yahoo/XSDR.L", and convert the price accordingly.

upsuper commented 1 year ago

A workaround, for now, is to create a custom price source like

from beancount.prices.source import SourcePrice
from beancount.prices.sources import yahoo

def div_100(sp: SourcePrice) -> SourcePrice:
    return SourcePrice(sp.price / 100, sp.time, sp.quote_currency)

class Source(yahoo.Source):
    def get_latest_price(self, ticker):
        source_price = super().get_latest_price(ticker)
        return div_100(source_price)

    def get_historical_price(self, ticker, time):
        source_price = super().get_historical_price(ticker, time)
        return div_100(source_price)

(Change the import module from beancount.prices to beanprice for the beanprice package.)

ileodo commented 1 year ago

can be archived by a plugin:

__copyright__ = "Copyright (C) 2023-2023  iLeoDo"
__license__ = "GNU GPLv2"

from typing import Dict
import collections
from decimal import Decimal
from beancount.core.data import Commodity, Currency, Price
import beancount.core.amount

__plugins__ = ("price_multiplier",)

import logging
logger = logging.getLogger("price_multiplier")
OverrideError = collections.namedtuple("CommodityError", "source message entry")

def price_multiplier(entries, unused_options_map, config_str=None):

    multiplier_map :Dict[Currency, Decimal] = {}
    for entry in entries:
        if isinstance(entry, Commodity):
            multiplier = entry.meta.get("multiplier", None)
            if multiplier:
                multiplier_map[entry.currency] = Decimal(multiplier)

    def transform(entry):
        if isinstance(entry, Price):
            if entry.currency in multiplier_map:
                override_entry = Price(entry.meta, entry.date, entry.currency, beancount.core.amount.mul(entry.amount, multiplier_map[entry.currency]))
                return override_entry
        return entry

    ret_entries = [
        transform(entry)
        for entry in entries
    ]
    errors = []

    return ret_entries, errors

in your bean file

plugin "plugins.price_multiplier"

1970-01-01 commodity ABCDEFG
    price: "GBP:yahoo/ABCDEFG"
    multiplier: 0.01