Repository: rotki/rotki

Issue:
Adding historic price with same `timestamp, toAsset, and fromAsset` doesn't replace the price of old entry.
## Problem Definition

For the add historic price endpoint, adding the new entry with `timestamp`, `toAsset`, and `fromAsset` that already exist, doesn't update the price of the entry.

Can we make it, so that the new entry with the same payload (except the price), updates the already existing entry price?
I see that `adding the latest price` uses this mechanism.

(Why not just use the edit endpoint?) Sometimes the front end cannot make sure if the entry already exists or not.
In doing so, we need to fetch all saved historical prices. By implementing the mechanism I said in the paragraph above, we can prevent unnecessary fetching for all saved historical prices.

This is needed in order to finish #5733, so can this be put on `bugfixes`?





Discussion:
Currently it's `INSERT OR IGNORE`. This is why this happens. The idea is if you wanted to edit an entry you just call the edid endpoint. Should not be hard to change though. 

```python
                write_cursor.execute(
                    """INSERT OR IGNORE INTO price_history(
                    from_asset, to_asset, source_type, timestamp, price
                    ) VALUES (?, ?, ?, ?, ?)
                    """,
                    serialized,
                )
```

The code, tests, docs of the functions andthe api would also need changing.

Resolving patch:
diff --git a/rotkehlchen/api/rest.py b/rotkehlchen/api/rest.py
--- a/rotkehlchen/api/rest.py
+++ b/rotkehlchen/api/rest.py
@@ -4012,6 +4012,10 @@ def add_manual_price(  # pylint: disable=no-self-use
         price: Price,
         timestamp: Timestamp,
     ) -> Response:
+        """
+        Add a manual historical price.
+        If the price for the specified pair and timestamp already exists, it is replaced.
+        """
         historical_price = HistoricalPrice(
             from_asset=from_asset,
             to_asset=to_asset,
diff --git a/rotkehlchen/globaldb/handler.py b/rotkehlchen/globaldb/handler.py
--- a/rotkehlchen/globaldb/handler.py
+++ b/rotkehlchen/globaldb/handler.py
@@ -1243,13 +1243,15 @@ def add_historical_prices(entries: list['HistoricalPrice']) -> None:
     def add_single_historical_price(entry: HistoricalPrice) -> bool:
         """
         Adds the given historical price entries in the DB.
-        Returns True if the operation succeeded and False otherwise
+        Returns True if the operation succeeded and False otherwise.
+        If the price for the specified asset pair, oracle type and timestamp already exists,
+        it is replaced.
         """
         try:
             with GlobalDBHandler().conn.write_ctx() as write_cursor:
                 serialized = entry.serialize_for_db()
                 write_cursor.execute(
-                    """INSERT OR IGNORE INTO price_history(
+                    """INSERT OR REPLACE INTO price_history(
                     from_asset, to_asset, source_type, timestamp, price
                     ) VALUES (?, ?, ?, ?, ?)
                     """,


Test patch:
diff --git a/rotkehlchen/tests/api/test_historical_assets_price.py b/rotkehlchen/tests/api/test_historical_assets_price.py
--- a/rotkehlchen/tests/api/test_historical_assets_price.py
+++ b/rotkehlchen/tests/api/test_historical_assets_price.py
@@ -77,7 +77,11 @@ def test_get_historical_assets_price(rotkehlchen_api_server):
     assert result['target_asset'] == 'USD'
 
 
-def _assert_expected_prices(data: list[dict[str, Any]], after_deletion: bool) -> None:
+def _assert_expected_prices(
+        data: list[dict[str, Any]],
+        after_deletion: bool,
+        crv_1611166340_price: str = '1.40',
+) -> None:
     assert len(data) == 2 if after_deletion else 3
 
     expected_data = []
@@ -93,7 +97,7 @@ def _assert_expected_prices(data: list[dict[str, Any]], after_deletion: bool) ->
         'from_asset': A_CRV.identifier,
         'to_asset': 'USD',
         'timestamp': 1611166340,
-        'price': '1.40',
+        'price': crv_1611166340_price,
     }, {
         'from_asset': A_CRV.identifier,
         'to_asset': 'USD',
@@ -210,7 +214,7 @@ def test_manual_historical_price(rotkehlchen_api_server, globaldb):
             'from_asset': A_CRV.identifier,
             'to_asset': 'USD',
             'timestamp': 1611166340,
-            'price': '1.40',
+            'price': '1.50',
         },
     )
     # Try to retrieve the assets price
@@ -224,6 +228,30 @@ def test_manual_historical_price(rotkehlchen_api_server, globaldb):
         },
     )
     data = assert_proper_response_with_result(response)
+    _assert_expected_prices(data, after_deletion=False, crv_1611166340_price='1.50')
+    # Check that calling PUT for an existing price replaces the price
+    response = requests.put(
+        api_url_for(
+            rotkehlchen_api_server,
+            'historicalassetspriceresource',
+        ),
+        json={
+            'from_asset': A_CRV.identifier,
+            'to_asset': 'USD',
+            'timestamp': 1611166340,
+            'price': '1.40',
+        },
+    )
+    response = requests.get(
+        api_url_for(
+            rotkehlchen_api_server,
+            'historicalassetspriceresource',
+        ),
+        params={
+            'from_asset': A_CRV.identifier,
+        },
+    )
+    data = assert_proper_response_with_result(response)
     _assert_expected_prices(data, after_deletion=False)
     # If we query against the to_asset we should get the same entry
     response = requests.get(