wash-sale-calculator
wash-sale-calculator copied to clipboard
AssertionError assert(not buy_lots_match(merge_from, merge_to))
$ 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
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.
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).
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
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!
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
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.
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?
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).