@@ -67,14 +67,16 @@ def intra_transactions(self) -> TransactionSet:
6767
6868class 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