Repository: freqtrade/freqtrade
Pull request: Ftx stoploss
## Summary
Introduce stoploss on exchange for FTX.


closes #3103 (only stoploss is remaining).

## Quick changelog

- Add methods to support stoploss on exchange
- FTX supports both stoploss-limit and stoploss-market (controlled via `order_types.stoploss` key).
- use `cancel_stoploss_order` and `get_stoploss_order` to determine which type of order we're querying. This defaults to `cancel_order` and `get_order` by default, but can be overwritten by exchange subclasses (as does FTX).


Problem statement:
Fee calculation + stoploss support + trade amounts on FTX
So, while testing live trading on FTX using freqtrade. I've spotted a few FTX inconsistencies that I'll be logging here, so that we can start working on them in the future:

1. Fee calculation seems to fail all the time with this error (Leveraged tokens, haven't tried perps or options trading yet):

`2020-03-23 15:31:19,595 - freqtrade.freqtradebot - INFO - Applying fee on amount for Trade(id=225, pair=DRGNBEAR/USD, amount=0.01231875, open_rate=2789.00000000, open_since=2020-03-22 20:00:34) failed: myTrade-Dict empty found`

2. Stoploss on exchange is not supported yet on FTX, looking at ftx.py it's obvious that it's lacking the param config that enables it as well as the needed stoploss() and stoploss_adjust() methods.

3. Freqtrade keeps getting out of sync on orders (amount) on FTX, occasionally I get these occurrences which I have to fix manually from the DB:

`2020-03-23 11:10:48,474 - freqtrade.freqtradebot - WARNING - Unable to sell trade: Not enough amount to sell. Trade-amount: 0.0091, Wallet: 0.0082`

Hints from issue discussion:
First of all - please use one issue per discovered problem. Otherwise discussions get confusing and we're stuck with issues we cannot ever close because one minor thing was never done, but the issue would be 90% done - but noone will be able to figure out which part is still missing.

1) that's not relevant for FTX i think. as far as i know, FTX does subtract fees from the "correct" side of the pair (not the destination pair, but the source pair) - so fees should apply correctly. This message mainly applies to binance, which subtracts fees from the Destination currency (or BNB - but then it's not relevant again).

2) that's not even really an issue - just something which is "to implement and test" ...but as mentioned slack - it's really just a "noone invested the time into this yet" more than anything.

3) Without a full log for this, it'll be impossible to find the solution to this (also - please use a separate issue for this.
On the fee calculation, since FTX can have multiple fee tiers which actually rely on volume .. or different discount; it would be ideal if we could query over api to get current account fees then use them from that moment in time for freqtrade?

https://help.ftx.com/hc/en-us/articles/360024479432-Fees

For 2, give me exactly what you would need to I can pass it to FTX devs and see if we could have stop-loss on exchange; 

For 3 I think you are trading with a very small amount and then when the leveraged token fluctuates then you are not reaching the minimum amount to sell.
for 2 - i have a (kindof working) implementation locally - but it needs a few of the open PR's (mainly #3112) to be merged to function properly, so i'll keep that to myself for now to avoid problems.

for 1) i will have a look at fees when i get time - it's part of #2649 ... as fees are not always handled correctly for many exchanges.
I've been testing PERP futures, stoplosses do not trigger because `Unable to sell trade: Not enough amount to sell`
@hclivess Please open a seperate issue for this, which fills out the issue template to show us which version etc. you're using.

Also, we'll be needing the logs from the time of the buy (a few lines before / after the buy) - as well as the logs from the time of the sell, otherwise we cannot verify your issue.
@xmatthias OK, thank you. I might be doing something wrong, but freqtrade thinks the wallet balance is 0, which it is, because there is no "token balance in the wallet", it's in a separate category
He's testing futures, wallet balance is actually 0, you dont have a token called X when you are trading futures. You are trading on leverage using USD or non USD as collateral, you never had something to sell but contracts you need to close, this is not in the current freqtrade logic @xmatthias 
Looking at futures for FTX won't make much sense until ccxt implements their unified futures interface (scheduled for Q2 - so we'll not look at it as we'd be replicating the work ccxt will do in this direction).

I'm not sure what is missing for FTX (it's the balance point for sure - which does not return future values) - but might be other things as well we will discover once we move forward with this in the future.


Now for the original points of this issue:
fee calculation has been reworked in #3239, (although i don't think it was broken for ftx before).
3 should also be fixed by #3239 as i suspect it had to do with fees (although we never got a log of this - so are unable to confirm.
stoploss support will come (i have a working implementation - but need to test that a bit further.

Commit message: Update documentation with FTX Stoploss on exchange

Patch to review:
diff --git a/docs/configuration.md b/docs/configuration.md
index da0f015e8b8..46719046372 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -272,7 +272,7 @@ the static list of pairs) if we should buy.
 
 ### Understand order_types
 
-The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
+The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
 
 This allows to buy using limit orders, sell using
 limit-orders, and create stoplosses using using market orders. It also allows to set the
@@ -288,8 +288,12 @@ If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
 `emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails.
 The below is the default which is used if this is not configured in either strategy or configuration file.
 
-Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
-`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`).
+Not all Exchanges support `stoploss_on_exchange`. If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type.
+
+If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
+`stoploss` defines the stop-price - and limit should be slightly below this.
+
+This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`).
 Calculation example: we bought the asset at 100$.
 Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$.
 
@@ -331,7 +335,7 @@ Configuration:
     refer to [the stoploss documentation](stoploss.md).
 
 !!! Note
-    If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order.
+    If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order.
 
 !!! Warning "Warning: stoploss_on_exchange failures"
     If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised.
diff --git a/docs/exchanges.md b/docs/exchanges.md
index 81f01702386..cd210eb69bf 100644
--- a/docs/exchanges.md
+++ b/docs/exchanges.md
@@ -64,6 +64,11 @@ print(res)
 
 ## FTX
 
+!!! Tip "Stoploss on Exchange"
+    FTX supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
+    You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide.
+
+
 ### Using subaccounts
 
 To use subaccounts with FTX, you need to edit the configuration and add the following:
diff --git a/docs/stoploss.md b/docs/stoploss.md
index 0e43817ec46..cd90a71b4f2 100644
--- a/docs/stoploss.md
+++ b/docs/stoploss.md
@@ -27,7 +27,7 @@ So this parameter will tell the bot how often it should update the stoploss orde
 This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
 
 !!! Note
-    Stoploss on exchange is only supported for Binance (stop-loss-limit) and Kraken (stop-loss-market) as of now.
+    Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now.
 
 ## Static Stop Loss
 
diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 038fc22bc11..e666b07ec4c 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -79,7 +79,7 @@ def __init__(self, config: Dict[str, Any], validate: bool = True) -> None:
 
         if config['dry_run']:
             logger.info('Instance is running with dry_run enabled')
-
+        logger.info(f"Using CCXT {ccxt.__version__}")
         exchange_config = config['exchange']
 
         # Deep merge ft_has with default ft_has options
@@ -952,6 +952,9 @@ def cancel_order(self, order_id: str, pair: str) -> Dict:
         except ccxt.BaseError as e:
             raise OperationalException(e) from e
 
+    # Assign method to get_stoploss_order to allow easy overriding in other classes
+    cancel_stoploss_order = cancel_order
+
     def is_cancel_order_result_suitable(self, corder) -> bool:
         if not isinstance(corder, dict):
             return False
@@ -1004,6 +1007,9 @@ def get_order(self, order_id: str, pair: str) -> Dict:
         except ccxt.BaseError as e:
             raise OperationalException(e) from e
 
+    # Assign method to get_stoploss_order to allow easy overriding in other classes
+    get_stoploss_order = get_order
+
     @retrier
     def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
         """
diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py
index 75915122bc3..73347f1ebb4 100644
--- a/freqtrade/exchange/ftx.py
+++ b/freqtrade/exchange/ftx.py
@@ -2,7 +2,12 @@
 import logging
 from typing import Dict
 
+import ccxt
+
+from freqtrade.exceptions import (DependencyException, InvalidOrderException,
+                                  OperationalException, TemporaryError)
 from freqtrade.exchange import Exchange
+from freqtrade.exchange.common import retrier
 
 logger = logging.getLogger(__name__)
 
@@ -10,5 +15,104 @@
 class Ftx(Exchange):
 
     _ft_has: Dict = {
+        "stoploss_on_exchange": True,
         "ohlcv_candle_limit": 1500,
     }
+
+    def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
+        """
+        Verify stop_loss against stoploss-order value (limit or price)
+        Returns True if adjustment is necessary.
+        """
+        return order['type'] == 'stop' and stop_loss > float(order['price'])
+
+    def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
+        """
+        Creates a stoploss order.
+        depending on order_types.stoploss configuration, uses 'market' or limit order.
+
+        Limit orders are defined by having orderPrice set, otherwise a market order is used.
+        """
+        limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
+        limit_rate = stop_price * limit_price_pct
+
+        ordertype = "stop"
+
+        stop_price = self.price_to_precision(pair, stop_price)
+
+        if self._config['dry_run']:
+            dry_order = self.dry_run_order(
+                pair, ordertype, "sell", amount, stop_price)
+            return dry_order
+
+        try:
+            params = self._params.copy()
+            if order_types.get('stoploss', 'market') == 'limit':
+                # set orderPrice to place limit order, otherwise it's a market order
+                params['orderPrice'] = limit_rate
+
+            amount = self.amount_to_precision(pair, amount)
+
+            order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
+                                           amount=amount, price=stop_price, params=params)
+            logger.info('stoploss order added for %s. '
+                        'stop price: %s.', pair, stop_price)
+            return order
+        except ccxt.InsufficientFunds as e:
+            raise DependencyException(
+                f'Insufficient funds to create {ordertype} sell order on market {pair}.'
+                f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
+                f'Message: {e}') from e
+        except ccxt.InvalidOrder as e:
+            raise InvalidOrderException(
+                f'Could not create {ordertype} sell order on market {pair}. '
+                f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
+                f'Message: {e}') from e
+        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
+            raise TemporaryError(
+                f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
+        except ccxt.BaseError as e:
+            raise OperationalException(e) from e
+
+    @retrier
+    def get_stoploss_order(self, order_id: str, pair: str) -> Dict:
+        if self._config['dry_run']:
+            try:
+                order = self._dry_run_open_orders[order_id]
+                return order
+            except KeyError as e:
+                # Gracefully handle errors with dry-run orders.
+                raise InvalidOrderException(
+                    f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
+        try:
+            orders = self._api.fetch_orders(pair, None, params={'type': 'stop'})
+
+            order = [order for order in orders if order['id'] == order_id]
+            if len(order) == 1:
+                return order[0]
+            else:
+                raise InvalidOrderException(f"Could not get Stoploss Order for id {order_id}")
+
+        except ccxt.InvalidOrder as e:
+            raise InvalidOrderException(
+                f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
+        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
+            raise TemporaryError(
+                f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
+        except ccxt.BaseError as e:
+            raise OperationalException(e) from e
+
+    @retrier
+    def cancel_stoploss_order(self, order_id: str, pair: str) -> Dict:
+        if self._config['dry_run']:
+            return {}
+        try:
+            return self._api.cancel_order(order_id, pair, params={'type': 'stop'})
+        except ccxt.InvalidOrder as e:
+            raise InvalidOrderException(
+                f'Could not cancel order. Message: {e}') from e
+        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
+            raise TemporaryError(
+                f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
+        except ccxt.BaseError as e:
+            raise OperationalException(e) from e
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index a74b0a5a159..4e4fe6e11cc 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -774,13 +774,13 @@ def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
 
         try:
             # First we check if there is already a stoploss on exchange
-            stoploss_order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) \
+            stoploss_order = self.exchange.get_stoploss_order(trade.stoploss_order_id, trade.pair) \
                 if trade.stoploss_order_id else None
         except InvalidOrderException as exception:
             logger.warning('Unable to fetch stoploss order: %s', exception)
 
         # We check if stoploss order is fulfilled
-        if stoploss_order and stoploss_order['status'] == 'closed':
+        if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
             trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
             self.update_trade_state(trade, stoploss_order, sl_order=True)
             # Lock pair for one candle to prevent immediate rebuys
@@ -807,7 +807,7 @@ def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
                 return False
 
         # If stoploss order is canceled for some reason we add it
-        if stoploss_order and stoploss_order['status'] == 'canceled':
+        if stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled'):
             if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
                                           rate=trade.stop_loss):
                 return False
@@ -840,7 +840,7 @@ def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> Non
                 logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) '
                             'in order to add another one ...', order['id'])
                 try:
-                    self.exchange.cancel_order(order['id'], trade.pair)
+                    self.exchange.cancel_stoploss_order(order['id'], trade.pair)
                 except InvalidOrderException:
                     logger.exception(f"Could not cancel stoploss order {order['id']} "
                                      f"for pair {trade.pair}")
@@ -1068,7 +1068,7 @@ def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> boo
         # First cancelling stoploss on exchange ...
         if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
             try:
-                self.exchange.cancel_order(trade.stoploss_order_id, trade.pair)
+                self.exchange.cancel_stoploss_order(trade.stoploss_order_id, trade.pair)
             except InvalidOrderException:
                 logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
 
diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py
index 823bf6dc064..2e02c3999d7 100644
--- a/freqtrade/persistence.py
+++ b/freqtrade/persistence.py
@@ -374,7 +374,7 @@ def update(self, order: Dict) -> None:
         elif order_type in ('market', 'limit') and order['side'] == 'sell':
             self.close(order['price'])
             logger.info('%s_SELL has been fulfilled for %s.', order_type.upper(), self)
-        elif order_type in ('stop_loss_limit', 'stop-loss'):
+        elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'):
             self.stoploss_order_id = None
             self.close_rate_requested = self.stop_loss
             logger.info('%s is hit for %s.', order_type.upper(), self)
diff --git a/setup.py b/setup.py
index 20963a15ffc..6d832e3f5c8 100644
--- a/setup.py
+++ b/setup.py
@@ -63,7 +63,7 @@
       tests_require=['pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', ],
       install_requires=[
           # from requirements-common.txt
-          'ccxt>=1.18.1080',
+          'ccxt>=1.24.96',
           'SQLAlchemy',
           'python-telegram-bot',
           'arrow',
diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
index 32163f696e4..dc272de36b2 100644
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -25,7 +25,7 @@
 from tests.conftest import get_patched_exchange, log_has, log_has_re
 
 # Make sure to always keep one exchange here which is NOT subclassed!!
-EXCHANGES = ['bittrex', 'binance', 'kraken', ]
+EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx']
 
 
 # Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
@@ -1258,7 +1258,8 @@ async def mock_candle_hist(pair, timeframe, since_ms):
 
     exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
     # one_call calculation * 1.8 should do 2 calls
-    since = 5 * 60 * 500 * 1.8
+
+    since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8
     ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
 
     assert exchange._async_get_candle_history.call_count == 2
@@ -1733,6 +1734,7 @@ def test_cancel_order_dry_run(default_conf, mocker, exchange_name):
     default_conf['dry_run'] = True
     exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
     assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {}
+    assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {}
 
 
 @pytest.mark.parametrize("exchange_name", EXCHANGES)
@@ -1817,6 +1819,25 @@ def test_cancel_order(default_conf, mocker, exchange_name):
                            order_id='_', pair='TKN/BTC')
 
 
+@pytest.mark.parametrize("exchange_name", EXCHANGES)
+def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
+    default_conf['dry_run'] = False
+    api_mock = MagicMock()
+    api_mock.cancel_order = MagicMock(return_value=123)
+    exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
+    assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == 123
+
+    with pytest.raises(InvalidOrderException):
+        api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
+        exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
+        exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC')
+    assert api_mock.cancel_order.call_count == 1
+
+    ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
+                           "cancel_stoploss_order", "cancel_order",
+                           order_id='_', pair='TKN/BTC')
+
+
 @pytest.mark.parametrize("exchange_name", EXCHANGES)
 def test_get_order(default_conf, mocker, exchange_name):
     default_conf['dry_run'] = True
@@ -1846,6 +1867,38 @@ def test_get_order(default_conf, mocker, exchange_name):
                            order_id='_', pair='TKN/BTC')
 
 
+@pytest.mark.parametrize("exchange_name", EXCHANGES)
+def test_get_stoploss_order(default_conf, mocker, exchange_name):
+    # Don't test FTX here - that needs a seperate test
+    if exchange_name == 'ftx':
+        return
+    default_conf['dry_run'] = True
+    order = MagicMock()
+    order.myid = 123
+    exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
+    exchange._dry_run_open_orders['X'] = order
+    assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123
+
+    with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
+        exchange.get_stoploss_order('Y', 'TKN/BTC')
+
+    default_conf['dry_run'] = False
+    api_mock = MagicMock()
+    api_mock.fetch_order = MagicMock(return_value=456)
+    exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
+    assert exchange.get_stoploss_order('X', 'TKN/BTC') == 456
+
+    with pytest.raises(InvalidOrderException):
+        api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
+        exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
+        exchange.get_stoploss_order(order_id='_', pair='TKN/BTC')
+    assert api_mock.fetch_order.call_count == 1
+
+    ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
+                           'get_stoploss_order', 'fetch_order',
+                           order_id='_', pair='TKN/BTC')
+
+
 @pytest.mark.parametrize("exchange_name", EXCHANGES)
 def test_name(default_conf, mocker, exchange_name):
     exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py
new file mode 100644
index 00000000000..bead63096d8
--- /dev/null
+++ b/tests/exchange/test_ftx.py
@@ -0,0 +1,163 @@
+# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
+# pragma pylint: disable=protected-access
+from random import randint
+from unittest.mock import MagicMock
+
+import ccxt
+import pytest
+
+from freqtrade.exceptions import (DependencyException, InvalidOrderException,
+                                  OperationalException, TemporaryError)
+from tests.conftest import get_patched_exchange
+from .test_exchange import ccxt_exceptionhandlers
+
+STOPLOSS_ORDERTYPE = 'stop'
+
+
+def test_stoploss_order_ftx(default_conf, mocker):
+    api_mock = MagicMock()
+    order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
+
+    api_mock.create_order = MagicMock(return_value={
+        'id': order_id,
+        'info': {
+            'foo': 'bar'
+        }
+    })
+
+    default_conf['dry_run'] = False
+    mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
+    mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
+
+    exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
+
+    # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders
+    order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
+                              order_types={'stoploss_on_exchange_limit_ratio': 1.05})
+
+    assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
+    assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
+    assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
+    assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
+    assert api_mock.create_order.call_args_list[0][1]['price'] == 190
+    assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
+
+    assert api_mock.create_order.call_count == 1
+
+    api_mock.create_order.reset_mock()
+
+    order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
+
+    assert 'id' in order
+    assert 'info' in order
+    assert order['id'] == order_id
+    assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
+    assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
+    assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
+    assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
+    assert api_mock.create_order.call_args_list[0][1]['price'] == 220
+    assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
+
+    api_mock.create_order.reset_mock()
+    order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
+                              order_types={'stoploss': 'limit'})
+
+    assert 'id' in order
+    assert 'info' in order
+    assert order['id'] == order_id
+    assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
+    assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
+    assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
+    assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
+    assert api_mock.create_order.call_args_list[0][1]['price'] == 220
+    assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params']
+    assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8
+
+    # test exception handling
+    with pytest.raises(DependencyException):
+        api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
+        exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
+        exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
+
+    with pytest.raises(InvalidOrderException):
+        api_mock.create_order = MagicMock(
+            side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately."))
+        exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
+        exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
+
+    with pytest.raises(TemporaryError):
+        api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
+        exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
+        exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
+
+    with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
+        api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
+        exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
+        exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
+
+
+def test_stoploss_order_dry_run_ftx(default_conf, mocker):
+    api_mock = MagicMock()
+    default_conf['dry_run'] = True
+    mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
+    mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
+
+    exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
+
+    api_mock.create_order.reset_mock()
+
+    order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
+
+    assert 'id' in order
+    assert 'info' in order
+    assert 'type' in order
+
+    assert order['type'] == STOPLOSS_ORDERTYPE
+    assert order['price'] == 220
+    assert order['amount'] == 1
+
+
+def test_stoploss_adjust_ftx(mocker, default_conf):
+    exchange = get_patched_exchange(mocker, default_conf, id='ftx')
+    order = {
+        'type': STOPLOSS_ORDERTYPE,
+        'price': 1500,
+    }
+    assert exchange.stoploss_adjust(1501, order)
+    assert not exchange.stoploss_adjust(1499, order)
+    # Test with invalid order case ...
+    order['type'] = 'stop_loss_limit'
+    assert not exchange.stoploss_adjust(1501, order)
+
+
+def test_get_stoploss_order(default_conf, mocker):
+    default_conf['dry_run'] = True
+    order = MagicMock()
+    order.myid = 123
+    exchange = get_patched_exchange(mocker, default_conf, id='ftx')
+    exchange._dry_run_open_orders['X'] = order
+    assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123
+
+    with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
+        exchange.get_stoploss_order('Y', 'TKN/BTC')
+
+    default_conf['dry_run'] = False
+    api_mock = MagicMock()
+    api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}])
+    exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
+    assert exchange.get_stoploss_order('X', 'TKN/BTC')['status'] == '456'
+
+    api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}])
+    exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
+    with pytest.raises(InvalidOrderException, match=r"Could not get Stoploss Order for id X"):
+        exchange.get_stoploss_order('X', 'TKN/BTC')['status']
+
+    with pytest.raises(InvalidOrderException):
+        api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
+        exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
+        exchange.get_stoploss_order(order_id='_', pair='TKN/BTC')
+    assert api_mock.fetch_orders.call_count == 1
+
+    ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx',
+                           'get_stoploss_order', 'fetch_orders',
+                           order_id='_', pair='TKN/BTC')
diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py
index d63dd66cc4d..0950979cfcc 100644
--- a/tests/exchange/test_kraken.py
+++ b/tests/exchange/test_kraken.py
@@ -11,6 +11,8 @@
 from tests.conftest import get_patched_exchange
 from tests.exchange.test_exchange import ccxt_exceptionhandlers
 
+STOPLOSS_ORDERTYPE = 'stop-loss'
+
 
 def test_buy_kraken_trading_agreement(default_conf, mocker):
     api_mock = MagicMock()
@@ -159,7 +161,6 @@ def test_get_balances_prod(default_conf, mocker):
 def test_stoploss_order_kraken(default_conf, mocker):
     api_mock = MagicMock()
     order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
-    order_type = 'stop-loss'
 
     api_mock.create_order = MagicMock(return_value={
         'id': order_id,
@@ -187,7 +188,7 @@ def test_stoploss_order_kraken(default_conf, mocker):
     assert 'info' in order
     assert order['id'] == order_id
     assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
-    assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
+    assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
     assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
     assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
     assert api_mock.create_order.call_args_list[0][1]['price'] == 220
@@ -218,7 +219,6 @@ def test_stoploss_order_kraken(default_conf, mocker):
 
 def test_stoploss_order_dry_run_kraken(default_conf, mocker):
     api_mock = MagicMock()
-    order_type = 'stop-loss'
     default_conf['dry_run'] = True
     mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
     mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
@@ -233,7 +233,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker):
     assert 'info' in order
     assert 'type' in order
 
-    assert order['type'] == order_type
+    assert order['type'] == STOPLOSS_ORDERTYPE
     assert order['price'] == 220
     assert order['amount'] == 1
 
@@ -241,7 +241,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker):
 def test_stoploss_adjust_kraken(mocker, default_conf):
     exchange = get_patched_exchange(mocker, default_conf, id='kraken')
     order = {
-        'type': 'stop-loss',
+        'type': STOPLOSS_ORDERTYPE,
         'price': 1500,
     }
     assert exchange.stoploss_adjust(1501, order)
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 487e3a60e16..dc5a5c7d3a2 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1126,7 +1126,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
     trade.stoploss_order_id = 100
 
     hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
-    mocker.patch('freqtrade.exchange.Exchange.get_order', hanging_stoploss_order)
+    mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', hanging_stoploss_order)
 
     assert freqtrade.handle_stoploss_on_exchange(trade) is False
     assert trade.stoploss_order_id == 100
@@ -1139,7 +1139,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
     trade.stoploss_order_id = 100
 
     canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
-    mocker.patch('freqtrade.exchange.Exchange.get_order', canceled_stoploss_order)
+    mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', canceled_stoploss_order)
     stoploss.reset_mock()
 
     assert freqtrade.handle_stoploss_on_exchange(trade) is False
@@ -1164,7 +1164,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
         'average': 2,
         'amount': limit_buy_order['amount'],
     })
-    mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit)
+    mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hit)
     assert freqtrade.handle_stoploss_on_exchange(trade) is True
     assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog)
     assert trade.stoploss_order_id is None
@@ -1183,7 +1183,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
     # It should try to add stoploss order
     trade.stoploss_order_id = 100
     stoploss.reset_mock()
-    mocker.patch('freqtrade.exchange.Exchange.get_order', side_effect=InvalidOrderException())
+    mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order',
+                 side_effect=InvalidOrderException())
     mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
     freqtrade.handle_stoploss_on_exchange(trade)
     assert stoploss.call_count == 1
@@ -1214,7 +1215,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
         buy=MagicMock(return_value={'id': limit_buy_order['id']}),
         sell=MagicMock(return_value={'id': limit_sell_order['id']}),
         get_fee=fee,
-        get_order=MagicMock(return_value={'status': 'canceled'}),
+        get_stoploss_order=MagicMock(return_value={'status': 'canceled'}),
         stoploss=MagicMock(side_effect=DependencyException()),
     )
     freqtrade = FreqtradeBot(default_conf)
@@ -1331,7 +1332,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
         }
     })
 
-    mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
+    mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging)
 
     # stoploss initially at 5%
     assert freqtrade.handle_trade(trade) is False
@@ -1346,7 +1347,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
 
     cancel_order_mock = MagicMock()
     stoploss_order_mock = MagicMock()
-    mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
+    mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock)
     mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock)
 
     # stoploss should not be updated as the interval is 60 seconds
@@ -1429,8 +1430,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
             'stopPrice': '0.1'
         }
     }
-    mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
-    mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
+    mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
+                 side_effect=InvalidOrderException())
+    mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging)
     freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
     assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
 
@@ -1439,7 +1441,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
 
     # Fail creating stoploss order
     caplog.clear()
-    cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock())
+    cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock())
     mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException())
     freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
     assert cancel_mock.call_count == 1
@@ -1510,7 +1512,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
         }
     })
 
-    mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
+    mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging)
 
     # stoploss initially at 20% as edge dictated it.
     assert freqtrade.handle_trade(trade) is False
@@ -1519,7 +1521,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
 
     cancel_order_mock = MagicMock()
     stoploss_order_mock = MagicMock()
-    mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
+    mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock)
     mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
 
     # price goes down 5%
@@ -2632,7 +2634,8 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
 
 def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, caplog) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf)
-    mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
+    mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
+                 side_effect=InvalidOrderException())
     mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300))
     sellmock = MagicMock()
     patch_exchange(mocker)
@@ -2680,7 +2683,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
         amount_to_precision=lambda s, x, y: y,
         price_to_precision=lambda s, x, y: y,
         stoploss=stoploss,
-        cancel_order=cancel_order,
+        cancel_stoploss_order=cancel_order,
     )
 
     freqtrade = FreqtradeBot(default_conf)
@@ -2771,7 +2774,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
         "fee": None,
         "trades": None
     })
-    mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_executed)
+    mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_executed)
 
     freqtrade.exit_positions(trades)
     assert trade.stoploss_order_id is None
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 1396e86f591..57960503e5a 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -62,8 +62,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
         get_fee=fee,
         amount_to_precision=lambda s, x, y: y,
         price_to_precision=lambda s, x, y: y,
-        get_order=stoploss_order_mock,
-        cancel_order=cancel_order_mock,
+        get_stoploss_order=stoploss_order_mock,
+        cancel_stoploss_order=cancel_order_mock,
     )
 
     mocker.patch.multiple(


Code review comments:
diff_hunk: @@ -2,13 +2,117 @@
 import logging
 from typing import Dict
 
+import ccxt
+
+from freqtrade.exceptions import (DependencyException, InvalidOrderException,
+                                  OperationalException, TemporaryError)
 from freqtrade.exchange import Exchange
+from freqtrade.exchange.common import retrier
 
 logger = logging.getLogger(__name__)
 
 
 class Ftx(Exchange):
 
     _ft_has: Dict = {
+        "stoploss_on_exchange": True,
         "ohlcv_candle_limit": 1500,
     }
+
+    def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
+        """
+        Verify stop_loss against stoploss-order value (limit or price)
+        Returns True if adjustment is necessary.
+        """
+        return order['type'] == 'stop' and stop_loss > float(order['price'])
+
+    def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
+        """
+        Creates a stoploss order.
+        depending on order_types.stoploss configuration, uses 'market' or limit order.
+
+        Limit orders are defined by having orderPrice set, otherwise a market order is used.
+        """
+        limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
+        limit_rate = stop_price * limit_price_pct
+
+        ordertype = "stop"
+
+        stop_price = self.price_to_precision(pair, stop_price)
+
+        if self._config['dry_run']:
+            dry_order = self.dry_run_order(
+                pair, ordertype, "sell", amount, stop_price)
+            return dry_order
+
+        try:
+            params = self._params.copy()
+            if order_types.get('stoploss', 'market') == 'limit':
+                # set orderPrice to place limit order, otherwise it's a market order
+                params['orderPrice'] = limit_rate
+
+            amount = self.amount_to_precision(pair, amount)
+
+            order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
+                                           amount=amount, price=stop_price, params=params)
+            logger.info('stoploss order added for %s. '
+                        'stop price: %s.', pair, stop_price)
+            return order
+        except ccxt.InsufficientFunds as e:
+            raise DependencyException(
+                f'Insufficient funds to create {ordertype} sell order on market {pair}.'
+                f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
+                f'Message: {e}') from e
+        except ccxt.InvalidOrder as e:
+            raise InvalidOrderException(
+                f'Could not create {ordertype} sell order on market {pair}. '
+                f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
+                f'Message: {e}') from e
+        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
+            raise TemporaryError(
+                f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
+        except ccxt.BaseError as e:
+            raise OperationalException(e) from e
+
+    @retrier
+    def get_stoploss_order(self, order_id: str, pair: str) -> Dict:
+        if self._config['dry_run']:
+            try:
+                order = self._dry_run_open_orders[order_id]
+                return order
+            except KeyError as e:
+                # Gracefully handle errors with dry-run orders.
+                raise InvalidOrderException(
+                    f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
+        try:
+            orders = self._api.fetch_orders(pair, None, params={'type': 'stop'})
+
+            order = [order for order in orders if order['id'] == order_id]
+            if len(order) == 1:
+                return order[0]
+            else:
+                raise InvalidOrderException(f"Could not get Stoploss Order for id {order_id}")
path: freqtrade/exchange/ftx.py
text: @user1:
```suggestion
                raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")
```

diff_hunk: @@ -2,13 +2,117 @@
 import logging
 from typing import Dict
 
+import ccxt
+
+from freqtrade.exceptions import (DependencyException, InvalidOrderException,
+                                  OperationalException, TemporaryError)
 from freqtrade.exchange import Exchange
+from freqtrade.exchange.common import retrier
 
 logger = logging.getLogger(__name__)
 
 
 class Ftx(Exchange):
 
     _ft_has: Dict = {
+        "stoploss_on_exchange": True,
         "ohlcv_candle_limit": 1500,
     }
+
+    def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
+        """
+        Verify stop_loss against stoploss-order value (limit or price)
+        Returns True if adjustment is necessary.
+        """
+        return order['type'] == 'stop' and stop_loss > float(order['price'])
+
+    def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
+        """
+        Creates a stoploss order.
+        depending on order_types.stoploss configuration, uses 'market' or limit order.
+
+        Limit orders are defined by having orderPrice set, otherwise a market order is used.
+        """
+        limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
+        limit_rate = stop_price * limit_price_pct
+
+        ordertype = "stop"
+
+        stop_price = self.price_to_precision(pair, stop_price)
+
+        if self._config['dry_run']:
+            dry_order = self.dry_run_order(
+                pair, ordertype, "sell", amount, stop_price)
+            return dry_order
+
+        try:
+            params = self._params.copy()
+            if order_types.get('stoploss', 'market') == 'limit':
+                # set orderPrice to place limit order, otherwise it's a market order
+                params['orderPrice'] = limit_rate
+
+            amount = self.amount_to_precision(pair, amount)
+
+            order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
+                                           amount=amount, price=stop_price, params=params)
+            logger.info('stoploss order added for %s. '
+                        'stop price: %s.', pair, stop_price)
+            return order
+        except ccxt.InsufficientFunds as e:
+            raise DependencyException(
+                f'Insufficient funds to create {ordertype} sell order on market {pair}.'
path: freqtrade/exchange/ftx.py
text: @user1:
```suggestion
                f'Insufficient funds to create {ordertype} sell order on market {pair}. '
```