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)
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)