From f36bc80ad12a9784e2d20a9b9b9a7d10e2d62208 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Jan 2020 19:43:02 +0100 Subject: [PATCH 1/3] Add parametrized tests for get_buy_rate --- tests/test_freqtradebot.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d00fab9c7..e0f2ecd3a 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: From 58ceda4b903172d153d34337c9bb4f72a2dda4dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Jan 2020 19:54:55 +0100 Subject: [PATCH 2/3] update wallets after forcesell --- freqtrade/rpc/rpc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d58b99f39..c38f36c1d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -425,6 +425,7 @@ class RPC: 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 @@ -437,6 +438,7 @@ class RPC: _exec_forcesell(trade) Trade.session.flush() + 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]: From aad10ceee35303370c4a01a1b88e92691b01b615 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Jan 2020 20:50:09 +0100 Subject: [PATCH 3/3] Add threading lock object for /forcesell Protects against stoploss_on_exchange order recreation in case of /forcesell (it's a timing issue, so may or may not happen). --- freqtrade/freqtradebot.py | 16 +++++++++++----- freqtrade/rpc/rpc.py | 37 +++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 387ddb063..e3856e200 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(): @@ -748,8 +754,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 c38f36c1d..41097c211 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -420,26 +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() 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() - self._freqtrade.wallets.update() - return {'result': f'Created sell order for trade {trade_id}.'} + return {'result': f'Created sell order for trade {trade_id}.'} def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]: """