fix capital gains
This commit is contained in:
@@ -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).
|
||||
"""
|
||||
|
||||
@@ -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_()
|
||||
|
||||
@@ -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
|
||||
start_balance = None if b is None or v is None else b - v
|
||||
end_balance = out[-1]['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)
|
||||
first_item = out[0]
|
||||
last_item = out[-1]
|
||||
if from_height or to_height:
|
||||
start_height = from_height
|
||||
end_height = to_height
|
||||
else:
|
||||
start_date = None
|
||||
end_date = None
|
||||
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)
|
||||
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 = last_item['bc_balance'].value
|
||||
|
||||
if from_timestamp is not None and to_timestamp is not 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 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)
|
||||
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 = {
|
||||
'begin': summary_start,
|
||||
'end': summary_end,
|
||||
'flow': flow,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user