code wiki / (root) / nx_taxlot_lib.nx

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}