beancount.prices

Fetch prices from the internet and output them as Beancount price directives.

This script accepts a list of Beancount input filenames, and fetches prices required to compute market values for current positions:

bean-price /home/joe/finances/joe.beancount

The list of fetching jobs to carry out is derived automatically from the input file (see section below for full details). It is also possible to provide a list of specific price fetching jobs to run, e.g.,

bean-price -e google/TSE:XUS yahoo/AAPL mysources.morningstar/RBF1005

The general format of each of these "source strings" is

<module>/[^]<ticker>

The "module" is the name of a Python module that contains a Source class which can be instantiated and connect to a data source to extract price data. These modules are automatically imported by name and instantiated in order to pull the price from a particular data source. This allows you to write your own supplementary fetcher codes without having to modify this script.

Default implementations are provided to provide access to prices from Yahoo! Finance or Google Finance, which cover a large universe of common public investment types (e.g. stock tickers). As a convenience, the module name is always first searched under the "beancount.prices.sources" package, where those default source implementations live. This is how, for example, in order to use the provided Google Finance data fetcher you don't have to write "beancount.prices.sources.yahoo/AAPL" but simply "yahoo/AAPL".

Date

By default, this script will fetch prices at the latest available date & time. You can use an option to fetch historical prices for a desired date instead:

bean-price --date=2015-02-03

Inverse

Sometimes, prices are available for the inverse of an instrument. This is often the case for currencies. For example, the price of "CAD" in USD" is provided by the USD/CAD market, which gives the price of a US dollar in Canadian dollars. In order specify this, you can prepend "^" to the instrument to instruct the driver to compute the inverse of the given price:

bean-price -e USD:google/^CURRENCY:USDCAD

If a source price is to be inverted, like this, the precision could be different than what is fetched. For instance, if the price of USD/CAD is 1.32759, it would output be this from the above directive:

2015-10-28 price CAD 0.753244601119 USD

By default, inverted rates will be rounded similarly to how other Price directives were rounding those numbers.

Swap Inverted

If you prefer to have the output Price entries with swapped currencies instead of inverting the rate itself, you can use the --swap-inverted option. In the previous example for the price of CAD, it would output this:

2015-10-28 price USD 1.32759 CAD

This works since the Beancount price database computes and interpolates the reciprocals automatically for all pairs of commodities in its database.

Prices Needed for a Beancount File

You can also provide a filename to extract the list of tickers to fetch from a Beancount input file, e.g.:

bean-price /home/joe/finances/joe.beancount

There are many ways to extract a list of commodities with needed prices from a Beancount input file:

  • Prices for all the holdings that were seen held-at-cost at a particular date.

  • Prices for holdings held at a particular date which were price converted from some other commodity in the past (i.e., for currencies).

  • The list of all Commodity directives present in the file. For each of those holdings, the corresponding Commodity directive is consulted and its "ticker" metadata field is used to specify where to attempt to fetch prices. You should have directives like this in your input file:

    2007-07-20 commodity VEA price: "google/NYSEARCA:VEA"

The "price" metadata can be a comma-separated list of sources to try out, in which case each of the sources will be looked at :

2007-07-20 commodity VEA
  price: "google/CURRENCY:USDCAD,yahoo/USDCAD"
  • Existing price directives for the same data are excluded by default, since the price is already in the file.

By default, the list of tickers to be fetched includes only the intersection of these lists. The general intent of the user of this script is to fetch missing prices, and only needed ones, for a particular date.

  • Use the --date option to change the applied date.
  • Use the --all option to fetch the entire set of prices, regardless of holdings and date.
  • Use --clobber to ignore existing price directives.

You can also print the list of prices to be fetched with the --dry-run option, which stops short of actually fetching the missing prices (it just prints the list of fetches it would otherwise attempt).

Caching

Prices are automatically cached. You can disable the cache with an option:

bean-price --no-cache

You can also instruct the script to clear the cache before fetching its prices:

bean-price --clear-cache

About Sources and Data Availability

IMPORTANT: Note that each source may support a different routine for getting its latest data and for fetching historical/dated data, and that each of these may differ in their support. For example, Google Finance does not support fetching historical data for its CURRENCY:* instruments.

beancount.prices.find_prices

A library of codes create price fetching jobs from strings and files.

beancount.prices.find_prices.DatedPrice (tuple)

DatedPrice(base, quote, date, sources)

beancount.prices.find_prices.DatedPrice.__getnewargs__(self) special

Return self as a plain tuple. Used by copy and pickle.

Source code in beancount/prices/find_prices.py
def __getnewargs__(self):
    'Return self as a plain tuple.  Used by copy and pickle.'
    return _tuple(self)

beancount.prices.find_prices.DatedPrice.__new__(_cls, base, quote, date, sources) special staticmethod

Create new instance of DatedPrice(base, quote, date, sources)

beancount.prices.find_prices.DatedPrice.__repr__(self) special

Return a nicely formatted representation string

Source code in beancount/prices/find_prices.py
def __repr__(self):
    'Return a nicely formatted representation string'
    return self.__class__.__name__ + repr_fmt % self

beancount.prices.find_prices.PriceSource (tuple)

PriceSource(module, symbol, invert)

beancount.prices.find_prices.PriceSource.__getnewargs__(self) special

Return self as a plain tuple. Used by copy and pickle.

Source code in beancount/prices/find_prices.py
def __getnewargs__(self):
    'Return self as a plain tuple.  Used by copy and pickle.'
    return _tuple(self)

beancount.prices.find_prices.PriceSource.__new__(_cls, module, symbol, invert) special staticmethod

Create new instance of PriceSource(module, symbol, invert)

beancount.prices.find_prices.PriceSource.__repr__(self) special

Return a nicely formatted representation string

Source code in beancount/prices/find_prices.py
def __repr__(self):
    'Return a nicely formatted representation string'
    return self.__class__.__name__ + repr_fmt % self

beancount.prices.find_prices.find_balance_currencies(entries, date=None)

Return currencies relevant for the given date.

This computes the account balances as of the date, and returns the union of: a) The currencies held at cost, and b) Currency pairs from previous conversions, but only for currencies with non-zero balances.

This is intended to produce the list of currencies whose prices are relevant at a particular date, based on previous history.

Parameters:
  • entries – A list of directives.

  • date – A datetime.date instance.

Returns:
  • A set of (base, quote) currencies.

Source code in beancount/prices/find_prices.py
def find_balance_currencies(entries, date=None):
    """Return currencies relevant for the given date.

    This computes the account balances as of the date, and returns the union of:
    a) The currencies held at cost, and
    b) Currency pairs from previous conversions, but only for currencies with
       non-zero balances.

    This is intended to produce the list of currencies whose prices are relevant
    at a particular date, based on previous history.

    Args:
      entries: A list of directives.
      date: A datetime.date instance.
    Returns:
      A set of (base, quote) currencies.
    """
    # Compute the balances.
    currencies = set()
    currencies_on_books = set()
    balances, _ = summarize.balance_by_account(entries, date)
    for _, balance in balances.items():
        for pos in balance:
            if pos.cost is not None:
                # Add currencies held at cost.
                currencies.add((pos.units.currency, pos.cost.currency))
            else:
                # Add regular currencies.
                currencies_on_books.add(pos.units.currency)

    # Create currency pairs from the currencies which are on account balances.
    # In order to figure out the quote currencies, we use the list of price
    # conversions until this date.
    converted = (find_currencies_converted(entries, date) |
                 find_currencies_priced(entries, date))
    for cbase in currencies_on_books:
        for base_quote in converted:
            base, quote = base_quote
            if base == cbase:
                currencies.add(base_quote)

    return currencies

beancount.prices.find_prices.find_currencies_at_cost(entries)

Return all currencies that were held at cost at some point.

This returns all of them, even if not on the books at a particular point in time. This code does not look at account balances.

Parameters:
  • entries – A list of directives.

  • date – A datetime.date instance.

Returns:
  • A list of (base, quote) currencies.

Source code in beancount/prices/find_prices.py
def find_currencies_at_cost(entries):
    """Return all currencies that were held at cost at some point.

    This returns all of them, even if not on the books at a particular point in
    time. This code does not look at account balances.

    Args:
      entries: A list of directives.
      date: A datetime.date instance.
    Returns:
      A list of (base, quote) currencies.
    """
    currencies = set()
    for entry in entries:
        if not isinstance(entry, data.Transaction):
            continue
        for posting in entry.postings:
            if posting.cost is not None and posting.cost.number is not None:
                currencies.add((posting.units.currency, posting.cost.currency))
    return currencies

beancount.prices.find_prices.find_currencies_converted(entries, date=None)

Return currencies from price conversions.

This function looks at all price conversions that occurred until some date and produces a list of them. Note: This does not include Price directives, only postings with price conversions.

Parameters:
  • entries – A list of directives.

  • date – A datetime.date instance.

Returns:
  • A list of (base, quote) currencies.

Source code in beancount/prices/find_prices.py
def find_currencies_converted(entries, date=None):
    """Return currencies from price conversions.

    This function looks at all price conversions that occurred until some date
    and produces a list of them. Note: This does not include Price directives,
    only postings with price conversions.

    Args:
      entries: A list of directives.
      date: A datetime.date instance.
    Returns:
      A list of (base, quote) currencies.
    """
    currencies = set()
    for entry in entries:
        if not isinstance(entry, data.Transaction):
            continue
        if date and entry.date >= date:
            break
        for posting in entry.postings:
            price = posting.price
            if posting.cost is not None or price is None:
                continue
            currencies.add((posting.units.currency, price.currency))
    return currencies

beancount.prices.find_prices.find_currencies_declared(entries, date=None)

Return currencies declared in Commodity directives.

If a 'price' metadata field is provided, include all the quote currencies there-in. Otherwise, the Commodity directive is ignored.

Parameters:
  • entries – A list of directives.

  • date – A datetime.date instance.

Returns:
  • A list of (base, quote, list of PriceSource) currencies. The list of (base, quote) pairs is guaranteed to be unique.

Source code in beancount/prices/find_prices.py
def find_currencies_declared(entries, date=None):
    """Return currencies declared in Commodity directives.

    If a 'price' metadata field is provided, include all the quote currencies
    there-in. Otherwise, the Commodity directive is ignored.

    Args:
      entries: A list of directives.
      date: A datetime.date instance.
    Returns:
      A list of (base, quote, list of PriceSource) currencies. The list of
      (base, quote) pairs is guaranteed to be unique.
    """
    currencies = []
    for entry in entries:
        if not isinstance(entry, data.Commodity):
            continue
        if date and entry.date >= date:
            break

        # Here we have to infer which quote currencies the commodity is for
        # (maybe down the road this should be better handled by providing a list
        # of quote currencies in the Commodity directive itself).
        #
        # First, we look for a "price" metadata field, which defines conversions
        # for various currencies. Each of these quote currencies generates a
        # pair in the output.
        source_str = entry.meta.get('price', None)
        if source_str is not None:
            if source_str == "":
                logging.debug("Skipping ignored currency (with empty price): %s",
                              entry.currency)
                continue
            try:
                source_map = parse_source_map(source_str)
            except ValueError:
                logging.warning("Ignoring currency with invalid 'price' source: %s",
                                entry.currency)
            else:
                for quote, psources in source_map.items():
                    currencies.append((entry.currency, quote, psources))
        else:
            # Otherwise we simply ignore the declaration. That is, a Commodity
            # directive without any "price" metadata would not register as a
            # declared currency.
            logging.debug("Ignoring currency with no metadata: %s", entry.currency)

    return currencies

beancount.prices.find_prices.find_currencies_priced(entries, date=None)

Return currencies seen in Price directives.

Parameters:
  • entries – A list of directives.

  • date – A datetime.date instance.

Returns:
  • A list of (base, quote) currencies.

Source code in beancount/prices/find_prices.py
def find_currencies_priced(entries, date=None):
    """Return currencies seen in Price directives.

    Args:
      entries: A list of directives.
      date: A datetime.date instance.
    Returns:
      A list of (base, quote) currencies.
    """
    currencies = set()
    for entry in entries:
        if not isinstance(entry, data.Price):
            continue
        if date and entry.date >= date:
            break
        currencies.add((entry.currency, entry.amount.currency))
    return currencies

beancount.prices.find_prices.format_dated_price_str(dprice)

Convert a dated price to a one-line printable string.

Parameters:
  • dprice – A DatedPrice instance.

Returns:
  • The string for a DatedPrice instance.

Source code in beancount/prices/find_prices.py
def format_dated_price_str(dprice):
    """Convert a dated price to a one-line printable string.

    Args:
      dprice: A DatedPrice instance.
    Returns:
      The string for a DatedPrice instance.
    """
    psstrs = ['{}({}{})'.format(psource.module.__name__,
                                '1/' if psource.invert else '',
                                psource.symbol)
              for psource in dprice.sources]
    base_quote = '{} /{}'.format(dprice.base, dprice.quote)
    return '{:<32} @ {:10} [ {} ]'.format(
        base_quote,
        dprice.date.isoformat() if dprice.date else 'latest',
        ','.join(psstrs))

beancount.prices.find_prices.get_price_jobs_at_date(entries, date=None, inactive=False, undeclared_source=None)

Get a list of prices to fetch from a stream of entries.

The active holdings held on the given date are included.

Parameters:
  • filename – A string, the name of a file to process.

  • date – A datetime.date instance.

  • inactive – Include currencies with no balance at the given date. The default is to only include those currencies which have a non-zero balance.

  • undeclared_source – A string, the name of the default source module to use to pull prices for commodities without a price source metadata on their Commodity directive declaration.

Returns:
  • A list of DatedPrice instances.

Source code in beancount/prices/find_prices.py
def get_price_jobs_at_date(entries, date=None, inactive=False, undeclared_source=None):
    """Get a list of prices to fetch from a stream of entries.

    The active holdings held on the given date are included.

    Args:
      filename: A string, the name of a file to process.
      date: A datetime.date instance.
      inactive: Include currencies with no balance at the given date. The default
        is to only include those currencies which have a non-zero balance.
      undeclared_source: A string, the name of the default source module to use to
        pull prices for commodities without a price source metadata on their
        Commodity directive declaration.
    Returns:
      A list of DatedPrice instances.

    """
    # Find the list of declared currencies, and from it build a mapping for
    # tickers for each (base, quote) pair. This is the only place tickers
    # appear.
    declared_triples = find_currencies_declared(entries, date)
    currency_map = {(base, quote): psources
                    for base, quote, psources in declared_triples}

    # Compute the initial list of currencies to consider.
    if undeclared_source:
        # Use the full set of possible currencies.
        cur_at_cost = find_currencies_at_cost(entries)
        cur_converted = find_currencies_converted(entries, date)
        cur_priced = find_currencies_priced(entries, date)
        currencies = cur_at_cost | cur_converted | cur_priced
        log_currency_list("Currency held at cost", cur_at_cost)
        log_currency_list("Currency converted", cur_converted)
        log_currency_list("Currency priced", cur_priced)
        default_source = import_source(undeclared_source)
    else:
        # Use the currencies from the Commodity directives.
        currencies = set(currency_map.keys())
        default_source = None

    log_currency_list("Currencies in primary list", currencies)

    # By default, restrict to only the currencies with non-zero balances at the
    # given date.
    if not inactive:
        balance_currencies = find_balance_currencies(entries, date)
        log_currency_list("Currencies held in assets", balance_currencies)
        currencies = currencies & balance_currencies

    log_currency_list("Currencies to fetch", currencies)

    # Build up the list of jobs to fetch prices for.
    jobs = []
    for base_quote in currencies:
        psources = currency_map.get(base_quote, None)
        base, quote = base_quote

        # If there are no sources, create a default one.
        if not psources:
            psources = [PriceSource(default_source, base, False)]

        jobs.append(DatedPrice(base, quote, date, psources))
    return sorted(jobs)

beancount.prices.find_prices.import_source(module_name)

Import the source module defined by the given name.

The default location is handled here.

Parameters:
  • short_module_name – A string, the name of a Python module, which may be within the default package or a full name.

Returns:
  • A corresponding Python module object.

Exceptions:
  • ImportError – If the module cannot be imported.

Source code in beancount/prices/find_prices.py
def import_source(module_name):
    """Import the source module defined by the given name.

    The default location is handled here.

    Args:
      short_module_name: A string, the name of a Python module, which may
        be within the default package or a full name.
    Returns:
      A corresponding Python module object.
    Raises:
      ImportError: If the module cannot be imported.
    """
    default_name = '{}.{}'.format(DEFAULT_PACKAGE, module_name)
    try:
        __import__(default_name)
        return sys.modules[default_name]
    except ImportError:
        try:
            __import__(module_name)
            return sys.modules[module_name]
        except ImportError as exc:
            raise ImportError('Could not find price source module "{}": {}'.format(
                module_name, exc))

beancount.prices.find_prices.log_currency_list(message, currencies)

Log a list of currencies to debug output.

Parameters:
  • message – A message string to prepend.

  • currencies – A list of (base, quote) currency pair.

Source code in beancount/prices/find_prices.py
def log_currency_list(message, currencies):
    """Log a list of currencies to debug output.

    Args:
      message: A message string to prepend.
      currencies: A list of (base, quote) currency pair.
    """
    logging.debug("-------- {}:".format(message))
    for base, quote in currencies:
        logging.debug("  {:>32}".format('{} /{}'.format(base, quote)))

beancount.prices.find_prices.parse_single_source(source)

Parse a single source string.

Source specifications follow the syntax:

<module>/[^]<ticker>

The <module> is resolved against the Python path, but first looked up under the package where the default price extractors lie.

Parameters:
  • source – A single source string specification.

Returns:
  • A PriceSource tuple, or

Exceptions:
  • ValueError – If invalid.

Source code in beancount/prices/find_prices.py
def parse_single_source(source):
    """Parse a single source string.

    Source specifications follow the syntax:

      <module>/[^]<ticker>

    The <module> is resolved against the Python path, but first looked up
    under the package where the default price extractors lie.

    Args:
      source: A single source string specification.
    Returns:
      A PriceSource tuple, or
    Raises:
      ValueError: If invalid.
    """
    match = re.match(r'([a-zA-Z]+[a-zA-Z0-9\._]+)/(\^?)([a-zA-Z0-9:=_\-\.]+)$', source)
    if not match:
        raise ValueError('Invalid source name: "{}"'.format(source))
    short_module_name, invert, symbol = match.groups()
    module = import_source(short_module_name)
    return PriceSource(module, symbol, bool(invert))

beancount.prices.find_prices.parse_source_map(source_map_spec)

Parse a source map specification string.

Source map specifications allow the specification of multiple sources for multiple quote currencies and follow the following syntax:

<currency1>:<source1>,<source2>,... <currency2>:<source1>,...

Where a <source> itself follows:

<module>/[^]<ticker>

The <module> is resolved against the Python path, but first looked up under the package where the default price extractors lie. The presence of a '^' character indicates that we should use the inverse of the rate pull from this source.

For example, for prices of AAPL in USD:

USD:google/NASDAQ:AAPL,yahoo/AAPL

Or for the exchange rate of a currency, such as INR in USD or in CAD:

USD:google/^CURRENCY:USDINR CAD:google/^CURRENCY:CADINR

Parameters:
  • source_map_spec – A string, a full source map specification to be parsed.

Returns:
  • FIXME – TODO

Exceptions:
  • ValueError – If an invalid pattern has been specified.

Source code in beancount/prices/find_prices.py
def parse_source_map(source_map_spec):
    """Parse a source map specification string.

    Source map specifications allow the specification of multiple sources for
    multiple quote currencies and follow the following syntax:

       <currency1>:<source1>,<source2>,... <currency2>:<source1>,...

    Where a <source> itself follows:

       <module>/[^]<ticker>

    The <module> is resolved against the Python path, but first looked up under
    the package where the default price extractors lie. The presence of a '^'
    character indicates that we should use the inverse of the rate pull from
    this source.

    For example, for prices of AAPL in USD:

       USD:google/NASDAQ:AAPL,yahoo/AAPL

    Or for the exchange rate of a currency, such as INR in USD or in CAD:

       USD:google/^CURRENCY:USDINR CAD:google/^CURRENCY:CADINR

    Args:
      source_map_spec: A string, a full source map specification to be parsed.
    Returns:
      FIXME: TODO
    Raises:
      ValueError: If an invalid pattern has been specified.
    """
    source_map = collections.defaultdict(list)
    for source_list_spec in re.split('[ ;]', source_map_spec):
        match = re.match('({}):(.*)$'.format(amount.CURRENCY_RE),
                         source_list_spec)
        if not match:
            raise ValueError('Invalid source map pattern: "{}"'.format(source_list_spec))

        currency, source_strs = match.groups()
        source_map[currency].extend(
            parse_single_source(source_str)
            for source_str in source_strs.split(','))
    return source_map

beancount.prices.price

Driver code for the price script.

beancount.prices.price.fetch_cached_price(source, symbol, date)

Call Source to fetch a price, but look and/or update the cache first.

This function entirely deals with caching and correct expiration. It keeps old prices if they were fetched in the past, and it quickly expires intra-day prices if they are fetched on the same day.

Parameters:
  • source – A Python module object.

  • symbol – A string, the ticker to fetch.

  • date – A datetime.date instance, None if we're to fetch the latest date.

Returns:
  • A SourcePrice instance.

Source code in beancount/prices/price.py
def fetch_cached_price(source, symbol, date):
    """Call Source to fetch a price, but look and/or update the cache first.

    This function entirely deals with caching and correct expiration. It keeps
    old prices if they were fetched in the past, and it quickly expires
    intra-day prices if they are fetched on the same day.

    Args:
      source: A Python module object.
      symbol: A string, the ticker to fetch.
      date: A datetime.date instance, None if we're to fetch the latest date.
    Returns:
      A SourcePrice instance.
    """
    # Compute a suitable timestamp from the date, if specified.
    if date is not None:
        # We query as for 4pm for the given date of the current timezone, if
        # specified.
        query_time = datetime.time(16, 0, 0)
        time_local = datetime.datetime.combine(date, query_time, tzinfo=tz.tzlocal())
        time = time_local.astimezone(tz.tzutc())
    else:
        time = None

    if _CACHE is None:
        # The cache is disabled; just call and return.
        result = (source.get_latest_price(symbol)
                  if time is None else
                  source.get_historical_price(symbol, time))

    else:
        # The cache is enabled and we have to compute the current/latest price.
        # Try to fetch from the cache but miss if the price is too old.
        md5 = hashlib.md5()
        md5.update(str((type(source).__module__, symbol, date)).encode('utf-8'))
        key = md5.hexdigest()
        timestamp_now = int(now().timestamp())
        try:
            timestamp_created, result_naive = _CACHE[key]

            # Convert naive timezone to UTC, which is what the cache is always
            # assumed to store. (The reason for this is that timezones from
            # aware datetime objects cannot be serialized properly due to bug.)
            if result_naive.time is not None:
                result = result_naive._replace(
                    time=result_naive.time.replace(tzinfo=tz.tzutc()))
            else:
                result = result_naive

            if (timestamp_now - timestamp_created) > _CACHE.expiration.total_seconds():
                raise KeyError
        except KeyError:
            logging.info("Fetching: %s (time: %s)", symbol, time)
            try:
                result = (source.get_latest_price(symbol)
                          if time is None else
                          source.get_historical_price(symbol, time))
            except ValueError as exc:
                logging.error("Error fetching %s: %s", symbol, exc)
                result = None

            # Make sure the timezone is UTC and make naive before serialization.
            if result and result.time is not None:
                time_utc = result.time.astimezone(tz.tzutc())
                time_naive = time_utc.replace(tzinfo=None)
                result_naive = result._replace(time=time_naive)
            else:
                result_naive = result

            if result_naive is not None:
                _CACHE[key] = (timestamp_now, result_naive)
    return result

beancount.prices.price.fetch_price(dprice, swap_inverted=False)

Fetch a price for the DatedPrice job.

Parameters:
  • dprice – A DatedPrice instances.

  • swap_inverted – A boolean, true if we should invert currencies instead of rate for an inverted price source.

Returns:
  • A Price entry corresponding to the output of the jobs processed.

Source code in beancount/prices/price.py
def fetch_price(dprice, swap_inverted=False):
    """Fetch a price for the DatedPrice job.

    Args:
      dprice: A DatedPrice instances.
      swap_inverted: A boolean, true if we should invert currencies instead of
        rate for an inverted price source.
    Returns:
      A Price entry corresponding to the output of the jobs processed.

    """
    for psource in dprice.sources:
        try:
            source = psource.module.Source()
        except AttributeError:
            continue
        srcprice = fetch_cached_price(source, psource.symbol, dprice.date)
        if srcprice is not None:
            break
    else:
        if dprice.sources:
            logging.error("Could not fetch for job: %s", dprice)
        return None

    base = dprice.base
    quote = dprice.quote or srcprice.quote_currency
    price = srcprice.price

    # Invert the rate if requested.
    if psource.invert:
        if swap_inverted:
            base, quote = quote, base
        else:
            price = ONE/price

    assert base is not None
    fileloc = data.new_metadata('<{}>'.format(type(psource.module).__name__), 0)

    # The datetime instance is required to be aware. We always convert to the
    # user's timezone before extracting the date. This means that if the market
    # returns a timestamp for a particular date, once we convert to the user's
    # timezone the returned date may be different by a day. The intent is that
    # whatever we print is assumed coherent with the user's timezone. See
    # discussion at
    # https://groups.google.com/d/msg/beancount/9j1E_HLEMBQ/fYRuCQK_BwAJ
    srctime = srcprice.time
    if srctime.tzinfo is None:
        raise ValueError("Time returned by the price source is not timezone aware.")
    date = srctime.astimezone(tz.tzlocal()).date()

    return data.Price(fileloc, date, base,
                      amount.Amount(price, quote or UNKNOWN_CURRENCY))

beancount.prices.price.filter_redundant_prices(price_entries, existing_entries, diffs=False)

Filter out new entries that are redundant from an existing set.

If the price differs, we override it with the new entry only on demand. This is because this would create conflict with existing price entries when parsing, if the new entries are simply inserted into the input.

Parameters:
  • price_entries – A list of newly created, proposed to be added Price directives.

  • existing_entries – A list of existing entries we are proposing to add to.

  • diffs – A boolean, true if we should output differing price entries at the same date.

Returns:
  • A filtered list of remaining entries, and a list of ignored entries.

Source code in beancount/prices/price.py
def filter_redundant_prices(price_entries, existing_entries, diffs=False):
    """Filter out new entries that are redundant from an existing set.

    If the price differs, we override it with the new entry only on demand. This
    is because this would create conflict with existing price entries when
    parsing, if the new entries are simply inserted into the input.

    Args:
      price_entries: A list of newly created, proposed to be added Price directives.
      existing_entries: A list of existing entries we are proposing to add to.
      diffs: A boolean, true if we should output differing price entries
        at the same date.
    Returns:
      A filtered list of remaining entries, and a list of ignored entries.
    """
    # Note: We have to be careful with the dates, because requesting the latest
    # price for a date may yield the price at a previous date. Clobber needs to
    # take this into account. See {1cfa25e37fc1}.
    existing_prices = {(entry.date, entry.currency): entry
                       for entry in existing_entries
                       if isinstance(entry, data.Price)}
    filtered_prices = []
    ignored_prices = []
    for entry in price_entries:
        key = (entry.date, entry.currency)
        if key in existing_prices:
            if diffs:
                existing_entry = existing_prices[key]
                if existing_entry.amount == entry.amount:
                    output = ignored_prices
            else:
                output = ignored_prices
        else:
            output = filtered_prices
        output.append(entry)
    return filtered_prices, ignored_prices

beancount.prices.price.now()

Indirection in order to be able to mock it out in the tests.

Source code in beancount/prices/price.py
def now():
    "Indirection in order to be able to mock it out in the tests."
    return datetime.datetime.now(datetime.timezone.utc)

beancount.prices.price.process_args()

Process the arguments. This also initializes the logging module.

Returns:
  • A tuple of – args: The argparse receiver of command-line arguments. jobs: A list of DatedPrice job objects. entries: A list of all the parsed entries.

Source code in beancount/prices/price.py
def process_args():
    """Process the arguments. This also initializes the logging module.

    Returns:
      A tuple of:
        args: The argparse receiver of command-line arguments.
        jobs: A list of DatedPrice job objects.
        entries: A list of all the parsed entries.
    """
    parser = version.ArgumentParser(description=beancount.prices.__doc__.splitlines()[0])

    # Input sources or filenames.
    parser.add_argument('sources', nargs='+', help=(
        'A list of filenames (or source "module/symbol", if -e is '
        'specified) from which to create a list of jobs.'))

    parser.add_argument('-e', '--expressions', '--expression', action='store_true', help=(
        'Interpret the arguments as "module/symbol" source strings.'))

    # Regular options.
    parser.add_argument('-v', '--verbose', action='count', help=(
        "Print out progress log. Specify twice for debugging info."))

    parser.add_argument('-d', '--date', action='store',
                        type=date_utils.parse_date_liberally, help=(
        "Specify the date for which to fetch the prices."))

    parser.add_argument('-i', '--inactive', action='store_true', help=(
        "Select all commodities from input files, not just the ones active on the date"))

    parser.add_argument('-u', '--undeclared', action='store', help=(
        "Include commodities viewed in the file even without a "
        "corresponding Commodity directive, from this default source. "
        "The currency name itself is used as the lookup symbol in this default source."))

    parser.add_argument('-c', '--clobber', action='store_true', help=(
        "Do not skip prices which are already present in input files; fetch them anyway."))

    parser.add_argument('-a', '--all', action='store_true', help=(
        "A shorthand for --inactive, --undeclared, --clobber."))

    parser.add_argument('-s', '--swap-inverted', action='store_true', help=(
        "For inverted sources, swap currencies instead of inverting the rate. "
        "For example, if fetching the rate for CAD from 'USD:google/^CURRENCY:USDCAD' "
        "results in 1.25, by default we would output \"price CAD  0.8000 USD\". "
        "Using this option we would instead output \" price USD   1.2500 CAD\"."))

    parser.add_argument('-n', '--dry-run', action='store_true', help=(
        "Don't actually fetch the prices, just print the list of the ones to be fetched."))

    # Caching options.
    cache_group = parser.add_argument_group('cache')
    cache_filename = path.join(tempfile.gettempdir(),
                               "{}.cache".format(path.basename(sys.argv[0])))
    cache_group.add_argument('--cache', dest='cache_filename',
                             action='store', default=cache_filename,
                             help="Enable the cache and with the given cache name.")
    cache_group.add_argument('--no-cache', dest='cache_filename',
                             action='store_const', const=None,
                             help="Disable the price cache.")

    cache_group.add_argument('--clear-cache', action='store_true',
                             help="Clear the cache prior to startup")

    args = parser.parse_args()

    verbose_levels = {None: logging.WARN,
                      0: logging.WARN,
                      1: logging.INFO,
                      2: logging.DEBUG}
    logging.basicConfig(level=verbose_levels[args.verbose],
                        format='%(levelname)-8s: %(message)s')

    if args.all:
        args.inactive = args.clobber = True
        args.undeclared = DEFAULT_SOURCE

    # Setup for processing.
    setup_cache(args.cache_filename, args.clear_cache)

    # Get the list of DatedPrice jobs to get from the arguments.
    logging.info("Processing at date: %s", args.date or datetime.date.today())
    jobs = []
    all_entries = []
    dcontext = None
    if args.expressions:
        # Interpret the arguments as price sources.
        for source_str in args.sources:
            psources = []
            try:
                psource_map = find_prices.parse_source_map(source_str)
            except ValueError:
                extra = "; did you provide a filename?" if path.exists(source_str) else ''
                msg = ('Invalid source "{{}}"{}. '.format(extra) +
                       'Supported format is "CCY:module/SYMBOL"')
                parser.error(msg.format(source_str))
            else:
                for currency, psources in psource_map.items():
                    jobs.append(find_prices.DatedPrice(
                        psources[0].symbol, currency, args.date, psources))
    else:
        # Interpret the arguments as Beancount input filenames.
        for filename in args.sources:
            if not path.exists(filename) or not path.isfile(filename):
                parser.error('File does not exist: "{}"; '
                             'did you mean to use -e?'.format(filename))
                continue
            logging.info('Loading "%s"', filename)
            entries, errors, options_map = loader.load_file(filename, log_errors=sys.stderr)
            if dcontext is None:
                dcontext = options_map['dcontext']
            jobs.extend(
                find_prices.get_price_jobs_at_date(
                    entries, args.date, args.inactive, args.undeclared))
            all_entries.extend(entries)

    return args, jobs, data.sorted(all_entries), dcontext

beancount.prices.price.reset_cache()

Reset the cache to its uninitialized state.

Source code in beancount/prices/price.py
def reset_cache():
    """Reset the cache to its uninitialized state."""
    global _CACHE
    if _CACHE is not None:
        _CACHE.close()
    _CACHE = None

beancount.prices.price.setup_cache(cache_filename, clear_cache)

Setup the results cache.

Parameters:
  • cache_filename – A string or None, the filename for the cache.

  • clear_cache – A boolean, if true, delete the cache before beginning.

Source code in beancount/prices/price.py
def setup_cache(cache_filename, clear_cache):
    """Setup the results cache.

    Args:
      cache_filename: A string or None, the filename for the cache.
      clear_cache: A boolean, if true, delete the cache before beginning.
    """
    if clear_cache and cache_filename and path.exists(cache_filename):
        logging.info("Clearing cache %s", cache_filename)
        os.remove(cache_filename)

    if cache_filename:
        logging.info('Using price cache at "%s" (with indefinite expiration)',
                     cache_filename)

        global _CACHE
        _CACHE = shelve.open(cache_filename, 'c')
        _CACHE.expiration = DEFAULT_EXPIRATION

beancount.prices.source

Interface definition for all price sources.

This module describes the contract to be fulfilled by all implementations of price sources.

beancount.prices.source.Source

Interface to be implemented by all price sources.

beancount.prices.source.Source.get_historical_price(self, ticker, time)

Return the historical price found for the symbol at the given date.

This could be the price of the close of the day, for instance. We assume that there is some single price representative of the day.

Parameters:
  • ticker – A string, the ticker to be fetched by the source. This ticker may include structure, such as the exchange code. Also note that this ticker is source-specified, and is not necessarily the same value as the commodity symbol used in the Beancount file.

  • time – The timestamp at which to query for the price. This is a timezone-aware timestamp you can convert to any timezone. For past dates we query for a time that is equivalent to 4pm in the user's timezone.

Returns:
  • A SourcePrice instance. If the price could not be fetched, None is returned and another source should be consulted. There is never any guarantee that a price source will be able to fetch its value; client code must be able to handle this. Also note that the price's returned time must be timezone-aware.

Source code in beancount/prices/source.py
def get_historical_price(self, ticker, time):
    """Return the historical price found for the symbol at the given date.

    This could be the price of the close of the day, for instance. We assume
    that there is some single price representative of the day.

    Args:
      ticker: A string, the ticker to be fetched by the source. This ticker
        may include structure, such as the exchange code. Also note that
        this ticker is source-specified, and is not necessarily the same
        value as the commodity symbol used in the Beancount file.
      time: The timestamp at which to query for the price. This is a
        timezone-aware timestamp you can convert to any timezone. For past
        dates we query for a time that is equivalent to 4pm in the user's
        timezone.
    Returns:
      A SourcePrice instance. If the price could not be fetched, None is
      returned and another source should be consulted. There is never any
      guarantee that a price source will be able to fetch its value; client
      code must be able to handle this. Also note that the price's returned
      time must be timezone-aware.
    """

beancount.prices.source.Source.get_latest_price(self, ticker)

Fetch the current latest price. The date may differ.

This routine attempts to fetch the most recent available price, and returns the actual date of the quoted price, which may differ from the date this call is made at. {1cfa25e37fc1}

Parameters:
  • ticker – A string, the ticker to be fetched by the source. This ticker may include structure, such as the exchange code. Also note that this ticker is source-specified, and is not necessarily the same value as the commodity symbol used in the Beancount file.

Returns:
  • A SourcePrice instance. If the price could not be fetched, None is returned and another source should be consulted. There is never any guarantee that a price source will be able to fetch its value; client code must be able to handle this. Also note that the price's returned time must be timezone-aware.

Source code in beancount/prices/source.py
def get_latest_price(self, ticker):
    """Fetch the current latest price. The date may differ.

    This routine attempts to fetch the most recent available price, and
    returns the actual date of the quoted price, which may differ from the
    date this call is made at. {1cfa25e37fc1}

    Args:
      ticker: A string, the ticker to be fetched by the source. This ticker
        may include structure, such as the exchange code. Also note that
        this ticker is source-specified, and is not necessarily the same
        value as the commodity symbol used in the Beancount file.
    Returns:
      A SourcePrice instance. If the price could not be fetched, None is
      returned and another source should be consulted. There is never any
      guarantee that a price source will be able to fetch its value; client
      code must be able to handle this. Also note that the price's returned
      time must be timezone-aware.
    """

beancount.prices.source.SourcePrice (tuple)

SourcePrice(price, time, quote_currency)

beancount.prices.source.SourcePrice.__getnewargs__(self) special

Return self as a plain tuple. Used by copy and pickle.

Source code in beancount/prices/source.py
def __getnewargs__(self):
    'Return self as a plain tuple.  Used by copy and pickle.'
    return _tuple(self)

beancount.prices.source.SourcePrice.__new__(_cls, price, time, quote_currency) special staticmethod

Create new instance of SourcePrice(price, time, quote_currency)

beancount.prices.source.SourcePrice.__repr__(self) special

Return a nicely formatted representation string

Source code in beancount/prices/source.py
def __repr__(self):
    'Return a nicely formatted representation string'
    return self.__class__.__name__ + repr_fmt % self

beancount.prices.sources special

Implementation of various price extractors.

This package is looked up by the driver script to figure out which extractor to use.

beancount.prices.sources.coinbase

A source fetching cryptocurrency prices from Coinbase.

Valid tickers are in the form "XXX-YYY", such as "BTC-USD".

Here is the API documentation: https://developers.coinbase.com/api/v2

For example: https://api.coinbase.com/v2/prices/BTC-GBP/spot

Timezone information: Input and output datetimes are specified via UTC timestamps.

beancount.prices.sources.coinbase.CoinbaseError (ValueError)

An error from the Coinbase API.

beancount.prices.sources.coinbase.Source (Source)

Coinbase API price extractor.

beancount.prices.sources.coinbase.Source.get_historical_price(self, ticker, time)

See contract in beancount.prices.source.Source.

Source code in beancount/prices/sources/coinbase.py
def get_historical_price(self, ticker, time):
    """See contract in beancount.prices.source.Source."""
    raise NotImplementedError(
        "As of Feb 2019, historical prices are not supported on Coinbase. "
        "Please check the API to see if this has changed: "
        "https://developers.coinbase.com/apo/v2")
beancount.prices.sources.coinbase.Source.get_latest_price(self, ticker)

See contract in beancount.prices.source.Source.

Source code in beancount/prices/sources/coinbase.py
def get_latest_price(self, ticker):
    """See contract in beancount.prices.source.Source."""
    return fetch_quote(ticker)

beancount.prices.sources.coinbase.fetch_quote(ticker)

Fetch a quote from Coinbase.

Source code in beancount/prices/sources/coinbase.py
def fetch_quote(ticker):
    """Fetch a quote from Coinbase."""
    url = "https://api.coinbase.com/v2/prices/{}/spot".format(ticker.lower())
    response = requests.get(url)
    if response.status_code != requests.codes.ok:
        raise CoinbaseError("Invalid response ({}): {}".format(response.status_code,
                                                               response.text))
    result = response.json()

    price = D(result['data']['amount']).quantize(D('0.01'))
    time = datetime.datetime.now(tz.tzutc())
    currency = result['data']['currency']

    return source.SourcePrice(price, time, currency)

beancount.prices.sources.iex

Fetch prices from the IEX 1.0 public API.

This is a really fantastic exchange API with a lot of relevant information.

Timezone information: There is currency no support for historical prices. The output datetime is provided as a UNIX timestamp.

beancount.prices.sources.iex.IEXError (ValueError)

An error from the IEX API.

beancount.prices.sources.iex.Source (Source)

IEX API price extractor.

beancount.prices.sources.iex.Source.get_historical_price(self, ticker, time)

See contract in beancount.prices.source.Source.

Source code in beancount/prices/sources/iex.py
def get_historical_price(self, ticker, time):
    """See contract in beancount.prices.source.Source."""
    raise NotImplementedError(
        "This is now implemented at https://iextrading.com/developers/docs/#hist and "
        "needs to be added here.")
beancount.prices.sources.iex.Source.get_latest_price(self, ticker)

See contract in beancount.prices.source.Source.

Source code in beancount/prices/sources/iex.py
def get_latest_price(self, ticker):
    """See contract in beancount.prices.source.Source."""
    return fetch_quote(ticker)

beancount.prices.sources.iex.fetch_quote(ticker)

Fetch the latest price for the given ticker.

Source code in beancount/prices/sources/iex.py
def fetch_quote(ticker):
    """Fetch the latest price for the given ticker."""

    url = "https://api.iextrading.com/1.0/tops/last?symbols={}".format(ticker.upper())
    response = requests.get(url)
    if response.status_code != requests.codes.ok:
        raise IEXError("Invalid response ({}): {}".format(
            response.status_code, response.text))

    results = response.json()
    if len(results) != 1:
        raise IEXError("Invalid number of responses from IEX: {}".format(
            response.text))
    result = results[0]

    price = D(result['price']).quantize(D('0.01'))

    # IEX is American markets.
    us_timezone = tz.gettz("America/New_York")
    time = datetime.datetime.fromtimestamp(result['time'] / 1000)
    time = time.astimezone(us_timezone)

    # As far as can tell, all the instruments on IEX are priced in USD.
    return source.SourcePrice(price, time, 'USD')

beancount.prices.sources.oanda

A source fetching currency prices from OANDA.

Valid tickers are in the form "XXX_YYY", such as "EUR_USD".

Here is the API documentation: https://developer.oanda.com/rest-live/rates/

For example: https://api-fxtrade.oanda.com/v1/candles?instrument=EUR_USD&granularity=D&start=2016-03-27T00%3A00%3A00Z&end=2016-04-04T00%3A00%3A00Z&candleFormat=midpoint

Timezone information: Input and output datetimes are specified via UTC timestamps.

beancount.prices.sources.oanda.Source (Source)

OANDA price source extractor.

beancount.prices.sources.oanda.Source.get_historical_price(self, ticker, time)

See contract in beancount.prices.source.Source.

Source code in beancount/prices/sources/oanda.py
def get_historical_price(self, ticker, time):
    """See contract in beancount.prices.source.Source."""
    time = time.astimezone(tz.tzutc())
    query_interval_begin = (time - datetime.timedelta(days=5))
    query_interval_end = (time + datetime.timedelta(days=1))
    params_dict = {
        'instrument': ticker,
        'granularity': 'H2',  # Every two hours.
        'candleFormat': 'midpoint',
        'start': query_interval_begin.isoformat('T'),
        'end': query_interval_end.isoformat('T'),
    }
    return _fetch_price(params_dict, time)
beancount.prices.sources.oanda.Source.get_latest_price(self, ticker)

See contract in beancount.prices.source.Source.

Source code in beancount/prices/sources/oanda.py
def get_latest_price(self, ticker):
    """See contract in beancount.prices.source.Source."""
    time = datetime.datetime.now(tz.tzutc())
    params_dict = {
        'instrument': ticker,
        'granularity': 'S5',  # Every two hours.
        'count': '10',
        'candleFormat': 'midpoint',
    }
    return _fetch_price(params_dict, time)

beancount.prices.sources.quandl

Fetch prices from Quandl's simple URL-based API.

Quandl is a useful source of alternative data and it offers a simple REST API that serves CSV and JSON and XML formats. There's also a Python client library, but we specifically avoid using that here, in order to keep Beancount dependency-free.

Many of the datasets are freely available, which is why this is included here. You can get information about the available databases and associated lists of symbols you can use here: https://www.quandl.com/search

If you have a paid account and would like to be able to access the premium databases from the Quandl site, you can set QUANDL_API_KEY environment variable.

Use the "<DATABASE_CODE>:<DATASET_CODE>" format to refer to Quandl symbols. Note that their symbols are usually identified by "<DATABASE_CODE>/<DATASET_CODE>".

(For now, this supports only the Time-Series API. There is also a Tables API, which could easily get integrated. We would just have to encode the 'datatable_code' and 'format' and perhaps other fields in the ticker name.)

Timezone information: Input and output datetimes are limited to dates, and I believe the dates are presumed to live in the timezone of each particular data source. (It's unclear, not documented.)

beancount.prices.sources.quandl.QuandlError (ValueError)

An error from the Quandl API.

beancount.prices.sources.quandl.Source (Source)

Quandl API price extractor.

beancount.prices.sources.quandl.Source.get_historical_price(self, ticker, time)

See contract in beancount.prices.source.Source.

Source code in beancount/prices/sources/quandl.py
def get_historical_price(self, ticker, time):
    """See contract in beancount.prices.source.Source."""
    return fetch_time_series(ticker, time)
beancount.prices.sources.quandl.Source.get_latest_price(self, ticker)

See contract in beancount.prices.source.Source.

Source code in beancount/prices/sources/quandl.py
def get_latest_price(self, ticker):
    """See contract in beancount.prices.source.Source."""
    return fetch_time_series(ticker)

beancount.prices.sources.quandl.fetch_time_series(ticker, time=None)

Fetch

Source code in beancount/prices/sources/quandl.py
def fetch_time_series(ticker, time=None):
    """Fetch"""
    # Create request payload.
    database, dataset = parse_ticker(ticker)
    url = "https://www.quandl.com/api/v3/datasets/{}/{}.json".format(database, dataset)
    payload = {"limit": 1}
    if time is not None:
        date = time.date()
        payload["start_date"] = (date - datetime.timedelta(days=10)).isoformat()
        payload["end_date"] = date.isoformat()

    # Add API key, if it is set in the environment.
    if 'QUANDL_API_KEY' in os.environ:
        payload['api_key'] = os.environ['QUANDL_API_KEY']

    # Fetch and process errors.
    response = requests.get(url, params=payload)
    if response.status_code != requests.codes.ok:
        raise QuandlError("Invalid response ({}): {}".format(response.status_code,
                                                             response.text))
    result = response.json()
    if 'quandl_error' in result:
        raise QuandlError(result['quandl_error']['message'])

    # Parse result container.
    dataset = result['dataset']
    column_names = dataset['column_names']
    date_index = column_names.index('Date')
    try:
        data_index = column_names.index('Adj. Close')
    except ValueError:
        data_index = column_names.index('Close')
    data = dataset['data'][0]

    # Gather time and assume it's in UTC timezone (Quandl does not provide the
    # market's timezone).
    time = datetime.datetime.strptime(data[date_index], '%Y-%m-%d')
    time = time.replace(tzinfo=tz.tzutc())

    # Gather price.
    # Quantize with the same precision default rendering of floats occur.
    price_float = data[data_index]
    price = D(price_float)
    match = re.search(r'(\..*)', str(price_float))
    if match:
        price = price.quantize(D(match.group(1)))

    # Note: There is no currency information in the response (surprising).
    return source.SourcePrice(price, time, None)

beancount.prices.sources.quandl.parse_ticker(ticker)

Convert ticker to Quandl codes.

Source code in beancount/prices/sources/quandl.py
def parse_ticker(ticker):
    """Convert ticker to Quandl codes."""
    if not re.match(r"[A-Z0-9]+:[A-Z0-9]+$", ticker):
        raise ValueError('Invalid code. Use "<DATABASE>/<DATASET>" format.')
    return tuple(ticker.split(":"))

beancount.prices.sources.yahoo

Fetch prices from Yahoo Finance's CSV API.

As of late 2017, the older Yahoo finance API deprecated. In particular, the ichart endpoint is gone, and the download endpoint requires a cookie (which could be gotten - here's some documentation for that http://blog.bradlucas.com/posts/2017-06-02-new-yahoo-finance-quote-download-url/).

We're using both the v7 and v8 APIs here, both of which are, as far as I can tell, undocumented:

https://query1.finance.yahoo.com/v7/finance/quote https://query1.finance.yahoo.com/v8/finance/chart/SYMBOL

Timezone information: Input and output datetimes are specified via UNIX timestamps, but the timezone of the particular market is included in the output.

beancount.prices.sources.yahoo.Source (Source)

Yahoo Finance CSV API price extractor.

beancount.prices.sources.yahoo.Source.get_historical_price(self, ticker, time)

See contract in beancount.prices.source.Source.

Source code in beancount/prices/sources/yahoo.py
def get_historical_price(self, ticker, time):
    """See contract in beancount.prices.source.Source."""
    if requests is None:
        raise YahooError("You must install the 'requests' library.")
    url = "https://query1.finance.yahoo.com/v8/finance/chart/{}".format(ticker)
    dt_start = time - datetime.timedelta(days=5)
    dt_end = time
    payload = {
        'period1': int(dt_start.timestamp()),
        'period2': int(dt_end.timestamp()),
        'interval': '1d',
    }
    payload.update(_DEFAULT_PARAMS)
    response = requests.get(url, params=payload)
    result = parse_response(response)

    meta = result['meta']
    timezone = datetime.timezone(datetime.timedelta(hours=meta['gmtoffset'] / 3600),
                                 meta['exchangeTimezoneName'])

    timestamp_array = result['timestamp']
    close_array = result['indicators']['quote'][0]['close']
    series = [(datetime.datetime.fromtimestamp(timestamp, tz=timezone), D(price))
              for timestamp, price in zip(timestamp_array, close_array)]

    # Get the latest data returned.
    latest = None
    for data_dt, price in sorted(series):
        if data_dt >= time:
            break
        latest = data_dt, price
    if latest is None:
        raise YahooError("Could not find price before {} in {}".format(time, series))

    currency = result['meta']['currency']
    return source.SourcePrice(price, data_dt, currency)
beancount.prices.sources.yahoo.Source.get_latest_price(self, ticker)

See contract in beancount.prices.source.Source.

Source code in beancount/prices/sources/yahoo.py
def get_latest_price(self, ticker):
    """See contract in beancount.prices.source.Source."""

    url = "https://query1.finance.yahoo.com/v7/finance/quote"
    fields = ['symbol', 'regularMarketPrice', 'regularMarketTime']
    payload = {
        'symbols': ticker,
        'fields': ','.join(fields),
        'exchange': 'NYSE',
    }
    payload.update(_DEFAULT_PARAMS)
    response = requests.get(url, params=payload)
    result = parse_response(response)
    try:
        price = D(result['regularMarketPrice'])

        timezone = datetime.timezone(
            datetime.timedelta(hours=result['gmtOffSetMilliseconds'] / 3600000),
            result['exchangeTimezoneName'])
        trade_time = datetime.datetime.fromtimestamp(result['regularMarketTime'],
                                                     tz=timezone)
    except KeyError:
        raise YahooError("Invalid response from Yahoo: {}".format(repr(result)))

    currency = parse_currency(result)

    return source.SourcePrice(price, trade_time, currency)

beancount.prices.sources.yahoo.YahooError (ValueError)

An error from the Yahoo API.

beancount.prices.sources.yahoo.parse_currency(result)

Infer the currency from the result.

Source code in beancount/prices/sources/yahoo.py
def parse_currency(result: Dict[str, Any]) -> str:
    """Infer the currency from the result."""
    if 'market' not in result:
        return None
    return _MARKETS.get(result['market'], None)

beancount.prices.sources.yahoo.parse_response(response)

Process as response from Yahoo.

Exceptions:
  • YahooError – If there is an error in the response.

Source code in beancount/prices/sources/yahoo.py
def parse_response(response: requests.models.Response) -> Dict:
    """Process as response from Yahoo.

    Raises:
      YahooError: If there is an error in the response.
    """
    json = response.json(parse_float=D)
    content = next(iter(json.values()))
    if response.status_code != requests.codes.ok:
        raise YahooError("Status {}: {}".format(response.status_code, content['error']))
    if len(json) != 1:
        raise YahooError("Invalid format in response from Yahoo; many keys: {}".format(
            ','.join(json.keys())))
    if content['error'] is not None:
        raise YahooError("Error fetching Yahoo data: {}".format(content['error']))
    return content['result'][0]