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 c410a01
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 5 deletions.
39 changes: 36 additions & 3 deletions src/rp2/abstract_accounting_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@


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

from rp2.abstract_transaction import AbstractTransaction
from rp2.in_transaction import InTransaction
Expand All @@ -41,11 +43,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 +77,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():
return HeapAccountingMethodIterator(self, 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 +111,18 @@ def __next__(self) -> InTransaction:
raise StopIteration(self)


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

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 +141,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")
6 changes: 6 additions & 0 deletions src/rp2/in_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,9 @@ 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:
if self.timestamp != other.timestamp:
return self.timestamp < other.timestamp
else:
return self.internal_id > other.internal_id
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 c410a01

Please sign in to comment.