wash-sale-calculator icon indicating copy to clipboard operation
wash-sale-calculator copied to clipboard

AssertionError assert(not buy_lots_match(merge_from, merge_to))

Open mag1024 opened this issue 8 years ago • 7 comments

$ cat input.csv 6,GOOG,,6/25/2015,3227.04,6/26/2015,3207.05,,0,29 6,GOOGL,,6/25/2015,3351.42,6/26/2015,3332.87,,0,18 10,GOOG,,6/25/2015,5378.4,6/26/2015,5345.08,,0,28

$ python wash.py -q -w input.csv -o output.csv Printing 3 lots: 6 GOOG () acq: 2015-06-25 3227.04 sell: 2015-06-26 3207.05 29 1 6 GOOGL () acq: 2015-06-25 3351.42 sell: 2015-06-26 3332.87 18 2 10 GOOG () acq: 2015-06-25 5378.40 sell: 2015-06-26 5345.08 28 3 Totals: Basis 11956.86 Proceeds 11885.00 Adj: 0.00 (basis-adj: 11956.86) Traceback (most recent call last): File "wash.py", line 182, in main() File "wash.py", line 174, in main out = perform_wash(lots, logger) File "wash.py", line 152, in perform_wash merge_buy_lots(loss, buy) File "wash.py", line 52, in merge_buy_lots assert(not buy_lots_match(merge_from, merge_to)) AssertionError

Possibly related to the latest pull, since similar input used to work in the past. Sorry, didn't have the time to dig into the logic and understand the failure yet.

mag1024 avatar Mar 25 '16 05:03 mag1024

I'm going to ping the author of the last commits. I don't have mag1024's email, so I'll update back here w/ updates (or Ben can update here).

adlr avatar Mar 25 '16 15:03 adlr

I took a look, and this is a legitimate problem in the way the perform_wash algorithm is trying to match up lots. What happens is that the 10 is split into a 6 and a 4 as a buy lot, and is used as replacement shares for the 6 GOOG. Then the inner loop continues to process the same set of lots, but this time the buy_lots and loss_lots match (specifically, the first entry in both lists is the 4 shares that were split). It tries to wash these shares against themselves, and fails because you can't wash shares against themselves.

In addition, I think the 6 shares that already had a wash sale should still be available to wash against another loss. Or more specifically, if you have three losses

6 A 6 B 6 C

and 6 B are replacements for 6 A, then 6 C should still be able to be replacements for 6 B. That didn't happen here, but the script crashed, so it might have happened in the next outer loop iteration. I'm just bringing this up so that it's considered when fixing this.

I'm not sure the best way to fix this, since it seems like a large (or impactful, at least) change as it affects ordering the losses. I don't know if there's a reason that it always chooses the first entry in each loss list, and doesn't try others if the first ones turn out to be the same. But I don't think we should remove my change, because I believe these losses should indeed cause wash sales, whereas in the previous version they did not. I'll think about this some more over the weekend to see if I can come up with a solution.

I put some extra print statements in, right before the remove_lots_from_list calls, to print out the buy and loss lots (trailing * means the line is red). You can see that there is only 1 red line in the last "pairing these", because it's trying to pair the same line with itself (as shown by the subsequent "Buy lots" and "Loss lots" output).

Printing 3 lots:
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 29 1
 6 GOOGL () acq: 2015-06-25  3351.42 sell: 2015-06-26  3332.87 18 2
10 GOOG () acq: 2015-06-25  5378.40 sell: 2015-06-26  5345.08 28 3
Totals: Basis 11956.86 Proceeds 11885.00 Adj: 0.00 (basis-adj: 11956.86)
Found the following losses
 6 GOOGL () acq: 2015-06-25  3351.42 sell: 2015-06-26  3332.87 18 2 *
10 GOOG () acq: 2015-06-25  5378.40 sell: 2015-06-26  5345.08 28 3 *
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 29 1 *
hit enter>
Here are the replacements
 6 GOOGL () acq: 2015-06-25  3351.42 sell: 2015-06-26  3332.87 18 2
10 GOOG () acq: 2015-06-25  5378.40 sell: 2015-06-26  5345.08 28 3 *
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 29 1 *
hit enter>
Splitting buy
 6 GOOGL () acq: 2015-06-25  3351.42 sell: 2015-06-26  3332.87 18 2
10 GOOG () acq: 2015-06-25  5378.40 sell: 2015-06-26  5345.08 28 3 *
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 29 1
hit enter>
into these
 6 GOOGL () acq: 2015-06-25  3351.42 sell: 2015-06-26  3332.87 18 2
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 28.1 3 *
 4 GOOG () acq: 2015-06-25  2151.36 sell: 2015-06-26  2138.03 28.2 3 *
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 29 1
hit enter>
pairing these
 6 GOOGL () acq: 2015-06-25  3351.42 sell: 2015-06-26  3332.87 18 2 *
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 28.1 3 *
 4 GOOG () acq: 2015-06-25  2151.36 sell: 2015-06-26  2138.03 28.2 3
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 29 1
hit enter>
Buy lots
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 28.1 3
 4 GOOG () acq: 2015-06-25  2151.36 sell: 2015-06-26  2138.03 28.2 3
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 29 1
hit enter>
Loss lots
 6 GOOGL () acq: 2015-06-25  3351.42 sell: 2015-06-26  3332.87 18 2
 4 GOOG () acq: 2015-06-25  2151.36 sell: 2015-06-26  2138.03 28.2 3
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 29 1
hit enter>
buy:  6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 28.1 3
loss:  6 GOOGL () acq: 2015-06-25  3351.42 sell: 2015-06-26  3332.87 18 2
pair complete
 6 GOOG () acq: 2015-06-24  3245.59 sell: 2015-06-26  3207.05 28.1 3,2 [IsRepl] *
 4 GOOG () acq: 2015-06-25  2151.36 sell: 2015-06-26  2138.03 28.2 3
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 29 1
hit enter>
pairing these
 6 GOOG () acq: 2015-06-24  3245.59 sell: 2015-06-26  3207.05 28.1 3,2 [IsRepl]
 4 GOOG () acq: 2015-06-25  2151.36 sell: 2015-06-26  2138.03 28.2 3 *
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 29 1
hit enter>
Buy lots
 4 GOOG () acq: 2015-06-25  2151.36 sell: 2015-06-26  2138.03 28.2 3
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 29 1
hit enter>
Loss lots
 4 GOOG () acq: 2015-06-25  2151.36 sell: 2015-06-26  2138.03 28.2 3
 6 GOOG () acq: 2015-06-25  3227.04 sell: 2015-06-26  3207.05 29 1
hit enter>
buy:  4 GOOG () acq: 2015-06-25  2151.36 sell: 2015-06-26  2138.03 28.2 3
loss:  4 GOOG () acq: 2015-06-25  2151.36 sell: 2015-06-26  2138.03 28.2 3
Traceback (most recent call last):
  File "wash.py", line 186, in <module>
    main()
  File "wash.py", line 178, in main
    out = perform_wash(lots, logger)
  File "wash.py", line 156, in perform_wash
    merge_buy_lots(loss, buy)
  File "wash.py", line 52, in merge_buy_lots
    assert(not buy_lots_match(merge_from, merge_to))
AssertionError

bbreslauer avatar Mar 26 '16 03:03 bbreslauer

Thanks, Ben! I wouldn't be surprised if the assumption that matching lots won't have the same sell date is baked into the code, and that probably needs to be updated.

I'm on vacation for a week now, so I'll have trouble debugging, but feel please continue your debugging if you have time.

Thanks!

adlr avatar Mar 26 '16 12:03 adlr

While not a fix - since it prevents wash sales that can be matched - a workaround is to assign all sales on a single day to a single BuyLot. Not accurate, but at least script completes, FWIW.

This passes - but does not match all wash sales, as described in comments above: Cnt,Sym,Desc,BuyDate,Basis,SellDate,Proceeds,AdjCode,Adj,FormPosition,BuyLot,IsReplacement 6,GOOG,,6/25/2015,3227.04,6/26/2015,3207.05,,0,29,LOT1 6,GOOGL,,6/25/2015,3351.42,6/26/2015,3332.87,,0,18,LOT1 10,GOOG,,6/25/2015,5378.4,6/26/2015,5345.08,,0,28,LOT1 10,GOOG,,7/20/2015,5378.4,7/21/2015,5345.08,,0,30,LOT4

This fails: Cnt,Sym,Desc,BuyDate,Basis,SellDate,Proceeds,AdjCode,Adj,FormPosition,BuyLot,IsReplacement 6,GOOG,,6/25/2015,3227.04,6/26/2015,3207.05,,0,29,LOT1 6,GOOGL,,6/25/2015,3351.42,6/26/2015,3332.87,,0,18,LOT2 10,GOOG,,6/25/2015,5378.4,6/26/2015,5345.08,,0,28,LOT3 10,GOOG,,7/20/2015,5378.4,7/21/2015,5345.08,,0,30,LOT4

avinash311 avatar Mar 28 '16 14:03 avinash311

Guys -- does the following look like correct output for the problematic input?

Cnt,Sym,Desc,BuyDate,Basis,SellDate,Proceeds,AdjCode,Adj,FormPosition,BuyLot,IsReplacement 4,GOOG,,06/23/2015,2177.05,06/26/2015,2138.03,,0.0,29.1,"1,3,2",True 2,GOOG,,06/23/2015,1088.53,06/26/2015,1069.02,,0.0,29.2,"1,3,2",True 4,GOOG,,06/24/2015,2163.73,06/26/2015,2138.03,W,25.69,28.1.1,"3,2",True 2,GOOG,,06/24/2015,1081.86,06/26/2015,1069.02,W,12.85,28.1.2,"3,2",True 6,GOOGL,,06/25/2015,3351.42,06/26/2015,3332.87,W,18.55,18,2, 4,GOOG,,06/25/2015,2151.36,06/26/2015,2138.03,,0.0,28.2,3,

I obtained it by throwing in breaks after both buy and sell splits conditions in order to force the outer loop to re-run and redo buy_lots_within_window with proper same-lot filtering.

mag1024 avatar Mar 30 '16 16:03 mag1024

Actually, I'm not entirely sure why the two loop structure is necessary in the first place. I prototyped a very dirty version that does everything in a single loop in my fork. Please take a look.

It works by repeatedly picking the (single) first unmatched loss then searching for candidate buys, splitting as necessary. The tests pass, but I have no idea if it embodies IRS rules correctly.

One thing I noticed is that on large inputs it gives rise to complex chaining with buy lot history that is dozens of lots long, but I guess that's how it's supposed to work?

mag1024 avatar Mar 30 '16 22:03 mag1024

avinash311, that's correct. It might be a perfectly fine way to do it in this case, if you consider those 3 to be part of the same lot. Would probably need to contact a financial advisor to determine that.

mag1024, I'm not sure why there's some splits of 4 and 2 shares, as opposed to having them combined into a 6 share lot, but other than that it looks reasonable (though I haven't gone through all the numbers).

I think the long chaining will tend to happen with lots of losses.

I don't think I can comment further on whether removing the inner loop is acceptable, because I haven't been able to fully wrap my head around the code and the assumptions being made. FWIW, I ended up writing my own version starting with many more tests, and which I'm more confident in (of course, that's certainly because I wrote it; I'm also sure it has bugs). It's also much longer, but has more documentation. Unfortunately, it doesn't quite read the same input files as this calculator, but they're not that different (basically some additional optional fields, which are primarily used for output, and holding the basis, proceeds, and adjustments as integers instead of floats).

bbreslauer avatar Mar 31 '16 06:03 bbreslauer