def pay(self,
            dtime,
            currency,
            amount,
            exchange,
            fee_ratio=0,
            custom_rate=None,
            report_info=None):
        self._check_order(dtime)
        amount = Decimal(amount)
        fee_ratio = Decimal(fee_ratio)
        if amount <= 0: return
        exchange = str(exchange).capitalize()
        if currency == self.currency:
            self._abort(
                'Payments with the base currency are not relevant here.')
        if exchange not in self.bags or not self.bags[exchange]:
            self._abort("You don't own any funds on %s" % exchange)
        if fee_ratio < 0 or fee_ratio > 1:
            self._abort("Fee ratio must be between 0 and 1.")
        if exchange not in self.totals:
            total = 0
        else:
            total = self.totals[exchange].get(currency, 0)
        if amount > total:
            self._abort("Amount to be paid ({1} {0}) is higher than total "
                        "available on {3}: {2} {0}.".format(
                            currency, amount, total, exchange))
        # expenses (original cost of spent money):
        cost = Decimal()
        # expenses only of short term trades:
        st_cost = Decimal()
        # proceeds (value of spent money at dtime):
        proc = Decimal()
        # proceeds only of short term trades:
        st_proc = Decimal()
        # exchange rate at time of payment:
        rate = Decimal(0)
        if custom_rate is not None:
            rate = Decimal(custom_rate)
        elif self.relation is None:
            log.debug('Relation is not provided, will use bag price as rate')
        else:
            try:
                rate = Decimal(
                    self.relation.get_rate(dtime, currency, self.currency))
            except KeyError:
                self._abort(
                    'Could not fetch the price for currency_pair %s_%s on '
                    '%s from provided CurrencyRelation object.' %
                    (currency, self.currency, dtime))
        # due payment:
        to_pay = amount
        log.info(
            "Paying %(to_pay).8f %(curr)s from %(exchange)s "
            "(including %(fees).8f %(curr)s fees)", {
                'to_pay': to_pay,
                'curr': currency,
                'exchange': exchange,
                'fees': to_pay * fee_ratio
            })
        # Find bags with this currency and use them to pay for
        # this:
        bag_index = None
        self.sort_bags(exchange)
        while to_pay > 0:
            bag_index, bag = self.pick_bag(exchange,
                                           currency,
                                           start_index=bag_index)

            # Spend as much as possible from this bag:
            log.info("Paying with bag from %s, containing %.8f %s", bag.dtime,
                     bag.amount, bag.currency)
            spent, bcost, remainder = bag.spend(to_pay)
            log.info("Contents of bag after payment: %.8f %s (spent %.8f %s)",
                     bag.amount, bag.currency, spent, currency)

            # The proceeds are the value of spent amount at dtime:
            if not rate:
                # XXX: Fallback rate to bag price
                rate = bag.price
            thisproc = spent * rate
            # update totals for the full payment:
            proc += thisproc
            cost += bcost
            short_term = is_short_term(bag.dtime, dtime)
            if short_term:
                st_proc += thisproc
                st_cost += bcost

            # fee-corrected proceeds for this partial sale:
            corrproc = thisproc * (1 - fee_ratio)
            # profit for this partial sale (not short term only):
            prof = corrproc - bcost

            log.info(
                "Profits in this transaction:\n"
                "    Original bag cost: %.3f %s (Price %.8f %s/%s)\n"
                "    Proceeds         : %.3f %s (Price %.8f %s/%s)\n"
                "    Proceeds w/o fees: %.3f %s\n"
                "    Profit           : %.3f %s\n"
                "    Taxable?         : %s (held for %s than a year)", bcost,
                self.currency, bag.price, bag.cost_currency, currency,
                thisproc, self.currency, rate, self.currency, currency,
                corrproc, self.currency, prof, self.currency,
                'yes' if short_term else 'no',
                'less' if short_term else 'more')

            # Store report data:
            repinfo = {'kind': 'payment', 'buy_currency': '', 'buy_ratio': 0}
            if report_info is not None:
                repinfo.update(report_info)
            if not repinfo.get('buy_currency', ''):
                repinfo['buy_ratio'] = 0
            self.report.add_payment(
                reports.PaymentReport(
                    kind=repinfo['kind'],
                    exchange=exchange,
                    sell_date=pd.Timestamp(dtime).tz_convert('UTC'),
                    currency=currency,
                    to_pay=to_pay,
                    fee_ratio=fee_ratio,
                    bag_date=bag.dtime,
                    bag_amount=bag.amount + spent,
                    bag_spent=spent,
                    cost_currency=bag.cost_currency,
                    spent_cost=bcost,
                    short_term=short_term,
                    ex_rate=rate,
                    proceeds=corrproc,
                    profit=prof,
                    buy_currency=repinfo['buy_currency'],
                    buy_ratio=repinfo['buy_ratio']))

            to_pay = remainder
            if to_pay > 0:
                log.info("Still to be paid with another bag: %.8f %s", to_pay,
                         currency)
            if bag.is_empty():
                del self.bags[exchange][bag_index]

        # update and clean up totals:
        if total - amount == 0:
            del self.totals[exchange][currency]
            if not self.totals[exchange]:
                del self.totals[exchange]
            if not self.bags[exchange]:
                del self.bags[exchange]
        else:
            self.totals[exchange][currency] = total - amount

        # Return the tuple (short_term_profit, total_proceeds):
        # Note: if it is not completely clear how we arrive at these
        # formulas (i.e. the proceeds are the value of the spent amount,
        # minus fees, at time of payment; the profit equals these
        # proceeds minus the initial cost of the full amount), here
        # is a different view point:
        # The cost and proceeds attributable to fees are split
        # proportionately from st_cost and st_proceeds, i.e.
        # the fee's cost is `fee_p * st_cost` and the lost proceeds
        # due to the fee is `fee_p * st_proceeds`.
        # The fee's cost is counted as loss:
        # (but only the taxable short term portion!)
        # ==>
        # Total profit =
        #  profit putting fees aside: (1-fee_p) * (st_proceeds - st_cost)
        #  - fee cost loss          : - fee_p * st_cost
        #  = (1 - fee_p) * st_proceeds - st_cost
        return st_proc * (1 - fee_ratio) - st_cost, proc * (1 - fee_ratio)
Example #2
0
    def pay(self,
            dtime,
            currency,
            amount,
            exchange,
            fee_ratio=0,
            custom_rate=None,
            report_info=None):
        """Spend an amount of funds.

        The money is taken out of the oldest bag on the exchange with
        the proper currency first, then from the next fitting bags in
        line each time a bag is emptied (FIFO principle). The bags'
        prices are not changed, but their current amount and base value
        (original cost) are decreased proportionally.

        This transaction and any profits or losses made will be logged
        and added to the capital gains report data.

        If *amount* is higher than the available total amount,
        ValueError is raised.

        :param dtime: (datetime) The date and time when the payment
            occured.
        :param amount: (number, decimal or parsable string)
            The amount being spent, including fees.
        :param currency: (string) The currency being spent.
        :param exchange: (string) The unique name of the
            exchange/wallet where the funds that are being spent are
            taken from.
        :param fee_ratio: (number, decimal or parsable string),
            0 <= fee_ratio <= 1; The ratio of *amount* that are fees.
            Default: 0.
        :param custom_rate: (number, decimal or parsable string),
            Default: None;
            Provide a custom rate for conversion of *currency* to the
            base currency. Usually (i.e. if this is None), the rate is
            fetched from the CurrencyRelation object provided when this
            BagFIFO object was created. In some cases, one should rather
            provide a rate, for example when base currency was bought
            with this payment, meaning a more specific rate for this
            trade can be provided than relying on the averaged historic
            data used otherwise.
        :param report_info: dict, default None;
            Additional information that will be added to the capital
            gains report data. Currently, the only keys looked for are:
            'kind', 'buy_currency' and 'buy_ratio'. For each one of
            them omitted in the dict, these default values will be used:

             `{'kind': 'payment', 'buy_currency': '', 'buy_ratio': 0}`,

            This is also the default dict used when *report_info* is
            `None`.

             - 'kind' is the type of transaction, i.e. 'sale',
               'withdrawal fee', 'payment' etc.;
             - 'buy_currency' is the currency bought in this trade;
             - 'buy_ratio' is the amount of 'buy_currency' bought with
               one unit of *currency*, i.e. `bought_amount / spent_amount`;
               only used if 'buy_currency' is not empty.

        :returns: the tuple `(short_term_profit, total_proceeds)`,
            with each value given in units of the base currency, where:

              - `short_term_profit`

               is the taxable short term profit (or
               loss if negative) made in this sale. This only takes into
               account the part of *amount* which was acquired less than
               a year prior to *dtime* (or whatever time period is used by
               `is_short_term`). The short term profit equals the proceeds
               made by liquidating this amount for its price at *dtime*
               minus its original cost, with the fees already substracted
               from these proceeds.

              - `total_proceeds`

               are the total proceeds returned from this
               sale, i.e. it includes the full *amount* (held for any amount
               of time) at its price at *dtime*, with fees already
               substracted. This value equals the base cost of any new
               currency purchased with this sale.

        """
        self._check_order(dtime)
        amount = Decimal(amount)
        fee_ratio = Decimal(fee_ratio)
        if amount <= 0: return
        exchange = str(exchange).capitalize()
        if currency == self.currency:
            self._abort(
                'Payments with the base currency are not relevant here.')
        if exchange not in self.bags or not self.bags[exchange]:
            self._abort("You don't own any funds on %s" % exchange)
        if fee_ratio < 0 or fee_ratio > 1:
            self._abort("Fee ratio must be between 0 and 1.")
        if exchange not in self.totals:
            total = 0
        else:
            total = self.totals[exchange].get(currency, 0)
        if amount > total:
            self._abort("Amount to be paid ({1} {0}) is higher than total "
                        "available on {3}: {2} {0}.".format(
                            currency, amount, total, exchange))
        # expenses (original cost of spent money):
        cost = Decimal()
        # expenses only of short term trades:
        st_cost = Decimal()
        # proceeds (value of spent money at dtime):
        proc = Decimal()
        # proceeds only of short term trades:
        st_proc = Decimal()
        # exchange rate at time of payment:
        if custom_rate is not None:
            rate = Decimal(custom_rate)
        else:
            try:
                rate = Decimal(
                    self.relation.get_rate(dtime, currency, self.currency))
            except KeyError:
                self._abort(
                    'Could not fetch the price for currency_pair %s_%s on '
                    '%s from provided CurrencyRelation object.' %
                    (currency, self.currency, dtime))
        # due payment:
        to_pay = amount
        log.info(
            "Paying %(to_pay).8f %(curr)s from %(exchange)s "
            "(including %(fees).8f %(curr)s fees)", {
                'to_pay': to_pay,
                'curr': currency,
                'exchange': exchange,
                'fees': to_pay * fee_ratio
            })
        # Find bags with this currency and use them to pay for
        # this, starting from first bag (FIFO):
        i = 0
        while to_pay > 0:
            # next bag in line with fitting currency:
            while self.bags[exchange][i].currency != currency:
                i += 1
                if i == len(self.bags[exchange]):
                    # Corrupt data error: don't dump state.
                    raise Exception(
                        "There are no bags left with the requested currency")
            bag = self.bags[exchange][i]

            # Spend as much as possible from this bag:
            log.info("Paying with bag from %s, containing %.8f %s", bag.dtime,
                     bag.amount, bag.currency)
            spent, bcost, remainder = bag.spend(to_pay)
            log.info("Contents of bag after payment: %.8f %s (spent %.8f %s)",
                     bag.amount, bag.currency, spent, currency)

            # The proceeds are the value of spent amount at dtime:
            thisproc = spent * rate
            # update totals for the full payment:
            proc += thisproc
            cost += bcost
            short_term = is_short_term(bag.dtime, dtime)
            if short_term:
                st_proc += thisproc
                st_cost += bcost

            # fee-corrected proceeds for this partial sale:
            corrproc = thisproc * (1 - fee_ratio)
            # profit for this partial sale (not short term only):
            prof = corrproc - bcost

            log.info(
                "Profits in this transaction:\n"
                "    Original bag cost: %.3f %s (Price %.8f %s/%s)\n"
                "    Proceeds         : %.3f %s (Price %.8f %s/%s)\n"
                "    Proceeds w/o fees: %.3f %s\n"
                "    Profit           : %.3f %s\n"
                "    Taxable?         : %s (held for %s than a year)", bcost,
                self.currency, bag.price, bag.cost_currency, currency,
                thisproc, self.currency, rate, self.currency, currency,
                corrproc, self.currency, prof, self.currency,
                'yes' if short_term else 'no',
                'less' if short_term else 'more')

            # Store report data:
            repinfo = {'kind': 'payment', 'buy_currency': '', 'buy_ratio': 0}
            if report_info is not None:
                repinfo.update(report_info)
            if not repinfo.get('buy_currency', ''):
                repinfo['buy_ratio'] = 0
            self.report.add_payment(
                reports.PaymentReport(
                    kind=repinfo['kind'],
                    exchange=exchange,
                    sell_date=pd.Timestamp(dtime).tz_convert('UTC'),
                    currency=currency,
                    to_pay=to_pay,
                    fee_ratio=fee_ratio,
                    bag_date=bag.dtime,
                    bag_amount=bag.amount + spent,
                    bag_spent=spent,
                    cost_currency=bag.cost_currency,
                    spent_cost=bcost,
                    short_term=short_term,
                    ex_rate=rate,
                    proceeds=corrproc,
                    profit=prof,
                    buy_currency=repinfo['buy_currency'],
                    buy_ratio=repinfo['buy_ratio']))

            to_pay = remainder
            if to_pay > 0:
                log.info("Still to be paid with another bag: %.8f %s", to_pay,
                         currency)
            if bag.is_empty():
                del self.bags[exchange][i]
            else:
                i += 1

        # update and clean up totals:
        if total - amount == 0:
            del self.totals[exchange][currency]
            if not self.totals[exchange]:
                del self.totals[exchange]
            if not self.bags[exchange]:
                del self.bags[exchange]
        else:
            self.totals[exchange][currency] = total - amount

        # Return the tuple (short_term_profit, total_proceeds):
        # Note: if it is not completely clear how we arrive at these
        # formulas (i.e. the proceeds are the value of the spent amount,
        # minus fees, at time of payment; the profit equals these
        # proceeds minus the initial cost of the full amount), here
        # is a different view point:
        # The cost and proceeds attributable to fees are split
        # proportionately from st_cost and st_proceeds, i.e.
        # the fee's cost is `fee_p * st_cost` and the lost proceeds
        # due to the fee is `fee_p * st_proceeds`.
        # The fee's cost is counted as loss:
        # (but only the taxable short term portion!)
        # ==>
        # Total profit =
        #  profit putting fees aside: (1-fee_p) * (st_proceeds - st_cost)
        #  - fee cost loss          : - fee_p * st_cost
        #  = (1 - fee_p) * st_proceeds - st_cost
        return st_proc * (1 - fee_ratio) - st_cost, proc * (1 - fee_ratio)