Repository: freqtrade/freqtrade

Issue:
Stop loss ignored if it got changed since the trade was opened.
Environment

Python Version: Python 3.6.6+ (python -V)
CCXT version: ccxt==1.17.399 (pip freeze | grep ccxt)
Branch: Develop
Last Commit ID: 7da127d (git log --format="%H" -n 1)

If there are opened trades and there is a change for the stop loss in the strategy, if the stop loss is set above the current position profitability, the stop loss is ignored.
Eg:

Position is currently at -12%, the stop loss is set at -0.2. The bot is stopped and the stop loss is changed from -0.2 to -0.1 but the position is ignored and not automatically sold although the threshold is set at -0.1 

Every position that was > -0.1 and reaches in the future the said -0.1 stop loss is executed as it should.
Stop loss not sync with actual configuration
## Step 2: Describe your environment
  * Python Version: 3.6.5
  * CCXT version: 1.18.523
  * Branch: Develop
  * Last Commit ID: 175fc859 
## Step 3: Describe the problem:

For configuration using  "trailing_stop": true  , with stoploss definied in strategy. If there are some open trades with this config and letter stoploss value is changed previous stoploss values for already open trades not sync with new value (in db values not updated). Is there any solution maybe with configurations settings to disable old stoploss usage in already opentrades, maybe config disables stoploss at all  (for now use db table update)? 


Discussion:
Are you talking about trailing stoploss ? this is the only way to change the stoploss as far as I know.
Nope, not trailing stop loss.
Assume you have a specific stop loss set in your strategy, for example -20% lose, sell the position
Your bot starts to make some trades and some positions are at -17% and some at -15%.
You stop the bot in this moment and modify the strategy and its stop loss to be -10% so you can get rid of the losers and have the bot open new positions.
All the positions that are at -17% and -15% are not liquidated, they are simply ignored. 
Any new position that reaches the newly set -10% stop loss, will be liquidated as intended.
this is a more general question. and yes this is a typical case which proves that we are not handling it correctly: your trades are not synced. you cannot stop the bot, it then would be disconnected from the exchange. 
The bot should be synced with the connected exchange when it is started. 
@xmatthias has a better memory than me. but I think we have this issue somewhere and definitely part of the next improvement.
The bot gets stopped for seconds. Change the stop loss. Re-start the bot. 
there are 2 issues with stop-loss - but only one applies here i think

* we don't use stop-loss on the exchange - which can lead to losses if the bot is stopped for longer - but is also difficult to implement as ccxt does not unify this currently, so different code per exchange would be needed - see #988  
* stop-loss is written to the database for better tracking when the trade is opened, but (i think) not updated if you change it in the configuration.

it's the 2nd problem which applies here i think.
You could temporarily circumvent this by manually updating the database (set stoploss to `null` so it gets populated by the new values). this would break trailing stoploss however ... (or at least i think it would) - but if you're not using that anyway it shouldn't be a problem.
or manually selling via `/forcesell` - both methods are not ideal.

I'd need to have a closer look at this - but i don't think fixing this is straightforward without breaking trailing stoploss support.

I'm also not sure if it should be seen as a bug or rather an enhancement - it's simply not supported ATM...
It is the 2nd scenario, indeed. Fixed it by manually selling using forcesell.
For me it is an enhancement. DB should mirror the exchange at boot. I think we should not put time for a fix on this before sync is implemented.
Not being able to change the stop loss is anything but an enhancement...
From trading point of view you are right :) 
especially with `trailling_stop=True`  this should work (but after the first iteration / change of the trailing stoploss).

It's known (#1304) to not work without trailing-stop - however we'll need to analyze why it's not working in this case.

@xmatthias is it possible somehow  with configuration changes to enable - "sell only positive" for already open trades ? 
change it in the configuration, and restart the bot / use `/reload_conf` from telegram ... almost everything related to selling (roi, sell-signal, ...) are based on strategy / configuration settings, and not stored in the database, so it'll apply to open trades. 

Stoploss is the exception from that.

Resolving patch:
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -90,6 +90,16 @@ def cleanup(self) -> None:
         self.rpc.cleanup()
         persistence.cleanup()
 
+    def startup(self) -> None:
+        """
+        Called on startup and after reloading the bot - triggers notifications and
+        performs startup tasks
+        """
+        self.rpc.startup_messages(self.config, self.pairlists)
+        if not self.edge:
+            # Adjust stoploss if it was changed
+            Trade.stoploss_reinitialization(self.strategy.stoploss)
+
     def process(self) -> bool:
         """
         Queries the persistence layer for open trades and handles them,
diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py
--- a/freqtrade/persistence.py
+++ b/freqtrade/persistence.py
@@ -422,3 +422,22 @@ def get_open_trades() -> List[Any]:
         Query trades from persistence layer
         """
         return Trade.query.filter(Trade.is_open.is_(True)).all()
+
+    @staticmethod
+    def stoploss_reinitialization(desired_stoploss):
+        """
+        Adjust initial Stoploss to desired stoploss for all open trades.
+        """
+        for trade in Trade.get_open_trades():
+            logger.info("Found open trade: %s", trade)
+
+            # skip case if trailing-stop changed the stoploss already.
+            if (trade.stop_loss == trade.initial_stop_loss
+               and trade.initial_stop_loss_pct != desired_stoploss):
+                # Stoploss value got changed
+
+                logger.info(f"Stoploss for {trade} needs adjustment.")
+                # Force reset of stoploss
+                trade.stop_loss = None
+                trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
+                logger.info(f"new stoploss: {trade.stop_loss}, ")
diff --git a/freqtrade/worker.py b/freqtrade/worker.py
--- a/freqtrade/worker.py
+++ b/freqtrade/worker.py
@@ -91,7 +91,7 @@ def _worker(self, old_state: Optional[State], throttle_secs: Optional[float] = N
             })
             logger.info('Changing state to: %s', state.name)
             if state == State.RUNNING:
-                self.freqtrade.rpc.startup_messages(self._config, self.freqtrade.pairlists)
+                self.freqtrade.startup()
 
         if state == State.STOPPED:
             # Ping systemd watchdog before sleeping in the stopped state


Test patch:
diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py
--- a/freqtrade/tests/conftest.py
+++ b/freqtrade/tests/conftest.py
@@ -2,6 +2,7 @@
 import json
 import logging
 import re
+from copy import deepcopy
 from datetime import datetime
 from functools import reduce
 from unittest.mock import MagicMock, PropertyMock
@@ -952,9 +953,10 @@ def buy_order_fee():
 
 @pytest.fixture(scope="function")
 def edge_conf(default_conf):
-    default_conf['max_open_trades'] = -1
-    default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
-    default_conf['edge'] = {
+    conf = deepcopy(default_conf)
+    conf['max_open_trades'] = -1
+    conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
+    conf['edge'] = {
         "enabled": True,
         "process_throttle_secs": 1800,
         "calculate_since_number_of_days": 14,
@@ -970,7 +972,7 @@ def edge_conf(default_conf):
         "remove_pumps": False
     }
 
-    return default_conf
+    return conf
 
 
 @pytest.fixture
diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py
--- a/freqtrade/tests/test_freqtradebot.py
+++ b/freqtrade/tests/test_freqtradebot.py
@@ -105,6 +105,7 @@ def test_cleanup(mocker, default_conf, caplog) -> None:
 def test_worker_running(mocker, default_conf, caplog) -> None:
     mock_throttle = MagicMock()
     mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle)
+    mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', MagicMock())
 
     worker = get_patched_worker(mocker, default_conf)
 
@@ -3126,10 +3127,27 @@ def test_get_sell_rate(default_conf, mocker, ticker, order_book_l2) -> None:
     assert rate == 0.043936
 
 
-def test_startup_messages(default_conf, mocker):
+def test_startup_state(default_conf, mocker):
     default_conf['pairlist'] = {'method': 'VolumePairList',
                                 'config': {'number_assets': 20}
                                 }
     mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
     worker = get_patched_worker(mocker, default_conf)
     assert worker.state is State.RUNNING
+
+
+def test_startup_trade_reinit(default_conf, edge_conf, mocker):
+
+    mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
+    reinit_mock = MagicMock()
+    mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', reinit_mock)
+
+    ftbot = get_patched_freqtradebot(mocker, default_conf)
+    ftbot.startup()
+    assert reinit_mock.call_count == 1
+
+    reinit_mock.reset_mock()
+
+    ftbot = get_patched_freqtradebot(mocker, edge_conf)
+    ftbot.startup()
+    assert reinit_mock.call_count == 0
diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py
--- a/freqtrade/tests/test_persistence.py
+++ b/freqtrade/tests/test_persistence.py
@@ -777,3 +777,63 @@ def test_to_json(default_conf, fee):
                       'stop_loss_pct': None,
                       'initial_stop_loss': None,
                       'initial_stop_loss_pct': None}
+
+
+def test_stoploss_reinitialization(default_conf, fee):
+    init(default_conf['db_url'])
+    trade = Trade(
+        pair='ETH/BTC',
+        stake_amount=0.001,
+        fee_open=fee.return_value,
+        open_date=arrow.utcnow().shift(hours=-2).datetime,
+        amount=10,
+        fee_close=fee.return_value,
+        exchange='bittrex',
+        open_rate=1,
+        max_rate=1,
+    )
+
+    trade.adjust_stop_loss(trade.open_rate, 0.05, True)
+    assert trade.stop_loss == 0.95
+    assert trade.stop_loss_pct == -0.05
+    assert trade.initial_stop_loss == 0.95
+    assert trade.initial_stop_loss_pct == -0.05
+    Trade.session.add(trade)
+
+    # Lower stoploss
+    Trade.stoploss_reinitialization(0.06)
+
+    trades = Trade.get_open_trades()
+    assert len(trades) == 1
+    trade_adj = trades[0]
+    assert trade_adj.stop_loss == 0.94
+    assert trade_adj.stop_loss_pct == -0.06
+    assert trade_adj.initial_stop_loss == 0.94
+    assert trade_adj.initial_stop_loss_pct == -0.06
+
+    # Raise stoploss
+    Trade.stoploss_reinitialization(0.04)
+
+    trades = Trade.get_open_trades()
+    assert len(trades) == 1
+    trade_adj = trades[0]
+    assert trade_adj.stop_loss == 0.96
+    assert trade_adj.stop_loss_pct == -0.04
+    assert trade_adj.initial_stop_loss == 0.96
+    assert trade_adj.initial_stop_loss_pct == -0.04
+
+    # Trailing stoploss (move stoplos up a bit)
+    trade.adjust_stop_loss(1.02, 0.04)
+    assert trade_adj.stop_loss == 0.9792
+    assert trade_adj.initial_stop_loss == 0.96
+
+    Trade.stoploss_reinitialization(0.04)
+
+    trades = Trade.get_open_trades()
+    assert len(trades) == 1
+    trade_adj = trades[0]
+    # Stoploss should not change in this case.
+    assert trade_adj.stop_loss == 0.9792
+    assert trade_adj.stop_loss_pct == -0.04
+    assert trade_adj.initial_stop_loss == 0.96
+    assert trade_adj.initial_stop_loss_pct == -0.04