Browse Source

fix capital gains

patch-4
ThomasV 4 years ago
parent
commit
bbdfde5b41
  1. 5
      electrum/gui/messages.py
  2. 87
      electrum/gui/qt/history_list.py
  3. 119
      electrum/wallet.py

5
electrum/gui/messages.py

@ -37,3 +37,8 @@ Another instance of this wallet (same seed) has an open channel with the same re
Are you sure?
"""
MSG_CAPITAL_GAINS = """
This summary covers only on-chain transactions (no lightning!). Capital gains are computed by attaching an acquisition price to each UTXO in the wallet, and uses the order of blockchain events (not FIFO).
"""

87
electrum/gui/qt/history_list.py

@ -39,6 +39,7 @@ from PyQt5.QtWidgets import (QMenu, QHeaderView, QLabel, QMessageBox,
QPushButton, QComboBox, QVBoxLayout, QCalendarWidget,
QGridLayout)
from electrum.gui import messages
from electrum.address_synchronizer import TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE
from electrum.i18n import _
from electrum.util import (block_explorer_URL, profiler, TxMinedInfo,
@ -49,7 +50,7 @@ from electrum.logging import get_logger, Logger
from .custom_model import CustomNode, CustomModel
from .util import (read_QIcon, MONOSPACE_FONT, Buttons, CancelButton, OkButton,
filename_field, MyTreeView, AcceptFileDragDrop, WindowModalDialog,
CloseButton, webopen)
CloseButton, webopen, WWLabel)
if TYPE_CHECKING:
from electrum.wallet import Abstract_Wallet
@ -547,40 +548,72 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
return datetime.datetime(date.year, date.month, date.day)
def show_summary(self):
h = self.parent.wallet.get_detailed_history()['summary']
if not h:
fx = self.parent.fx
show_fiat = fx and fx.is_enabled() and fx.get_history_config()
if not show_fiat:
self.parent.show_message(_("Enable fiat exchange rate with history."))
return
h = self.wallet.get_detailed_history(fx=fx)
summary = h['summary']
if not summary:
self.parent.show_message(_("Nothing to summarize."))
return
start_date = h.get('start_date')
end_date = h.get('end_date')
start = summary['begin']
end = summary['end']
flow = summary['flow']
start_date = start.get('date')
end_date = end.get('date')
format_amount = lambda x: self.parent.format_amount(x.value) + ' ' + self.parent.base_unit()
format_fiat = lambda x: str(x) + ' ' + self.parent.fx.ccy
d = WindowModalDialog(self, _("Summary"))
d.setMinimumSize(600, 150)
vbox = QVBoxLayout()
msg = messages.to_rtf(messages.MSG_CAPITAL_GAINS)
vbox.addWidget(WWLabel(msg))
grid = QGridLayout()
grid.addWidget(QLabel(_("Start")), 0, 0)
grid.addWidget(QLabel(self.format_date(start_date)), 0, 1)
grid.addWidget(QLabel(str(h.get('fiat_start_value')) + '/BTC'), 0, 2)
grid.addWidget(QLabel(_("Initial balance")), 1, 0)
grid.addWidget(QLabel(format_amount(h['start_balance'])), 1, 1)
grid.addWidget(QLabel(str(h.get('fiat_start_balance'))), 1, 2)
grid.addWidget(QLabel(_("End")), 2, 0)
grid.addWidget(QLabel(self.format_date(end_date)), 2, 1)
grid.addWidget(QLabel(str(h.get('fiat_end_value')) + '/BTC'), 2, 2)
grid.addWidget(QLabel(_("Final balance")), 4, 0)
grid.addWidget(QLabel(format_amount(h['end_balance'])), 4, 1)
grid.addWidget(QLabel(str(h.get('fiat_end_balance'))), 4, 2)
grid.addWidget(QLabel(_("Income")), 5, 0)
grid.addWidget(QLabel(format_amount(h.get('incoming'))), 5, 1)
grid.addWidget(QLabel(str(h.get('fiat_incoming'))), 5, 2)
grid.addWidget(QLabel(_("Expenditures")), 6, 0)
grid.addWidget(QLabel(format_amount(h.get('outgoing'))), 6, 1)
grid.addWidget(QLabel(str(h.get('fiat_outgoing'))), 6, 2)
grid.addWidget(QLabel(_("Capital gains")), 7, 0)
grid.addWidget(QLabel(str(h.get('fiat_capital_gains'))), 7, 2)
grid.addWidget(QLabel(_("Unrealized gains")), 8, 0)
grid.addWidget(QLabel(str(h.get('fiat_unrealized_gains', ''))), 8, 2)
grid.addWidget(QLabel(_("Begin")), 0, 1)
grid.addWidget(QLabel(_("End")), 0, 2)
#
grid.addWidget(QLabel(_("Date")), 1, 0)
grid.addWidget(QLabel(self.format_date(start_date)), 1, 1)
grid.addWidget(QLabel(self.format_date(end_date)), 1, 2)
#
grid.addWidget(QLabel(_("BTC balance")), 2, 0)
grid.addWidget(QLabel(format_amount(start['BTC_balance'])), 2, 1)
grid.addWidget(QLabel(format_amount(end['BTC_balance'])), 2, 2)
#
grid.addWidget(QLabel(_("BTC Fiat price")), 3, 0)
grid.addWidget(QLabel(format_fiat(start.get('BTC_fiat_price'))), 3, 1)
grid.addWidget(QLabel(format_fiat(end.get('BTC_fiat_price'))), 3, 2)
#
grid.addWidget(QLabel(_("Fiat balance")), 4, 0)
grid.addWidget(QLabel(format_fiat(start.get('fiat_balance'))), 4, 1)
grid.addWidget(QLabel(format_fiat(end.get('fiat_balance'))), 4, 2)
#
grid.addWidget(QLabel(_("Acquisition price")), 5, 0)
grid.addWidget(QLabel(format_fiat(start.get('acquisition_price', ''))), 5, 1)
grid.addWidget(QLabel(format_fiat(end.get('acquisition_price', ''))), 5, 2)
#
grid.addWidget(QLabel(_("Unrealized capital gains")), 6, 0)
grid.addWidget(QLabel(format_fiat(start.get('unrealized_gains', ''))), 6, 1)
grid.addWidget(QLabel(format_fiat(end.get('unrealized_gains', ''))), 6, 2)
#
grid2 = QGridLayout()
grid2.addWidget(QLabel(_("BTC incoming")), 0, 0)
grid2.addWidget(QLabel(format_amount(flow['BTC_incoming'])), 0, 1)
grid2.addWidget(QLabel(_("Fiat incoming")), 1, 0)
grid2.addWidget(QLabel(format_fiat(flow.get('fiat_incoming'))), 1, 1)
grid2.addWidget(QLabel(_("BTC outgoing")), 2, 0)
grid2.addWidget(QLabel(format_amount(flow['BTC_outgoing'])), 2, 1)
grid2.addWidget(QLabel(_("Fiat outgoing")), 3, 0)
grid2.addWidget(QLabel(format_fiat(flow.get('fiat_outgoing'))), 3, 1)
#
grid2.addWidget(QLabel(_("Realized capital gains")), 4, 0)
grid2.addWidget(QLabel(format_fiat(flow.get('realized_capital_gains'))), 4, 1)
vbox.addLayout(grid)
vbox.addWidget(QLabel(_('Cash flow')))
vbox.addLayout(grid2)
vbox.addLayout(Buttons(CloseButton(d)))
d.setLayout(vbox)
d.exec_()

119
electrum/wallet.py

@ -955,13 +955,21 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return transactions
@profiler
def get_detailed_history(self, from_timestamp=None, to_timestamp=None,
fx=None, show_addresses=False, from_height=None, to_height=None):
def get_detailed_history(
self,
from_timestamp=None,
to_timestamp=None,
fx=None,
show_addresses=False,
from_height=None,
to_height=None):
# History with capital gains, using utxo pricing
# FIXME: Lightning capital gains would requires FIFO
if (from_timestamp is not None or to_timestamp is not None) \
and (from_height is not None or to_height is not None):
raise Exception('timestamp and block height based filtering cannot be used together')
show_fiat = fx and fx.is_enabled() and fx.get_history_config()
out = []
income = 0
expenditures = 0
@ -995,7 +1003,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
else:
income += value
# fiat computations
if fx and fx.is_enabled() and fx.get_history_config():
if show_fiat:
fiat_fields = self.get_tx_item_fiat(tx_hash=tx_hash, amount_sat=value, fx=fx, tx_fee=tx_fee)
fiat_value = fiat_fields['fiat_value'].value
item.update(fiat_fields)
@ -1007,36 +1015,74 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
out.append(item)
# add summary
if out:
b, v = out[0]['bc_balance'].value, out[0]['bc_value'].value
first_item = out[0]
last_item = out[-1]
if from_height or to_height:
start_height = from_height
end_height = to_height
else:
start_height = first_item['height'] - 1
end_height = last_item['height']
b = first_item['bc_balance'].value
v = first_item['bc_value'].value
start_balance = None if b is None or v is None else b - v
end_balance = out[-1]['bc_balance'].value
end_balance = last_item['bc_balance'].value
if from_timestamp is not None and to_timestamp is not None:
start_date = timestamp_to_datetime(from_timestamp)
end_date = timestamp_to_datetime(to_timestamp)
else:
start_date = None
end_date = None
start_timestamp = from_timestamp
end_timestamp = to_timestamp
else:
start_timestamp = first_item['timestamp']
end_timestamp = last_item['timestamp']
start_coins = self.get_utxos(
domain=None,
block_height=start_height,
confirmed_funding_only=True,
confirmed_spending_only=True,
nonlocal_only=True)
end_coins = self.get_utxos(
domain=None,
block_height=end_height,
confirmed_funding_only=True,
confirmed_spending_only=True,
nonlocal_only=True)
def summary_point(timestamp, height, balance, coins):
date = timestamp_to_datetime(timestamp)
out = {
'date': date,
'block_height': height,
'BTC_balance': Satoshis(balance),
}
if show_fiat:
ap = self.acquisition_price(coins, fx.timestamp_rate, fx.ccy)
lp = self.liquidation_price(coins, fx.timestamp_rate, timestamp)
out['acquisition_price'] = Fiat(ap, fx.ccy)
out['liquidation_price'] = Fiat(lp, fx.ccy)
out['unrealized_gains'] = Fiat(lp - ap, fx.ccy)
out['fiat_balance'] = Fiat(fx.historical_value(balance, date), fx.ccy)
out['BTC_fiat_price'] = Fiat(fx.historical_value(COIN, date), fx.ccy)
return out
summary_start = summary_point(start_timestamp, start_height, start_balance, start_coins)
summary_end = summary_point(end_timestamp, end_height, end_balance, end_coins)
flow = {
'BTC_incoming': Satoshis(income),
'BTC_outgoing': Satoshis(expenditures)
}
if show_fiat:
flow['fiat_currency'] = fx.ccy
flow['fiat_incoming'] = Fiat(fiat_income, fx.ccy)
flow['fiat_outgoing'] = Fiat(fiat_expenditures, fx.ccy)
flow['realized_capital_gains'] = Fiat(capital_gains, fx.ccy)
summary = {
'start_date': start_date,
'end_date': end_date,
'from_height': from_height,
'to_height': to_height,
'start_balance': Satoshis(start_balance),
'end_balance': Satoshis(end_balance),
'incoming': Satoshis(income),
'outgoing': Satoshis(expenditures)
'begin': summary_start,
'end': summary_end,
'flow': flow,
}
if fx and fx.is_enabled() and fx.get_history_config():
unrealized = self.unrealized_gains(None, fx.timestamp_rate, fx.ccy)
summary['fiat_currency'] = fx.ccy
summary['fiat_capital_gains'] = Fiat(capital_gains, fx.ccy)
summary['fiat_incoming'] = Fiat(fiat_income, fx.ccy)
summary['fiat_outgoing'] = Fiat(fiat_expenditures, fx.ccy)
summary['fiat_unrealized_gains'] = Fiat(unrealized, fx.ccy)
summary['fiat_start_balance'] = Fiat(fx.historical_value(start_balance, start_date), fx.ccy)
summary['fiat_end_balance'] = Fiat(fx.historical_value(end_balance, end_date), fx.ccy)
summary['fiat_start_value'] = Fiat(fx.historical_value(COIN, start_date), fx.ccy)
summary['fiat_end_value'] = Fiat(fx.historical_value(COIN, end_date), fx.ccy)
else:
summary = {}
return {
@ -1044,6 +1090,13 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
'summary': summary
}
def acquisition_price(self, coins, price_func, ccy):
return Decimal(sum(self.coin_price(coin.prevout.txid.hex(), price_func, ccy, self.get_txin_value(coin)) for coin in coins))
def liquidation_price(self, coins, price_func, timestamp):
p = price_func(timestamp)
return sum([coin.value_sats() for coin in coins]) * p / Decimal(COIN)
def default_fiat_value(self, tx_hash, fx, value_sat):
return value_sat / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate)
@ -2356,14 +2409,6 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
timestamp = self.get_tx_height(txid).timestamp
return price_func(timestamp if timestamp else time.time())
def unrealized_gains(self, domain, price_func, ccy):
coins = self.get_utxos(domain)
now = time.time()
p = price_func(now)
ap = sum(self.coin_price(coin.prevout.txid.hex(), price_func, ccy, self.get_txin_value(coin)) for coin in coins)
lp = sum([coin.value_sats() for coin in coins]) * p / Decimal(COIN)
return lp - ap
def average_price(self, txid, price_func, ccy) -> Decimal:
""" Average acquisition price of the inputs of a transaction """
input_value = 0

Loading…
Cancel
Save