@@ -36,10 +36,18 @@ class SeekLotResult:
3636
3737
3838@dataclass (frozen = True , eq = True )
39- class InTransactionSpec :
39+ class InTransactionDescriptor :
4040 spot_price : int
4141 amount : int
4242
43+ @dataclass (frozen = True , eq = True )
44+ class _Test :
45+ description : str
46+ lot_selection_method : AbstractAccountingMethod
47+ in_transactions : List [InTransactionDescriptor ]
48+ amounts_to_match : List [int ]
49+ want : List [SeekLotResult ]
50+
4351
4452class TestAccountingMethod (unittest .TestCase ):
4553 _configuration : Configuration
@@ -51,10 +59,10 @@ def setUpClass(cls) -> None:
5159 def setUp (self ) -> None :
5260 self .maxDiff = None # pylint: disable=invalid-name
5361
54- def _initialize_acquired_lots (self , in_transaction_spec_list : List [InTransactionSpec ]) -> List [InTransaction ]:
62+ def _initialize_acquired_lots (self , in_transaction_descriptors : List [InTransactionDescriptor ]) -> List [InTransaction ]:
5563 date = datetime .strptime ("2021-01-01" , "%Y-%m-%d" )
5664 in_transactions : List [InTransaction ] = []
57- for i , in_transaction_spec in enumerate (in_transaction_spec_list ):
65+ for i , in_transaction_descriptor in enumerate (in_transaction_descriptors ):
5866 in_transactions .append (
5967 InTransaction (
6068 self ._configuration ,
@@ -63,49 +71,46 @@ def _initialize_acquired_lots(self, in_transaction_spec_list: List[InTransaction
6371 "Coinbase" ,
6472 "Bob" ,
6573 "Buy" ,
66- RP2Decimal (in_transaction_spec .spot_price ),
67- RP2Decimal (in_transaction_spec .amount ),
74+ RP2Decimal (in_transaction_descriptor .spot_price ),
75+ RP2Decimal (in_transaction_descriptor .amount ),
6876 row = 1 + i ,
6977 )
7078 )
7179 date += timedelta (days = 1 )
7280 return in_transactions
7381
7482 # This function adds all acquired lots at first and then does amount pairings.
75- def _test_fixed_lot_candidates (
76- self , lot_selection_method : AbstractAccountingMethod , in_transactions : List [ InTransaction ], amounts_to_match : List [ int ], want : List [ SeekLotResult ]
77- ) -> None :
83+ def _run_test_fixed_lot_candidates ( self , lot_selection_method : AbstractAccountingMethod , test : _Test ) -> None :
84+ print ( f" \n Description: { test . description : } " )
85+ in_transactions = self . _initialize_acquired_lots ( test . in_transactions )
7886 acquired_lot_candidates = lot_selection_method .create_lot_candidates (in_transactions , {})
7987 acquired_lot_candidates .set_to_index (len (in_transactions ) - 1 )
80- print (in_transactions )
8188 i = 0
82- for int_amount in amounts_to_match :
89+ for int_amount in test . amounts_to_match :
8390 amount = RP2Decimal (int_amount )
8491 while True :
8592 result = lot_selection_method .seek_non_exhausted_acquired_lot (acquired_lot_candidates , amount )
8693 if result is None :
8794 break
8895 if result .amount >= amount :
8996 acquired_lot_candidates .set_partial_amount (result .acquired_lot , result .amount - amount )
90- print (i , want [i ], amount , result )
91- self .assertEqual (result .amount , RP2Decimal (want [i ].amount ))
92- self .assertEqual (result .acquired_lot .row , want [i ].row )
97+ self .assertEqual (result .amount , RP2Decimal (test .want [i ].amount ))
98+ self .assertEqual (result .acquired_lot .row , test .want [i ].row )
9399 i += 1
94100 break
95101 acquired_lot_candidates .clear_partial_amount (result .acquired_lot )
96102 amount -= result .amount
97- print (i , want [i ], amount , result )
98- self .assertEqual (result .amount , RP2Decimal (want [i ].amount ))
99- self .assertEqual (result .acquired_lot .row , want [i ].row )
103+ self .assertEqual (result .amount , RP2Decimal (test .want [i ].amount ))
104+ self .assertEqual (result .acquired_lot .row , test .want [i ].row )
100105 i += 1
101106
102107 # This function grows lot_candidates dynamically: it adds an acquired lot, does an amount pairing and repeats.
103- def _test_dynamic_lot_candidates (
104- self , lot_selection_method : AbstractAccountingMethod , in_transactions : List [ InTransaction ], amounts_to_match : List [ int ], want : List [ SeekLotResult ]
105- ) -> None :
108+ def _run_test_dynamic_lot_candidates ( self , lot_selection_method : AbstractAccountingMethod , test : _Test ) -> None :
109+ print ( f" \n Description: { test . description : } " )
110+ in_transactions = self . _initialize_acquired_lots ( test . in_transactions )
106111 acquired_lot_candidates = lot_selection_method .create_lot_candidates ([], {})
107112 i = 0
108- for int_amount in amounts_to_match :
113+ for int_amount in test . amounts_to_match :
109114 amount = RP2Decimal (int_amount )
110115 while True :
111116 if i < len (in_transactions ):
@@ -116,125 +121,117 @@ def _test_dynamic_lot_candidates(
116121 break
117122 if result .amount >= amount :
118123 acquired_lot_candidates .set_partial_amount (result .acquired_lot , result .amount - amount )
119- print (i , want [i ], amount , result )
120- self .assertEqual (result .amount , RP2Decimal (want [i ].amount ))
121- self .assertEqual (result .acquired_lot .row , want [i ].row )
124+ self .assertEqual (result .amount , RP2Decimal (test .want [i ].amount ))
125+ self .assertEqual (result .acquired_lot .row , test .want [i ].row )
122126 i += 1
123127 break
124128 acquired_lot_candidates .clear_partial_amount (result .acquired_lot )
125129 amount -= result .amount
126- print (i , want [i ], amount , result )
127- self .assertEqual (result .amount , RP2Decimal (want [i ].amount ))
128- self .assertEqual (result .acquired_lot .row , want [i ].row )
130+ self .assertEqual (result .amount , RP2Decimal (test .want [i ].amount ))
131+ self .assertEqual (result .acquired_lot .row , test .want [i ].row )
129132 i += 1
130133
131- def test_lot_candidates_with_fifo (self ) -> None :
132- lot_selection_method = AccountingMethodFIFO ()
133-
134- # Simple test.
135- self ._test_fixed_lot_candidates (
136- lot_selection_method = lot_selection_method ,
137- in_transactions = self ._initialize_acquired_lots ([InTransactionSpec (10 , 10 ), InTransactionSpec (11 , 20 ), InTransactionSpec (12 , 30 )]),
138- amounts_to_match = [6 , 4 , 2 , 18 , 3 ],
139- want = [SeekLotResult (10 , 1 ), SeekLotResult (4 , 1 ), SeekLotResult (20 , 2 ), SeekLotResult (18 , 2 ), SeekLotResult (30 , 3 )],
140- )
141-
142- # Test with requested amount greater than acquired lot.
143- self ._test_fixed_lot_candidates (
144- lot_selection_method = lot_selection_method ,
145- in_transactions = self ._initialize_acquired_lots ([InTransactionSpec (10 , 10 ), InTransactionSpec (11 , 20 ), InTransactionSpec (12 , 30 )]),
146- amounts_to_match = [15 , 10 , 5 ],
147- want = [SeekLotResult (10 , 1 ), SeekLotResult (20 , 2 ), SeekLotResult (15 , 2 ), SeekLotResult (5 , 2 )],
148- )
149-
150- # Test with dynamic lot candidates
151- self ._test_dynamic_lot_candidates (
152- lot_selection_method = lot_selection_method ,
153- in_transactions = self ._initialize_acquired_lots ([InTransactionSpec (10 , 10 ), InTransactionSpec (11 , 20 ), InTransactionSpec (12 , 30 )]),
154- amounts_to_match = [6 , 4 , 2 , 18 , 3 ],
155- want = [SeekLotResult (10 , 1 ), SeekLotResult (4 , 1 ), SeekLotResult (20 , 2 ), SeekLotResult (18 , 2 ), SeekLotResult (30 , 3 )],
156- )
157-
158- def test_lot_candidates_with_lifo (self ) -> None :
159- lot_selection_method = AccountingMethodLIFO ()
160-
161- # Simple test.
162- self ._test_fixed_lot_candidates (
163- lot_selection_method = lot_selection_method ,
164- in_transactions = self ._initialize_acquired_lots ([InTransactionSpec (10 , 10 ), InTransactionSpec (11 , 20 ), InTransactionSpec (12 , 30 )]),
165- amounts_to_match = [7 , 23 , 19 , 1 , 9 ],
166- want = [SeekLotResult (30 , 3 ), SeekLotResult (23 , 3 ), SeekLotResult (20 , 2 ), SeekLotResult (1 , 2 ), SeekLotResult (10 , 1 )],
167- )
168-
169- # Test with requested amount greater than acquired lot.
170- self ._test_fixed_lot_candidates (
171- lot_selection_method = lot_selection_method ,
172- in_transactions = self ._initialize_acquired_lots ([InTransactionSpec (10 , 10 ), InTransactionSpec (11 , 20 ), InTransactionSpec (12 , 30 )]),
173- amounts_to_match = [55 , 5 ],
174- want = [SeekLotResult (30 , 3 ), SeekLotResult (20 , 2 ), SeekLotResult (10 , 1 ), SeekLotResult (5 , 1 )],
175- )
176-
177- # Test with dynamic lot candidates
178- self ._test_dynamic_lot_candidates (
179- lot_selection_method = lot_selection_method ,
180- in_transactions = self ._initialize_acquired_lots ([InTransactionSpec (10 , 10 ), InTransactionSpec (11 , 20 ), InTransactionSpec (12 , 30 )]),
181- amounts_to_match = [4 , 15 , 27 , 14 ],
182- want = [SeekLotResult (10 , 1 ), SeekLotResult (20 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (3 , 3 ), SeekLotResult (5 , 2 ), SeekLotResult (6 , 1 )],
183- )
184-
185- def test_fixed_lot_candidates_with_hifo (self ) -> None :
186- lot_selection_method = AccountingMethodHIFO ()
187-
188- # Simple test.
189- self ._test_fixed_lot_candidates (
190- lot_selection_method = lot_selection_method ,
191- in_transactions = self ._initialize_acquired_lots ([InTransactionSpec (10 , 10 ), InTransactionSpec (12 , 20 ), InTransactionSpec (11 , 30 )]),
192- amounts_to_match = [15 , 5 , 20 , 10 , 7 ],
193- want = [SeekLotResult (20 , 2 ), SeekLotResult (5 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (10 , 3 ), SeekLotResult (10 , 1 )],
194- )
195-
196- # Test with requested amount greater than acquired lot.
197- self ._test_fixed_lot_candidates (
198- lot_selection_method = lot_selection_method ,
199- in_transactions = self ._initialize_acquired_lots ([InTransactionSpec (10 , 10 ), InTransactionSpec (12 , 20 ), InTransactionSpec (11 , 30 )]),
200- amounts_to_match = [15 , 5 , 35 , 5 ],
201- want = [SeekLotResult (20 , 2 ), SeekLotResult (5 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (10 , 1 ), SeekLotResult (5 , 1 )],
202- )
203-
204- # Test with dynamic lot candidates
205- self ._test_dynamic_lot_candidates (
206- lot_selection_method = lot_selection_method ,
207- in_transactions = self ._initialize_acquired_lots ([InTransactionSpec (10 , 10 ), InTransactionSpec (12 , 20 ), InTransactionSpec (11 , 30 )]),
208- amounts_to_match = [4 , 16 , 40 ],
209- want = [SeekLotResult (10 , 1 ), SeekLotResult (20 , 2 ), SeekLotResult (4 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (6 , 1 )],
210- )
211-
212- def test_fixed_lot_candidates_with_lofo (self ) -> None :
213- lot_selection_method = AccountingMethodLOFO ()
214-
215- # Simple test.
216- self ._test_fixed_lot_candidates (
217- lot_selection_method = lot_selection_method ,
218- in_transactions = self ._initialize_acquired_lots ([InTransactionSpec (12 , 10 ), InTransactionSpec (10 , 20 ), InTransactionSpec (11 , 30 )]),
219- amounts_to_match = [15 , 5 , 20 , 10 , 7 ],
220- want = [SeekLotResult (20 , 2 ), SeekLotResult (5 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (10 , 3 ), SeekLotResult (10 , 1 )],
221- )
222-
223- # Test with requested amount greater than acquired lot.
224- self ._test_fixed_lot_candidates (
225- lot_selection_method = lot_selection_method ,
226- in_transactions = self ._initialize_acquired_lots ([InTransactionSpec (12 , 10 ), InTransactionSpec (10 , 20 ), InTransactionSpec (11 , 30 )]),
227- amounts_to_match = [15 , 5 , 35 , 5 ],
228- want = [SeekLotResult (20 , 2 ), SeekLotResult (5 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (10 , 1 ), SeekLotResult (5 , 1 )],
229- )
230-
231- # Test with dynamic lot candidates
232- self ._test_dynamic_lot_candidates (
233- lot_selection_method = lot_selection_method ,
234- in_transactions = self ._initialize_acquired_lots ([InTransactionSpec (12 , 10 ), InTransactionSpec (10 , 20 ), InTransactionSpec (11 , 30 )]),
235- amounts_to_match = [4 , 16 , 40 ],
236- want = [SeekLotResult (10 , 1 ), SeekLotResult (20 , 2 ), SeekLotResult (4 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (6 , 1 )],
237- )
134+ def test_with_fixed_lot_candidates (self ) -> None :
135+ # Go-style, table-based tests. The want field contains the expected results.
136+ tests : List [_Test ] = [
137+ _Test (
138+ description = "Simple test (FIFO)" ,
139+ lot_selection_method = AccountingMethodFIFO (),
140+ in_transactions = [InTransactionDescriptor (10 , 10 ), InTransactionDescriptor (11 , 20 ), InTransactionDescriptor (12 , 30 )],
141+ amounts_to_match = [6 , 4 , 2 , 18 , 3 ],
142+ want = [SeekLotResult (10 , 1 ), SeekLotResult (4 , 1 ), SeekLotResult (20 , 2 ), SeekLotResult (18 , 2 ), SeekLotResult (30 , 3 )],
143+ ),
144+ _Test (
145+ description = "Requested amount greater than acquired lot (FIFO)" ,
146+ lot_selection_method = AccountingMethodFIFO (),
147+ in_transactions = [InTransactionDescriptor (10 , 10 ), InTransactionDescriptor (11 , 20 ), InTransactionDescriptor (12 , 30 )],
148+ amounts_to_match = [15 , 10 , 5 ],
149+ want = [SeekLotResult (10 , 1 ), SeekLotResult (20 , 2 ), SeekLotResult (15 , 2 ), SeekLotResult (5 , 2 )],
150+ ),
151+ _Test (
152+ description = "Simple test (LIFO)" ,
153+ lot_selection_method = AccountingMethodLIFO (),
154+ in_transactions = [InTransactionDescriptor (10 , 10 ), InTransactionDescriptor (11 , 20 ), InTransactionDescriptor (12 , 30 )],
155+ amounts_to_match = [7 , 23 , 19 , 1 , 9 ],
156+ want = [SeekLotResult (30 , 3 ), SeekLotResult (23 , 3 ), SeekLotResult (20 , 2 ), SeekLotResult (1 , 2 ), SeekLotResult (10 , 1 )],
157+ ),
158+ _Test (
159+ description = "Requested amount greater than acquired lot (LIFO)" ,
160+ lot_selection_method = AccountingMethodLIFO (),
161+ in_transactions = [InTransactionDescriptor (10 , 10 ), InTransactionDescriptor (11 , 20 ), InTransactionDescriptor (12 , 30 )],
162+ amounts_to_match = [55 , 5 ],
163+ want = [SeekLotResult (30 , 3 ), SeekLotResult (20 , 2 ), SeekLotResult (10 , 1 ), SeekLotResult (5 , 1 )],
164+ ),
165+ _Test (
166+ description = "Simple test (HIFO)" ,
167+ lot_selection_method = AccountingMethodHIFO (),
168+ in_transactions = [InTransactionDescriptor (10 , 10 ), InTransactionDescriptor (12 , 20 ), InTransactionDescriptor (11 , 30 )],
169+ amounts_to_match = [15 , 5 , 20 , 10 , 7 ],
170+ want = [SeekLotResult (20 , 2 ), SeekLotResult (5 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (10 , 3 ), SeekLotResult (10 , 1 )],
171+ ),
172+ _Test (
173+ description = "Requested amount greater than acquired lot (HIFO)" ,
174+ lot_selection_method = AccountingMethodHIFO (),
175+ in_transactions = [InTransactionDescriptor (10 , 10 ), InTransactionDescriptor (12 , 20 ), InTransactionDescriptor (11 , 30 )],
176+ amounts_to_match = [15 , 5 , 35 , 5 ],
177+ want = [SeekLotResult (20 , 2 ), SeekLotResult (5 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (10 , 1 ), SeekLotResult (5 , 1 )],
178+ ),
179+ _Test (
180+ description = "Simple test (LOFO)" ,
181+ lot_selection_method = AccountingMethodLOFO (),
182+ in_transactions = [InTransactionDescriptor (12 , 10 ), InTransactionDescriptor (10 , 20 ), InTransactionDescriptor (11 , 30 )],
183+ amounts_to_match = [15 , 5 , 20 , 10 , 7 ],
184+ want = [SeekLotResult (20 , 2 ), SeekLotResult (5 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (10 , 3 ), SeekLotResult (10 , 1 )],
185+ ),
186+ _Test (
187+ description = "Requested amount greater than acquired lot (LOFO)" ,
188+ lot_selection_method = AccountingMethodLOFO (),
189+ in_transactions = [InTransactionDescriptor (12 , 10 ), InTransactionDescriptor (10 , 20 ), InTransactionDescriptor (11 , 30 )],
190+ amounts_to_match = [15 , 5 , 35 , 5 ],
191+ want = [SeekLotResult (20 , 2 ), SeekLotResult (5 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (10 , 1 ), SeekLotResult (5 , 1 )],
192+ )
193+
194+ ]
195+ for test in tests :
196+ with self .subTest (name = f"{ test .description } " ):
197+ self ._run_test_fixed_lot_candidates (lot_selection_method = test .lot_selection_method , test = test )
198+
199+
200+ def test_with_dynamic_lot_candidates (self ) -> None :
201+ # Go-style, table-based tests. The want field contains the expected results.
202+ tests : List [_Test ] = [
203+ _Test (
204+ description = "Dynamic test (FIFO)" ,
205+ lot_selection_method = AccountingMethodFIFO (),
206+ in_transactions = [InTransactionDescriptor (10 , 10 ), InTransactionDescriptor (11 , 20 ), InTransactionDescriptor (12 , 30 )],
207+ amounts_to_match = [6 , 4 , 2 , 18 , 3 ],
208+ want = [SeekLotResult (10 , 1 ), SeekLotResult (4 , 1 ), SeekLotResult (20 , 2 ), SeekLotResult (18 , 2 ), SeekLotResult (30 , 3 )],
209+ ),
210+ _Test (
211+ description = "Dynamic test (LIFO)" ,
212+ lot_selection_method = AccountingMethodLIFO (),
213+ in_transactions = [InTransactionDescriptor (10 , 10 ), InTransactionDescriptor (11 , 20 ), InTransactionDescriptor (12 , 30 )],
214+ amounts_to_match = [4 , 15 , 27 , 14 ],
215+ want = [SeekLotResult (10 , 1 ), SeekLotResult (20 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (3 , 3 ), SeekLotResult (5 , 2 ), SeekLotResult (6 , 1 )],
216+ ),
217+ _Test (
218+ description = "Dynamic test (HIFO)" ,
219+ lot_selection_method = AccountingMethodHIFO (),
220+ in_transactions = [InTransactionDescriptor (10 , 10 ), InTransactionDescriptor (12 , 20 ), InTransactionDescriptor (11 , 30 )],
221+ amounts_to_match = [4 , 16 , 40 ],
222+ want = [SeekLotResult (10 , 1 ), SeekLotResult (20 , 2 ), SeekLotResult (4 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (6 , 1 )],
223+ ),
224+ _Test (
225+ description = "Dynamic test (LOFO)" ,
226+ lot_selection_method = AccountingMethodLOFO (),
227+ in_transactions = [InTransactionDescriptor (12 , 10 ), InTransactionDescriptor (10 , 20 ), InTransactionDescriptor (11 , 30 )],
228+ amounts_to_match = [4 , 16 , 40 ],
229+ want = [SeekLotResult (10 , 1 ), SeekLotResult (20 , 2 ), SeekLotResult (4 , 2 ), SeekLotResult (30 , 3 ), SeekLotResult (6 , 1 )],
230+ )
231+ ]
232+ for test in tests :
233+ with self .subTest (name = f"{ test .description } " ):
234+ self ._run_test_dynamic_lot_candidates (lot_selection_method = test .lot_selection_method , test = test )
238235
239236
240237if __name__ == "__main__" :
0 commit comments