Skip to content

Commit

Permalink
Allow staking income to be negative via Staking-typed out transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
eprbell committed Jun 9, 2024
1 parent 80b58cb commit 87f17ec
Show file tree
Hide file tree
Showing 18 changed files with 32 additions and 22 deletions.
4 changes: 2 additions & 2 deletions docs/input_files.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The input spreadsheet is in .ods format and contains one or more sheets. Each sh
* the second row is the table header: the meaning of each header cell is defined in the **in_header** section of the config file
* the following rows describe one **IN**-transaction each
* the last row contains the **TABLE END** keyword in column A
* The **OUT**-table (optional) contains transactions describing crypto flowing out (e.g. donate, fee, gift, sell):
* The **OUT**-table (optional) contains transactions describing crypto flowing out (e.g. donate, fee, gift, sell, staking):
* the first row contains the **OUT** keyword in column A
* the second row is the table header: the meaning of each header cell is defined in the **out_header** section of the config file
* the following rows describe one **OUT**-transaction each
Expand Down Expand Up @@ -74,7 +74,7 @@ Here follows an example of an input spreadsheet with 2 sheets (one for BTC and o
* **asset**: which cryptocurrency was transacted (e.g. BTC, ETH, etc.). It must match the name of the spreadsheet and one of the values in the **assets** section of the config file.
* **exchange**: exchange or wallet on which the transaction occurred (e.g. Coinbase, Coinbase Pro, BlockFi, etc.). It must match one of the values in the **exchanges** section of the config file.
* **holder**: exchange account or wallet owner. It must match one of the values in the **holders** section of the config file.
* **transaction_type**: DONATE, FEE, GIFT or SELL.
* **transaction_type**: DONATE, FEE, GIFT, SELL or STAKING (this is useful to represent staking losses, due to node being offline, etc.).
* **spot_price**: value of 1 unit of the given cryptocurrency at the time the transaction occurred.
* **crypto_out_no_fee**: how much of the given cryptocurrency was sold or sent with the transaction (excluding fees).
* **crypto_fee**: crypto value of the transaction fees.
Expand Down
4 changes: 4 additions & 0 deletions docs/user_faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
* [How to Handle Crypto Interest?](#how-to-handle-crypto-interest)
* [How to Handle Income from Mining?](#how-to-handle-income-from-mining)
* [How to Handle Income from Staking?](#how-to-handle-income-from-staking)
* [How to Handle Losses from Staking?](#how-to-handle-losses-from-staking)
* [How to Handle Income from Crypto Wages?](#how-to-handle-income-from-crypto-wages)
* [How to Handle Crypto Rewards?](#how-to-handle-crypto-rewards)
* [How to Handle Fee-only DeFi Transactions?](#how-to-handle-fee-only-defi-transactions)
Expand Down Expand Up @@ -259,6 +260,9 @@ Use an in-transaction and mark the transaction type as MINING. RP2 will collect
### How to Handle Income from Staking?
Use an in-transaction and mark the transaction type as STAKING. RP2 will collect gain/loss computations for all such transactions in a tab in the tax_report_us output. Also read question on [which tax forms to file](#which-crypto-tax-forms-to-file) and see the [input files](input_files.md) section of the documentation for format details.

### How to Handle Losses from Staking?
This is useful to capture situations where the protocol penalizes users (e.g. when their node is offline for too long, etc.). Use an out-transaction and mark the transaction type as STAKING. RP2 will collect gain/loss computations for all such transactions in a tab in the tax_report_us output. Also read question on [which tax forms to file](#which-crypto-tax-forms-to-file) and see the [input files](input_files.md) section of the documentation for format details.

### How to Handle Income from Crypto Wages?
Use an in-transaction and mark the transaction type as WAGES. RP2 will collect gain/loss computations for all such transactions in a tab in the tax_report_us output. Also read question on [which tax forms to file](#which-crypto-tax-forms-to-file) and see the [input files](input_files.md) section of the documentation for format details.

Expand Down
Binary file modified input/golden/test_data4_es_es_fifo_rp2_full_report.ods
Binary file not shown.
Binary file modified input/golden/test_data4_fifo_rp2_full_report.ods
Binary file not shown.
Binary file modified input/golden/test_data4_fifo_tax_report_us.ods
Binary file not shown.
Binary file modified input/golden/test_data4_hifo_rp2_full_report.ods
Binary file not shown.
Binary file modified input/golden/test_data4_hifo_tax_report_us.ods
Binary file not shown.
Binary file modified input/golden/test_data4_jp_en_fifo_rp2_full_report.ods
Binary file not shown.
Binary file modified input/golden/test_data4_jp_en_fifo_tax_report_jp.ods
Binary file not shown.
Binary file modified input/golden/test_data4_lifo_rp2_full_report.ods
Binary file not shown.
Binary file modified input/golden/test_data4_lifo_tax_report_us.ods
Binary file not shown.
Binary file modified input/test_data4.ods
Binary file not shown.
3 changes: 3 additions & 0 deletions src/rp2/abstract_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,6 @@ def fiat_deduction(self) -> RP2Decimal:

def is_taxable(self) -> bool:
raise NotImplementedError("Abstract method")

def is_earning(self) -> bool:
raise NotImplementedError("Abstract method")
20 changes: 10 additions & 10 deletions src/rp2/gain_loss.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def __init__(

self.__crypto_amount: RP2Decimal = configuration.type_check_positive_decimal("crypto_amount", crypto_amount, non_zero=True)

if not taxable_event.transaction_type.is_earn_type():
if not taxable_event.is_earning():
if acquired_lot is None:
raise RP2TypeError("acquired_lot must not be None for non-earn-typed taxable_events")
InTransaction.type_check("acquired_lot", acquired_lot)
Expand Down Expand Up @@ -171,18 +171,18 @@ def taxable_event_fraction_percentage(self) -> RP2Decimal:
@property
def acquired_lot_fraction_percentage(self) -> RP2Decimal:
if not self.acquired_lot:
# Earn-typed taxable events don't have a acquired_lot
if not self.taxable_event.transaction_type.is_earn_type():
raise RP2RuntimeError("Internal error: acquired lot is None but taxable event is not earn-typed")
# Earning events don't have an acquired_lot
if not self.taxable_event.is_earning():
raise RP2RuntimeError("Internal error: acquired lot is None but taxable event is not an earning")
return ZERO
return self.crypto_amount / self.acquired_lot.crypto_balance_change

@property
def fiat_cost_basis(self) -> RP2Decimal:
if not self.acquired_lot:
# Earn-typed taxable events don't have a acquired_lot and their cost basis is 0
if not self.taxable_event.transaction_type.is_earn_type():
raise RP2RuntimeError("Internal error: acquired lot is None but taxable event is not earn-typed")
# Earning events don't have an acquired_lot and their cost basis is 0
if not self.taxable_event.is_earning():
raise RP2RuntimeError("Internal error: acquired lot is None but taxable event is not an earning")
return ZERO
# The cost basis is fiat_in + fee (as explained in https://www.irs.gov/publications/p544 and
# https://taxbit.com/cryptocurrency-tax-guide).
Expand All @@ -196,8 +196,8 @@ def fiat_gain(self) -> RP2Decimal:

def is_long_term_capital_gains(self) -> bool:
if not self.acquired_lot:
# Earn-typed taxable events don't have a acquired lot and are always considered short term capital gains
if not self.taxable_event.transaction_type.is_earn_type():
raise RP2RuntimeError("Internal error: acquired lot is None but taxable event is not earn-typed")
# Earning events don't have an acquired lot and are always considered short term capital gains
if not self.taxable_event.is_earning():
raise RP2RuntimeError("Internal error: acquired lot is None but taxable event is not an earning")
return False
return (self.taxable_event.timestamp - self.acquired_lot.timestamp).days >= self.configuration.country.get_long_term_capital_gain_period()
11 changes: 4 additions & 7 deletions src/rp2/in_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,7 @@ def __init__(

self.__exchange: str = configuration.type_check_exchange("exchange", exchange)
self.__holder: str = configuration.type_check_holder("holder", holder)
self.__crypto_in: RP2Decimal
if self.transaction_type == TransactionType.STAKING:
# Staking income can be negative: in certain cases the protocol can remove from the stash rather than
# add to it (e.g. if the node stays offline too long).
self.__crypto_in = configuration.type_check_decimal("crypto_in", crypto_in)
else:
self.__crypto_in = configuration.type_check_positive_decimal("crypto_in", crypto_in, non_zero=True)
self.__crypto_in: RP2Decimal = configuration.type_check_positive_decimal("crypto_in", crypto_in, non_zero=True)
self.__crypto_fee: RP2Decimal = configuration.type_check_positive_decimal("crypto_fee", crypto_fee) if crypto_fee else ZERO
self.__fiat_fee: RP2Decimal = configuration.type_check_positive_decimal("fiat_fee", fiat_fee) if fiat_fee else ZERO

Expand Down Expand Up @@ -212,3 +206,6 @@ def is_crypto_fee_defined(self) -> bool:

def is_taxable(self) -> bool:
return self.transaction_type.is_earn_type()

def is_earning(self) -> bool:
return self.is_taxable()
3 changes: 3 additions & 0 deletions src/rp2/intra_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,6 @@ def fiat_balance_change(self) -> RP2Decimal:

def is_taxable(self) -> bool:
return self.fiat_fee > ZERO

def is_earning(self) -> bool:
return False
5 changes: 4 additions & 1 deletion src/rp2/out_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def __init__(
self.__fiat_fee = configuration.type_check_positive_decimal("fiat_fee", fiat_fee)
self.__fiat_out_with_fee = self.__fiat_out_no_fee + self.__fiat_fee

if self.transaction_type not in (TransactionType.DONATE, TransactionType.FEE, TransactionType.GIFT, TransactionType.SELL):
if self.transaction_type not in (TransactionType.DONATE, TransactionType.FEE, TransactionType.GIFT, TransactionType.SELL, TransactionType.STAKING):
raise RP2ValueError(
f"{self.asset} {type(self).__name__} ({self.timestamp}, id {self.internal_id}): invalid transaction type {self.transaction_type}"
)
Expand Down Expand Up @@ -213,3 +213,6 @@ def fiat_balance_change(self) -> RP2Decimal:

def is_taxable(self) -> bool:
return True

def is_earning(self) -> bool:
return False
4 changes: 2 additions & 2 deletions src/rp2/tax_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ def _create_unfiltered_gain_and_loss_set(
Configuration.type_check_positive_decimal("taxable_event_amount", taxable_event_amount)
Configuration.type_check_positive_decimal("acquired_lot_amount", acquired_lot_amount)

if taxable_event.transaction_type.is_earn_type():
# Handle earn-typed transactions first: they have no acquired-lot
if taxable_event.is_earning():
# Handle earnings first: they have no acquired-lot
gain_loss = GainLoss(configuration, taxable_event_amount, taxable_event, None)
LOGGER.debug(
"tax_engine: taxable is earn: %s / %s + %s = %s: %s",
Expand Down

0 comments on commit 87f17ec

Please sign in to comment.