Source code for piecash.core.commodity

from __future__ import division
from __future__ import unicode_literals

import pytz

_type = type

import datetime
from decimal import Decimal

from sqlalchemy import Column, VARCHAR, INTEGER, ForeignKey, BIGINT, Index
from sqlalchemy.orm import relation
from sqlalchemy.orm.exc import MultipleResultsFound

from ._commodity_helper import quandl_fx
from .._common import CallableList, GncConversionError
from .._common import GnucashException, hybrid_property_gncnumeric
from .._declbase import DeclarativeBaseGuid
from ..sa_extra import _DateAsDateTime
from ..yahoo_client import get_latest_quote, download_quote


[docs]class GncCommodityError(GnucashException): pass
[docs]class GncPriceError(GnucashException): pass
[docs]class Price(DeclarativeBaseGuid): """ A single Price for a commodity. Attributes: commodity (:class:`Commodity`): commodity to which the Price relates currency (:class:`Commodity`): currency in which the Price is expressed date (:class:`datetime.date`): date object representing the day at which the price is relevant source (str): source of the price type (str): last, ask, bid, unknown, nav value (:class:`decimal.Decimal`): the price itself """ __tablename__ = 'prices' __table_args__ = {} # column definitions commodity_guid = Column('commodity_guid', VARCHAR(length=32), ForeignKey('commodities.guid'), nullable=False) currency_guid = Column('currency_guid', VARCHAR(length=32), ForeignKey('commodities.guid'), nullable=False) date = Column('date', _DateAsDateTime(neutral_time=False), nullable=False) source = Column('source', VARCHAR(length=2048)) type = Column('type', VARCHAR(length=2048)) _value_num = Column('value_num', BIGINT(), nullable=False) _value_denom = Column('value_denom', BIGINT(), nullable=False) value = hybrid_property_gncnumeric(_value_num, _value_denom) # relation definitions commodity = relation('Commodity', back_populates="prices", foreign_keys=[commodity_guid], ) currency = relation('Commodity', foreign_keys=[currency_guid], ) def __init__(self, commodity, currency, date, value, type="unknown", source="user:price"): self.commodity = commodity self.currency = currency assert _type(date) is datetime.date self.date = date self.value = value self.type = type self.source = source def __str__(self): return "Price<{:%Y-%m-%d} : {} {}/{}>".format(self.date, self.value, self.currency.mnemonic, self.commodity.mnemonic)
[docs] def object_to_validate(self, change): if change[-1] != "deleted": yield self
[docs] def validate(self): # check uniqueness of namespace/mnemonic try: self.book.query(Price).filter_by(commodity=self.commodity, currency=self.currency, source=self.source, date=self.date).one() except MultipleResultsFound: raise ValueError("{} already exists in this book".format(self))
[docs]class Commodity(DeclarativeBaseGuid): """ A GnuCash Commodity. Attributes: cusip (str): cusip code fraction (int): minimal unit of the commodity (e.g. 100 for 1/100) namespace (str): CURRENCY for currencies, otherwise any string to group multiple commodities together mnemonic (str): the ISO symbol for a currency or the stock symbol for stocks (used for online quotes) quote_flag (int): 1 if piecash/GnuCash quotes will retrieve online quotes for the commodity quote_source (str): the quote source for GnuCash (piecash always use yahoo for stock and quandl for currencies quote_tz (str): the timezone to assign on the online quotes base_currency (:class:`Commodity`): The base_currency for a commodity: - if the commodity is a currency, returns the "default currency" of the book (ie the one of the root_account) - if the commodity is not a currency, returns the currency encoded in the quoted_currency slot accounts (list of :class:`piecash.core.account.Account`): list of accounts which have the commodity as commodity transactions (list of :class:`piecash.core.transaction.Transaction`): list of transactions which have the commodity as currency prices (iterator of :class:`Price`): iterator on prices related to the commodity (it is a sqlalchemy query underneath) """ __tablename__ = 'commodities' __table_args__ = (Index('_unique_cdty', 'namespace', 'mnemonic', unique=True, mysql_length={'namespace': 200, 'mnemonic': 10}, ), ) # column definitions namespace = Column('namespace', VARCHAR(length=2048), nullable=False) mnemonic = Column('mnemonic', VARCHAR(length=2048), nullable=False) fullname = Column('fullname', VARCHAR(length=2048)) cusip = Column('cusip', VARCHAR(length=2048)) fraction = Column('fraction', INTEGER(), nullable=False) quote_flag = Column('quote_flag', INTEGER(), nullable=False) quote_source = Column('quote_source', VARCHAR(length=2048)) quote_tz = Column('quote_tz', VARCHAR(length=2048)) @property def base_currency(self): b = self.book if b is None: raise GnucashException("The commodity should be linked to a session to have a 'base_currency'") if self.namespace == "CURRENCY": # get the base currency as first commodity in DB return b.default_currency else: # retrieve currency from quoted_currency kvp # TODO: recover from the web (as fallback) mnemonic = self.get("quoted_currency", None) if mnemonic: return b.currencies(mnemonic=mnemonic) else: raise GnucashException("The commodity '{}' has no information about its base currency. " "Add a kvp item named 'quoted_currency' with the mnemonic of the " "currency to have proper behavior".format(self.mnemonic)) # relation definitions accounts = relation('Account', back_populates='commodity', cascade='all, delete-orphan', collection_class=CallableList) transactions = relation('Transaction', back_populates='currency', cascade='all, delete-orphan', collection_class=CallableList, ) prices = relation("Price", back_populates='commodity', foreign_keys=[Price.commodity_guid], cascade='all, delete-orphan', lazy="dynamic", ) def __init__(self, namespace, mnemonic, fullname, fraction=100, cusip="", quote_flag=0, quote_source=None, quote_tz='', book=None): if quote_source is None: quote_source = "currency" if namespace == "CURRENCY" else "yahoo" if book is not None: book.add(self) self.namespace = namespace self.mnemonic = mnemonic self.fullname = fullname self.fraction = fraction self.cusip = cusip self.quote_flag = quote_flag self.quote_source = quote_source self.quote_tz = quote_tz if book is not None: book.flush() def __str__(self): return "Commodity<{}:{}>".format(self.namespace, self.mnemonic) @property def precision(self): return len(str(self.fraction)) - 1
[docs] def currency_conversion(self, currency): """ Return the latest conversion factor to convert self to currency Attributes: currency (:class:`piecash.core.commodity.Commodity`): the currency to which the Price need to be converted Returns: a Decimal that can be multiplied by an amount expressed in self.commodity to get an amount expressed in currency Raises: GncConversionError: not possible to convert self to the currency """ # conversion is done from self.commodity to commodity (if possible) sc2c = self.prices.filter_by(currency=currency).order_by(Price.date.desc()).first() if sc2c: return sc2c.value # conversion is done directly from commodity to self.commodity (if possible) c2sc = currency.prices.filter_by(currency=self).order_by(Price.date.desc()).first() if c2sc: return Decimal(1) / c2sc.value raise GncConversionError("Cannot convert {} to {}".format(self, currency))
[docs] def update_prices(self, start_date=None): """ Retrieve online prices for the commodity: - for currencies, it will get from quandl the exchange rates between the currency and its base_currency - for stocks, it will get from yahoo the daily closing prices expressed in its base_currency Args: start_date (:class:`datetime.date`): prices will be updated as of the start_date. If None, start_date is today - 7 days. .. note:: if prices are already available in the GnuCash file, the function will only retrieve prices as of the max(start_date, last quoted price date) .. todo:: add some frequency to retrieve prices only every X (week, month, ...) """ if self.book is None: raise GncPriceError("Cannot update price for a commodity not attached to a book") # get last_price updated last_price = self.prices.order_by(Price.date.desc()).limit(1).first() if start_date is None: start_date = datetime.datetime.today().date() + datetime.timedelta(days=-7) if last_price: start_date = max(last_price.date + datetime.timedelta(days=1), start_date) if self.namespace == "CURRENCY": # get reference currency (from book.root_account) default_currency = self.base_currency if default_currency == self: raise GncPriceError("Cannot update exchange rate for base currency") # through Quandl for exchange rates quotes = quandl_fx(self.mnemonic, default_currency.mnemonic, start_date) for q in quotes: p = Price(commodity=self, currency=default_currency, date=datetime.datetime.strptime(q.date, "%Y-%m-%d").date(), value=str(q.rate)) else: symbol = self.mnemonic share = get_latest_quote(symbol) currency = self.book.currencies(mnemonic=share.currency) tz = pytz.timezone(share.timezone) # get historical data for q in download_quote( symbol, start_date, datetime.date.today(), tz ): Price(commodity=self, currency=currency, date=q.date, value=q.close, type='last')
[docs] def object_to_validate(self, change): if change[-1] != "deleted": yield self
[docs] def validate(self): # check uniqueness of namespace/mnemonic try: self.book.query(Commodity).filter_by(namespace=self.namespace, mnemonic=self.mnemonic).one() except MultipleResultsFound: raise ValueError("{} already exists in this book".format(self))