Skip to content

Commit 11763b7

Browse files
committed
Fixed fee handling bug, added support for per-wallet model without transfer pointers (used by global allocation)
1 parent f8963bd commit 11763b7

File tree

1 file changed

+32
-22
lines changed

1 file changed

+32
-22
lines changed

src/rp2/transfer_analyzer.py

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,16 @@ def intra_transactions(self) -> TransactionSet:
6767

6868
class TransferAnalyzer:
6969
def __init__(
70-
self, configuration: Configuration, transfer_semantics: AbstractAccountingMethod, universal_input_data: InputData
70+
self, configuration: Configuration, transfer_semantics: AbstractAccountingMethod, universal_input_data: InputData, skip_transfer_pointers: bool = False
7171
):
72+
# TODO: add run-time argument type checks.
7273
self.__configuration = Configuration.type_check("configuration", configuration)
7374
if not isinstance(transfer_semantics, AbstractAccountingMethod):
7475
raise RP2TypeError(f"Parameter 'transfer_semantics' is not of type AbstractAccountingMethod: {transfer_semantics}")
7576
self.__transfer_semantics = transfer_semantics
7677
self.__universal_input_data = InputData.type_check("universal_input_data", universal_input_data)
77-
# TODO: add run-time argument type checks.
78+
# skip_transfer_pointers is used in global allocation, where the artificial transactions are used only as guides and are replaced by new ones decided by the allocation method.
79+
self.__skip_transfer_pointers = Configuration.type_check_bool("skip_transfer_pointers", skip_transfer_pointers)
7880

7981
# Utility function to create an artificial InTransaction modeling the "to" side of an IntraTransaction
8082
def _create_to_in_transaction(self, from_in_transaction: InTransaction, transfer_transaction: IntraTransaction, amount: RP2Decimal) -> InTransaction:
@@ -112,17 +114,18 @@ def _create_to_in_transaction(self, from_in_transaction: InTransaction, transfer
112114
cost_basis_timestamp=cost_basis_timestamp_string,
113115
)
114116

115-
# Update the originates_from field of the artificial transaction and the to_lots field of all its ancestors.
116-
current_transaction: Optional[InTransaction] = result
117-
to_account = Account(transfer_transaction.to_exchange, transfer_transaction.to_holder)
118-
while True:
119-
current_transaction = current_transaction.from_lot if current_transaction is not None else None
120-
if current_transaction is None:
121-
break
122-
current_account = Account(current_transaction.exchange, current_transaction.holder)
123-
result.originates_from[current_account] = current_transaction
124-
to_lots = current_transaction.to_lots.setdefault(to_account, [])
125-
to_lots.append(result)
117+
if not self.__skip_transfer_pointers:
118+
# Update the originates_from field of the artificial transaction and the to_lots field of all its ancestors.
119+
current_transaction: Optional[InTransaction] = result
120+
to_account = Account(transfer_transaction.to_exchange, transfer_transaction.to_holder)
121+
while True:
122+
current_transaction = current_transaction.from_lot if current_transaction is not None else None
123+
if current_transaction is None:
124+
break
125+
current_account = Account(current_transaction.exchange, current_transaction.holder)
126+
result.originates_from[current_account] = current_transaction
127+
to_lots = current_transaction.to_lots.setdefault(to_account, [])
128+
to_lots.append(result)
126129

127130
return result
128131

@@ -156,6 +159,7 @@ def _process_remaining_transfer_amount(
156159
current_in_lot_and_amount: AcquiredLotAndAmount,
157160
transfer: IntraTransaction,
158161
remaining_amount: RP2Decimal,
162+
fee: RP2Decimal,
159163
) -> None:
160164
from_account = Account(transfer.from_exchange, transfer.from_holder)
161165
from_per_wallet_transactions = wallet_2_per_wallet_transactions[from_account]
@@ -184,7 +188,7 @@ def _process_remaining_transfer_amount(
184188
to_per_wallet_transactions.in_transactions.set_to_index(len(to_per_wallet_transactions.in_transactions.acquired_lot_list) - 1)
185189
# Remove the remaining amount from the actual amount of the current in lot.
186190
from_per_wallet_transactions.in_transactions.set_partial_amount(
187-
current_in_lot_and_amount.acquired_lot, current_in_lot_and_amount.amount - remaining_amount
191+
current_in_lot_and_amount.acquired_lot, current_in_lot_and_amount.amount - remaining_amount - fee
188192
)
189193

190194
# This function performs transfer analysis on an InputData and generates as many new InputData objects as there are wallets.
@@ -223,19 +227,21 @@ def analyze(self) -> Dict[Account, InputData]:
223227
per_wallet_transactions = wallet_2_per_wallet_transactions[account]
224228
per_wallet_transactions.out_transactions.add_entry(transaction)
225229

226-
# Find the acquired lots that cover the out transaction and mark them as actually (or fully) spent.
227-
amount_left_to_dispose_of = transaction.crypto_out_with_fee
230+
# Find the acquired lots that cover the out transaction and mark them as partially (or fully) spent.
231+
amount_left_to_dispose_of = transaction.crypto_out_no_fee
232+
fee = transaction.crypto_fee
228233
while True:
229234
current_in_lot_and_amount = self.__transfer_semantics.seek_non_exhausted_acquired_lot(
230235
per_wallet_transactions.in_transactions, transaction.crypto_out_with_fee
231236
)
232237
if current_in_lot_and_amount is None:
233238
raise RP2ValueError(
234-
f"Insufficient balance on {account} to cover out transaction (amount {amount_left_to_dispose_of} {transaction.asset}): {transaction}"
239+
f"Insufficient balance on {account} to cover out transaction (amount {amount_left_to_dispose_of + fee} {transaction.asset}): {transaction}"
235240
)
236-
if current_in_lot_and_amount.amount >= amount_left_to_dispose_of:
241+
if current_in_lot_and_amount.amount >= amount_left_to_dispose_of + fee:
242+
# Pay the fee only in the last lot.
237243
per_wallet_transactions.in_transactions.set_partial_amount(
238-
current_in_lot_and_amount.acquired_lot, current_in_lot_and_amount.amount - amount_left_to_dispose_of
244+
current_in_lot_and_amount.acquired_lot, current_in_lot_and_amount.amount - amount_left_to_dispose_of - fee
239245
)
240246
break
241247
per_wallet_transactions.in_transactions.clear_partial_amount(current_in_lot_and_amount.acquired_lot)
@@ -260,24 +266,27 @@ def analyze(self) -> Dict[Account, InputData]:
260266
),
261267
)
262268

263-
# Find the acquired lots that cover the transfer and mark them as actually (or fully) transferred.
269+
# Find the acquired lots that cover the transfer and mark them as partially (or fully) transferred.
264270
amount_left_to_transfer = transaction.crypto_received
271+
fee = transaction.crypto_sent - transaction.crypto_received
265272
original_actual_amounts: Dict[InTransaction, RP2Decimal] = {}
266273
while True:
267274
current_in_lot_and_amount = self.__transfer_semantics.seek_non_exhausted_acquired_lot(
268275
from_per_wallet_transactions.in_transactions, transaction.crypto_received
269276
)
270277
if current_in_lot_and_amount is None:
271278
raise RP2ValueError(
272-
f"Insufficient balance on {from_account} to send funds (amount {amount_left_to_transfer} {transaction.asset}): {transaction}"
279+
f"Insufficient balance on {from_account} to send funds (amount {amount_left_to_transfer + fee} {transaction.asset}): {transaction}"
273280
)
274281
original_actual_amounts[current_in_lot_and_amount.acquired_lot] = current_in_lot_and_amount.amount
275-
if current_in_lot_and_amount.amount >= amount_left_to_transfer:
282+
if current_in_lot_and_amount.amount >= amount_left_to_transfer + fee:
283+
# Pay the fee only in the last lot.
276284
self._process_remaining_transfer_amount(
277285
wallet_2_per_wallet_transactions,
278286
current_in_lot_and_amount,
279287
transaction,
280288
amount_left_to_transfer,
289+
fee,
281290
)
282291
if transaction.is_self_transfer():
283292
from_per_wallet_transactions.in_transactions.reset_partial_amounts(self.__transfer_semantics, original_actual_amounts)
@@ -287,6 +296,7 @@ def analyze(self) -> Dict[Account, InputData]:
287296
current_in_lot_and_amount,
288297
transaction,
289298
current_in_lot_and_amount.amount,
299+
ZERO,
290300
)
291301
amount_left_to_transfer -= current_in_lot_and_amount.amount
292302
else:

0 commit comments

Comments
 (0)