Repository navigation
Per-wallet lot tracking: end-to-end wiring, IRS FAQ tests, and LTCG fix - #155
macanudo527 wants to merge 66 commits into
Conversation
… and remove check for selected_acquired_lot_amount > taxable_event_amount.
Replace Deprecated Link Checker
Add Badge for DeepWiki Docs
…rical-values-in-ODS-file Updated README.dev.md: update local pre-commit repos
…/github.com/jayr0d/rp2 into bugfix/142-Non-numerical-values-in-ODS-file
…alues-in-ODS-file Bugfix/142 non numerical values in ods file
gain_loss.py: change >= to > in is_long_term_capital_gains() so that exactly 365 days is correctly classified as short-term. IRS defines long-term as "more than one year" (IRC §1222), not "one year or more". test_gain_loss.py: four new tests covering IRS Digital Assets FAQ scenarios that had no unit-level coverage: - test_ltcg_boundary: 365 days = STCG, 366 = LTCG, leap-year edge case - test_earn_type_income_recognition: HARDFORK, AIRDROP, MINING, STAKING, WAGES, INCOME all produce ordinary income at FMV with zero cost basis (Rev. Rul. 2019-24, Notice 2014-21, Rev. Rul. 2023-14, FAQ Q57-61) - test_donate_gift_disposal_gain_loss: DONATE/GIFT disposals use the same proceeds-minus-basis formula as SELL (FAQ Q75-78) - test_holding_period_resets_after_exchange: received asset holding period starts fresh on exchange date regardless of prior lot age (Q74) - test_fee_out_transaction_gain_loss: FEE-typed disposal recognises gain/loss on crypto used purely to pay a network fee (FAQ Q97) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLAUDE.md: new codebase guide for Claude Code covering architecture, design conventions, common commands, and a per-test IRS authority map for test_gain_loss.py. user_faq.md: added IRS rule citations and guidance notes to seven sections — airdrops (Rev. Rul. 2019-24), hard forks (Q103-107), mining (Notice 2014-21 Q8), staking (Rev. Rul. 2023-14), wages (FAQ Q57-61), crypto-to-crypto swaps (Q74 holding period reset), and intra-transaction fees (Q81/Q97). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three new tests in test_gain_loss.py covering IRS rules not previously exercised: purchase fee increases cost basis (Q8/Q56), earned crypto carries FMV-at-receipt as basis for subsequent sale (Q13/Q59), and crypto-to-crypto exchange triggers gain/loss on the disposed asset (Q16-Q17/Q64-Q66). Each test links directly to the relevant IRS FAQ anchor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every citation in the user FAQ, input_files.md and CLAUDE.md now links to its text, not just the first one per section, including the older FAQ citations (IRC 61, 165, 1091, 1223(11), 1256, Rev. Rul. 2019-24, Notice 2014-21). Rev. Proc. 2024-28 sections link to their page of the IRS PDF (#page=N); Treasury Regulation paragraphs link to their eCFR anchor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZgEJzUPyGHYd2xoXqcp5R
|
@eprbell here's the from-scratch review you suggested. The prompt is below (your outline, expanded with the regulatory ground truth, edge cases and test requirements I wanted covered). This is just the prompt and summary. There were a lot of changes it took most of a day for Opus 5.5 to grind through it all. If you need more details please let me know. @jayr0d If you are free, can you look over the code as well? It's a lot. I'm going to give it a manual look over this week. PromptDecisions I made when it stopped at the gap check:
Later I asked for per-coin allocation overrides, the leap-year holding-period fix, a readability pass, and tests built from the worked examples in the #135 Reddit threads. I asked it to leave everything else as issues. Result: the PR description is updated with the details. In short:
Several bugs in the earlier version are fixed, including sales that exhaust a lot, #149, income counted twice on transferred lots, and FIFO by arrival instead of acquisition date. There are property tests (hypothesis) for balance and basis conservation, and all CI checks are green. I'm filing the remaining items as separate issues: reports showing the wallet (1099-DA), Form 8949 covered/noncovered boxes, specific unit allocation, Japanese averaging methods, gifts and inheritance, wrapped tokens, and a few smaller ones. Ready for your review whenever you have time. Here are the issues -> #159 #160 #161 #162 #163 #164 #165 #166 #167 Drafted with Claude Code |
|
OK, I've started reviewing: it's a lot of code, so I expect this will take a while and probably require a few iterations. |
|
@eprbell I understand. I tried to keep the scope tight, but Opus found a lot it had issues with. I asked it to document everything and make the code easier to read. I'm not sure how much refactoring to do. I didn't want to completely rewrite everything and make it more difficult to review. Anyway, I thought if we could start a little early, we could get it done by the end of the year. |
|
No, it's good. We may as well use this opportunity to do a full review of the RP2 code base: AI makes it manageable to find issues and to review large diffs. I'm reviewing both manually and with Astra + Opus 5.5. |
|
Here's the first round of review: it incorporates my own comments with Astra's and Fable 5.1's. The content is LLM-friendly (it has conventions on how to address findings, detailed operational instructions, etc.), but it should be relatively easy to read for humans as well (if a bit dry: it partially follows ASD-STE100). I will keep comments in a structured, append-only format, so it's easy to verify them against future versions of the PR (to avoid regressions). I recommend changes before merge. The current implementation has confirmed inventory and report failures. It also needs country policy, configuration, documentation, and test changes. This review applies to commit This review was prepared with LLM assistance. The reported calculation failures were reproduced with synthetic data. Other findings were checked against code, tests, documentation, or primary sources. Architecture and scope PR 155 builds directly on PR 138 and corrects FIFO ordering to use original acquisition dates, consistent with §1.1012-1(j)(1). Its single chronological pass handles transfers and disposals together, and artificial destination lots do not create new income. It preserves purchase fees in lot basis and corrects the US calendar holding-period rule, subject to the date-policy and compatibility changes below. Keep the three computation phases: universal calculation before the switch, opening allocation, and per-wallet calculation after the switch. Keep Implement per-wallet support for the US first. Keep the framework suitable for other countries. Keep allocation input simple and add the allocation output report. Include consistent time zone handling. Both computation paths must use shared implementations of equivalent lot-consumption and pairing rules, as specified in R13. Keep related repairs in this PR. Use separate commits and tests to make independent behavior changes easy to review. I will confirm the input and behavior decisions for R07, R08, and R18 in this PR thread before you implement those repairs. Until then, do not infer a new input format, change historical lot ordering, or implement negative-STAKING tax treatment. P1 means high priority. P2 means normal priority. All rows marked “In this PR” are requested before merge; R16 records supported scope.
R01 — In this PR: replace The CLI permits US 2025 calculations to use universal application when Remove Document that this first implementation supports a universal history through 2024 and per-wallet application from 2025. A per-wallet history before 2025 is outside this initial path. Other countries retain their supported behavior. They must not acquire US settings or transition rules by accident. Acceptance: test the normal CLI without R02 — In this PR: remove US The new choice suppresses disposal of fee units and changes later-sale basis. It is not just a report classification. IRS answers A81 and A97 treat cryptocurrency paid for transfer services as disposed of. The expense's deduction or capitalization is a separate question. IRS digital-asset FAQs Example: buy one unit for $100. Transfer it when its value is $200 per unit. Pay 0.1 unit as the fee. Sell the received 0.9 unit for $270. Existing disposal behavior reports MOVE value $20, basis $10, and gain $10. The later sale has basis $90 and gain $180. Carryover removes the MOVE row and changes sale basis to $100. If there are no other expenses, the Investment Expenses sheet disappears. Remove Acceptance: preserve fee quantity, source-lot details, market value, basis, and gain/loss in MOVE output. Verify the example in actual report cells. Keep the report usable by tax professionals. Do not infer deductibility from the sheet name or try to resolve every expense-law question in this PR. R05 — Related report failures and regression checksUnder carryover, buy 10 units for $1,000; send 10 and receive nine after a one-unit fee. Sell one. The engine assigns $111.111… basis, so the remaining eight units have $888.888… under that mode. The report shows $900. Selling all nine instead leaves $100 of phantom reported basis and raises Remove the carryover path under R02 and verify the surviving disposal behavior. Acceptance under disposal: the fee consumes $100 basis. Selling one received unit leaves eight units with $800 basis. Selling all nine received units leaves zero quantity and zero basis, with no report error. Check original-lot sold percentages across artificial descendants. The existing report test checks a percentage of 1; add a partly consumed original lot with remaining basis. R03 — In this PR: fix opening allocation followed by a transfer cycle. Reproduction: buy 10 units in wallet A in 2024. Allocate at the switch. Transfer four units A → B in 2025, then return four B → A. There are no fees. The calculation fails with The ancestor walk stores ancestors by Separate live inventory identity from historical source links. Restore quantities only to active lots. Stopping live-cycle traversal at the allocation boundary is one possible design. Keep Acceptance: the example ends with 10 units in A and zero in B. Preserve total basis and original dates. Add partial returns, intervening sales, fees, multiple hops, split allocations, and all supported selection methods. Add the example as a deterministic regression test and include cycles in generated histories that cross the switch. R04 — In this PR: report each wallet's actual remaining basis. Reproduction: in 2025, buy one unit for $100 in A and one for $1,000 in B. With no other activity, The engine returns original input data. The existing open-positions report distributes global average basis. That inherited algorithm does not provide per-wallet basis. Carry the required wallet-lot state through the computation/report interface. Keep original input transactions available for source references. Reports need the remaining quantities, basis, and original acquisition dates at their reporting date. Do not substitute a global average or a final inventory snapshot from after that date. Acceptance: inspect actual ODS cells for the example, partial sales, complete sales, transfers, and date filters. Reconcile wallet totals with asset totals. Preserve universal reporting behavior outside the new policy. R06 — In this PR: use consistent tax dates and strict time zone resolution. With
Derive tax dates and years from one configured policy. Apply it to the switch, annual method selection, report years, date filters, and calendar-date holding-period inputs. Preserve original timestamps. Keep transaction ordering based on absolute instants. Apply this policy to both computation phases where the configuration applies. Preserve other countries' behavior where no new zone policy applies. Prefer Acceptance: test the example, both sides of year boundaries, differing source offsets, daylight-saving changes, report filters, and different host zones. Check supported platforms. Document intended historical-result changes. Include the resolved zone and transition instant in R10's report metadata. Use the same date policy for R15's holding-period calculation. R07 — In this PR: identify broker-withheld exchange units correctly. For the specified broker withholding from digital assets received in a crypto-to-crypto exchange, the received-unit identification rule overrides normal lot selection. A cash purchase with a crypto fee is not automatically this case. 26 CFR §1.1012-1(h)(1)(iv) and (j)(3)(iii) The parser creates an ordinary FEE disposal from an acquisition's crypto fee. A synthetic input with an older lot at $100, received units at $200, and 0.1 unit withheld selects the older lot under FIFO. It reports $10 basis and $10 gain. The special rule identifies the received units. IRS A98–A99 also describe their gain and transaction-cost treatment. IRS FAQs Define explicit input information that links the exchange legs and identifies broker withholding from received units. Ordinary SELL plus BUY entries do not preserve that relationship. Implement selection of the received units and the corresponding expense allocation. Keep ordinary BUY fees on their existing path. Explain the input extension before implementation; do not infer this special case from every acquisition fee. A limitation notice alone does not repair the calculation. Acceptance: distinguish cash purchases, ordinary fee payments, and the specified withholding case. Test parser-to-report results and the linked exchange's cost allocation. The example above shows wrong lot selection. Tests must also verify the expense adjustments for the linked exchange. R08 — In this PR: define and test HIFO/LOFO ordering. HIFO ranks by Define the intended ordering rule before changing the sort key. Purchase-price ordering can be valid, but must not be described as adjusted-basis ordering. If the contract changes to adjusted unit basis, include the correction with a defined policy for historical selections. Acceptance: fee-bearing acquisitions distinguish price from basis; allocation and disposal each follow their documented method. Do not silently change previously established allocations. R09 — In this PR: preserve zero-receipt transfers without creating empty destinations. An INTRA with one unit sent and zero received passes input validation. Universal calculation accepts it. Per-wallet calculation processes the fee, creates a destination with no acquisitions, then raises Create destination inventory only when units are received, or use an equivalent local repair. Do not reject this existing input through shared Acceptance: consume the fee from the source wallet, preserve MOVE details, create no spurious destination lot, and generate reports successfully. Cover a previously unused destination and an existing destination. Preserve universal behavior. R10 — In this PR: keep allocation input simple and add an opening-allocation report. The current PR reads Keep the ordered-wallet rule. For each asset, derive unused universal lots and physical wallet balances independently from historical transactions. Sort the unused lots by the selected rule. Fill each wallet to its opening balance, splitting lots as required. Preserve each piece's source ID, quantity, basis, and original acquisition date. Do not add fake purchases or real transfers to represent opening allocation. One funded wallet can be allocated automatically. Multiple funded wallets need a complete order and method. Preserve validation for unknown or missing funded wallets, duplicate entries, negative balances, and quantity mismatch. Known wallets with zero opening balance can remain in the order and receive no units. Preserve per-asset overrides and deterministic tie handling. Do not add a manual fragment importer, separate allocation input file, or configuration-locking mechanism. Document the limit: an established allocation is supported only if this rule can reproduce it. Specific-unit imports and prior per-wallet history are outside this initial path. Add an output report, preferably an ODS sheet. Capture the actual opening allocation before later activity changes inventory. Required allocation-report fields
Allocation currently runs only if post-switch transactions exist. A 2025 report with only a 2024 purchase skips it. Run allocation when the requested period reaches the transition and opening holdings need it. Later trading must not be required. A report ending before the switch must not require future allocation settings. Do not base this choice on today's machine date. Acceptance: test all four allocation methods, split lots, multiple wallets/assets, overrides, zero balances, absent later activity, and a report ending before the switch. Check actual report cells. Later sales must not change the recorded opening allocation. Revenue Procedure 2024-28 requires records of remaining wallet units and unused basis. Original acquisition dates remain paired with basis. Its global rule must reflect a timely established choice. A new configuration or report cannot prove that historical requirement. The report format is a product choice. Revenue Procedure 2024-28, §§3.03, 4.02 and 5.02 The global rule's pre-2025 record deadline differs from allocation completion under §5.02(4)–(5). State the applicable rule accurately. The safe harbor does not validate the amount of unused basis (§5.04). Preserve reconciliation errors. Include transactions through the actual end of December 31. R11 — In this PR: preserve old coverage and add tests of the complete feature.
Use this migration map before deleting those tests. Allocator-test migration map
Recompute expectations from unused universal basis. Do not copy the prototype's per-wallet remaining quantities, dummy prices, synthetic-INTRA strings, or artificial-ID sequence. Preserve economic behavior and useful invariants. Add end-to-end generated histories through Add a per-wallet golden dataset with opening allocation, transfer fees, transfers, a cycle after allocation, and an annual method change. Check cells and formulas, including R04/R10/R17 output. Add the country/CLI, zone, and failure cases in the other findings. File existence alone is insufficient. The ten ODS files changed against PR 138 are byte-identical to those at the main comparison commit. They belong to the earlier LOFO correction. Their changed assignments and totals agree with an independent replay. They add no per-wallet golden coverage. A comparison between two modes on this new head also does not prove compatibility with an older release. Acceptance: replacements exercise the production path and pass before the old implementation is removed. Record the disposition of each affected scenario/assertion under R14. Run the relevant full tests and required static checks after implementation. Report the exact commit and checks; do not infer complete coverage from a passing count. R12 — In this PR: keep general documentation country-neutral and correct stale text. General documentation should describe RP2 behavior, configuration, inputs, and limits. Remove unrelated US tax discussions about wages, gifts, inheritance, donations, losses, and derivatives. Put necessary US support information in concise country documentation. Avoid maintaining a broad tax manual or scattered US regulatory links in general pages. Update README.dev's universal-only description. Remove duplicate Specific corrections in the proposed FAQ text:
Remove the unrelated material rather than expanding it to cover every exception. Keep necessary remaining claims precise. Acceptance: docs, help text, examples, and validation messages agree with the implemented country policy, fee behavior, time zones, and supported inputs. Remove obsolete flags and settings from examples. Describe RP2's R13 — In this PR: apply the design guidelines and share equivalent operations. Follow the design guidelines. The changes below address specific interface and maintenance problems:
The universal loop and Pass each path's permitted lot pool and accounting method to the shared operations. Universal application can select across accounts; per-wallet application must constrain selection to the affected wallet. Keep transfer delivery, source links, and cycle restoration in the per-wallet orchestration. Preserve accounting-method plugin contracts and transaction ordering. Keep original input, historical source links, live wallet inventory, opening-allocation records, country policy, and report views distinct. R03/R04/R17 show the consequences of mixing those roles. Exact private APIs are implementation choices. Acceptance: exercise the shared operations through both computation paths. For inputs where the same lots are eligible, verify equal consumed quantities, basis, and remaining balances. Separately test cases where wallet restrictions must change the result. Cover partial lots, exhausted lots, multiple lots, fees, and method changes. Mutating an input dictionary after configuration construction must not change validated behavior. Shared arithmetic must preserve existing balance validation. Run the required type and static checks. Isolate the shared-code refactor from tax-rule changes in its own commit. Compare universal calculation and report results before and after that refactor, including pre-2025 histories and existing golden files. The refactor must preserve those results. Check intentional changes under R06/R15 in separate commits and tests. A comparison between the two new paths alone cannot detect a regression shared by both. R14 — In this PR: remove verified unused code after preserving coverage. Remove The documentation workflow uses lychee and For every affected test or assertion, record: protected behavior; preserve/adapt, extend, or remove; reason; and replacement or surviving test. Delete an assertion only when its behavior is deliberately removed, its representation is obsolete, or named coverage makes it redundant. Keep useful invariants. Passing tests of dead code do not preserve production coverage. Preserve the behavior of Acceptance: replacement tests pass against the active implementation before removal. Explain each removed behavior and confirm that no production/plugin caller was lost. Do not retain unused implementations for hypothetical future use. R15 — In this PR: test the US holding-period correction and preserve the non-US threshold. Keep the valid US calendar-anniversary correction. It changes some pre-2025 results, so isolate it in a commit with boundary tests and release notes. Preserve original acquisition dates through allocation and transfers. IRS holding-period guidance The default country comparison also changes Correct the PR's compatibility claim. Other countries still use day counts, but their threshold results changed. “Pre-2025 unchanged” cannot mean equivalence with old releases when the US correction changes them. Acceptance: test the acquisition anniversary, the following day, leap-year boundaries, historical US results, and the exact non-US threshold. Compare against the prior implementation where compatibility is claimed. Keep calendar-date conversion under R06 distinct from the holding-period rule. R16 — Accepted scope: keep one transfer/disposal method within each year. The maintainer already accepted this scope in issue 135. Independent transfer selection is not requested in this PR. Keep the shared annual method, document it, and test annual changes. Keep four concepts distinct: application mode, opening allocation, transfer selection, and disposal selection. The last two share the supported annual setting. That product restriction does not remove applicable identification or recordkeeping duties. R17 — In this PR: restore original-lot traceability in the full report. Reproduction: buy two units on Coinbase, transfer both to Kraken, then sell one. The acquired-lot cells show The report builds its row map from original input transactions, then looks up an artificial acquisition. No input row matches that artificial ID. Use If a report filter excludes the source row, show a stable original ID/reference without a broken hyperlink. Keep artificial IDs only as additional internal trace information if useful. Minimum source traceability is required for the supported per-wallet report. It does not require a complete 1099-DA reconciliation feature. Acceptance: inspect cells and formulas for direct acquisitions, allocated lots, split transfers, multiple hops, cycles, and filtered reports. R04 separately checks remaining wallet positions. These report fixes can share infrastructure, but their immediate causes differ. R18 — In this PR: define negative-STAKING behavior and remove the late failure. A ten-unit purchase followed by a STAKING amount of minus one constructs successfully. Define whether negative STAKING is a supported input. If it is unsupported, reject it during input validation with a clear explanation and document that limit. If it is supported, specify its inventory, basis, income, and report effects before implementing the computation. Constructor acceptance alone must not imply complete support. An earlier error is a validation repair, not support for slashing. Acceptance: cover the selected contract through parsing, validation, computation, and reports as applicable. Preserve positive-STAKING behavior. Correct the misleading validation/support text under R12 and retain the example as a regression test. Privacy, sources, and verification limits No added runtime transmission of private data or credential leak was demonstrated in the examined code and diff. This is not a complete dependency security audit. Logs and errors can contain transaction details and account/holder identifiers. Use synthetic public examples. Shared diagnostics should support removal of private values; keep useful local diagnostics available to the owner. The legal review uses Treasury regulations, Revenue Procedure 2024-28, Notices 2025-7 and 2026-20, and current IRS FAQs. Those sources have different authority levels. The notices provide temporary broker-identification relief through 2026 under stated conditions. They do not postpone wallet tracking or replace safe-harbor requirements. A later RP2 run cannot establish that a historical identification was made on time. Notice 2025-7, Notice 2026-20. In your reply, please address each R-number and name the commit that fixes it, so I can re-check every item on the new head. |
Review item R01 (eprbell#155). A mode the law requires can't depend on an optional flag: without -w, US data from 2025 silently used universal application (with only a warning). Treas. Reg. 1.1012-1(j)(6) applies per-wallet identification "to all acquisitions and dispositions of digital assets on or after January 1, 2025". - New ApplicationPolicy (application_policy.py): per policy period, the allowed application modes and the default. AbstractCountry exposes it via get_application_policy(), replacing get_per_wallet_application_start_year(). Default: universal in every year. US: universal until 2024, per-wallet from 2025, one allowed mode per period. - -w is removed. rp2_main resolves each asset's mode for every year with transactions before computing anything. US data from 2025 without the per-wallet settings is an error, never a fallback to universal. - Country choices move to [country.<code>] (e.g. [country.us]), replacing [per_wallet] (now an error that points to the new section). The optional application_mode is checked against every year with transactions: a value a year doesn't allow is rejected. A section for another country, and per-wallet fields for a universal-only country (JP, ES, IE, generic), are rejected so that no country gets US settings by accident. - compute_tax_per_wallet() takes the switch year and checks it against the country's policy. - Docs: no flag; per-wallet application is selected automatically; this first implementation supports a universal history through 2024 followed by per-wallet application from 2025. Tests: policy unit tests (year boundaries, explicit choices, switch year, invalid policies, universal-only countries); configuration tests (country section, other country's section, old section, invalid mode, JP fields); CLI tests (no -w, -w rejected, pre-2025 output unchanged with the section, 2025 data with and without the section, conflicting modes, JP isolation); engine rejects switch years the policy doesn't allow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZgEJzUPyGHYd2xoXqcp5R
pylint 4.1.2 (astroid 4.3) reports arguments-differ on every RP2Decimal operator, because Decimal's C implementation also accepts an optional context argument. The overrides are only called as operators, so this is the same false positive the file already disables as signature-differs. CI installs the latest pylint, so this broke Static Analysis independently of the code changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZgEJzUPyGHYd2xoXqcp5R
|
@eprbell thanks for the detailed review. I'm working through it one R-number at a time, each in its own commit with its own tests, and I'll post another comment as items land so you can re-check each one on the new head. This will hopefully keep things logically separated and make it easier to focus on reviewing one particular thing at a time. Status on
R01: replace
Tests, which cover the acceptance list:
Checks on
CI fix ( Link check (
Following the existing pattern for bot-blocking domains, Unrelated finding: |
eCFR redirects non-browser clients to unblock.federalregister.gov
("programmatic access to these sites is limited to access to our extensive
developer APIs"), so the Treasury Regulation links added to the docs fail the
Documentation Check although they work in a browser. Same treatment as the
other bot-blocking domains already excluded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ZgEJzUPyGHYd2xoXqcp5R
The Documentation Check reported 5 errors, all in links that predate this PR: - CHANGELOG.md: taxbit.com/cryptocurrency-tax-guide returns 404; the guide is now at www.taxbit.com/blogs/cryptocurrency-tax-guide; - README.md, README.dev.md: deepwiki.com returns 429 Too Many Requests to the link checker, so it's excluded like the other domains that block bots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZgEJzUPyGHYd2xoXqcp5R
Review item R02 (eprbell#155). basis_carryover suppressed the disposal of the fee units and changed the basis of later sales. The IRS digital-asset FAQs (added Dec. 15, 2025) settle the question: A81, a transfer between one's own wallets is non-taxable "except to the extent of any digital assets you use, or are withheld, to pay for transaction services to effect the transfer"; A97, those units are disposed of and gain or loss is recognized; A53, amounts paid to transfer between one's own wallets "are not treated as digital asset transaction costs" (so they can't be added to the basis of the received units). - Removed TransferFeeTreatment, _get_transfer_fee_treatment(), the carryover branches of TransferAnalyzer (fee pieces, received pieces, extra basis on artificial lots), the carryover self-transfer error, the fee exclusion from taxable events, the transfer_fee_treatment config field (now an invalid field) and its docs. A transfer fee is always a disposal; cycle handling (A->B->A) no longer depends on the treatment. Acceptance (report cells, tax_report_us): buy 1 unit for $100, transfer it at $200/unit paying 0.1 unit, sell the 0.9 received for $270: Investment Expenses has the MOVE row (0.1, proceeds $20, basis $10, gain $10, source lot i1, transfer t1); Capital Gains has the sale (0.9, $270, basis $90, gain $180). Before the change, basis_carryover dropped the MOVE row and set the sale basis to $100, and a fee without the setting was an error. Test dispositions: - test_per_wallet_tax_engine "fee ... treated as a disposal": kept, no setting needed; asserts the received units keep their $900 basis. - "fee ... basis carried over": removed (behavior deliberately removed). - "fee without an explicit fee treatment: error": removed, now identical to the disposal case. - "eprbell#149 fee spanning two lots", report test with artificial lots: kept, setting dropped. - test_per_wallet_properties conservation: kept for the disposal path; every unit of every lot must now be held or disposed of with a gain/loss (the weaker carryover assertions are gone). - test_configuration: transfer_fee_treatment is now rejected. - New: test_transfer_fee_is_a_disposal_in_reports (report cells), with a read_sheet_rows() helper in tests/ods_diff.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZgEJzUPyGHYd2xoXqcp5R
|
@eprbell R02 is done in
|
Adds test_transfer_fee_leaves_no_phantom_basis, which checks the remaining basis in the engine and in the actual report cells (full report Sent/Sold column, Open Positions by asset and by exchange) after a transfer fee: - buy 10 for $1,000, move them paying 1 as the fee, sell 1 of the 9 received: 8 left with $800 of basis (the fee used up $100); - sell all 9 received: no units and no basis left, all three reports are generated (the removed carryover option crashed Open Positions with KeyError: 'B1' here); - a lot partly used up across wallets: 40% sold, 6 units left with $600 of basis (4 on Coinbase, 2 on Kraken); - uneven amounts (3 units at $7, 0.1 fee): no rounding remainder. On the commit before R02, with basis_carryover enabled, all four cases fail, reproducing both bugs from the review. No engine change was needed: R02 removed the code that caused them. Also moves the expected gain/loss conversion into _want_tuples() so _run_test() and the new test share it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZgEJzUPyGHYd2xoXqcp5R
|
@eprbell R05 is done in What R05 checks, in plain English When you move coins between your own wallets and pay the network fee in coins, those fee coins count as sold. They use up their own share of what you paid for them. The coins that arrive keep the rest of the original cost and the original purchase date. So once fees and sales are taken out, the cost the reports show must exactly match the coins you still hold. Example: buy 10 coins for $1,000 ($100 each). Move them to another wallet, paying 1 coin as the fee, so 9 coins arrive.
The removed Legal basis (links for anyone checking)
The test: Each case checks the gains and losses from the engine and the actual cells of all three reports. In every case, coins are bought at $100, the fee is paid when they're worth $150, and sales happen at $200.
The third case covers your request for a lot that is only partly used up and still has basis left. The original lot's sold fraction counts both the fee and the sale made from the wallet the coins were moved to. The existing report test only checked a fraction of 1. Proof that the test catches the old bugs: I ran the same test on the commit before R02 with Checks on
|
…witch purchase Bug: buy 10 units in wallet A in 2024, allocate them at the switch, then move 4 units A -> B and back in 2025. The calculation stopped with "start-of-cycle transaction's returned amount exceeds its crypto_in: 14 > 10". Cause: originates_from (the lot each earlier wallet held, used to return units at the end of a round trip) was built by following from_lot all the way back. Past a lot allocated at the switch, from_lot leads to the input lot bought before the switch, which is in the same wallet but no wallet holds after the switch. The walk overwrote the allocated lot with that purchase, and the returned units were added to it: its 10 units (it had no remaining-amount entry) plus 4. Fix: originates_from of a new transfer lot is the lot the units leave from plus that lot's own originates_from (nearest lot wins). Lots created by the allocation have an empty originates_from, so the chain stops there. from_lot is unchanged: original_lot, acquisition dates and reports still follow it, and so does to_lots. Tests: - test_allocated_lots_survive_transfer_cycles: 9 cases x FIFO, LIFO, HIFO, LOFO, checking gain/losses and each wallet's final holdings (amount, basis, acquisition date per original lot): the review's example, partial return, a sale in between, fees on both legs, multiple hops, two round trips, a split allocation, two lots at different prices, and a long-term sale after the round trip. All 36 subtests fail without the fix. - test_invariants_hold_across_the_switch: property test on random histories that start in 2024, cross the switch with an allocation and often send funds back where they came from: wallet holdings equal balances, every unit and all basis is held or disposed of. It finds the bug without the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZgEJzUPyGHYd2xoXqcp5R
|
@eprbell R03 is fixed in What went wrong, in plain English When coins come back to a wallet they were in before, RP2 puts them back into the lot they left from. This keeps the wallet's lot count right and keeps the coins' cost and date. To find that lot, the code walked back through the coins' history ( Example (your reproduction): buy 10 coins in wallet A in 2024.
The fix Each transfer lot now records the lots its coins actually sat in, wallet by wallet: the lot they just left, plus that lot's own record. Lots created by the allocation start with an empty record, so the chain stops at the allocation. It never reaches the pre-switch purchase. If a wallet shows up twice, the most recent lot wins.
I added comments with this example in Why this is the right result (links for anyone checking)
Tests
Proof that the tests catch the bug: without the fix, all 36 subtests fail. The property test finds it too, with a 0.007-coin history ("returned amount exceeds its crypto_in: 0.007 > 0.006"). With the fix, everything passes. Checks on
|
…ules test_per_wallet_tax_engine.py was close to pylint's 1,000-line limit, and the remaining review items add more tests. This commit only moves code, with no change to any test or assertion: - tests/per_wallet_common.py: short-form transactions (_In, _Out, _Intra), expected gain/losses, wallets, _allocation_config() and AbstractPerWalletTest (input data, accounting engine and gain/loss tuple helpers), shared by the two files below; - tests/test_per_wallet_reports.py: the tests that check report cells (artificial lots, R02 transfer fee, R05 phantom basis); - tests/test_per_wallet_tax_engine.py: the engine tests. Same results before and after: 197 tests, 243 subtests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZgEJzUPyGHYd2xoXqcp5R
Bug: in 2025 buy 1 unit for $100 in Coinbase and 1 for $1,000 in Kraken. The Asset - Exchange sheet showed $550 for each wallet: the report spread the asset's total cost basis over all wallets in proportion to their balances, which is how universal application works, but in per-wallet application each wallet has its own lots (IRC 1012(c)(1), Treas. Reg. 1.1012-1(j)). Fix: - TransferAnalyzer.analyze_and_pair(holdings_date) records what each wallet holds when its single chronological pass reaches the first transaction dated after holdings_date (same date rule as BalanceSet), as a list of WalletLot (new: wallet, lot, units held; cost basis, original acquisition date and original lot are derived from the lot). - compute_tax_per_wallet() passes the configuration's to_date and stores the holdings in ComputedData.wallet_lots. If to_date is before the switch, wallet_lots is None and reports work as in universal application. compute_tax() is unchanged (None). - ComputedData checks that the lots of every wallet add up to its balance at to_date (internal error otherwise). - The open positions report uses wallet_lots when present: each wallet's balance and cost basis come from its lots, each holder's row is the sum of its wallets. Universal assets keep the exact same code and output. - Account is now orderable (by exchange, then holder). Tests (tests/test_per_wallet_reports.py, actual ODS cells): test_open_positions_show_each_wallet_actual_cost_basis covers the review's example, a partial sale, a complete sale, a transfer with a fee, to date and from date filters, coins bought before 2025 (the allocation at the switch decides each wallet's basis), a to date before 2025 and universal application. 5 of the 9 cases fail with the old report; the others match the old output by design. The R03 tests and the switch-crossing property test now read the holdings from ComputedData.wallet_lots instead of patching the analyzer, and the property test checks them at random report dates (it fails if the final holdings are used instead). Docs: docs/output_files.md, docs/user_faq.md, CLAUDE.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZgEJzUPyGHYd2xoXqcp5R
CI failed on a3abfe6 on Python 3.13 (macOS and Windows; the other jobs were cancelled): 79 tests raised "TypeError: 'BalanceSetIterator' object is not iterable" in the new ComputedData._check_wallet_lots(). BalanceSetIterator defined __next__() but not __iter__(). Plain for loops don't need it, but from Python 3.13 list and dict comprehensions call iter() on the iterator too, so the dict comprehension over the BalanceSet failed. Python 3.10-3.12 accept it, which is why the local 3.11 run passed. BalanceSetIterator now derives from Iterator[Balance], like EntrySetIterator, which provides __iter__(). Checked: full suite on Python 3.11, 3.13 and 3.14 (198 tests, 252 subtests each). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZgEJzUPyGHYd2xoXqcp5R
1. Don't stop the run when a wallet's lots and its balance differ. Sales have an optional crypto_out_with_fee input field: the tax computation (and so the wallet lots) uses it, while BalanceSet uses crypto_out_no_fee + crypto_fee. If both are in the input and differ (e.g. exchange rounding: 1.00100001 vs 1 + 0.001), the new check in ComputedData raised "Internal error" and no report was generated, while the commit before R04 completed. The difference is now a warning that points to the input; reports use the lots. The tests still check exact equality, so a code bug would fail CI. Test: test_inconsistent_sale_amounts_are_a_warning (fails on a3abfe6 with the internal error). 2. Docs: coins with no transactions in 2025 or later are still computed with universal application (the allocation at the switch isn't computed for them), so their wallets get the average cost basis and -f leaves out their lots bought before it. user_faq.md said only report periods ending before 2025 do; it's now a listed limitation. 3. WalletLot: the hash generated by dataclass included the amount, an unhashable RP2Decimal, so hashing a WalletLot raised TypeError. It's now hashed on (wallet, lot). Test: test_wallet_lot. 4. TransferAnalyzer.analyze_and_pair() type-checks holdings_date at entry (a string or a datetime raised a bare TypeError mid-pass). Test: test_holdings_date_must_be_a_date. 5. Property test: the report date is now drawn from the last step before the switch onwards, so most examples check the per-wallet holdings again (about a quarter used to stop at the "before the switch" check). Checked: full suite on Python 3.10, 3.11, 3.13 and 3.14 (201 tests, 254 subtests each); mypy, flake8, bandit clean; pylint 10.00 with both 4.0.9 and 4.1.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ZgEJzUPyGHYd2xoXqcp5R
Summary
Per-wallet application for the US (
-w), following Treas. Reg. §1.1012-1(j) and Rev. Proc. 2024-28. Tax years before 2025 are unchanged.How it works
compute_tax(). Output is identical to runs without-w.timezone, Rev. Proc. 2024-28 §3.10): the lots still unused under universal application are allocated to the wallets that hold funds. This is the "global allocation" of §5.02(2)(b), done byUnusedBasisAllocator. Basis and acquisition date stay together (§3.04). The rule is a lot-sorting method plus a wallet order, with optional per-asset overrides (<field>.<asset>).TransferAnalyzer.analyze_and_pair()pairs each sale and transfer with lots of its own wallet only, in chronological order.AcquisitionDateFifo, per §1.1012-1(j)(3)(i)).get_per_wallet_application_start_year(), which is 2025 for the US and None for everyone else, sorp2_jp -wis rejected. US runs with 2025+ data and no-wprint a warning.Unsettled law is explicit, never defaulted. New
[per_wallet]config section:timezone(mandatory);transfer_fee_treatment(disposalorbasis_carryover): required if a post-switch transfer has a crypto fee;unused_basis_allocation_methodandunused_basis_allocation_wallet_order: required if more than one wallet holds funds at the switch.A transaction whose own timestamp falls in a different tax year than the configured
timezonegives is an error, not a guess.Bugs fixed (in the earlier version of this PR, and in shared code)
basis_carryover, basis was lost on A→B→A cycles.Docs: user FAQ (per-wallet application, what counts as a wallet, locked staking rewards),
input_files.md([per_wallet]),supported_countries.md, README, CLAUDE.md.Test plan
pytest --tb=native --verbose); mypy, pylint 10.00, black, flake8, bandit clean.test_per_wallet_tax_engine.py: hand-computed cases, including:test_per_wallet_properties.py(hypothesis):test_ods_output_diff_per_wallet.py:-woutput for pre-2025 datasets is identical to universal output, for every method and report;rp2_jp -wis rejected.test_gain_loss.py::test_ltcg_boundary: calendar-rule boundaries, including leap years.Not in this PR (follow-up issues)
Reports showing the wallet and original lot (1099-DA reconciliation), Form 8949 covered/noncovered boxes, specific unit allocation, rule-based wallet orders, wrapped/bridged tokens, Japanese averaging methods, gifts and inheritance, more test coverage, small cleanups, multiple switches, and specific identification of transferred lots.
🤖 Generated with Claude Code