diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index afe23d5a5..8d55da9e9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -7,6 +7,7 @@ import traceback from datetime import datetime from math import isclose from os import getpid +from threading import Lock from typing import Any, Dict, List, Optional, Tuple import arrow @@ -27,7 +28,6 @@ from freqtrade.state import State from freqtrade.strategy.interface import IStrategy, SellType from freqtrade.wallets import Wallets - logger = logging.getLogger(__name__) @@ -92,6 +92,8 @@ class FreqtradeBot: # the initial state of the bot. # Keep this at the end of this initialization method. self.rpc: RPCManager = RPCManager(self) + # Protect sell-logic from forcesell and viceversa + self._sell_lock = Lock() def cleanup(self) -> None: """ @@ -132,8 +134,12 @@ class FreqtradeBot: self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist), self.strategy.informative_pairs()) - # First process current opened trades (positions) - self.exit_positions(trades) + # Protect from collisions with forcesell. + # Without this, freqtrade my try to recreate stoploss_on_exchange orders + # while selling is in process, since telegram messages arrive in an different thread. + with self._sell_lock: + # First process current opened trades (positions) + self.exit_positions(trades) # Then looking for buy opportunities if self.get_free_open_trades(): @@ -744,8 +750,8 @@ class FreqtradeBot: Check and execute sell """ should_sell = self.strategy.should_sell( - trade, sell_rate, datetime.utcnow(), buy, sell, - force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 + trade, sell_rate, datetime.utcnow(), buy, sell, + force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) if should_sell.sell_flag: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d58b99f39..41097c211 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -420,24 +420,27 @@ class RPC: if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') - if trade_id == 'all': - # Execute sell for all open orders - for trade in Trade.get_open_trades(): - _exec_forcesell(trade) + with self._freqtrade._sell_lock: + if trade_id == 'all': + # Execute sell for all open orders + for trade in Trade.get_open_trades(): + _exec_forcesell(trade) + Trade.session.flush() + self._freqtrade.wallets.update() + return {'result': 'Created sell orders for all open trades.'} + + # Query for trade + trade = Trade.get_trades( + trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ] + ).first() + if not trade: + logger.warning('forcesell: Invalid argument received') + raise RPCException('invalid argument') + + _exec_forcesell(trade) Trade.session.flush() - return {'result': 'Created sell orders for all open trades.'} - - # Query for trade - trade = Trade.get_trades( - trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ] - ).first() - if not trade: - logger.warning('forcesell: Invalid argument received') - raise RPCException('invalid argument') - - _exec_forcesell(trade) - Trade.session.flush() - return {'result': f'Created sell order for trade {trade_id}.'} + self._freqtrade.wallets.update() + return {'result': f'Created sell order for trade {trade_id}.'} def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]: """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3df15fc5f..7697c777f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -906,30 +906,22 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: assert ("ETH/BTC", default_conf["ticker_interval"]) in refresh_mock.call_args[0][0] -def test_balance_fully_ask_side(mocker, default_conf) -> None: - default_conf['bid_strategy']['ask_last_balance'] = 0.0 +@pytest.mark.parametrize("ask,last,last_ab,expected", [ + (20, 10, 0.0, 20), # Full ask side + (20, 10, 1.0, 10), # Full last side + (20, 10, 0.5, 15), # Between ask and last + (20, 10, 0.7, 13), # Between ask and last + (20, 10, 0.3, 17), # Between ask and last + (5, 10, 1.0, 5), # last bigger than ask + (5, 10, 0.5, 5), # last bigger than ask +]) +def test_get_buy_rate(mocker, default_conf, ask, last, last_ab, expected) -> None: + default_conf['bid_strategy']['ask_last_balance'] = last_ab freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - MagicMock(return_value={'ask': 20, 'last': 10})) + MagicMock(return_value={'ask': ask, 'last': last})) - assert freqtrade.get_buy_rate('ETH/BTC') == 20 - - -def test_balance_fully_last_side(mocker, default_conf) -> None: - default_conf['bid_strategy']['ask_last_balance'] = 1.0 - freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - MagicMock(return_value={'ask': 20, 'last': 10})) - - assert freqtrade.get_buy_rate('ETH/BTC') == 10 - - -def test_balance_bigger_last_ask(mocker, default_conf) -> None: - default_conf['bid_strategy']['ask_last_balance'] = 1.0 - freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - MagicMock(return_value={'ask': 5, 'last': 10})) - assert freqtrade.get_buy_rate('ETH/BTC') == 5 + assert freqtrade.get_buy_rate('ETH/BTC') == expected def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: