Repository: rotki/rotki

Issue:
Allow to filter trades by ignored/not ignored
## Abstract

Allow users to filter ignored/un ignored trades in the trade page

## Motivation

This is useful when doing changes to the PnL report to check what is ignored and what is not
 


Discussion:

Resolving patch:
diff --git a/rotkehlchen/api/v1/resources.py b/rotkehlchen/api/v1/resources.py
--- a/rotkehlchen/api/v1/resources.py
+++ b/rotkehlchen/api/v1/resources.py
@@ -912,10 +912,8 @@ def delete(self, async_query: bool, ids: list[int]) -> Response:
 class TradesResource(BaseMethodView):
 
     def make_get_schema(self) -> TradesQuerySchema:
-        with self.rest_api.rotkehlchen.data.db.conn.read_ctx() as cursor:
-            settings = self.rest_api.rotkehlchen.data.db.get_settings(cursor)
         return TradesQuerySchema(
-            treat_eth2_as_eth=settings.treat_eth2_as_eth,
+            db=self.rest_api.rotkehlchen.data.db,
         )
 
     put_schema = TradeSchema()
diff --git a/rotkehlchen/api/v1/schemas.py b/rotkehlchen/api/v1/schemas.py
--- a/rotkehlchen/api/v1/schemas.py
+++ b/rotkehlchen/api/v1/schemas.py
@@ -413,12 +413,9 @@ class TradesQuerySchema(
     location = LocationField(load_default=None)
     include_ignored_trades = fields.Boolean(load_default=True)
 
-    def __init__(
-            self,
-            treat_eth2_as_eth: bool,
-    ) -> None:
+    def __init__(self, db: 'DBHandler') -> None:
         super().__init__()
-        self.treat_eth2_as_eth = treat_eth2_as_eth
+        self.db = db
 
     @validates_schema
     def validate_trades_query_schema(
@@ -456,14 +453,21 @@ def make_trades_query(
     ) -> dict[str, Any]:
         base_assets: Optional[tuple['Asset', ...]] = None
         quote_assets: Optional[tuple['Asset', ...]] = None
+        trades_idx_to_ignore = None
+        with self.db.conn.read_ctx() as cursor:
+            treat_eth2_as_eth = self.db.get_settings(cursor).treat_eth2_as_eth
+            if data['include_ignored_trades'] is not True:
+                ignored_action_ids = self.db.get_ignored_action_ids(cursor, ActionType.TRADE)
+                trades_idx_to_ignore = ignored_action_ids[ActionType.TRADE]
+
         if data['base_asset'] is not None:
             base_assets = (data['base_asset'],)
         if data['quote_asset'] is not None:
             quote_assets = (data['quote_asset'],)
 
-        if self.treat_eth2_as_eth is True and data['base_asset'] == A_ETH:
+        if treat_eth2_as_eth is True and data['base_asset'] == A_ETH:
             base_assets = (A_ETH, A_ETH2)
-        elif self.treat_eth2_as_eth is True and data['quote_asset'] == A_ETH:
+        elif treat_eth2_as_eth is True and data['quote_asset'] == A_ETH:
             quote_assets = (A_ETH, A_ETH2)
 
         filter_query = TradesFilterQuery.make(
@@ -476,6 +480,7 @@ def make_trades_query(
             quote_assets=quote_assets,
             trade_type=[data['trade_type']] if data['trade_type'] is not None else None,
             location=data['location'],
+            trades_idx_to_ignore=trades_idx_to_ignore,
         )
 
         return {
diff --git a/rotkehlchen/db/filtering.py b/rotkehlchen/db/filtering.py
--- a/rotkehlchen/db/filtering.py
+++ b/rotkehlchen/db/filtering.py
@@ -1,5 +1,5 @@
 import logging
-from collections.abc import Iterable
+from collections.abc import Collection, Iterable
 from dataclasses import dataclass
 from typing import Any, Generic, Literal, NamedTuple, Optional, TypeVar, Union, cast
 
@@ -65,7 +65,7 @@ def prepare(self) -> str:
 class DBFilter():
     and_op: bool
 
-    def prepare(self) -> tuple[list[str], list[Any]]:
+    def prepare(self) -> tuple[list[str], Collection[Any]]:
         raise NotImplementedError('prepare should be implemented by subclasses')
 
 
@@ -77,7 +77,7 @@ class DBNestedFilter(DBFilter):
 
     def prepare(self) -> tuple[list[str], list[Any]]:
         filterstrings = []
-        bindings = []
+        bindings: list[Any] = []
         for single_filter in self.filters:
             filters, single_bindings = single_filter.prepare()
             if len(filters) == 0:
@@ -297,7 +297,7 @@ def prepare(
             with_order: bool = True,
     ) -> tuple[str, list[Any]]:
         query_parts = []
-        bindings = []
+        bindings: list[Any] = []
         filterstrings = []
 
         if self.join_clause is not None:
@@ -513,9 +513,9 @@ def prepare(self) -> tuple[list[str], list[Any]]:
 @dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
 class DBIgnoreValuesFilter(DBFilter):
     column: str
-    values: list[Any]
+    values: Collection[Any]
 
-    def prepare(self) -> tuple[list[str], list[Any]]:
+    def prepare(self) -> tuple[list[str], Collection[Any]]:
         if len(self.values) == 0:
             return [], []
         return [f'{self.column} NOT IN ({", ".join(["?"] * len(self.values))})'], self.values
@@ -603,6 +603,7 @@ def make(
             quote_assets: Optional[tuple[Asset, ...]] = None,
             trade_type: Optional[list[TradeType]] = None,
             location: Optional[Location] = None,
+            trades_idx_to_ignore: Optional[set[str]] = None,
     ) -> 'TradesFilterQuery':
         if order_by_rules is None:
             order_by_rules = [('timestamp', True)]
@@ -646,6 +647,12 @@ def make(
         if location is not None:
             filter_query.location_filter = DBLocationFilter(and_op=True, location=location)
             filters.append(filter_query.location_filter)
+        if trades_idx_to_ignore is not None:
+            filters.append(DBIgnoreValuesFilter(
+                and_op=True,
+                column='id',
+                values=trades_idx_to_ignore,
+            ))
 
         filter_query.timestamp_filter = DBTimestampFilter(
             and_op=True,
@@ -1286,7 +1293,7 @@ class MultiTableFilterQuery():
     def prepare(self, target_table_name: str) -> tuple[str, list[Any]]:
         """Generates query and bindings for table `target_table_name`."""
         query_parts = []
-        bindings = []
+        bindings: list[Any] = []
         filterstrings = []
 
         for (single_filter, table_name) in self.filters:


Test patch:
diff --git a/rotkehlchen/tests/api/test_trades.py b/rotkehlchen/tests/api/test_trades.py
--- a/rotkehlchen/tests/api/test_trades.py
+++ b/rotkehlchen/tests/api/test_trades.py
@@ -18,6 +18,7 @@
     assert_proper_response_with_result,
     assert_simple_ok_response,
 )
+from rotkehlchen.tests.utils.factories import make_random_trades
 from rotkehlchen.tests.utils.history import (
     assert_binance_trades_result,
     assert_poloniex_trades_result,
@@ -1150,43 +1151,7 @@ def test_query_trades_associated_locations(rotkehlchen_api_server_with_exchanges
 def test_ignoring_trades(rotkehlchen_api_server):
     """Check that ignoring trades filter works as expected."""
     rotki = rotkehlchen_api_server.rest_api.rotkehlchen
-    trades = [Trade(
-        timestamp=Timestamp(1596429934),
-        location=Location.EXTERNAL,
-        base_asset=A_WETH,
-        quote_asset=A_EUR,
-        trade_type=TradeType.BUY,
-        amount=AssetAmount(FVal('1')),
-        rate=Price(FVal('320')),
-        fee=Fee(ZERO),
-        fee_currency=A_EUR,
-        link='',
-        notes='',
-    ), Trade(
-        timestamp=Timestamp(1596429934),
-        location=Location.KRAKEN,
-        base_asset=A_WETH,
-        quote_asset=A_EUR,
-        trade_type=TradeType.BUY,
-        amount=AssetAmount(FVal('1')),
-        rate=Price(FVal('320')),
-        fee=Fee(ZERO),
-        fee_currency=A_EUR,
-        link='',
-        notes='',
-    ), Trade(
-        timestamp=Timestamp(1596429934),
-        location=Location.BISQ,
-        base_asset=A_WETH,
-        quote_asset=A_EUR,
-        trade_type=TradeType.BUY,
-        amount=AssetAmount(FVal('1')),
-        rate=Price(FVal('320')),
-        fee=Fee(ZERO),
-        fee_currency=A_EUR,
-        link='',
-        notes='',
-    )]
+    trades = make_random_trades(3)
     trade_to_ignore = trades[0].identifier
 
     # populate db with trades
@@ -1225,3 +1190,59 @@ def test_ignoring_trades(rotkehlchen_api_server):
     result = assert_proper_response_with_result(response)
     assert len(result['entries']) == len(trades) - 1
     assert trade_to_ignore not in {entry['entry']['trade_id'] for entry in result['entries']}
+
+
+def test_ignoring_trades_with_pagination(rotkehlchen_api_server):
+    """Check that pagination is respected when `include_ignored_trades` is True."""
+    rotki = rotkehlchen_api_server.rest_api.rotkehlchen
+    trades = make_random_trades(10)
+    trades_to_ignore = [trades[0].identifier, trades[1].identifier]
+
+    # populate db with trades
+    with rotki.data.db.user_write() as cursor:
+        rotki.data.db.add_trades(cursor, trades)
+
+    # ignore two trades
+    response = requests.put(
+        api_url_for(
+            rotkehlchen_api_server,
+            'ignoredactionsresource',
+        ), json={'action_type': 'trade', 'data': trades_to_ignore},
+    )
+    assert_simple_ok_response(response)
+    with rotki.data.db.conn.read_ctx() as cursor:
+        result = rotki.data.db.get_ignored_action_ids(cursor, None)
+    assert result[ActionType.TRADE] == set(trades_to_ignore)
+
+    # now fetch trades with pagination and `include_ignored_trades` is False
+    response = requests.get(
+        api_url_for(
+            rotkehlchen_api_server,
+            'tradesresource',
+        ), json={
+            'include_ignored_trades': 'False',
+            'limit': 7,
+            'offset': 0,
+        },
+    )
+    result = assert_proper_response_with_result(response)
+    assert len(result['entries']) == 7
+    assert result['entries_found'] == 8
+    assert result['entries_total'] == len(trades)
+    assert all([entry['entry']['trade_id'] not in trades_to_ignore for entry in result['entries']])
+
+    # now fetch trades with pagination and `include_ignored_trades` is True
+    response = requests.get(
+        api_url_for(
+            rotkehlchen_api_server,
+            'tradesresource',
+        ), json={
+            'include_ignored_trades': 'True',
+            'limit': 7,
+            'offset': 0,
+        },
+    )
+    result = assert_proper_response_with_result(response)
+    assert len(result['entries']) == 7
+    assert result['entries_found'] == 10
+    assert result['entries_total'] == len(trades)
diff --git a/rotkehlchen/tests/utils/factories.py b/rotkehlchen/tests/utils/factories.py
--- a/rotkehlchen/tests/utils/factories.py
+++ b/rotkehlchen/tests/utils/factories.py
@@ -10,20 +10,26 @@
 from rotkehlchen.accounting.structures.types import HistoryEventSubType, HistoryEventType
 from rotkehlchen.assets.asset import CryptoAsset, EvmToken
 from rotkehlchen.chain.evm.types import string_to_evm_address
-from rotkehlchen.constants import ONE
+from rotkehlchen.constants import ONE, ZERO
+from rotkehlchen.constants.assets import A_ETH, A_USDC
+from rotkehlchen.exchanges.data_structures import Trade
 from rotkehlchen.fval import FVal
 from rotkehlchen.types import (
     AddressbookEntry,
     ApiKey,
     ApiSecret,
+    AssetAmount,
     ChainID,
     ChecksumEvmAddress,
     EvmTokenKind,
     EvmTransaction,
+    Fee,
     Location,
+    Price,
     SupportedBlockchain,
     Timestamp,
     TimestampMS,
+    TradeType,
     make_evm_tx_hash,
 )
 from rotkehlchen.utils.misc import ts_now
@@ -216,6 +222,29 @@ def make_random_user_notes(num_notes: int) -> list[dict[str, Any]]:
     return notes
 
 
+def make_random_trades(num_trades: int) -> list[Trade]:
+    """Make random trades to be used in tests."""
+    trades = []
+    for idx in range(num_trades):
+        trade_type = random.choice([TradeType.BUY, TradeType.SELL])
+        trades.append(
+            Trade(
+                timestamp=Timestamp(random.randint(100000, 10000000)),
+                trade_type=trade_type,
+                location=Location.EXTERNAL,
+                base_asset=A_ETH,
+                quote_asset=A_USDC,
+                amount=AssetAmount(FVal(idx)),
+                rate=Price(FVal(idx)),
+                fee=Fee(ZERO),
+                fee_currency=A_USDC,
+                link='',
+                notes='',
+            ),
+        )
+    return trades
+
+
 UNIT_BTC_ADDRESS1 = '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2'
 UNIT_BTC_ADDRESS2 = '1CounterpartyXXXXXXXXXXXXXXXUWLpVr'
 UNIT_BTC_ADDRESS3 = '18ddjB7HWTVxzvTbLp1nWvaBxU3U2oTZF2'