Repository: rotki/rotki

Issue:
Selling an asset that will fork prefork time should also reduce its bought amount
## Problem Definition

For assets that will fork, like `ETH` pre-`ETC` fork and `BTC` pre-`BCH` fork at the moment rotkehlchen adds equal amount of the after-fork asset for every buy before the fork happens. Logic is [here](https://github.com/rotkehlchenio/rotkehlchen/blob/3e51f3a3d90982f60fd314338185d714cc2a794d/rotkehlchen/accounting/events.py#L92).

But that can lead to the user having more of the forked asset for selling than in reality since rotkehlchen currently does not also reduce the amount for every sell of the original asset pre fork.

## Task

- [ ] Adjust the prefork acquisition logic to also reduce the forked asset for every sell of the original asset pre-fork.
- [ ] Write tests for this




Discussion:

Resolving patch:
diff --git a/rotkehlchen/accounting/events.py b/rotkehlchen/accounting/events.py
--- a/rotkehlchen/accounting/events.py
+++ b/rotkehlchen/accounting/events.py
@@ -1,6 +1,6 @@
 import logging
 
-from rotkehlchen.constants import BTC_BCH_FORK_TS, ETH_DAO_FORK_TS
+from rotkehlchen.constants import BTC_BCH_FORK_TS, ETH_DAO_FORK_TS, ZERO
 from rotkehlchen.errors import PriceQueryUnknownFromAsset
 from rotkehlchen.fval import FVal
 from rotkehlchen.history import FIAT_CURRENCIES, NoPriceForGivenTimestamp
@@ -89,7 +89,40 @@ def get_rate_in_profit_currency(self, asset, timestamp):
         assert isinstance(rate, (FVal, int))  # TODO Remove. Is temporary assert
         return rate
 
-    def handle_prefork_acquisitions(
+    def reduce_asset_amount(self, asset: Asset, amount: FVal) -> bool:
+        """Searches all buy events for asset and reduces them by amount
+        Returns True if enough buy events to reduce the asset by amount were
+        found and False otherwise.
+        """
+        if asset not in self.events or len(self.events[asset].buys) == 0:
+            return False
+
+        remaining_amount_from_last_buy = -1
+        remaining_amount = amount
+        for idx, buy_event in enumerate(self.events[asset].buys):
+            if remaining_amount < buy_event.amount:
+                stop_index = idx
+                remaining_amount_from_last_buy = buy_event.amount - remaining_amount
+                # stop iterating since we found all buys to satisfy reduction
+                break
+            else:
+                remaining_amount -= buy_event.amount
+                if idx == len(self.events[asset].buys) - 1:
+                    stop_index = idx + 1
+
+        # Otherwise, delete all the used up buys from the list
+        del self.events[asset].buys[:stop_index]
+        # and modify the amount of the buy where we stopped if there is one
+        if remaining_amount_from_last_buy != -1:
+            self.events[asset].buys[0] = self.events[asset].buys[0]._replace(
+                amount=remaining_amount_from_last_buy,
+            )
+        elif remaining_amount != ZERO:
+            return False
+
+        return True
+
+    def handle_prefork_asset_buys(
             self,
             bought_asset,
             bought_amount,
@@ -101,10 +134,6 @@ def handle_prefork_acquisitions(
     ):
         # TODO: Should fee also be taken into account here?
         if bought_asset == 'ETH' and timestamp < ETH_DAO_FORK_TS:
-            log.debug(
-                'Acquiring ETH before the DAO fork provides equal amount of ETC',
-                timestamp=timestamp,
-            )
             self.add_buy(
                 'ETC',
                 bought_amount,
@@ -118,10 +147,6 @@ def handle_prefork_acquisitions(
 
         if bought_asset == 'BTC' and timestamp < BTC_BCH_FORK_TS:
             # Acquiring BTC before the BCH fork provides equal amount of BCH
-            log.debug(
-                'Acquiring BTC before the BCH fork provides equal amount of BCH',
-                timestamp=timestamp,
-            )
             self.add_buy(
                 'BCH',
                 bought_amount,
@@ -133,6 +158,23 @@ def handle_prefork_acquisitions(
                 is_virtual=True,
             )
 
+    def handle_prefork_asset_sells(self, sold_asset, sold_amount, timestamp):
+        if sold_asset == 'ETH' and timestamp < ETH_DAO_FORK_TS:
+            if not self.reduce_asset_amount(asset='ETC', amount=sold_amount):
+                log.critical(
+                    'No documented buy found for ETC (ETH equivalent) before {}'.format(
+                        tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'),
+                    ),
+                )
+
+        if sold_asset == 'BTC' and timestamp < BTC_BCH_FORK_TS:
+            if not self.reduce_asset_amount(asset='BCH', amount=sold_amount):
+                log.critical(
+                    'No documented buy found for BCH (BTC equivalent) before {}'.format(
+                        tsToDate(timestamp, formatstr='%d/%m/%Y %H:%M:%S'),
+                    ),
+                )
+
     def add_buy_and_corresponding_sell(
             self,
             bought_asset,
@@ -222,7 +264,7 @@ def add_buy(
         paid_with_asset_rate = self.get_rate_in_profit_currency(paid_with_asset, timestamp)
         buy_rate = paid_with_asset_rate * trade_rate
 
-        self.handle_prefork_acquisitions(
+        self.handle_prefork_asset_buys(
             bought_asset=bought_asset,
             bought_amount=bought_amount,
             paid_with_asset=paid_with_asset,
@@ -361,6 +403,8 @@ def add_sell(
             ),
         )
 
+        self.handle_prefork_asset_sells(selling_asset, selling_amount, timestamp)
+
         if loan_settlement:
             log.debug(
                 'Loan Settlement Selling Event',
@@ -593,7 +637,7 @@ def search_buys_calculate_profit(
             self.events[selling_asset].buys[0] = self.events[selling_asset].buys[0]._replace(
                 amount=remaining_amount_from_last_buy,
             )
-        elif remaining_sold_amount != FVal(0):
+        elif remaining_sold_amount != ZERO:
             # if we still have sold amount but no buys to satisfy it then we only
             # found buys to partially satisfy the sell
             adjusted_amount = selling_amount - taxfree_amount


Test patch:
diff --git a/rotkehlchen/tests/test_accounting.py b/rotkehlchen/tests/test_accounting.py
--- a/rotkehlchen/tests/test_accounting.py
+++ b/rotkehlchen/tests/test_accounting.py
@@ -158,42 +158,65 @@ def test_selling_crypto_bought_with_crypto(accountant):
     assert accountant.taxable_trade_pl.is_close("73.8764769569")
 
 
-history3 = [
-    {
-        "timestamp": 1446979735,
-        "pair": "ETH_EUR",
-        "type": "buy",
-        "rate": 0.2315893,
-        "cost": 335.804485,
-        "cost_currency": "EUR",
-        "fee": 0,
-        "fee_currency": "ETH",
-        "amount": 1450,
-        "location": "external",
-    }, {
-        'timestamp': 1481979135,
-        'pair': 'ETC_EUR',  # cryptocompare hourly ETC/EUR price: 1.78
-        'type': 'sell',
-        'rate': 1.78,
-        'cost': 979,
-        'cost_currency': 'EUR',
-        'fee': 0.9375,
-        'fee_currency': 'EUR',
-        'amount': 550,
-        'location': 'kraken',
-    },
-]
-
-
-def test_buying_eth_before_daofork(accountant):
+def test_buying_selling_eth_before_daofork(accountant):
+    history3 = [
+        {
+            "timestamp": 1446979735,  # 11/08/2015
+            "pair": "ETH_EUR",
+            "type": "buy",
+            "rate": 0.2315893,
+            "cost": 335.804485,
+            "cost_currency": "EUR",
+            "fee": 0,
+            "fee_currency": "ETH",
+            "amount": 1450,
+            "location": "external",
+        }, {  # selling ETH prefork should also reduce our ETC amount
+            'timestamp': 1461021812,  # 18/04/2016 (taxable)
+            'pair': 'ETH_EUR',  # cryptocompare hourly ETC/EUR price: 7.88
+            'type': 'sell',
+            'rate': 7.88,
+            'cost': 394,
+            'cost_currency': 'EUR',
+            'fee': 0.5215,
+            'fee_currency': 'EUR',
+            'amount': 50,
+            'location': 'kraken',
+        }, {  # sell ETC after the fork
+            'timestamp': 1481979135,  # 17/12/2016
+            'pair': 'ETC_EUR',  # cryptocompare hourly ETC/EUR price: 1.78
+            'type': 'sell',  # not-taxable -- considered bought with ETH so after year
+            'rate': 1.78,
+            'cost': 979,
+            'cost_currency': 'EUR',
+            'fee': 0.9375,
+            'fee_currency': 'EUR',
+            'amount': 550,
+            'location': 'kraken',
+        }, {  # selling ETH after fork should not affect ETC amount
+            'timestamp': 1482138141,  # 19/12/2016
+            'pair': 'ETH_EUR',  # cryptocompare hourly ETC/EUR price: 7.45
+            'type': 'sell',  # not taxable after 1 year
+            'rate': 7.45,
+            'cost': 74.5,
+            'cost_currency': 'EUR',
+            'fee': 0.12,
+            'fee_currency': 'EUR',
+            'amount': 10,
+            'location': 'kraken',
+        },
+    ]
     accounting_history_process(accountant, 1436979735, 1495751688, history3)
-    assert accountant.general_trade_pl.is_close("850.688385")
-    assert accountant.taxable_trade_pl.is_close("0")
+    # make sure that the intermediate ETH sell before the fork reduced our ETC
+    assert accountant.get_calculated_asset_amount('ETC') == FVal(850)
+    assert accountant.get_calculated_asset_amount('ETH') == FVal(1390)
+    assert accountant.general_trade_pl.is_close('1304.651527')
+    assert accountant.taxable_trade_pl.is_close('381.899035')
 
 
-def test_buying_btc_before_bchfork(accountant):
+def test_buying_selling_btc_before_bchfork(accountant):
     history = [{
-        "timestamp": 1491593374,
+        "timestamp": 1491593374,  # 04/07/2017
         "pair": "BTC_EUR",
         "type": "buy",
         "rate": 1128.905,
@@ -203,8 +226,19 @@ def test_buying_btc_before_bchfork(accountant):
         "fee_currency": "EUR",
         "amount": 6.5,
         "location": "external",
-    }, {
-        'timestamp': 1512693374,
+    }, {  # selling BTC prefork should also reduce the BCH equivalent -- taxable
+        "timestamp": 1500595200,  # 21/07/2017
+        "pair": "BTC_EUR",
+        "type": "sell",
+        "rate": 2380.835,
+        "cost": 1190.4175,
+        "cost_currency": "EUR",
+        "fee": 0.15,
+        "fee_currency": "EUR",
+        "amount": 0.5,
+        "location": "external",
+    }, {  # selling BCH after the fork -- taxable
+        'timestamp': 1512693374,  # 08/12/2017
         'pair': 'BCH_EUR',  # cryptocompare hourly BCH/EUR price: 995.935
         'type': 'sell',
         'rate': 995.935,
@@ -214,18 +248,33 @@ def test_buying_btc_before_bchfork(accountant):
         'fee_currency': 'EUR',
         'amount': 2.1,
         'location': 'kraken',
+    }, {
+        'timestamp': 1514937600,  # 03/01/2018
+        'pair': 'BTC_EUR',  # cryptocompare hourly BCH/EUR price: 995.935
+        'type': 'sell',
+        'rate': 12404.88,
+        'cost': 14885.856,
+        'cost_currency': 'EUR',
+        'fee': 0.52,
+        'fee_currency': 'EUR',
+        'amount': 1.2,
+        'location': 'kraken',
     }]
     accounting_history_process(accountant, 1436979735, 1519693374, history)
 
+    amount_BCH = FVal(3.9)
+    amount_BTC = FVal(4.8)
     buys = accountant.events.events['BCH'].buys
     assert len(buys) == 1
-    assert buys[0].amount == FVal(4.4)
+    assert buys[0].amount == amount_BCH
     assert buys[0].timestamp == 1491593374
     assert buys[0].rate == FVal('1128.905')
     assert buys[0].fee_rate.is_close(FVal('0.0846153846154'))
+    assert accountant.get_calculated_asset_amount('BCH') == amount_BCH
+    assert accountant.get_calculated_asset_amount('BTC') == amount_BTC
 
-    assert accountant.general_trade_pl.is_close("-279.67469231")
-    assert accountant.taxable_trade_pl.is_close("-279.67469231")
+    assert accountant.general_trade_pl.is_close("13876.6464615")
+    assert accountant.taxable_trade_pl.is_close("13876.6464615")
 
 
 history5 = history1 + [{
diff --git a/rotkehlchen/tests/test_accounting_events.py b/rotkehlchen/tests/test_accounting_events.py
--- a/rotkehlchen/tests/test_accounting_events.py
+++ b/rotkehlchen/tests/test_accounting_events.py
@@ -22,6 +22,14 @@ def test_search_buys_calculate_profit_after_year(accountant):
             fee_rate=FVal(0.0019),
         ),
     )
+    events['BTC'].buys.append(
+        BuyEvent(
+            amount=FVal(3),  # 25/10/2016
+            timestamp=1477378304,
+            rate=FVal(603.415),
+            fee_rate=FVal(0.0017),
+        ),
+    )
 
     (
         taxable_amount,
@@ -37,7 +45,7 @@ def test_search_buys_calculate_profit_after_year(accountant):
     assert taxfree_bought_cost.is_close(FVal('1340.5005'))
     assert taxable_bought_cost.is_close(FVal('1837.3557'))
 
-    assert (len(accountant.events.events[asset].buys)) == 1, 'first buy should have been used'
+    assert (len(accountant.events.events[asset].buys)) == 2, 'first buy should have been used'
     remaining_amount = accountant.events.events[asset].buys[0].amount
     assert remaining_amount == FVal(12), '3 of 15 should have been consumed'
 
@@ -253,3 +261,93 @@ def test_search_buys_calculate_profit_sell_more_than_bought_after_year(accountan
     assert taxable_bought_cost.is_close(FVal('0'))
 
     assert (len(accountant.events.events[asset].buys)) == 0, 'only buy should have been used'
+
+
+def test_reduce_asset_amount(accountant):
+    asset = 'BTC'
+    events = accountant.events.events
+    events[asset] = Events(list(), list())
+    events[asset].buys.append(
+        BuyEvent(
+            amount=FVal(1),
+            timestamp=1446979735,  # 08/11/2015
+            rate=FVal(268.1),
+            fee_rate=FVal(0.0001),
+        ),
+    )
+    events['BTC'].buys.append(
+        BuyEvent(
+            amount=FVal(1),
+            timestamp=1467378304,  # 31/06/2016
+            rate=FVal(612.45),
+            fee_rate=FVal(0.0019),
+        ),
+    )
+    events['BTC'].buys.append(
+        BuyEvent(
+            amount=FVal(3),  # 25/10/2016
+            timestamp=1477378304,
+            rate=FVal(603.415),
+            fee_rate=FVal(0.0017),
+        ),
+    )
+
+    assert accountant.events.reduce_asset_amount(asset, FVal(1.5))
+    assert (len(accountant.events.events[asset].buys)) == 2, '1 buy should be used'
+    remaining_amount = accountant.events.events[asset].buys[0].amount
+    assert remaining_amount == FVal(0.5), '0.5 of 2nd buy should remain'
+
+
+def test_reduce_asset_amount_exact(accountant):
+    asset = 'BTC'
+    events = accountant.events.events
+    events[asset] = Events(list(), list())
+    events[asset].buys.append(
+        BuyEvent(
+            amount=FVal(1),
+            timestamp=1446979735,  # 08/11/2015
+            rate=FVal(268.1),
+            fee_rate=FVal(0.0001),
+        ),
+    )
+    events['BTC'].buys.append(
+        BuyEvent(
+            amount=FVal(1),
+            timestamp=1467378304,  # 31/06/2016
+            rate=FVal(612.45),
+            fee_rate=FVal(0.0019),
+        ),
+    )
+
+    assert accountant.events.reduce_asset_amount(asset, FVal(2))
+    assert (len(accountant.events.events[asset].buys)) == 0, 'all buys should be used'
+
+
+def test_reduce_asset_amount_not_bought(accountant):
+    asset = 'BTC'
+    assert not accountant.events.reduce_asset_amount(asset, FVal(3))
+
+
+def test_reduce_asset_amount_more_that_bought(accountant):
+    asset = 'BTC'
+    events = accountant.events.events
+    events[asset] = Events(list(), list())
+    events[asset].buys.append(
+        BuyEvent(
+            amount=FVal(1),
+            timestamp=1446979735,  # 08/11/2015
+            rate=FVal(268.1),
+            fee_rate=FVal(0.0001),
+        ),
+    )
+    events['BTC'].buys.append(
+        BuyEvent(
+            amount=FVal(1),
+            timestamp=1467378304,  # 31/06/2016
+            rate=FVal(612.45),
+            fee_rate=FVal(0.0019),
+        ),
+    )
+
+    assert not accountant.events.reduce_asset_amount(asset, FVal(3))
+    assert (len(accountant.events.events[asset].buys)) == 0, 'all buys should be used'