Skip to content

Commit

Permalink
Make LIFO/HIFO accounting methods O(n*log(m))
Browse files Browse the repository at this point in the history
  • Loading branch information
qwhelan committed May 18, 2024
1 parent b61e26f commit 4725a3c
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 5 deletions.
37 changes: 34 additions & 3 deletions src/rp2/abstract_accounting_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@


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
Expand All @@ -41,11 +42,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]] = []
self.__acquired_lot_2_partial_amount = acquired_lot_2_partial_amount
self.__up_to_index = 0
self.__from_index = 0

def set_up_to_index(self, up_to_index: int) -> None:
# Control how far to advance the iterator, caller is responsible for updating
if self.__accounting_method.use_heap():
for i in range(self.__up_to_index, up_to_index + 1):
lot = self.__acquired_lot_list[i]
self.add_selected_lot_to_heap(lot)
self.__up_to_index = up_to_index

def set_from_index(self, from_index: int) -> None:
Expand All @@ -69,8 +76,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.__up_to_index, self.__accounting_method.lot_candidates_order())
def add_selected_lot_to_heap(self, lot: InTransaction) -> None:
heap_item = (self.__accounting_method.heap_key(lot), lot)
heappush(self.__acquired_lot_heap, heap_item)

def __iter__(self) -> Union["AccountingMethodIterator", "HeapAccountingMethodIterator"]:
if self.__accounting_method.use_heap(): # pylint: disable=no-else-return
return HeapAccountingMethodIterator(self.__acquired_lot_heap)
else:
return AccountingMethodIterator(self.__acquired_lot_list, self.__from_index, self.__up_to_index, self.__accounting_method.lot_candidates_order())


class AccountingMethodIterator:
Expand All @@ -96,6 +110,17 @@ def __next__(self) -> InTransaction:
raise StopIteration(self)


class HeapAccountingMethodIterator:
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 +139,9 @@ 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")
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:
return self.timestamp < other.timestamp
6 changes: 6 additions & 0 deletions src/rp2/plugin/accounting_method/fifo.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,9 @@ def seek_non_exhausted_acquired_lot(

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

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

def use_heap(self) -> bool:
return False
10 changes: 9 additions & 1 deletion src/rp2/plugin/accounting_method/hifo.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,22 @@ 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:
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)
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
9 changes: 8 additions & 1 deletion src/rp2/plugin/accounting_method/lifo.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
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 +59,15 @@ def seek_non_exhausted_acquired_lot(

if selected_acquired_lot_amount > ZERO and selected_acquired_lot:
lot_candidates.clear_partial_amount(selected_acquired_lot)
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
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")

0 comments on commit 4725a3c

Please sign in to comment.