Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make LIFO/HIFO accounting methods O(n*log(m)) #116

Merged
merged 15 commits into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 52 additions & 4 deletions src/rp2/abstract_accounting_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,20 @@


from enum import Enum
from typing import Dict, List, NamedTuple, Optional
from heapq import heappop, heappush
from typing import Dict, List, NamedTuple, Optional, Tuple, Union

from rp2.abstract_transaction import AbstractTransaction
from rp2.in_transaction import InTransaction
from rp2.rp2_decimal import ZERO, RP2Decimal
from rp2.rp2_error import RP2RuntimeError


class AbstractAccountingMethodIterator:
def __next__(self) -> InTransaction:
raise NotImplementedError("abstract function")


class AcquiredLotAndAmount(NamedTuple):
acquired_lot: InTransaction
amount: RP2Decimal
Expand All @@ -41,11 +47,17 @@ def __init__(
) -> None:
self.__accounting_method: AbstractAccountingMethod = accounting_method
self.__acquired_lot_list = acquired_lot_list
self.__acquired_lot_heap: List[Tuple[Union[float, RP2Decimal], InTransaction]] = []
qwhelan marked this conversation as resolved.
Show resolved Hide resolved
self.__acquired_lot_2_partial_amount = acquired_lot_2_partial_amount
self.__to_index = 0
self.__from_index = 0

def set_to_index(self, to_index: int) -> None:
# Control how far to advance the iterator, caller is responsible for updating
if self.__accounting_method.use_heap():
qwhelan marked this conversation as resolved.
Show resolved Hide resolved
for i in range(self.__to_index, to_index + 1):
eprbell marked this conversation as resolved.
Show resolved Hide resolved
lot = self.__acquired_lot_list[i]
self.add_selected_lot_to_heap(lot)
self.__to_index = to_index

def set_from_index(self, from_index: int) -> None:
Expand All @@ -55,6 +67,18 @@ def set_from_index(self, from_index: int) -> None:
def from_index(self) -> int:
return self.__from_index

@property
def to_index(self) -> int:
return self.__to_index

@property
def acquired_lot_heap(self) -> List[Tuple[Union[float, RP2Decimal], InTransaction]]:
return self.__acquired_lot_heap

@property
def acquired_lot_list(self) -> List[InTransaction]:
return self.__acquired_lot_list

def has_partial_amount(self, acquired_lot: InTransaction) -> bool:
return acquired_lot in self.__acquired_lot_2_partial_amount

Expand All @@ -69,11 +93,15 @@ def set_partial_amount(self, acquired_lot: InTransaction, amount: RP2Decimal) ->
def clear_partial_amount(self, acquired_lot: InTransaction) -> None:
self.set_partial_amount(acquired_lot, ZERO)

def __iter__(self) -> "AccountingMethodIterator":
return AccountingMethodIterator(self.__acquired_lot_list, self.__from_index, self.__to_index, self.__accounting_method.lot_candidates_order())
def add_selected_lot_to_heap(self, lot: InTransaction) -> None:
qwhelan marked this conversation as resolved.
Show resolved Hide resolved
heap_item = (self.__accounting_method.heap_key(lot), lot)
heappush(self.__acquired_lot_heap, heap_item)

def __iter__(self) -> AbstractAccountingMethodIterator:
return self.__accounting_method._get_accounting_method_iterator(self)

class AccountingMethodIterator:

class ListAccountingMethodIterator(AbstractAccountingMethodIterator):
def __init__(self, acquired_lot_list: List[InTransaction], from_index: int, to_index: int, order_type: AcquiredLotCandidatesOrder) -> None:
self.__acquired_lot_list = acquired_lot_list
self.__start_index = from_index if order_type == AcquiredLotCandidatesOrder.OLDER_TO_NEWER else to_index
Expand All @@ -96,6 +124,17 @@ def __next__(self) -> InTransaction:
raise StopIteration(self)


class HeapAccountingMethodIterator(AbstractAccountingMethodIterator):
def __init__(self, acquired_lot_heap: List[Tuple[Union[float, RP2Decimal], InTransaction]]) -> None:
self.__acquired_lot_heap = acquired_lot_heap

def __next__(self) -> InTransaction:
while len(self.__acquired_lot_heap) > 0:
_, result = heappop(self.__acquired_lot_heap)
return result
raise StopIteration(self)


class AbstractAccountingMethod:
def seek_non_exhausted_acquired_lot(
self,
Expand All @@ -114,3 +153,12 @@ def name(self) -> str:

def __repr__(self) -> str:
return self.name

def heap_key(self, lot: InTransaction) -> Union[RP2Decimal, float]:
raise NotImplementedError("Abstract function")

def use_heap(self) -> bool:
raise NotImplementedError("Abstract function")

def _get_accounting_method_iterator(self, lot_candidates: AcquiredLotCandidates) -> AbstractAccountingMethodIterator:
raise NotImplementedError("Abstract function")
4 changes: 4 additions & 0 deletions src/rp2/abstract_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ def to_string(self, indent: int = 0, repr_format: bool = True, extra_data: Optio
def internal_id(self) -> str:
return str(self.__internal_id)

@property
def internal_id_int(self) -> int:
qwhelan marked this conversation as resolved.
Show resolved Hide resolved
return self.__internal_id

@property
def timestamp(self) -> datetime:
return self.__timestamp
Expand Down
3 changes: 3 additions & 0 deletions src/rp2/in_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,6 @@ def is_crypto_fee_defined(self) -> bool:

def is_taxable(self) -> bool:
return self.transaction_type.is_earn_type()

def __lt__(self, other: "InTransaction") -> bool:
qwhelan marked this conversation as resolved.
Show resolved Hide resolved
return self.timestamp < other.timestamp
10 changes: 10 additions & 0 deletions src/rp2/plugin/accounting_method/fifo.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
AcquiredLotAndAmount,
AcquiredLotCandidates,
AcquiredLotCandidatesOrder,
ListAccountingMethodIterator,
)
from rp2.abstract_transaction import AbstractTransaction
from rp2.in_transaction import InTransaction
Expand Down Expand Up @@ -63,3 +64,12 @@ def seek_non_exhausted_acquired_lot(

def lot_candidates_order(self) -> AcquiredLotCandidatesOrder:
return AcquiredLotCandidatesOrder.OLDER_TO_NEWER

def heap_key(self, lot: InTransaction) -> float:
raise ValueError()

def use_heap(self) -> bool:
return False
qwhelan marked this conversation as resolved.
Show resolved Hide resolved

def _get_accounting_method_iterator(self, lot_candidates: AcquiredLotCandidates) -> ListAccountingMethodIterator:
return ListAccountingMethodIterator(lot_candidates.acquired_lot_list, lot_candidates.from_index, lot_candidates.to_index, self.lot_candidates_order())
15 changes: 14 additions & 1 deletion src/rp2/plugin/accounting_method/hifo.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
AcquiredLotAndAmount,
AcquiredLotCandidates,
AcquiredLotCandidatesOrder,
HeapAccountingMethodIterator,
)
from rp2.abstract_transaction import AbstractTransaction
from rp2.in_transaction import InTransaction
Expand Down Expand Up @@ -51,14 +52,26 @@ def seek_non_exhausted_acquired_lot(
# The acquired lot has zero partial amount
continue

if selected_acquired_lot is None or selected_acquired_lot.spot_price < acquired_lot.spot_price:
if acquired_lot_amount > ZERO:
qwhelan marked this conversation as resolved.
Show resolved Hide resolved
selected_acquired_lot_amount = acquired_lot_amount
selected_acquired_lot = acquired_lot
break

if selected_acquired_lot_amount > ZERO and selected_acquired_lot:
lot_candidates.clear_partial_amount(selected_acquired_lot)
if selected_acquired_lot_amount > taxable_event_amount:
eprbell marked this conversation as resolved.
Show resolved Hide resolved
lot_candidates.add_selected_lot_to_heap(selected_acquired_lot)
return AcquiredLotAndAmount(acquired_lot=selected_acquired_lot, amount=selected_acquired_lot_amount)
return None

def lot_candidates_order(self) -> AcquiredLotCandidatesOrder:
return AcquiredLotCandidatesOrder.OLDER_TO_NEWER

def heap_key(self, lot: InTransaction) -> RP2Decimal:
return -lot.spot_price

def use_heap(self) -> bool:
return True

def _get_accounting_method_iterator(self, lot_candidates: AcquiredLotCandidates) -> HeapAccountingMethodIterator:
return HeapAccountingMethodIterator(lot_candidates.acquired_lot_heap)
14 changes: 13 additions & 1 deletion src/rp2/plugin/accounting_method/lifo.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@
AcquiredLotAndAmount,
AcquiredLotCandidates,
AcquiredLotCandidatesOrder,
HeapAccountingMethodIterator,
)
from rp2.abstract_transaction import AbstractTransaction
from rp2.in_transaction import InTransaction
from rp2.rp2_decimal import ZERO, RP2Decimal


# LIFO plugin. See https://www.investopedia.com/terms/l/lifo.asp. This plugin uses universal application, not per-wallet application:
# Last In, First Out (LIFO) plugin. See https://www.investopedia.com/terms/l/lifo.asp. This plugin uses universal application, not per-wallet application:
# this means there is one queue for each coin across every wallet and exchange and the accounting method is applied to each such queue.
# More on this at https://www.forbes.com/sites/shehanchandrasekera/2020/09/17/what-crypto-taxpayers-need-to-know-about-fifo-lifo-hifo-specific-id/
# Note that under LIFO the date acquired must still be before or on the date sold: for details see
Expand Down Expand Up @@ -59,8 +60,19 @@ def seek_non_exhausted_acquired_lot(

if selected_acquired_lot_amount > ZERO and selected_acquired_lot:
lot_candidates.clear_partial_amount(selected_acquired_lot)
if selected_acquired_lot_amount > taxable_event_amount:
eprbell marked this conversation as resolved.
Show resolved Hide resolved
lot_candidates.add_selected_lot_to_heap(selected_acquired_lot)
return AcquiredLotAndAmount(acquired_lot=selected_acquired_lot, amount=selected_acquired_lot_amount)
return None

def lot_candidates_order(self) -> AcquiredLotCandidatesOrder:
return AcquiredLotCandidatesOrder.NEWER_TO_OLDER

def heap_key(self, lot: InTransaction) -> float:
return -lot.timestamp.timestamp()

def use_heap(self) -> bool:
return True

def _get_accounting_method_iterator(self, lot_candidates: AcquiredLotCandidates) -> HeapAccountingMethodIterator:
return HeapAccountingMethodIterator(lot_candidates.acquired_lot_heap)
3 changes: 3 additions & 0 deletions src/rp2/rp2_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,8 @@ def __rmod__(self, other: object) -> "RP2Decimal":
raise RP2TypeError(f"Operand has non-Decimal value {repr(other)}")
return RP2Decimal(Decimal.__rmod__(self, other))

def __neg__(self) -> "RP2Decimal":
return RP2Decimal(Decimal.__neg__(self))


ZERO: RP2Decimal = RP2Decimal("0")
Loading