-
Notifications
You must be signed in to change notification settings - Fork 49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
USA to drop universal application: implement per-wallet application #135
Comments
Some useful links on this topic:
|
This is what the great magnificent supercomputer in the sky aka CoPilot had to say about this: To modify the project to track transactions by individual wallets and calculate the cost basis based on the purchase price for the crypto asset in each wallet, you will need to make changes to several parts of the codebase. Here is a step-by-step guide to help you implement this feature:
Step 1: Update the TransactionSet ClassAdd wallet information to the Step 2: Modify the InputData ClassUpdate the Step 3: Update the GainLossSet ClassModify the Step 4: Modify tax_engine.pyUpdate the Here is an example of how you can modify the tax_engine.py: def _create_unfiltered_taxable_event_set(configuration: Configuration, input_data: InputData) -> Dict[str, TransactionSet]:
wallet_taxable_event_sets: Dict[str, TransactionSet] = {}
for wallet in input_data.wallets:
taxable_event_set: TransactionSet = TransactionSet(configuration, "MIXED", input_data.asset, MIN_DATE, MAX_DATE)
for transaction_set in [
input_data.get_unfiltered_in_transaction_set(wallet),
input_data.get_unfiltered_out_transaction_set(wallet),
input_data.get_unfiltered_intra_transaction_set(wallet),
]:
for entry in transaction_set:
transaction = cast(AbstractTransaction, entry)
if transaction.is_taxable():
taxable_event_set.add_entry(transaction)
wallet_taxable_event_sets[wallet] = taxable_event_set
return wallet_taxable_event_sets
def _create_unfiltered_gain_and_loss_set(
configuration: Configuration, accounting_engine: AccountingEngine, input_data: InputData, wallet_taxable_event_sets: Dict[str, TransactionSet]
) -> Dict[str, GainLossSet]:
wallet_gain_loss_sets: Dict[str, GainLossSet] = {}
for wallet, taxable_event_set in wallet_taxable_event_sets.items():
gain_loss_set: GainLossSet = GainLossSet(configuration, input_data.asset, MIN_DATE, MAX_DATE)
new_accounting_engine: AccountingEngine = accounting_engine.__class__(accounting_engine.years_2_methods)
taxable_event_iterator: Iterator[AbstractTransaction] = iter(cast(Iterable[AbstractTransaction], taxable_event_set))
acquired_lot_iterator: Iterator[InTransaction] = iter(cast(Iterable[InTransaction], input_data.get_unfiltered_in_transaction_set(wallet)))
new_accounting_engine.initialize(taxable_event_iterator, acquired_lot_iterator)
try:
gain_loss: GainLoss
taxable_event: AbstractTransaction
acquired_lot: Optional[InTransaction]
taxable_event_amount: RP2Decimal
acquired_lot_amount: RP2Decimal
total_amount: RP2Decimal = ZERO
(taxable_event, acquired_lot, taxable_event_amount, acquired_lot_amount) = _get_next_taxable_event_and_acquired_lot(
new_accounting_engine, None, None, ZERO, ZERO
)
while taxable_event:
AbstractTransaction.type_check("taxable_event", taxable_event)
if acquired_lot is None:
raise RP2RuntimeError("Parameter 'acquired_lot' is None")
InTransaction.type_check("acquired_lot", acquired_lot)
if taxable_event_amount == acquired_lot_amount:
gain_loss = GainLoss(configuration, taxable_event_amount, taxable_event, acquired_lot)
total_amount += taxable_event_amount
gain_loss_set.add_entry(gain_loss)
(taxable_event, acquired_lot, taxable_event_amount, acquired_lot_amount) = _get_next_taxable_event_and_acquired_lot(
new_accounting_engine, None, None, ZERO, ZERO
)
elif taxable_event_amount < acquired_lot_amount:
gain_loss = GainLoss(configuration, taxable_event_amount, taxable_event, acquired_lot)
total_amount += taxable_event_amount
gain_loss_set.add_entry(gain_loss)
(taxable_event, acquired_lot, taxable_event_amount, acquired_lot_amount) = _get_next_taxable_event_and_acquired_lot(
new_accounting_engine, taxable_event, acquired_lot, ZERO, acquired_lot_amount - taxable_event_amount
)
else:
gain_loss = GainLoss(configuration, acquired_lot_amount, taxable_event, acquired_lot)
total_amount += acquired_lot_amount
gain_loss_set.add_entry(gain_loss)
(taxable_event, acquired_lot, taxable_event_amount, acquired_lot_amount) = _get_next_taxable_event_and_acquired_lot(
new_accounting_engine, taxable_event, None, taxable_event_amount - acquired_lot_amount, ZERO
)
except AcquiredLotsExhaustedException:
raise RP2ValueError("Total in-transaction crypto value < total taxable crypto value") from None
except TaxableEventsExhaustedException:
pass
wallet_gain_loss_sets[wallet] = gain_loss_set
return wallet_gain_loss_sets
def compute_tax(configuration: Configuration, accounting_engine: AccountingEngine, input_data: InputData) -> Dict[str, ComputedData]:
Configuration.type_check("configuration", configuration)
AccountingEngine.type_check("accounting_engine", accounting_engine)
InputData.type_check("input_data", input_data)
wallet_taxable_event_sets = _create_unfiltered_taxable_event_set(configuration, input_data)
wallet_gain_loss_sets = _create_unfiltered_gain_and_loss_set(configuration, accounting_engine, input_data, wallet_taxable_event_sets)
computed_data: Dict[str, ComputedData] = {}
for wallet, gain_loss_set in wallet_gain_loss_sets.items():
computed_data[wallet] = ComputedData(
input_data.asset,
wallet_taxable_event_sets[wallet],
gain_loss_set,
input_data,
configuration.from_date,
configuration.to_date,
)
return computed_data This example assumes that the TransactionSet and GainLossSet classes can handle wallet-specific transactions. By following these steps, you can modify the project to track transactions by individual wallets and calculate the cost basis based on the purchase price for the crypto asset in each wallet. It goes on to say that class AccountingEngine:
def __init__(self, years_2_methods: Dict[int, str]):
self.years_2_methods = years_2_methods
self.wallets = {}
def initialize(self, taxable_event_iterator: Iterator[AbstractTransaction], acquired_lot_iterator: Iterator[InTransaction], wallet: str):
self.wallets[wallet] = {
"taxable_event_iterator": taxable_event_iterator,
"acquired_lot_iterator": acquired_lot_iterator,
}
def get_next_taxable_event_and_amount(
self,
wallet: str,
taxable_event: Optional[AbstractTransaction],
acquired_lot: Optional[InTransaction],
taxable_event_amount: RP2Decimal,
acquired_lot_amount: RP2Decimal,
) -> Tuple[AbstractTransaction, Optional[InTransaction], RP2Decimal, RP2Decimal]:
wallet_data = self.wallets[wallet]
taxable_event_iterator = wallet_data["taxable_event_iterator"]
acquired_lot_iterator = wallet_data["acquired_lot_iterator"]
# Logic to get the next taxable event and acquired lot for the specified wallet
# ...
def get_acquired_lot_for_taxable_event(
self,
wallet: str,
taxable_event: AbstractTransaction,
acquired_lot: Optional[InTransaction],
taxable_event_amount: RP2Decimal,
acquired_lot_amount: RP2Decimal,
) -> Tuple[AbstractTransaction, Optional[InTransaction], RP2Decimal, RP2Decimal]:
wallet_data = self.wallets[wallet]
acquired_lot_iterator = wallet_data["acquired_lot_iterator"]
# Logic to get the acquired lot for the specified taxable event and wallet
# ... Basically, just adding wallet storage to each part of the engine. Is there something I'm missing? This is AI generated, so please go over it carefully. |
Thanks, I'll read it. I had some rough ideas on how to approach the problem:
My only problem right now is finding the time to work on it... |
I think we can take it piece by piece. The first piece is to modify the initial reading in of the data to sort the different lots into different wallets. We can probably start there and build out some tests for it. I should have about the next month or so to work on and submit code. I don't think it will take that much time as long as we are pretty systematic about it. For example, I think the first step is to create a function in I guess this function would sort in and out transactions pretty easily, just whatever exchange they happened on. Then, intra transaction will be split into non-taxable in and out transactions in their respective wallets. I think this handles this first step right?
Then we can cycle through the wallets probably in a multithread way to process all the transactions using the current engine. That will cover the next step:
And finally merge all the I can probably put the code together as long as you can review it by the end of the year. |
Yes, that sounds reasonable, however I'll add a few more considerations that complicate the picture slightly:
There are additional complications such as which method to use for transfers (FIFO, LIFO, etc.). Some options:
I think we should start option one or two. I think we should write a brief high level design of this feature first: let me see if I can come up with a quick document in the weekend, to start the discussion. |
Sorry, I guess I didn't realize until just now that this only applies to 2025, so for when we file taxes in 2026. For some reason, I thought we had to have this ready for filing taxes in 2025. I was in a panic. I guess we still have time, but if you outline what is needed I can try to have a whack at it. |
Yes, according to the Reddit thread, new rules are effective from 1/1/2025. So we have 1 year to figure it out. |
I'm making progress on the design of per-wallet application but it is still unfinished. I realized we can apply the existing accounting method layer to the new logic to pick which lot to transfer, which is nice. However we need a few additions to the existing infrastructure:
Here's the unfinished design so far. How to switch from universal to per-wallet from one year to the next is still TBD. |
Okay, I just looked up details about Japan, and they use universal wallet and you can make use of FIFO, LIFO, etc... based on all of your holdings as a whole. So, we will have to combine universal wallet with FIFO, etc... Does your plan account for that? |
Universal application + FIFO/LIFO/HIFO/LOFO is already supported today (you can even change from one accounting method to another year over year). See: https://github.com/eprbell/rp2/blob/main/docs/user_faq.md#can-i-change-accounting-method. Based on what you're saying it sounds like we can add more accounting methods here: https://github.com/eprbell/rp2/blob/main/src/rp2/plugin/country/jp.py#L41 The design I'm working on supports any combination of per-wallet/universal application + FIFO/LIFO/HIFO/LOFO (including changing from one combination to another year over year). This high-generality approach is proving both interesting and hard to do, so it will require some more time to iron out all the details. It's good that we can reuse the existing accounting method infrastructure for lot selection in transfers, but the problem goes deeper than I thought. When I'm closer to something presentable, we can discuss it and brainstorm a bit. |
The per-wallet application design is more or less complete. It can still be improved, but it captures most of the concepts: feel free to take a look and let me know what you think. Next I will probably do a bit of prototyping to make sure the ideas behind the design hold water. |
@eprbell I read through it and it looks pretty sound. I'll have to give it some time and read through it again just to double check, but I think this will handle what we need. Thanks for working it out. It looks like a lot of work and you gave some good examples. |
I'm making good progress on the implementation and unit testing of the transfer analysis function. Sorry, @macanudo527, I know you expressed some interest in working on this: I wanted to write a prototype to verify the design, but ended up finding more and more corner cases, and adjusting the code accordingly to deal with them. So basically what started as a prototype is becoming the real thing. I will open a PR for this though: it would be good to get your feedback before merging. |
No worries, I can just spend time on some of the other core functions instead. Looking forward to it. I'll be out for the end of the year (Dec 23rd - Jan 7th), but after that I can look at it. |
Sounds good (we're in no rush). Have a great time during the holidays! |
US tax payers, watch this informative interview by Andreas Antonopulous on per-wallet application in the US. |
Hi. Hope you don't mind me commenting as a non-code-contributing member of the RP2 community. I watched the Andreas Antonopulous interview. Honestly, I found it extremely difficult to follow due to him constantly interrupting the guest. I found this video by "Crypto Tax Girl" to be much more clear and easy to follow. If you have a chance to watch it, I'd be interested if you feel it lines up with your understanding of these changes. A have a few questions if I may:
Thoughts? Thanks for all you do to make RP2 available and up-to-date! Much appreciated. |
Feedback is always welcome from anybody (code-contributing or not). Thanks for the video: I haven't watched it yet, but I will in the weekend. I think the main takeaway from Andreas' video is to follow the procedure he and his guest recommended before 12/31/24: basically consolidate everything into one wallet (if possible). The Description in the video summarizes the steps to take. This will simplify accounting going forward. As for your questions:
Hope this helps. |
Thank you for the reply! FWIW, I reviewed the design document -- twice -- but since it's very code centric, and I'm not familiar with the overall application design, I wasn't able to understand all that much about the approach being taken. (That's not a issue. It just is what it is.) Regarding your reply, let me see if I understand: So there are "transfer semantics" and "accounting method", each of which could be FIFO, LIFO, HIFO, or LOFO. Does that mean that if "transfer" is set to FIFO and "accounting" is set to HIFO that, when a transfer is done, the basis with the oldest date (first in) will be moved to the new wallet. And, similarly, when a token is sold within a wallet, the basis with the highest price (highest in) will be associated with the sale? Am I understanding that correctly? Assuming I am, I'm still not clear what happens to a token when it is transferred to another wallet and "transfer semantics" is FIFO. Does it get assign a new "in date" in the destination wallet or does it retain its original "in date" from when it was purchased? In my original post I asked it this way:
At the time I imagined each wallet would have a "queue" of transactions, but now I understand it's probably more like a pool of transactions that can be sorted in any way at runtime, as needed, based on whether a transfer is being done ("transfer semantics") or a sell is being done ("accounting method"). Is that correct? That being the case, I would guess that the transferred token (and corresponding basis) would retain the original purchase date even after it is moved to the destination wallet. Here's an example (a variant of yours from the design):
If both "transfer semantics" and "accounting method" are FIFO, does that mean that the Does that make any sense? Hopefully. Thoughts? As for the second question, I'm not sure what you mean by this:
I understand that, per Andreas, it probably makes sense to try and consolidate wallets as much as possible. But are you going to actually make a "declaration" before 1/1/25 and either email it to yourself or use the blockchain time-stamping approach Andreas suggested to have something that can be provided to the IRS, if needed, as proof of claiming "safe harbor"? And, if so, what is that declaration going to contain? Thanks! |
I'm replying inline, however keep in mind that what I'm saying is based on my current ideas for a design that is still in flux. So don't make tax decisions solely based on what I'm describing here, because it may change. This is why I was highlighting Andreas' solution: it makes your crypto accounting simple and clear, regardless of what happens with tax software.
Yes to both questions. The transfer semantics is what is used to populate per-wallet queues from the universal queue that is used up to the end of 2024 (it is also used when transferring from one per-wallet queue to another).
Good question. The current idea is to create an artificial InTransaction in the "to" wallet. This artificial InTransaction has:
I think this was already answered above. It goes into a new queue that is specific to wallet B (that's the whole idea behind per-wallet application). However you need to consider the case in which you have 1 BTC in wallet A and you transfer 0.5 BTC to wallet B. In this case you're splitting the original lot. Currently this is captured by leaving the 1 BTC in the queue of wallet A and creating an artificial transaction for 0.5 BTC in wallet B. The two transactions are linked and are updated together by the tax engine (for more on this check this).
Not quite: see explanation above about one queue per wallet.
In your example, using FIFO for everything, the 4/1 OutTransaction would use the 2/1 InTransaction. Note that the Kraken queue would also have an arificial InTransaction on 3/1 containing 4 BTC and linked to the 1/1 transaction. But in your example the artificial transaction is not exercised because the 2/1 transaction has enough funds to cover the 2 BTC of the OutTransaction. If the OutTransaction had, say, 7 BTC instead of 2, then the code would use first the entire 2/1 lot and then 2 BTC from the artficial InTransaction (this also causes its parent transaction 1/1 to be updated as explained here).
I mean that RP2 won't let the user select which lot goes into which wallet queue arbitrarily. RP2 will take an algorithmic approach: the user selects the transfer semantics and the code moves the funds around.
This is probably a question for your tax advisor, but the rough idea is to move everything to one single wallet and then take snapshots of all accounts, generate an RP2 report and timestamp everything on Dec 31st. By moving to a single wallet you're essentially causing universal and per-wallet approach to match, because there is now only one wallet having one queue with all the funds. Thanks for asking questions and engaging in conversation. It's good to brainstorm the ideas behind the design and see if they hold water. |
Thanks for the reply! Agreed about the conversation and brainstorming. Even though I'm not coding this, I find it very helpful for my own understanding.
This was a surprise to me, so I decided to post basically this exact question on the Reddit forum to see if @JustinCPA would respond, which he did. He seems to say the opposite of what you've said. I feel like this could be an issue for the current design -- at least for US taxpayers, and assuming @JustinCPA is correct. Thoughts? |
Sounds like you found a bug in the design! Thanks for getting in the weeds and asking JustinCPA: his explanation is convincing. The bug is that the artificial InTransaction was using the timestamp of the transfer instead of the timestamp of the from InTransaction. I already fixed the code so that the behavior is as explained by Justin. I will be posting some initial code for the transfer analysis algorithm in a PR soon (together with unit tests). |
Great! Thanks! One thing that comes to mind: You may well be taking care of this already, so forgive me if you are, but you may want to make sure that transfers, if they are using the timestamp of the "from"
Of course the transactions on 2/1 and 3/1 seem obviously invalid when looked at like that. But, I could imagine a scenario where the 5 BTC transferred on 4/1 from Coinbase to Kraken inherit the "from" Keep in mind I don't understand the overall app design or the design for these changes. I'm just looking at this as an outsider. Thanks! |
One more thought: Is it possible to use different transfer semantics to:
Don't think that I would need this, but -- if I'm understanding correctly -- others may. Take a look at this post for context. Basically, I think some may want to use HIFO for populating the wallets from the universal queue, and then FIFO going forward (as I understand that is required for "global allocation" / non-spec-ID). |
Ah, good point. This means that my previous approach was only 50% wrong :-) because the artificial transaction needs both timestamps: one for holding period and the other for fund availability. Let me think a bit on how to best model this: we probably need a subclass of InTransaction to capture this.
No worries: your feedback as a user has been very valuable. Keep it coming! |
Interesting: the current design allows for changing transfer semantics and accounting method year over year in any combination supported by the country plugin. What you're describing would be an extra one-time only transfer semantics for initial per-wallet queue population: this is not supported yet. With the existing design you could select HIFO transfer semantics in 2025 and then switch to FIFO in following years: not exactly what you're asking for but it's an approximation. I think we should finish the basic design and implementation of per-wallet application and then we can think about something like this as a potential advanced feature. |
Of course. Just figured I'd mention it for your awareness. (I personally don't anticipate needing it.)
I suppose, though I don't think that would comply with the IRS requirements. I'm no expert, but my understanding is that it has to either be FIFO or specific identification going forward. I totally get your point, though. "One step at a time" :-) |
No need for apologies: we're all volunteers here, so any contribution is appreciated, but without any pressure or obligation! Thanks for asking to clarify terminology: I'm guilty of mixing up these confusing safe harbor terms sometimes. Answers inline below.
Yes, it means January 2023.
Correct: I went back and corrected my earlier replies in which I mixed them up a bit.
Yes, all correct.
Yes, that's the idea, with step 2 only occurring when switching from universal application to per-wallet application.
I should have clarified better. The example I gave is what is the universal data that is generated after running step 1 and before feeding it to step 2. However the "pairing" I talked about refers to step 3. Does this make sense? Let me know if anything is still unclear. |
Thanks for clarifying @eprbell. But I still feel unsure about whether we're "on the same page". Would you mind responding to the second part of my post, beginning "In Step 1 ("universal application") with HIFO ...", where I walk through the detailed example of what would occur in the 3 steps. I want to make sure you agree and, if you don't agree on any points, I feel we should hash out "why". If I'm misunderstanding, I want to make sure I can correct that misunderstanding. Also, are we in agreement on the following?
Something else I'd like to clarify (related to the "earlier date" thing). Forgive me if this is obvious, and already handled this way, but I'm thinking about it and just want to be sure: When processing steps 1 and 3, my assumption is that transactions should be processed in date order (earliest to latest), regardless of the accounting method used, and any transactions that occurred after the transaction currently being processed should be "invisible" and entirely ignored. For example, if the accounting method is HIFO, and there is a sell on 5/25, the effective HIFO queue of unused basis should only included transactions that occurred prior to the 5/25 transaction for the purpose of assigning basis to the 5/25 sell. As I understand it, this is super important, because if you run RP2 on a set of transactions in the middle of the year, it should allocate basis to sell transactions (that have occurred up to that point) in exactly the same way as if you run it at the end of the year (when additional buy transactions may have occurred). Stated another way, I don't believe it would be correct to build a HIFO queue of all transactions (essentially "basis lots") for a wallet, and then pull from that queue when processing individual sell transactions and ignore date. Sell transactions should only use unused basis with an earlier date, regardless of accounting method. My writing can be a bit "stream of consciousness" and, in retrospect, I could probably have structured the above a bit more logically. But hopefully it makes sense. Thoughts? |
I am about to leave for a few hours so I don't have time to answer this right now, but I will in the next day or two in a separate message. Meanwhile I answered your other questions below inline.
Correct.
Correct.
Correct: that's also how RP2 works today. And I think it should work the same way for steps 1, 2 and 3.
I'm not sure if we understand this in the same way. The way I was planning to do this is (still working on it so it may change):
Right, this is the same as your third bullet above, I believe, and it's correct: this is how RP2 behaves today and I think universal application, global allocation and per-wallet application should continue to behave in the same way. |
Cool. Thanks!
FWIW, though I don't think there is any harm in implicitly including step 2 in this, as I understand it, step 2 doesn't really have the same logic as steps 1 and 3, as there is no pairing of buy and sell transactions in step 2 and, therefore, no requirement to enforce that "only earlier dates are considered". Step 2 is simply using the accounting method to order transactions for global allocation to wallets. (If I'm misunderstanding, please let me know.)
I hope you don't mind me being super "forward" here, but since you've always seemed very open to feedback, I'll press on: Though I'm not helping to implement RP2, I do have a software engineering background (mostly with Java), and I have done a decent amount of design and refactoring work in my time. As I read the above, honestly, it make me nervous. The idea of permanently adding artificial transactions and a second date ( I may just be speaking nonsense here, but maybe what I'm saying will at least trigger some useful ideas: If it were me, I'd take a step back and ask myself, "if I were designing RP2 from scratch, what data structure would I design that would support both universal and per-wallet application in the cleanest, most logical way?" Presumably such a data structure would not include artificial transactions and extra dates. Then I'd do something along the following lines:
The idea here is that the bulk of what RP2 does -- represented by steps 1 ("universal application") and 3 ("per-wallet application") -- should be as clean, easy to understand, unified, and hopefully immune to logic bugs as possible. That code is what you (and other RP2 devs) are going to have to live with long after these "global allocation" changes are in the distant past. Is dealing with artificial transactions and extra dates years from now the best approach? Is there a better alternative? (Anything that's going to require devs to also understand "global allocation" years from now seems less than ideal.) All of that said, maybe what you have is the cleanest, best approach for supporting the requirements. It may be. (Maybe I just don't understand it well enough.) You're the expert here! Guess I just want to maybe stir some thought about possible alternatives. Hopefully it's at least somewhat helpful as you plan the next steps. Thanks for considering. |
One more comment:
To summarize/re-state this, I think that per-wallet application should only ever use one date, and it should be the date associated with the cost basis lots that were assigned to the wallet during global allocation. (Any other date that even exists in the data structure is liable to cause confusion and result in bugs, IMO.) Thoughts? |
Answers inline below (they reflect my latest understanding of the rules, which has changed since I first wrote that example).
Correct so far.
No, I think the original date of purchase is still relevant, because it allows RP2 to distinguish long vs short-term capital gains. I think global allocation should not have the effect of resetting the type of gains. So the new per-wallet model keeps both dates around.
Correct.
Correct. |
Let's be clear about the difference between the original purchase date associated with a "cost basis lot" (always a fixed date+basis pair) and the original purchase date associated with a "physical coin". (Forgive my odd use of "physical" here, but it's how I think of an actual coin residing in an actual wallet.) I believe we disagree here. You're saying that the original purchase date of the "physical coin" is relevant for determining long-term vs. short term, right? I disagree. All that should matter is the original purchase date of the cost basis lot that is assigned to the coin. If it were as you're saying, that would enable the taxpayer to massively affect how much they pay in taxes by reassigning cost basis (and therefore gains/losses when selling) between "physical coins" with different original purchase dates, effectively turning large short-term gains into long-term gains (or visa versa). I can't imagine the IRS ever being OK with that. Am I missing something? |
Yes, this is correct. I think, semantically speaking, global allocation is equivalent to a bunch of zero-fee transfers at the end of the year to reallocate funds according to the allocation method.
These are fairly generic statements. The design already has over 100 hours of thinking that went into it and, while I'm not saying it's perfect, it's the simplest, cleanest I could make it so far. I'm sure it will still change, based on my evolving understanding and feedback from others, however any change at this point will require precise pinpointing at a part of it and a strong reason justifying the change. BTW, I do need to update the design Wiki document with the latest developments: it's a bit outdated at this point. The reasons the design adds extra objects and attributes are to capture concepts that are not covered otherwise, once we add support for the per-wallet model:
If you have time/interest, I would encourage you to look at the tests to convince yourself if the design (and implementation) work or not. They are quite easy to read and don't require familiarity with the internals: they are a set of tables, each of which shows inputs to a particular function (like transfer analysis or global allocation) and expected output. So far I uploaded the transfer analysis ones (see tests/test_transfer_analysis_semantics_dependent.py and tests/test_transfer_analysis_semantics_independent.py in #138), but global allocation is coming. This way you could:
|
Not sure I'm following, could you produce an example so it's easier to reason about this? |
Thanks for putting up with all my "devil's advocate" messages. I'm genuinely not trying to be difficult for the fun of it, but to help with considering everything from different angles. (We all have blind spots.)
I was actually in the process of doing so when you replied :-) But it took me a while to think it through more thoroughly myself in order to come up with a reasonable example:
If you'll bear with me creating some new terminology for the purposes of discussion, let's say we have:
So, with the above terminology, at the end of the year 2024 we'd have the following:
If we completely ignore global allocation for now and just consider the 2/1/25 sell, I think we can probably agree that the sell of the 1 BTC on Coinbase will result in a long-term gain of $20k ($60k-$40k) and the sell of the 1 BTC on Kraken will result in a short-term gain of $40k ($60k-$20k). But now let's add global allocation into the mix. And let's say, for example, that global allocation in this case results in the "swapping" of basis lots between Coinbase and Kraken. Global allocation runs at the end of 2024 (for U.S. taxpayers), so at the beginning of 2025 we'd have this:
Now what happens with the sell on 2/1/25? If I'm understanding what you're saying correctly, the "physical date" would be used to determine long-term vs. short-term capital gains. So the 1 BTC on Coinbase would be sold for a long-term gain of $40k ($60k-$20k) and the 1 BTC on Kraken would be sold for a short-term gain of $20k ($60k-$40k). I don't think this is correct, as it would allow the taxpayer to significantly manipulate how much they pay in taxes. Rather than paying LT $20k and ST $40k (as in the earlier example), now they're paying LT $40k and ST $20k. The way I see it, "physical date" is not relevant. The only date that should matter is "basis date" (including for determining long-term vs. short-term capital gains). If that is the case, then the 1 BTC on Coinbase would be sold for a short-term gain of $40k ($60k-$20k) and the 1 BTC on Kraken would be sold for a long-term gain of $20k ($60k-$40k). From a tax perspective, that's exactly the situation further up in this post before we considered global allocation. And I think that's correct, because it doesn't allow the taxpayer to significantly change what they pay in taxes simply by swapping around some dates as part of global allocation. (Keep in mind that, in all these examples, all the BTC has been sold, so this is the final picture from a tax perspective.) Does this make sense? Am I understanding what you were saying? Thoughts? Thanks. One more addition to this: I think "basis date" should also be used for ordering transactions for accounting method, when a date is applicable (such as for FIFO or LIFO). |
Totally fair! Definitely didn't mean to minimize how much work has gone into all of this.
I would definitely like to help, and I can maybe try looking at the tests, but knowing the way my brain works I think that would be a struggle. I really need to get a top-down understanding of the design first (not just for these changes, but for RP2 as a whole) before I can properly dig into the tests. I'll dig in some more and see if I can get a better high-level understanding of how RP2 is designed. (If there are any particular resources you can point me to, that would be helpful. But either way I'll investigate...) |
The pointers for the new per-wallet semantics are the Wiki design (in need of update), the code and this discussion. RP2 dev docs are at: https://github.com/eprbell/rp2/blob/main/README.dev.md However my suggestion is that hopefully understanding the design, while helpful, is not necessary for analyzing the tests: what's needed is understanding the tax rules. For example, if you understand how the process of transforming a universal queue to multiple per-wallet queues is supposed to work at a high level, you can look at the transfer analysis tests and find out if the code works as expected or not. For example, let's consider the first test in tests/test_transfer_analysis_semantics_dependent.py in #138 (this is part of the
It's compact, self-documenting and hopefully not too hard to read:
Here's how we would reason about this test:
There are a few more fields in the descriptors but they are not essential for the purposes of this exercise (and I want to keep it as simple as possible). This tests is repeated once for each of the 4 supported accounting methods. If we were to look at the same test in function
The Coinbase account is the same, but the Kraken one is a bit shuffled compared to its FIFO counterpart, due to HIFO. Anyway, this is a long post, but hopefully it's helpful for anybody (including non programmers) who wants to:
|
Thank you for this. I read through it a few times (and likely will a few more), and it is very helpful. I think I have a better understanding of how these particular tests work, and can hopefully spend some time going through them in more detail in the days ahead.
I get that, and I don't mean to try and push you into doing more detailed documentation at this stage (as I realize you've got a lot on your plate, and documentation probably isn't top priority right now). However, it would help me a lot if I could understand the big picture just a little better, as I'm struggling to grasp how the high-level pieces of logic and data work together (or are intended to work together once this is all in place): I think of the new RP2 processing as being made up of 3 sequential steps: step 1 ("universal application"), step 2 ("global allocation"), and step 3 ("per-wallet application"). Is the per-wallet transfer logic (tested by the above tests) only going to be used in step 3? (I think so.) Can you tell me what the "universal queue" looks like? Can you tell me where I'd find that in the source? Is it just a list of unused basis lots, or is it an ordered list of all And how do you know (in step 1) how much of each coin is in each wallet at any point in time? (That is known based on the Would you mind briefly describing the big picture of how this processing will occur through these 3 steps, and how the main data structures will be used? Assume you have as input a set of RP2 spreadsheets with data from 2022 through 2026. And assume the user has configured to use LIFO for step 1 ("universal application"), HIFO for step 2 ("global allocation"), and FIFO for step 3 ("per-wallet application"). As a starting point I'll try to describe what I understand (or think I understand): In step 1 you'd want to apply cost basis to sell transactions from the universal queue using LIFO. I assume this is all existing logic and data structures (i.e. no artificial Step 2 ("global allocation") -- which would be processed as at end of day 12/31/24 (for U.S. taxpayers) -- is the most vague for me. You'd use HIFO to allocate the unused basis to the coins in each wallet. But how? Does this involve creating artificial And then in step 3 ("per-wallet application") I think I have a high level understanding of how that would work going forward. Obviously the accounting method would be FIFO (in this example), and it would be applied from starting point of what is output by step 2, right? If you could help me put these various pieces together in my mind -- without needing to write a book -- I'd really appreciate it. Thanks for putting up with all of my messages. I feel like I've probably been quite exasperating at times. (Sorry about that!) I do feel like I'm getting a better understanding, though, and I'm hoping to be able to put it to use in the days ahead (hopefully without needing to bother you as much). Thanks again! |
Answers inline below.
Yes, I understand. Current priority is finishing global allocation, then updating the tax engine to support the new per-wallet model. I'll try to update the design doc but not sure when I'll be able to. Thanks for the words of appreciation. I'm also grateful for your engagement: it was very helpful in shaping my understanding of transfer analysis (you found a couple of issues in the way I initially understood it).
Yes, with the extra complication that it will be possible to also go back from 3 to 1: the US plug-in won't support that but the RP2 tax engine will allow other country plug-ins to do so if they want.
Yes. And these tests exercise precisely step 3, which receives the universal data that is output by global allocation and transform it into a set of per-wallet data.
It's InputData in the code.
All the accounting is managed by the accounting engine which fractions and pairs taxable events and acquired lots, keeping track of partial amounts as well.
That's in the Wiki design document (needs updating). Unfortunately it can't be done briefly.
Yes: the entry point for tax computation is the tax engine. There are artificial transactions in the current (universal-application-only) version of RP2, but they are used to model other tax situations.
Yes. You're left with a partial amount for each acquired lot, containing the amount left in it (an InTransaction may be matched only partially to a tax event by the accounting engine: so the remainder needs to be tracked at the in-transaction level.
I have a first unfinished version of global allocation that is able to process correctly a simple test. The rough idea is:
Yes.
No worries! |
Thank you! This is great! I'll take some time to dig deeper in the coming days. |
Hi @eprbell. I spent some time today trying to get my mind around the tests, but didn't get too far.
Despite digging around in the code some, and in the documentation (including the Per-Wallet-Application design), I left almost as confused as when I started. I'll try to explain. First of all, I'll restate the example assumptions from earlier:
And let's assume, also, that we're dealing with a U.S. example where step 2 runs EOY 2024. Step 1 will process the universal queue using LIFO (up to EOY 2024). Step 2 will introduce artificial transactions (with zero fees) to essentially allocated basis to different wallets using HIFO. Step 3 will then run "transfer analysis" to segregate the universal input data received (as output from step 2) into individual queues for each wallet. Now we haven't talked about a "Step 4", but is there a step 4 that then loops through each wallet (the output from step 3) and essentially does the tax calculations of step 1 -- but for each wallet individually? Am I understanding that correctly so far? If I am understanding correctly, then let's consider the first test:
It only includes an integer day for each transaction, and the assumption is that timestamp will be derived from a particular starting date, right? That being the case, then I think it's safe to assume that "year" is irrelevant for these tests, right? But let's put it into the "real world" for a moment. In this U.S. example we're talking about, where step 2 ("global allocation") runs EOY 2024, should we assume that the input transactions in these tests are (a) all <= 2024, (b) all >= 2025, or (c) include both <= 2024 and >= 2025 transactions? Stated another way, do these test transactions include transactions that will been processed in step 1 ("universal application") as well as transactions that will be processed in steps 3 (and 4?) under "per-wallet"? That leads me to a follow-up point of confusion: The
So that means it can only use one account method, rather than (up to) one per tax year, right? I'm not sure how to phrase this as a question, but (if I'm understanding correctly) it seems to me that What am I missing? Hopefully you can help me get some clarity on the above. Thanks! |
Answers inline below.
Yes, this is all correct.
Yes, for the purposes of unit tests the starting date is 2024-01-01.
All unit tests occur within one single year (starting from the starting date mentioned above).
These are transfer analysis unit tests (corresponding to step 3 only in your list) and they only test transfer analysis: they all occur within one year and they receive a universal application set of transactions that they transform in its respective per-wallet application set of transactions. So you can ignore the other steps for now.
No, the transfer semantics is passed in to the constructor: the caller will pass it in from a data structure that maps years to transfer semantics (similar to what already happens for accounting methods). But this will be implemented later when we put together all the components. For now we're still building individual components in a bottom-up manner: we started with transfer analysis, which comes with its set of unit tests (and that's all there is so far). Next up is global allocation (I'll be uploading it soon), which will have its own set of unit tests, etc. When we will have all the components will be able to glue everything together and also write end-to-end tests, which will be able to show the full flow.
I think the main confusion is that you're expecting end-to-end tests (including all steps from 1 to 4), whereas only transfer analysis unit tests are available so far. The way to approach reading these unit tests is to ask: if I were to feed this set of universal-application transactions and transfer semantics to the transfer analysis algorithm, what sets of per-wallet transactions do I expect to get? All the other questions you're asking will become relevant when we have more components and some glue holding them together. Hope this helps. |
Thank you for the reply, @eprbell. I appreciate your ongoing willingness to engage and put up with all of my questions. Thanks for clarifying about the test being very constrained in scope, and not "end-to-end". Makes sense. Despite my efforts to limit the scope of my study to the tests, my brain insists on trying to understand the bigger picture (at least at a high level). I can't help it. Please bear with me. To that end I re-read this entire thread today, and again reviewed the per-wallet design. Don't understand the design entirely, but overall things are starting to become more clear. From the design:
So you're basically trying to maintain a data structure that, at any point in time, supports both the "universal" picture as well as the "per-wallet" picture, so you can easily switch back and forth? Is that right? From earlier in this thread:
Is it correct that step 3 ("transfer analysis") essentially processes the If so, I'm feeling some heartburn about this. (It certainly could be lack of understanding on my part.) Since the steps are executed in sequence, does that mean that the cost basis that is to be moved between wallets for each IMO, transactions must be processed in strict date order, with Thoughts? Am I completely misunderstanding this? Also, I think "transfer semantics" and "accounting method" should always be the same for a given tax year, right? From a tax point of view, is there ever a reason they could be different? Thanks! |
No problem: the reason I'm uploading design and implementation early and publicly is to get early feedback from the community, so it's all good.
Yes, correct.
Step 3 analyzes transfers (intra-transactions) in order to materialize the "to" part of the transfer as artificial in-transactions. This enables step 4 (tax computation) to occur on a wallet by wallet basis: without step 3 we would have missing information to compute taxes per-wallet. Step 4 processes taxable events, not just out transactions and it fractions them and pairs them with their respective acquired lots (also appropriately fractioned). The pairing is done according to the accounting method.
Cost basis doesn't change: when transfer analysis materializes artificial in-transactions, their spot price is the one from the "from" lot (the from lot is picked according to transfer semantics): see https://github.com/eprbell/rp2/blob/per_wallet_application/src/rp2/transfer_analyzer.py#L110. This cost basis doesn't change, even if the artificial in-transaction becomes the "from" lot of another transfer.
Not sure if I understand what you mean. The best way to reason about this would probably be with an example.
Intersting question: I haven't thought about this. My design doesn't impose such a constraint, but perhaps it makes sense. Maybe this is a question for JustinCPA or another tax professional. |
Thanks for the reply, @eprbell.
Sure. Let me give it a shot. Let's look at a simple example from two perspectives. And let's assume FIFO (for both transfer semantics and accounting method) as it's the easiest. Keep in mind I may be misunderstanding the design, and maybe this is a non-issue -- but it's how I see it at the moment. 1) Design Approach First of all, let's look at the example using my understanding of the design approach: Step 3 "transfer analysis" would first go through the transactions in date order and process Step 4 would then process each wallet individually, and when it gets to the 3/1/24 2) Correct approach?? Next, let's look at how I believe it should work. Quoting from earlier:
We don't need to talk about the The next relevant transaction would be the Hopefully it's clear that the two approaches process cost basis differently. In (1) the sell had a total basis of $600 and the coins moved to Kraken had a total basis of (6 x $100) + (1 x $120) = $720. In (2) the sell had a total basis of $500 and the coins moved to Kraken had a total basis of (1 x $100) + (6 x $120) = $820. Assuming I'm understanding the design approach correctly (and maybe I'm not), I don't think it will produce a correct result. IMO, the "correct" approach from a tax perspective would be that all transactions should be processed in date sequence -- or, at least, the end result should be exactly the same as if they were processed in date sequence. (BTW, I think processing in date sequence should apply regardless of accounting method or transfer semantics. In this example we're using FIFO, as it's easiest, but even if we were using HIFO, I believe transactions should still be processed in strict date order.) Thoughts?
I can ask Justin this. |
So it seems that, except for "specific identification" (which RP2 won't support), transfer semantics and accounting method would be the same. (He doesn't specifically say there could never be a case were they might be different, but probably the RP2 default should be that they are the same, right?) https://www.reddit.com/r/CryptoTax/comments/1hk31yd/comment/mbbrcyx/ |
Thanks for providing the example, @gbtorrance: your second interpretation is correct. I implemented your scenario as a unit test using all 4 transfer semantics, so that this discussion becomes part of continuous integration testing. See: Let's look at the FIFO test, since that's the semantics you picked in the example. Notice the following:
I hope this sheds some light. |
I appreciate you asking Justin and reporting back! This sounds reasonable to me and avoids complications that, while technically correct, aren't too user-friendly. I'm OK with keeping accounting method and transfer semantics the same within a given year (at least as a start).
|
Thanks for the response, @eprbell. I'm happy that the new test is confirming my understanding of how it should work. However, it does leave me even more confused about the high-level approach, because I was convinced it wasn't working like that (in the design you described). I think it comes back to this:
I'm obviously just not getting it :-( You say, "Step 3 analyzes transfers (intra-transactions) in order to materialize the 'to' part of the transfer as artificial in-transactions". I understood this to mean that step 3 would also determine the cost basis to be associated with the new artificial in-transactions (i.e. the basis to be transferred to the new wallets). This would be done by selecting available basis from earlier in-transactions. But since step 3 is only about transfers (i.e. intra-transactions) -- as I understood it! -- the impact of out-transactions would be ignored (during step 3), resulting in the scenario described in the first example above ("Design approach") and quoted below:
But based on what you're saying now (and the new test), it is not working in the way I thought it was. So I remain confused. You do also say in the quote above, "Step 4 processes taxable events, not just out transactions". Does that mean step 4 also processes intra-transactions, and determines cost basis for those? (I thought intra-transactions, and associated cost basis, were processed exclusively in step 3, not step 4.) Anyway, maybe I just need to wait until this is implemented and documented more completely to properly understand the big picture. I don't want to keep wasting your time "going around in circles" on this, as it's probably not adding value and is just distracting you from the implementation. Thanks again for the reply. |
Sounds good. Thanks! |
Your intuition is correct: steps 3 and 4 will probably need some kind of integration. It's good that you brought that up, because that's an important part of how the system is supposed to work. However I think the disconnect comes from the fact that I'm building the new system bottom-up (componentized view), whereas you're thinking top-down (full system view). So I'm focusing on the algorithms of the various individual components first (i.e. transfer analysis and global allocation). Once that is done I'll start putting these components together and write integration code (which will include making sure the funds that are moved around in step 3 are accounted for correctly in step 4). I haven't spent a lot of time thinking about that problem yet, but one possible solution could be to do the transfer analysis keeping track of both taxable events and transfers (as we do today), but then return the taxable events partial amounts at the end of transfer analysis (this way when step 4 starts it has the amounts for the taxable events where it expects them to be). Another option would be to integrate steps 3 and 4. There are probably other options to evaluate but we'll cross that bridge when we get there. At this time the job is to ensure transfer analysis and global allocation work well as components. P.S.: to clarify further, step 3 (transfer analysis) focuses on transfers but it must account for taxable events as well in chronological order because outgoing funds affect which lots are subsequently transferred.
|
Thanks for the reply, @eprbell.
Yes, I think that's right. FWIW, I am very detail oriented, but for that reason -- maybe counter-intuitively -- I feel I need to understand the "detail" of the full system view before I can really drill down into the "detail" of the component view. (It is what it is.)
I think this gets to the heart of my concern. Though I can't claim to understand all of what you wrote in the earlier paragraph, as long as step 3 (transfer analysis) is factoring in both intra and out transactions, chronologically, when determining how cost basis is allocated to intra transactions for transfers, then hopefully it will be OK (as confirmed by the new test). From an outsider perspective, it does kinda feel like step 3 and step 4 are potentially duplicating effort, but maybe I'm just not understanding the design you have in mind. I think we agree that "transfer semantics" / "accounting method" should be the same for a given tax year, in both steps 3 and 4, right? (I guess what I'm getting at is, if there is ever a "re-processing" of transactions in step 4 that were processed in step 3 for transfer analysis, the same transfer semantics / accounting method should be used when re-processing.) FWIW, I look forward to being able to look at things from the "end to end" perspective. At the moment I feel I'm just seeing individual pieces and not grasping how they fit together. But I understand that will need to wait. One step at a time :-) Thanks again! |
That's good and your feedback already proved valuable in multiple cases.
We'll see when we get to that part: there are multiple ways to approach that problem so it's difficult to predict how it will look like in advance.
Again, I'll stay vague here until I get a chance to spend some time on that particular problem (integration of 3 and 4). It depends on what solution we will pick. But regardless of that, I agree that transfer analysis and accounting method should probably not change within the same year to avoid unnecessary complications for the user.
Yep, we'll get there! |
Sounds good. Thanks! |
Discussion on this topic started here: #134
The text was updated successfully, but these errors were encountered: