nx_taxlot_lib.nx source
↩ module page · 98 lines · 5005 B
1// nx_taxlot_lib.nx -- CAPITAL-GAINS TAX-LOT ACCOUNTING with the IRS wash-sale rule. Integer-exact.
2//
3// Realizing a capital gain/loss is not subtraction -- it is lot selection, holding-period classification,
4// and the wash-sale disallowance (IRC section 1091). This organ does all three in exact integer minor units.
5//
6// *THE WASH-SALE FAIL-CLOSED PROPERTY (the flagship): if you sell at a LOSS and buy a substantially
7// identical security within +/-30 days, the loss is DISALLOWED for the current year. Consumer tax tools
8// routinely mis-handle this and let a taxpayer claim a loss the IRS will reject. Here the disallowed loss is
9// not silently dropped and not silently claimed -- it is computed proportionally to the replacement shares
10// and DEFERRED into the replacement lot's cost basis (tl_adjusted_replacement_basis), so it is preserved,
11// not destroyed (history is additive). A GAIN is never washed; a loss with no replacement in the window is
12// fully allowed. allowed + disallowed == the original loss, always (conservation).
13//
14// FIFO lot consumption is fail-closed against overdraw: it returns the shares actually sold, which is less
15// than requested when the book is short -- it cannot sell shares the book does not hold.
16//
17// NO FLOATS. Days are integer day-numbers; money is minor units. Thresholds are declared constants (Rule 11).
18// license_tier: ORIGINAL No hw writes (Rule 26). LIB.
19
20const TL_LONG_TERM_DAYS: i64 = 365 // held MORE THAN one year = long-term (IRS: strictly greater)
21const TL_WASH_WINDOW_DAYS: i64 = 30 // +/-30 days around the sale = the wash-sale window
22const TL_LONG: i64 = 1
23const TL_SHORT: i64 = 0
24const TL_BAD: i64 = 0 - 2000000002 // invalid input
25
26// realized gain (positive) or loss (negative): proceeds minus cost basis, exact minor units.
27func tl_realized_gain(proceeds: i64, basis: i64) -> i64 {
28 return proceeds - basis
29}
30
31// holding-period class: LONG only if held STRICTLY MORE than a year (exactly 365 days is short-term).
32func tl_holding_class(acquire_day: i64, sale_day: i64) -> i64 {
33 let held: i64 = sale_day - acquire_day
34 if held > TL_LONG_TERM_DAYS { return TL_LONG }
35 return TL_SHORT
36}
37
38// *WASH-SALE DISALLOWED AMOUNT. loss_amount is the realized loss as a POSITIVE number (0 or negative means
39// the sale was a gain -> nothing is washed). If a replacement purchase falls within +/-30 days of the sale,
40// disallow the loss in proportion to the replacement shares matched against the sold shares.
41func tl_wash_disallowed(loss_amount: i64, sale_day: i64, replacement_day: i64, replacement_shares: i64, sold_shares: i64) -> i64 {
42 if loss_amount <= 0 { return 0 }
43 if sold_shares <= 0 { return TL_BAD }
44 var gap: i64 = sale_day - replacement_day
45 if gap < 0 { gap = 0 - gap }
46 if gap > TL_WASH_WINDOW_DAYS { return 0 }
47 var matched: i64 = replacement_shares
48 if sold_shares < matched { matched = sold_shares }
49 return loss_amount * matched / sold_shares
50}
51
52// the loss the taxpayer may actually claim this year = original loss minus the washed portion.
53func tl_allowed_loss(loss_amount: i64, sale_day: i64, replacement_day: i64, replacement_shares: i64, sold_shares: i64) -> i64 {
54 let dis: i64 = tl_wash_disallowed(loss_amount, sale_day, replacement_day, replacement_shares, sold_shares)
55 if dis == TL_BAD { return TL_BAD }
56 return loss_amount - dis
57}
58
59// *the disallowed loss is DEFERRED into the replacement lot's basis -- preserved, not destroyed.
60func tl_adjusted_replacement_basis(replacement_basis: i64, disallowed_loss: i64) -> i64 {
61 if disallowed_loss < 0 { return TL_BAD }
62 return replacement_basis + disallowed_loss
63}
64
65// FIFO SALE over parallel lot arrays (oldest lot = index 0). Consumes oldest first, carrying each lot's
66// exact basis. Writes out[0]=proceeds, out[1]=basis consumed, out[2]=realized gain. Returns shares actually
67// sold: less than requested when the book underflows -- it CANNOT sell shares it does not hold.
68func tl_sell_fifo(lot_shares: *i64, lot_basis: *i64, n: i64, sell_shares: i64, sell_price: i64, out: *i64) -> i64 {
69 var remaining: i64 = sell_shares
70 var basis_used: i64 = 0
71 var i: i64 = 0
72 while i < n {
73 if remaining > 0 {
74 if lot_shares[i] > 0 {
75 var take: i64 = lot_shares[i]
76 if remaining < take { take = remaining }
77 let lbu: i64 = lot_basis[i] * take / lot_shares[i]
78 basis_used = basis_used + lbu
79 lot_shares[i] = lot_shares[i] - take
80 lot_basis[i] = lot_basis[i] - lbu
81 remaining = remaining - take
82 }
83 }
84 i = i + 1
85 }
86 let sold: i64 = sell_shares - remaining
87 let proceeds: i64 = sold * sell_price
88 out[0] = proceeds
89 out[1] = basis_used
90 out[2] = proceeds - basis_used
91 return sold
92}
93
94// label for a holding class.
95func tl_class_str(c: i64) -> *u8 {
96 if c == TL_LONG { return "LONG-TERM" as *u8 }
97 return "SHORT-TERM" as *u8
98}