def test_replacement_is_first_bought(self): lots = lots_lib.Lots([ self.first_gain, self.first_gain_earlier_sale, self.first_gain_later_sale ]) self.assertSameLot(self.first_gain_earlier_sale, wash.best_replacement_lot(self.loss, lots))
def test_parse_valid_csv_file(self): csv_data = [ 'Num Shares,Symbol,Description,Buy Date,Adjusted Buy Date,Basis,' 'Adjusted Basis,Sell Date,Proceeds,Adjustment Code,Adjustment,' 'Form Position,Buy Lot,Replacement For,Is Replacement,' 'Loss Processed', '10,ABC,A,9/15/2014,9/14/2014,2000,2100,10/5/2014,1800,W,200,form1,' 'lot1,lot3|lot4,true,true', '10,ABC,A,9/15/2014,,2000,,10/5/2014,1800,W,200,form2,lot2,,false,', '20,ABC,A,9/25/2014,,3000,,11/5/2014,1800,,,,,,' ] lots = lots_lib.Lots.create_from_csv_data(csv_data) expected_lots_rows = [] expected_lots_rows.append( lots_lib.Lot(10, 'ABC', 'A', datetime.date(2014, 9, 15), datetime.date(2014, 9, 14), 2000, 2100, datetime.date(2014, 10, 5), 1800, 'W', 200, 'form1', 'lot1', ['lot3', 'lot4'], True, True)) expected_lots_rows.append( lots_lib.Lot(10, 'ABC', 'A', datetime.date(2014, 9, 15), datetime.date(2014, 9, 15), 2000, 2000, datetime.date(2014, 10, 5), 1800, 'W', 200, 'form2', 'lot2', [], False, False)) expected_lots_rows.append( lots_lib.Lot(20, 'ABC', 'A', datetime.date(2014, 9, 25), datetime.date(2014, 9, 25), 3000, 3000, datetime.date(2014, 11, 5), 1800, '', 0, '', '', [], False, False)) expected_lots = lots_lib.Lots(expected_lots_rows) self.assertSameLots(lots, expected_lots)
def test_two_similar_lots(self): # There are two lots that have the same values, but were bought # separately. lot1 = create_lot(10, 2012, 1, 1, 120, 2012, 1, 10, 110) lot2 = create_lot(10, 2012, 1, 1, 120, 2012, 1, 10, 110) lots = lots_lib.Lots([lot1, lot2]) self.assertSameLot(lot2, wash.best_replacement_lot(lot1, lots))
def test_wash_against_small_replacement(self): lots = lots_lib.Lots([self.loss, self.small_first_gain]) final_lots = copy.deepcopy(lots) disallowed_loss = final_lots.lots()[0] # Create the split lot. split_lot = copy.deepcopy(disallowed_loss) split_lot.num_shares = 4 split_lot.basis *= 4. / 10. split_lot.adjusted_basis *= 4. / 10. split_lot.proceeds *= 4. / 10. final_lots.add(split_lot) disallowed_loss.num_shares = 6 disallowed_loss.basis *= 6. / 10. disallowed_loss.adjusted_basis *= 6. / 10. disallowed_loss.proceeds *= 6. / 10. disallowed_loss.adjustment_code = 'W' disallowed_loss.adjustment = 6 disallowed_loss.loss_processed = True replacement = final_lots.lots()[1] replacement.adjusted_basis = 106 replacement.adjusted_buy_date -= (self.loss.sell_date - self.loss.buy_date) replacement.replacement_for = ['_1'] replacement.is_replacement = True wash.wash_one_lot(self.loss, lots) self.assertSameLots(lots, final_lots)
def assertSameLot(self, a, b): self.assertIs( a, b, msg="{} is not {}: \n{}".format(id(a), id(b), lots_lib.Lots([a, b])), )
def assertLotIsNone(self, a): self.assertIsNone( a, msg="{} is not None: \n{}".format( id(a), lots_lib.Lots([a]) if a else None), )
def test_wash_against_large_replacement(self): lots = lots_lib.Lots([self.loss, self.large_first_gain]) final_lots = copy.deepcopy(lots) disallowed_loss = final_lots.lots()[0] disallowed_loss.adjustment_code = 'W' disallowed_loss.adjustment = 10 disallowed_loss.loss_processed = True replacement = final_lots.lots()[1] # Create the split lot. split_lot = copy.deepcopy(replacement) split_lot.num_shares = 8 split_lot.basis = int(round(split_lot.basis * 8. / 18.)) split_lot.adjusted_basis = int( round(split_lot.adjusted_basis * 8. / 18.)) split_lot.proceeds = int(round(split_lot.proceeds * 8. / 18.)) final_lots.add(split_lot) replacement.num_shares = 10 replacement.basis = int(round(replacement.basis * 10. / 18.)) replacement.adjusted_basis = int( round(replacement.adjusted_basis * 10. / 18.)) replacement.proceeds = int(round(replacement.proceeds * 10. / 18.)) replacement.adjusted_basis += 10 replacement.adjusted_buy_date -= (self.loss.sell_date - self.loss.buy_date) replacement.replacement_for = ['_1'] replacement.is_replacement = True wash.wash_one_lot(self.loss, lots) self.assertSameLots(lots, final_lots)
def merge_split_lots(lots): """Merge split lots back together, assuming lots is sorted with respect to original_form_position so only sequential records need to be merged.""" orig = lots.lots() out = [] # First lot in new sequence prev = copy.copy(orig[0]) for lot in orig[1:]: assert (prev.original_form_position <= lot.original_form_position) if lot.original_form_position == prev.original_form_position: assert (lot.symbol == prev.symbol) prev.num_shares += lot.num_shares prev.basis += lot.basis prev.adjusted_basis += lot.adjusted_basis prev.proceeds += lot.proceeds prev.adjustment += lot.adjustment prev.buy_lot += '|' + lot.buy_lot if lot.adjustment_code: prev.adjustment_code = lot.adjustment_code else: # Loop has moved on to a different lot, finished with current out.append(prev) prev = copy.copy(lot) if prev: out.append(prev) return lots_lib.Lots(out)
def test_write_csv_data(self): lots_rows = [] lots_rows.append(lots_lib.Lot( 10, 'ABC', 'A', datetime.date(2014, 9, 15), datetime.date( 2014, 9, 14), 2000, 2100, datetime.date(2014, 10, 5), 1800, 'W', 200, 'form1', 'lot1', ['lot3', 'lot4'], True, True)) lots_rows.append(lots_lib.Lot(10, 'ABC', 'A', datetime.date( 2014, 9, 15), datetime.date(2014, 9, 15), 2000, 2000, datetime.date(2014, 10, 5), 1800, 'W', 200, 'form2', 'lot2', [], False, False)) lots_rows.append(lots_lib.Lot(20, 'ABC', 'A', datetime.date( 2014, 9, 25), datetime.date(2014, 9, 25), 3000, 3000, datetime.date(2014, 11, 5), 1800, '', 0, '', '', [], False, False)) lots = lots_lib.Lots(lots_rows) actual_output = StringIO.StringIO() lots.write_csv_data(actual_output) expected_csv_data = [ 'Num Shares,Symbol,Description,Buy Date,Adjusted Buy Date,Basis,' 'Adjusted Basis,Sell Date,Proceeds,Adjustment Code,Adjustment,' 'Form Position,Buy Lot,Replacement For,Is Replacement,' 'Loss Processed', '10,ABC,A,09/15/2014,09/14/2014,2000,2100,10/05/2014,1800,W,200,' 'form1,lot1,lot3|lot4,True,True', '10,ABC,A,09/15/2014,,2000,,10/05/2014,1800,W,200,form2,lot2,,,', '20,ABC,A,09/25/2014,,3000,,11/05/2014,1800,,,,_1,,,' ] actual_output.seek(0) self.assertSequenceEqual( [line.rstrip() for line in actual_output.readlines()], expected_csv_data)
def main(): parser = argparse.ArgumentParser() parser.add_argument("-o", "--out_file") parser.add_argument("-w", "--do_wash", metavar="in_file") parser.add_argument("-q", "--quiet", action="store_true") parser.add_argument( "-d", "--output-dollars", action="store_true", help="If set, outputs dollars instead of cents for money columns", ) parsed = parser.parse_args() if parsed.quiet: logger = logger_lib.NullLogger() else: logger = logger_lib.TermLogger() if parsed.do_wash: lots = lots_lib.Lots([]) with open(parsed.do_wash) as f: lots = lots_lib.Lots.create_from_csv_data(f) logger.print_lots("Start lots", lots) wash_all_lots(lots, logger) if parsed.out_file: with open(parsed.out_file, "w") as f: lots.write_csv_data(f, parsed.output_dollars) else: logger.print_lots("Final lots", lots)
def test_two_lots_from_same_buy_lot(self): # There are two lots that have the same values, and were bought # together. lot1 = create_lot(10, 2012, 1, 1, 120, 2012, 1, 10, 110) lot1.buy_lot = '1' lot2 = create_lot(10, 2012, 1, 1, 120, 2012, 1, 10, 110) lot2.buy_lot = '1' lots = lots_lib.Lots([lot1, lot2]) self.assertLotIsNone(wash.best_replacement_lot(lot1, lots))
def test_no_wash_if_no_replacement_shares(self): lots = lots_lib.Lots( [self.loss, self.very_early_gain, self.very_late_gain]) final_lots = copy.deepcopy(lots) loss = final_lots.lots()[0] loss.loss_processed = True wash.wash_one_lot(self.loss, lots) self.assertSameLots(lots, final_lots)
def test_replacement_for_large_loss(self): lots = lots_lib.Lots([self.unsold, self.first_gain, self.large_loss]) final_lots = copy.deepcopy(lots) wash_lot = wash.best_replacement_lot(self.large_loss, lots) self.assertSameLot(self.first_gain, wash_lot) self.assertEqual(10, wash_lot.num_shares) self.assertEqual(3, lots.size()) lots.sort(key=cmp_to_key(lots_lib.Lot.cmp_by_original_buy_date)) final_lots.sort(key=cmp_to_key(lots_lib.Lot.cmp_by_original_buy_date)) self.assertSameLots(lots, final_lots)
def test_contents_not_equal(self): lots = lots_lib.Lots([]) lots.add(lots_lib.Lot(1, '', '', datetime.date(2014, 9, 2), datetime.date(2014, 9, 2), 0, 0, datetime.date(2014, 11, 5), 0, '', 0, 'form2', '', [], False, False)) lots.add(lots_lib.Lot(5, '', '', datetime.date(2014, 9, 1), datetime.date(2014, 9, 1), 0, 0, datetime.date(2014, 10, 5), 0, '', 0, 'form1', '', [], False, False)) lots.add(lots_lib.Lot(3, '', '', datetime.date(2014, 9, 2), datetime.date(2014, 9, 2), 0, 0, datetime.date(2014, 11, 5), 0, '', 0, 'form1', '', [], False, False)) other_lots = copy.deepcopy(lots) other_lots.lots()[0].num_shares = 2 self.assertFalse(lots.contents_equal(other_lots))
def test_wash_against_subsequent_loss(self): lots = lots_lib.Lots([self.loss, self.later_loss]) final_lots = copy.deepcopy(lots) disallowed_loss = final_lots.lots()[0] disallowed_loss.adjustment_code = 'W' disallowed_loss.adjustment = 10 disallowed_loss.loss_processed = True replacement = final_lots.lots()[1] replacement.adjusted_basis = 150 replacement.adjusted_buy_date -= (self.loss.sell_date - self.loss.buy_date) replacement.replacement_for = ['_1'] replacement.is_replacement = True wash.wash_one_lot(self.loss, lots) self.assertSameLots(lots, final_lots)
def test_wash_against_unsold(self): lots = lots_lib.Lots([self.loss, self.unsold]) final_lots = copy.deepcopy(lots) disallowed_loss = final_lots.lots()[0] disallowed_loss.adjustment_code = "W" disallowed_loss.adjustment = 10 disallowed_loss.loss_processed = True replacement = final_lots.lots()[1] replacement.adjusted_basis = 140 replacement.adjusted_buy_date -= (self.loss.sell_date - self.loss.buy_date) replacement.replacement_for = ["_1"] replacement.is_replacement = True wash.wash_one_lot(self.loss, lots) self.assertSameLots(lots, final_lots)
def main(): parser = argparse.ArgumentParser() parser.add_argument('-o', '--out_file') parser.add_argument('-w', '--do_wash', metavar='in_file') parser.add_argument('-q', '--quiet', action="store_true") parser.add_argument('-m', '--merge_split_lots', action="store_true", help='''Any split lots are merged back together at end. This makes it easier to match the output to the input lots, but can cause the buy-dates to be slightly incorrect since a lot can only have a single buy date. In this mode, some wash sale lots may have a loss that is greater than the adjustment amount, instead of being identical, i.e., only part of the loss in the lot is actually a wash sale. This is expected in this mode..''' ) parser.add_argument('-a', '--always_show_adjusted', action="store_true", help='''Always fill in the adjusted buy date and basis. If no adjustments were made, then the unadjusted basis and buy date are used instead.''') parsed = parser.parse_args() if parsed.quiet: logger = logger_lib.NullLogger() else: logger = logger_lib.TermLogger() if parsed.do_wash: lots = lots_lib.Lots([]) with open(parsed.do_wash) as f: lots = lots_lib.Lots.create_from_csv_data(f) logger.print_lots('Start lots', lots) wash_all_lots(lots, logger) if parsed.merge_split_lots: lots.sort(cmp=cmp_by_original_form_position) lots = merge_split_lots(lots) if parsed.out_file: with open(parsed.out_file, 'w') as f: lots.write_csv_data(parsed.always_show_adjusted, f) else: logger.print_lots('Final lots', lots)
def main(): parser = argparse.ArgumentParser() parser.add_argument('-o', '--out_file') parser.add_argument('-w', '--do_wash', metavar='in_file') parser.add_argument('-q', '--quiet', action="store_true") parsed = parser.parse_args() if parsed.quiet: logger = logger_lib.NullLogger() else: logger = logger_lib.TermLogger() if parsed.do_wash: lots = lots_lib.Lots([]) with open(parsed.do_wash) as f: lots = lots_lib.Lots.create_from_csv_data(f) logger.print_lots('Start lots', lots) wash_all_lots(lots, logger) if parsed.out_file: with open(parsed.out_file, 'w') as f: lots.write_csv_data(f) else: logger.print_lots('Final lots', lots)
def test_write_csv_data_dollars(self): lots_rows = [] lots_rows.append( lots_lib.Lot( 20, "ABC", "A", datetime.date(2014, 9, 25), datetime.date(2014, 9, 25), 3010, 3010, datetime.date(2014, 11, 5), 1850, "", 0, "", "", [], False, False, )) lots = lots_lib.Lots(lots_rows) actual_output = StringIO() lots.write_csv_data(actual_output, True) expected_csv_data = [ "Num Shares,Symbol,Description,Buy Date,Adjusted Buy Date,Basis," "Adjusted Basis,Sell Date,Proceeds,Adjustment Code,Adjustment," "Form Position,Buy Lot,Replacement For,Is Replacement," "Loss Processed", "20,ABC,A,09/25/2014,,$30,,11/05/2014,$19,,,,_1,,,", ] actual_output.seek(0) self.assertSequenceEqual( [line.rstrip() for line in actual_output.readlines()], expected_csv_data, )
def test_only_loss_lot_exists(self): lots = lots_lib.Lots([self.loss]) self.assertLotIsNone(wash.best_replacement_lot(self.loss, lots))
def test_replacement_for_small_loss(self): lots = lots_lib.Lots([self.unsold, self.first_gain, self.small_loss]) wash_lot = wash.best_replacement_lot(self.small_loss, lots) self.assertSameLot(self.first_gain, wash_lot)
def test_unsold(self): lots = lots_lib.Lots([self.loss1, self.unsold, self.loss2]) self.assertSameLot(self.loss1, wash.earliest_loss_lot(lots))
def test_gain(self): lots = lots_lib.Lots([self.loss1, self.gain, self.loss2]) self.assertSameLot(self.loss1, wash.earliest_loss_lot(lots))
def test_replacement_checks_sell_date(self): # If there are multiple possible replacements that were bought on the # same day, the one with the earlier sell date is chosen. lots = lots_lib.Lots([self.unsold, self.first_gain, self.loss]) self.assertSameLot(self.first_gain, wash.best_replacement_lot(self.loss, lots))
def test_two_losses(self): lots = lots_lib.Lots([self.loss1, self.loss2, self.loss3]) self.assertSameLot(self.loss1, wash.earliest_loss_lot(lots))
def test_loss_not_in_lots(self): lots = lots_lib.Lots([self.unsold, self.first_gain]) self.assertSameLot(self.first_gain, wash.best_replacement_lot(self.loss, lots))
def test_replacement_is_unsold(self): lots = lots_lib.Lots([self.unsold, self.loss]) self.assertSameLot(self.unsold, wash.best_replacement_lot(self.loss, lots))
def test_replacement_for_loss_multiple_options(self): lots = lots_lib.Lots( [self.loss, self.small_first_gain, self.large_first_gain]) wash_lot = wash.best_replacement_lot(self.loss, lots) self.assertSameLot(self.small_first_gain, wash_lot)
def test_lot_sold_before_loss_is_not_replacement(self): lots = lots_lib.Lots([self.loss, self.gain_just_before_loss]) self.assertLotIsNone(wash.best_replacement_lot(self.loss, lots))
def test_already_used_replacement_is_not_used_again(self): lot1 = create_lot(10, 2012, 1, 1, 120, 2012, 1, 10, 110) lot2 = create_lot(10, 2012, 1, 1, 120, 2012, 1, 10, 110) lot2.is_replacement = True lots = lots_lib.Lots([lot1, lot2]) self.assertLotIsNone(wash.best_replacement_lot(lot1, lots))