Repository: freqtrade/freqtrade

Issue:
gateio: Telegram /status USDT 'Entry Amount' not always correct
## Describe your environment

  * Operating system: Fedora 34 with docker
  * Python Version: 3.10
  * CCXT version: 1.90.40
  * Freqtrade Version: docker-494e0529

## Describe the problem:

Telegram /status Entry Amount not always correct. 

### Steps to reproduce:

Not sure.. can't find a common factor. Happens with longs and shorts, with prices above and below 1. I'm using leverage 1. 

Config snippet:
```
  "bot_name": "freqtrade",
    "initial_state": "running",
    "force_entry_enable": false,
    "internals": {
        "process_throttle_secs": 5
    },
    "max_open_trades": 5,
    "stake_currency": "USDT",
    "stake_amount": "unlimited",
    "tradable_balance_ratio": 0.99,
    "fiat_display_currency": "USD",
    "dry_run": false,
    "cancel_open_orders_on_exit": false,
    "trading_mode": "futures",
    "margin_mode": "isolated",
    "unfilledtimeout": {
        "entry": 10,
        "exit": 30,
        "exit_timeout_count": 0,
        "unit": "minutes"
    },
    "entry_pricing": {
        "price_side": "other",
        "use_order_book": true,
        "order_book_top": 1,
        "price_last_balance": 0.0,
        "check_depth_of_market": {
            "enabled": false,
            "bids_to_ask_delta": 1
        }
    },
    "exit_pricing":{
        "price_side": "other",
        "use_order_book": true,
        "order_book_top": 1
    },
```

Relevant strategy config:
```
class Strat(IStrategy):
    INTERFACE_VERSION = 3
    timeframe = '5m'
    can_short: bool = True
    minimal_roi = {"0": 1}
    stoploss = -0.05
    trailing_stop = True
    trailing_only_offset_is_reached = True
    trailing_stop_positive = 0.04
    trailing_stop_positive_offset = 0.12
    process_only_new_candles = False
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = True
    startup_candle_count: int = 360
    order_types = {
        "entry": "limit",
        "exit": "limit",
        "emergency_exit": "market",
        "stoploss": "limit",
        "stoploss_on_exchange": True,
        "stoploss_on_exchange_interval": 60,
        "stoploss_on_exchange_limit_ratio": 0.99
    }

    order_time_in_force = {
        'entry': 'gtc',
        'exit': 'gtc'
    }
```

### Observed Results:

  * What happened?

For example, this one is correct:

```
Trade ID: 11 (since 6 hours ago)
Current Pair: LINK/USDT:USDT
Direction: Short
Leverage: 1.0
Amount: 3.0 (18.4071 USDT)
Enter Tag: <snip>
Open Rate: 6.13570000
Open Date: 2022-07-13 09:16:25
Current Rate: 5.94310000
Current Profit: 3.06%
Stoploss: 6.44248500 (-5.00%)
Stoploss distance: 0.49938500 (8.40%)
  
Entry #1:
Entry Amount: 3.0 (18.40710000 USDT)
Average Entry Price: 6.1357
```
Here, the USDT Entry amount is `Entry Amount` * `Average Entry Price`: 3.0 * 6.1357 = 18.4071 USDT.  

This one is wrong:
```
Trade ID: 10 (since 11 hours ago)
Current Pair: ALGO/USDT:USDT
Direction: Short
Leverage: 1.0
Amount: 60.0 (17.988 USDT)
Enter Tag: <snip>
Open Rate: 0.29980000
Open Date: 2022-07-13 04:30:09
Current Rate: 0.29960000
Current Profit: -0.01%
Stoploss: 0.31479000 (-5.00%)
Stoploss distance: 0.01519000 (5.07%)
  
Entry #1:
Entry Amount: 60.0 (179.88000000 USDT)
Average Entry Price: 0.2998
```
Entry amount should be `Entry Amount` * `Average Entry Price`: 60.0 * 0.2998 = 17.988 USDT. But it's listed as 179.88 , factor 10 higher.

  * What did you expect to happen?

Correct calculations :)

### Relevant code exceptions or logs
Logs:
```
2022-07-13 04:30:08,398 - freqtrade.wallets - INFO - Wallets synced.
2022-07-13 04:30:08,699 - freqtrade.freqtradebot - INFO - Short signal found: about create a new trade for ALGO/USDT:USDT with stake_amount: 19.951062482046567 ...
2022-07-13 04:30:09,246 - freqtrade.freqtradebot - INFO - Order #181043227715 was created for ALGO/USDT:USDT and status is closed.
2022-07-13 04:30:10,317 - freqtrade.wallets - INFO - Wallets synced.
2022-07-13 04:30:10,322 - freqtrade.rpc.rpc_manager - INFO - Sending rpc message: {'trade_id': 10, 'type': entry, 'buy_tag': '<snip>', 'enter_tag': '<snip>', 'exchange': 'Gateio', 'pair': 'ALGO/USDT:USDT', 'leverage': 1.0, 'direction': 'Short', 'limit': 0.2998, 'open_rate': 0.2998, 'order_type': 'limit', 'stake_amount': 17.988, 'stake_currency': 'USDT', 'fiat_currency': 'USD', 'amount': 60.0, 'open_date': datetime.datetime(2022, 7, 13, 4, 30, 9, 247107), 'current_rate': 0.2997}
2022-07-13 04:30:10,516 - freqtrade.freqtradebot - INFO - Found open order for Trade(id=10, pair=ALGO/USDT:USDT, amount=60.00000000, is_short=True, leverage=1.0, open_rate=0.29980000, open_since=2022-07-13 04:30:09)
2022-07-13 04:30:10,797 - freqtrade.persistence.trade_model - INFO - Updating trade (id=10) ...
2022-07-13 04:30:10,797 - freqtrade.persistence.trade_model - INFO - LIMIT_SELL has been fulfilled for Trade(id=10, pair=ALGO/USDT:USDT, amount=60.00000000, is_short=True, leverage=1.0, open_rate=0.29980000, open_since=2022-07-13 04:30:09).
2022-07-13 04:30:11,599 - freqtrade.wallets - INFO - Wallets synced.
2022-07-13 04:30:11,600 - freqtrade.rpc.rpc_manager - INFO - Sending rpc message: {'trade_id': 10, 'type': entry_fill, 'buy_tag': '<snip>', 'enter_tag': '<snip>', 'exchange': 'Gateio', 'pair': 'ALGO/USDT:USDT', 'leverage': 1.0, 'direction': 'Short', 'limit': 0.2998, 'open_rate': 0.2998, 'order_type': None, 'stake_amount': 17.988, 'stake_currency': 'USDT', 'fiat_currency': 'USD', 'amount': 60.0, 'open_date': datetime.datetime(2022, 7, 13, 4, 30, 9, 247107), 'current_rate': 0.2997}
2022-07-13 04:30:11,601 - freqtrade.rpc.telegram - INFO - Notification 'entry_fill' not sent.
2022-07-13 04:30:14,433 - freqtrade.wallets - INFO - Wallets synced.
2022-07-13 04:30:17,361 - freqtrade.exchange.exchange - INFO - stoploss limit order added for ALGO/USDT:USDT. stop price: 0.31479. limit: 0.318
```

SQL:
```
sqlite> select * from orders where ft_trade_id=10;
25|10|sell|ALGO/USDT:USDT|0|181043227715|closed|ALGO/USDT:USDT|limit|sell|0.2997|0.2998|60.0|60.0|0.0|179.88||2022-07-13 04:30:09.126000|2022-07-13 04:30:10.516905|2022-07-13 04:30:10.516919|
26|10|stoploss|ALGO/USDT:USDT|1|23925211|open|ALGO/USDT:USDT|limit|buy|0.318||60.0|0.0|60.0|0.0|0.3148|2022-07-13 04:30:17.000000||2022-07-13 20:31:39.830030|
```





Discussion:
The commonality (especially as it's 10x) is quite clear - it's the "contractSize" factor - so it's pair dependant.
We're "usually" calculating this out of all elements (i thought everywhere that's visible) - but apparently this location (single orders) has been missed.

will look into this - but as it's not "directly" trade related (it's only really visible while you're doing trades) - priority of it will be a bit lower.

Resolving patch:
diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -79,6 +79,7 @@ class Exchange:
         "ccxt_futures_name": "swap",
         "fee_cost_in_contracts": False,  # Fee cost needs contract conversion
         "needs_trading_fees": False,  # use fetch_trading_fees to cache fees
+        "order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
     }
     _ft_has: Dict = {}
     _ft_has_futures: Dict = {}
@@ -423,7 +424,7 @@ def _order_contracts_to_amount(self, order: Dict) -> Dict:
         if 'symbol' in order and order['symbol'] is not None:
             contract_size = self._get_contract_size(order['symbol'])
             if contract_size != 1:
-                for prop in ['amount', 'cost', 'filled', 'remaining']:
+                for prop in self._ft_has.get('order_props_in_contracts', []):
                     if prop in order and order[prop] is not None:
                         order[prop] = order[prop] * contract_size
         return order
@@ -821,7 +822,7 @@ def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: flo
             'price': rate,
             'average': rate,
             'amount': _amount,
-            'cost': _amount * rate / leverage,
+            'cost': _amount * rate,
             'type': ordertype,
             'side': side,
             'filled': 0,
@@ -1646,7 +1647,7 @@ def calculate_fee_rate(
         fee_curr = fee.get('currency')
         if fee_curr is None:
             return None
-        fee_cost = fee['cost']
+        fee_cost = float(fee['cost'])
         if self._ft_has['fee_cost_in_contracts']:
             # Convert cost via "contracts" conversion
             fee_cost = self._contracts_to_amount(symbol, fee['cost'])
@@ -1654,7 +1655,7 @@ def calculate_fee_rate(
         # Calculate fee based on order details
         if fee_curr == self.get_pair_base_currency(symbol):
             # Base currency - divide by amount
-            return round(fee['cost'] / amount, 8)
+            return round(fee_cost / amount, 8)
         elif fee_curr == self.get_pair_quote_currency(symbol):
             # Quote currency - divide by cost
             return round(fee_cost / cost, 8) if cost else None
@@ -1685,7 +1686,7 @@ def extract_cost_curr_rate(self, fee: Dict, symbol: str, cost: float,
         :param amount: Amount of the order
         :return: Tuple with cost, currency, rate of the given fee dict
         """
-        return (fee['cost'],
+        return (float(fee['cost']),
                 fee['currency'],
                 self.calculate_fee_rate(
                     fee,
diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py
--- a/freqtrade/exchange/gateio.py
+++ b/freqtrade/exchange/gateio.py
@@ -1,12 +1,13 @@
 """ Gate.io exchange subclass """
 import logging
 from datetime import datetime
-from typing import Dict, List, Optional, Tuple
+from typing import Any, Dict, List, Optional, Tuple
 
 from freqtrade.constants import BuySell
 from freqtrade.enums import MarginMode, TradingMode
 from freqtrade.exceptions import OperationalException
 from freqtrade.exchange import Exchange
+from freqtrade.misc import safe_value_fallback2
 
 
 logger = logging.getLogger(__name__)
@@ -34,6 +35,7 @@ class Gateio(Exchange):
     _ft_has_futures: Dict = {
         "needs_trading_fees": True,
         "fee_cost_in_contracts": False,  # Set explicitly to false for clarity
+        "order_props_in_contracts": ['amount', 'filled', 'remaining'],
     }
 
     _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
@@ -96,12 +98,29 @@ def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
                             }
         return trades
 
+    def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
+        if self.trading_mode == TradingMode.FUTURES:
+            return safe_value_fallback2(order, order, 'id_stop', 'id')
+        return order['id']
+
     def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
-        return self.fetch_order(
+        order = self.fetch_order(
             order_id=order_id,
             pair=pair,
             params={'stop': True}
         )
+        if self.trading_mode == TradingMode.FUTURES:
+            if order['status'] == 'closed':
+                # Places a real order - which we need to fetch explicitly.
+                new_orderid = order.get('info', {}).get('trade_id')
+                if new_orderid:
+                    order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params)
+                    order1['id_stop'] = order1['id']
+                    order1['id'] = order_id
+                    order1['stopPrice'] = order.get('stopPrice')
+
+                    return order1
+        return order
 
     def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
         return self.cancel_order(
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -332,6 +332,8 @@ def update_closed_trades_without_assigned_fees(self):
             if not trade.is_open and not trade.fee_updated(trade.exit_side):
                 # Get sell fee
                 order = trade.select_order(trade.exit_side, False)
+                if not order:
+                    order = trade.select_order('stoploss', False)
                 if order:
                     logger.info(
                         f"Updating {trade.exit_side}-fee on trade {trade}"


Test patch:
diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -1135,7 +1135,7 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name, leverag
     assert order["symbol"] == "ETH/BTC"
     assert order["amount"] == 1
     assert order["leverage"] == leverage
-    assert order["cost"] == 1 * 200 / leverage
+    assert order["cost"] == 1 * 200
 
 
 @pytest.mark.parametrize("side,startprice,endprice", [
diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py
--- a/tests/exchange/test_gateio.py
+++ b/tests/exchange/test_gateio.py
@@ -53,6 +53,25 @@ def test_fetch_stoploss_order_gateio(default_conf, mocker):
     assert fetch_order_mock.call_args_list[0][1]['pair'] == 'ETH/BTC'
     assert fetch_order_mock.call_args_list[0][1]['params'] == {'stop': True}
 
+    default_conf['trading_mode'] = 'futures'
+    default_conf['margin_mode'] = 'isolated'
+
+    exchange = get_patched_exchange(mocker, default_conf, id='gateio')
+
+    exchange.fetch_order = MagicMock(return_value={
+        'status': 'closed',
+        'id': '1234',
+        'stopPrice': 5.62,
+        'info': {
+            'trade_id': '222555'
+        }
+        })
+
+    exchange.fetch_stoploss_order('1234', 'ETH/BTC')
+    assert exchange.fetch_order.call_count == 2
+    assert exchange.fetch_order.call_args_list[0][1]['order_id'] == '1234'
+    assert exchange.fetch_order.call_args_list[1][1]['order_id'] == '222555'
+
 
 def test_cancel_stoploss_order_gateio(default_conf, mocker):
     exchange = get_patched_exchange(mocker, default_conf, id='gateio')