From 8a17615b5a96fd59ed39ba317e4acb741d0f881b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Aug 2019 19:41:11 +0200 Subject: [PATCH 01/15] move exceptionhandling from create_order() to calling functions --- freqtrade/exchange/exchange.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a8e974991..53723ad51 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -320,7 +320,7 @@ class Exchange(object): if (order_types.get("stoploss_on_exchange") and not self._ft_has.get("stoploss_on_exchange", False)): raise OperationalException( - 'On exchange stoploss is not supported for %s.' % self.name + f'On exchange stoploss is not supported for {self.name}.' ) def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: @@ -469,11 +469,26 @@ class Exchange(object): params = self._params.copy() params.update({'stopPrice': stop_price}) - - order = self.create_order(pair, ordertype, 'sell', amount, rate, params) - logger.info('stoploss limit order added for %s. ' - 'stop price: %s. limit: %s', pair, stop_price, rate) - return order + try: + order = self.create_order(pair, ordertype, 'sell', amount, rate, params) + logger.info('stoploss limit order added for %s. ' + 'stop price: %s. limit: %s', pair, stop_price, rate) + return order + except ccxt.InsufficientFunds as e: + raise DependencyException( + f'Insufficient funds to create {ordertype} sell order on market {pair}.' + f'Tried to sell amount {amount} at rate {rate}.' + f'Message: {e}') from e + except ccxt.InvalidOrder as e: + raise DependencyException( + f'Could not create {ordertype} sell order on market {pair}. ' + f'Tried to sell amount {amount} at rate {rate}.' + 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_balance(self, currency: str) -> float: From ea179a8e38a2dbaaf38e7b6a1904c174215aab83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Aug 2019 09:43:10 +0200 Subject: [PATCH 02/15] stoploss_limit shall not use `create_order()` It needs to handle exceptions differently --- freqtrade/exchange/exchange.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 53723ad51..7a384b7cc 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -470,7 +470,12 @@ class Exchange(object): params = self._params.copy() params.update({'stopPrice': stop_price}) try: - order = self.create_order(pair, ordertype, 'sell', amount, rate, params) + amount = self.symbol_amount_prec(pair, amount) + + rate = self.symbol_price_prec(pair, rate) + + order = self._api.create_order(pair, ordertype, 'sell', + amount, rate, params) logger.info('stoploss limit order added for %s. ' 'stop price: %s. limit: %s', pair, stop_price, rate) return order From defa1c027d911c348586fbf7c0419113a122d796 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Aug 2019 09:50:37 +0200 Subject: [PATCH 03/15] Move stoploss_limit to binance subclass --- freqtrade/exchange/binance.py | 52 ++++++++++++++++++++++++++++++++++ freqtrade/exchange/exchange.py | 48 ++++--------------------------- 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 18e754e3f..b63ef59c8 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -2,6 +2,9 @@ import logging from typing import Dict +import ccxt + +from freqtrade import DependencyException, OperationalException, TemporaryError from freqtrade.exchange import Exchange logger = logging.getLogger(__name__) @@ -25,3 +28,52 @@ class Binance(Exchange): limit = min(list(filter(lambda x: limit <= x, limit_range))) return super().get_order_book(pair, limit) + + def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict: + """ + creates a stoploss limit order. + this stoploss-limit is binance-specific. + It may work with a limited number of other exchanges, but this has not been tested yet. + + """ + ordertype = "stop_loss_limit" + + stop_price = self.symbol_price_prec(pair, stop_price) + + # Ensure rate is less than stop price + if stop_price <= rate: + raise OperationalException( + 'In stoploss limit order, stop price should be more than limit price') + + if self._config['dry_run']: + dry_order = self.dry_run_order( + pair, ordertype, "sell", amount, stop_price) + return dry_order + + params = self._params.copy() + params.update({'stopPrice': stop_price}) + try: + amount = self.symbol_amount_prec(pair, amount) + + rate = self.symbol_price_prec(pair, rate) + + order = self._api.create_order(pair, ordertype, 'sell', + amount, rate, params) + logger.info('stoploss limit order added for %s. ' + 'stop price: %s. limit: %s', pair, stop_price, rate) + return order + except ccxt.InsufficientFunds as e: + raise DependencyException( + f'Insufficient funds to create {ordertype} sell order on market {pair}.' + f'Tried to sell amount {amount} at rate {rate}.' + f'Message: {e}') from e + except ccxt.InvalidOrder as e: + raise DependencyException( + f'Could not create {ordertype} sell order on market {pair}. ' + f'Tried to sell amount {amount} at rate {rate}.' + 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 diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7a384b7cc..534e2ea65 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -450,50 +450,14 @@ class Exchange(object): def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict: """ creates a stoploss limit order. - NOTICE: it is not supported by all exchanges. only binance is tested for now. - TODO: implementation maybe needs to be moved to the binance subclass + Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each + exchange's subclass. + The exception below should never raise, since we disallow + starting the bot in validate_ordertypes() + Note: Changes to this interface need to be applied to all sub-classes too. """ - ordertype = "stop_loss_limit" - stop_price = self.symbol_price_prec(pair, stop_price) - - # Ensure rate is less than stop price - if stop_price <= rate: - raise OperationalException( - 'In stoploss limit order, stop price should be more than limit price') - - if self._config['dry_run']: - dry_order = self.dry_run_order( - pair, ordertype, "sell", amount, stop_price) - return dry_order - - params = self._params.copy() - params.update({'stopPrice': stop_price}) - try: - amount = self.symbol_amount_prec(pair, amount) - - rate = self.symbol_price_prec(pair, rate) - - order = self._api.create_order(pair, ordertype, 'sell', - amount, rate, params) - logger.info('stoploss limit order added for %s. ' - 'stop price: %s. limit: %s', pair, stop_price, rate) - return order - except ccxt.InsufficientFunds as e: - raise DependencyException( - f'Insufficient funds to create {ordertype} sell order on market {pair}.' - f'Tried to sell amount {amount} at rate {rate}.' - f'Message: {e}') from e - except ccxt.InvalidOrder as e: - raise DependencyException( - f'Could not create {ordertype} sell order on market {pair}. ' - f'Tried to sell amount {amount} at rate {rate}.' - 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 + raise OperationalException(f"stoploss_limit not implemented for {self.name}.") @retrier def get_balance(self, currency: str) -> float: From 067c122bf3e4e53d45eee4b48e6083a531ef5e66 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Aug 2019 09:52:14 +0200 Subject: [PATCH 04/15] Adapt test to use Binance class --- freqtrade/tests/test_freqtradebot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index dab7a9ff7..1119157c4 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2414,7 +2414,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit) freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.order_types['stoploss_on_exchange'] = True @@ -2454,7 +2454,6 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, freqtrade.process_maybe_execute_sell(trade) assert trade.stoploss_order_id is None assert trade.is_open is False - print(trade.sell_reason) assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value assert rpc_mock.call_count == 2 From 2c66b33fd107e0829cba668002d549a56bac4c56 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Aug 2019 09:57:21 +0200 Subject: [PATCH 05/15] Adapt some tests to use Binance subclass for stoplosslimit --- freqtrade/exchange/exchange.py | 2 +- freqtrade/tests/exchange/test_exchange.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 534e2ea65..6c93f9128 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -457,7 +457,7 @@ class Exchange(object): Note: Changes to this interface need to be applied to all sub-classes too. """ - raise OperationalException(f"stoploss_limit not implemented for {self.name}.") + raise OperationalException(f"stoploss_limit is not implemented for {self.name}.") @retrier def get_balance(self, currency: str) -> float: diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index e453b5dca..0dc07e409 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1474,17 +1474,17 @@ def test_stoploss_limit_order(default_conf, mocker): # 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) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) with pytest.raises(TemporaryError): api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) with pytest.raises(OperationalException): @@ -1493,6 +1493,12 @@ def test_stoploss_limit_order(default_conf, mocker): exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) +def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf, 'bittrex') + with pytest.raises(OperationalException, match=r"stoploss_limit is not implemented .*"): + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + def test_stoploss_limit_order_dry_run(default_conf, mocker): api_mock = MagicMock() order_type = 'stop_loss_limit' From cbf09b5ad9781f42b0b836d45fbbd3936576c59f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Aug 2019 10:07:47 +0200 Subject: [PATCH 06/15] Improve docstring for Exception --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 14f0bb819..5ccc2ff3c 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -11,7 +11,7 @@ class DependencyException(Exception): class OperationalException(Exception): """ - Requires manual intervention. + Requires manual intervention and will usually stop the bot. This happens when an exchange returns an unexpected error during runtime or given configuration is invalid. """ From a4c8b5bf5da8228c3300e3d4c83c763e290b0a57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Aug 2019 10:08:06 +0200 Subject: [PATCH 07/15] Move binance-specific test to test_binance.py --- freqtrade/exchange/binance.py | 5 +- freqtrade/tests/exchange/test_binance.py | 90 +++++++++++++++++++++++ freqtrade/tests/exchange/test_exchange.py | 78 -------------------- 3 files changed, 93 insertions(+), 80 deletions(-) create mode 100644 freqtrade/tests/exchange/test_binance.py diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index b63ef59c8..5834f26cd 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -50,9 +50,10 @@ class Binance(Exchange): pair, ordertype, "sell", amount, stop_price) return dry_order - params = self._params.copy() - params.update({'stopPrice': stop_price}) try: + params = self._params.copy() + params.update({'stopPrice': stop_price}) + amount = self.symbol_amount_prec(pair, amount) rate = self.symbol_price_prec(pair, rate) diff --git a/freqtrade/tests/exchange/test_binance.py b/freqtrade/tests/exchange/test_binance.py new file mode 100644 index 000000000..4afb7fcc4 --- /dev/null +++ b/freqtrade/tests/exchange/test_binance.py @@ -0,0 +1,90 @@ +from random import randint +from unittest.mock import MagicMock + +import ccxt +import pytest + +from freqtrade import DependencyException, OperationalException, TemporaryError +from freqtrade.tests.conftest import get_patched_exchange + + +def test_stoploss_limit_order(default_conf, mocker): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_type = 'stop_loss_limit' + + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + + default_conf['dry_run'] = False + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') + + with pytest.raises(OperationalException): + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200) + + api_mock.create_order.reset_mock() + + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' + assert api_mock.create_order.call_args[0][1] == order_type + assert api_mock.create_order.call_args[0][2] == 'sell' + assert api_mock.create_order.call_args[0][3] == 1 + assert api_mock.create_order.call_args[0][4] == 200 + assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220} + + # 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, 'binance') + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + with pytest.raises(TemporaryError): + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + 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, 'binance') + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + +def test_stoploss_limit_order_dry_run(default_conf, mocker): + api_mock = MagicMock() + order_type = 'stop_loss_limit' + default_conf['dry_run'] = True + mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') + + with pytest.raises(OperationalException): + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200) + + api_mock.create_order.reset_mock() + + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + + assert 'id' in order + assert 'info' in order + assert 'type' in order + + assert order['type'] == order_type + assert order['price'] == 220 + assert order['amount'] == 1 diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 0dc07e409..afd7b441a 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1436,62 +1436,6 @@ def test_get_fee(default_conf, mocker, exchange_name): 'get_fee', 'calculate_fee') -def test_stoploss_limit_order(default_conf, mocker): - api_mock = MagicMock() - order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) - order_type = 'stop_loss_limit' - - api_mock.create_order = MagicMock(return_value={ - 'id': order_id, - 'info': { - 'foo': 'bar' - } - }) - - default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) - - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - - with pytest.raises(OperationalException): - order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200) - - api_mock.create_order.reset_mock() - - order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) - - assert 'id' in order - assert 'info' in order - assert order['id'] == order_id - assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' - assert api_mock.create_order.call_args[0][1] == order_type - assert api_mock.create_order.call_args[0][2] == 'sell' - assert api_mock.create_order.call_args[0][3] == 1 - assert api_mock.create_order.call_args[0][4] == 200 - assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220} - - # 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, 'binance') - exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) - - with pytest.raises(DependencyException): - api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) - - with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) - - with pytest.raises(OperationalException): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock) - exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) - def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, 'bittrex') @@ -1499,29 +1443,7 @@ def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker): exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) -def test_stoploss_limit_order_dry_run(default_conf, mocker): - api_mock = MagicMock() - order_type = 'stop_loss_limit' - default_conf['dry_run'] = True - mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - - with pytest.raises(OperationalException): - order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200) - - api_mock.create_order.reset_mock() - - order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) - - assert 'id' in order - assert 'info' in order - assert 'type' in order - - assert order['type'] == order_type - assert order['price'] == 220 - assert order['amount'] == 1 def test_merge_ft_has_dict(default_conf, mocker): From 5e12b05424df197a5d724d9bb164b139f775076a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Aug 2019 10:13:35 +0200 Subject: [PATCH 08/15] Improve test coverage --- freqtrade/tests/exchange/test_exchange.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index afd7b441a..d9f350c7b 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -101,18 +101,21 @@ def test_destroy(default_conf, mocker, caplog): def test_init_exception(default_conf, mocker): default_conf['exchange']['name'] = 'wrong_exchange_name' - with pytest.raises( - OperationalException, - match='Exchange {} is not supported'.format(default_conf['exchange']['name'])): + with pytest.raises(OperationalException, + match=f"Exchange {default_conf['exchange']['name']} is not supported"): Exchange(default_conf) default_conf['exchange']['name'] = 'binance' - with pytest.raises( - OperationalException, - match='Exchange {} is not supported'.format(default_conf['exchange']['name'])): + with pytest.raises(OperationalException, + match=f"Exchange {default_conf['exchange']['name']} is not supported"): mocker.patch("ccxt.binance", MagicMock(side_effect=AttributeError)) Exchange(default_conf) + with pytest.raises(OperationalException, + match=r"Initialization of ccxt failed. Reason: DeadBeef"): + mocker.patch("ccxt.binance", MagicMock(side_effect=ccxt.BaseError("DeadBeef"))) + Exchange(default_conf) + def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) @@ -1436,16 +1439,12 @@ def test_get_fee(default_conf, mocker, exchange_name): 'get_fee', 'calculate_fee') - def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, 'bittrex') with pytest.raises(OperationalException, match=r"stoploss_limit is not implemented .*"): exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) - - - def test_merge_ft_has_dict(default_conf, mocker): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), From 3232251fea8f05c309169c9449261dc720ea5366 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Aug 2019 15:01:27 +0200 Subject: [PATCH 09/15] Refactor downloading ohlcv from utils to history --- freqtrade/data/history.py | 29 +++++++++++++++++++++++++++++ freqtrade/utils.py | 32 ++++++++------------------------ 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 007357d9a..aff9f5c74 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -280,6 +280,35 @@ def download_pair_history(datadir: Optional[Path], return False +def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str], + dl_path: Path, timerange: TimeRange, + erase=False) -> List[str]: + """ + Refresh stored ohlcv data for backtesting and hyperopt operations. + Used by freqtrade download-data + :return: Pairs not available + """ + pairs_not_available = [] + for pair in pairs: + if pair not in exchange.markets: + pairs_not_available.append(pair) + logger.info(f"Skipping pair {pair}...") + continue + for ticker_interval in timeframes: + + dl_file = pair_data_filename(dl_path, pair, ticker_interval) + if erase and dl_file.exists(): + logger.info( + f'Deleting existing data for pair {pair}, interval {ticker_interval}.') + dl_file.unlink() + + logger.info(f'Downloading pair {pair}, interval {ticker_interval}.') + download_pair_history(datadir=dl_path, exchange=exchange, + pair=pair, ticker_interval=str(ticker_interval), + timerange=timerange) + return pairs_not_available + + def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: """ Get the maximum timeframe for the given backtest data diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 56e60ec82..162493a3f 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -2,13 +2,13 @@ import logging import sys from argparse import Namespace from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, List import arrow from freqtrade.configuration import Configuration, TimeRange from freqtrade.configuration.directory_operations import create_userdata_dir -from freqtrade.data.history import download_pair_history +from freqtrade.data.history import refresh_backtest_ohlcv_data from freqtrade.exchange import available_exchanges from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode @@ -75,39 +75,23 @@ def start_download_data(args: Namespace) -> None: logger.info(f'About to download pairs: {config["pairs"]}, ' f'intervals: {config["timeframes"]} to {dl_path}') - pairs_not_available = [] + pairs_not_available: List[str] = [] try: # Init exchange exchange = ExchangeResolver(config['exchange']['name'], config).exchange - for pair in config["pairs"]: - if pair not in exchange.markets: - pairs_not_available.append(pair) - logger.info(f"Skipping pair {pair}...") - continue - for ticker_interval in config["timeframes"]: - pair_print = pair.replace('/', '_') - filename = f'{pair_print}-{ticker_interval}.json' - dl_file = dl_path.joinpath(filename) - if config.get("erase") and dl_file.exists(): - logger.info( - f'Deleting existing data for pair {pair}, interval {ticker_interval}.') - dl_file.unlink() - - logger.info(f'Downloading pair {pair}, interval {ticker_interval}.') - download_pair_history(datadir=dl_path, exchange=exchange, - pair=pair, ticker_interval=str(ticker_interval), - timerange=timerange) + pairs_not_available = refresh_backtest_ohlcv_data( + exchange, pairs=config["pairs"], timeframes=config["timeframes"], + dl_path=Path(config['datadir']), timerange=timerange, erase=config.get("erase")) except KeyboardInterrupt: sys.exit("SIGINT received, aborting ...") finally: if pairs_not_available: - logger.info( - f"Pairs [{','.join(pairs_not_available)}] not available " - f"on exchange {config['exchange']['name']}.") + logger.info(f"Pairs [{','.join(pairs_not_available)}] not available " + f"on exchange {config['exchange']['name']}.") # configuration.resolve_pairs_list() print(config) From da7da2ce524e0bd41576251af7d1a48ee71184ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Aug 2019 15:02:40 +0200 Subject: [PATCH 10/15] Change tests to split function --- freqtrade/tests/data/test_history.py | 43 +++++++++++++- freqtrade/tests/test_utils.py | 87 ++++++---------------------- 2 files changed, 61 insertions(+), 69 deletions(-) diff --git a/freqtrade/tests/data/test_history.py b/freqtrade/tests/data/test_history.py index 7360f3c1c..ec4a05e63 100644 --- a/freqtrade/tests/data/test_history.py +++ b/freqtrade/tests/data/test_history.py @@ -5,7 +5,7 @@ import os import uuid from pathlib import Path from shutil import copyfile -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock import arrow import pytest @@ -17,6 +17,7 @@ from freqtrade.data import history from freqtrade.data.history import (download_pair_history, load_cached_data_for_updating, load_tickerdata_file, make_testdata_path, + refresh_backtest_ohlcv_data, trim_tickerlist) from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json @@ -558,3 +559,43 @@ def test_validate_backtest_data(default_conf, mocker, caplog) -> None: assert not history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', min_date, max_date, timeframe_to_minutes('5m')) assert len(caplog.record_tuples) == 0 + + +def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog): + dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock()) + mocker.patch( + 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) + ) + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + mocker.patch.object(Path, "unlink", MagicMock()) + + ex = get_patched_exchange(mocker, default_conf) + timerange = TimeRange.parse_timerange("20190101-20190102") + refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"], + timeframes=["1m", "5m"], dl_path=make_testdata_path(None), + timerange=timerange, erase=True + ) + + assert dl_mock.call_count == 4 + assert dl_mock.call_args[1]['timerange'].starttype == 'date' + + assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog) + + +def test_download_data_no_markets(mocker, default_conf, caplog): + dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock()) + mocker.patch( + 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) + ) + ex = get_patched_exchange(mocker, default_conf) + timerange = TimeRange.parse_timerange("20190101-20190102") + unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"], + timeframes=["1m", "5m"], + dl_path=make_testdata_path(None), + timerange=timerange, erase=False + ) + + assert dl_mock.call_count == 0 + assert "ETH/BTC" in unav_pairs + assert "XRP/BTC" in unav_pairs + assert log_has("Skipping pair ETH/BTC...", caplog) diff --git a/freqtrade/tests/test_utils.py b/freqtrade/tests/test_utils.py index d04e62b28..9e09fd298 100644 --- a/freqtrade/tests/test_utils.py +++ b/freqtrade/tests/test_utils.py @@ -1,5 +1,4 @@ import re -from pathlib import Path from unittest.mock import MagicMock, PropertyMock import pytest @@ -70,74 +69,8 @@ def test_create_datadir(caplog, mocker): assert len(caplog.record_tuples) == 0 -def test_download_data(mocker, markets, caplog): - dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock()) - patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) - ) - mocker.patch.object(Path, "exists", MagicMock(return_value=True)) - mocker.patch.object(Path, "unlink", MagicMock()) - - args = [ - "download-data", - "--exchange", "binance", - "--pairs", "ETH/BTC", "XRP/BTC", - "--erase", - ] - start_download_data(get_args(args)) - - assert dl_mock.call_count == 4 - assert dl_mock.call_args[1]['timerange'].starttype is None - assert dl_mock.call_args[1]['timerange'].stoptype is None - assert log_has("Deleting existing data for pair ETH/BTC, interval 1m.", caplog) - assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog) - - -def test_download_data_days(mocker, markets, caplog): - dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock()) - patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) - ) - mocker.patch.object(Path, "exists", MagicMock(return_value=True)) - mocker.patch.object(Path, "unlink", MagicMock()) - - args = [ - "download-data", - "--exchange", "binance", - "--pairs", "ETH/BTC", "XRP/BTC", - "--days", "20", - ] - - start_download_data(get_args(args)) - - assert dl_mock.call_count == 4 - assert dl_mock.call_args[1]['timerange'].starttype == 'date' - - assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog) - - -def test_download_data_no_markets(mocker, caplog): - dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock()) - patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) - args = [ - "download-data", - "--exchange", "binance", - "--pairs", "ETH/BTC", "XRP/BTC", - ] - start_download_data(get_args(args)) - - assert dl_mock.call_count == 0 - assert log_has("Skipping pair ETH/BTC...", caplog) - assert log_has("Pairs [ETH/BTC,XRP/BTC] not available on exchange binance.", caplog) - - def test_download_data_keyboardInterrupt(mocker, caplog, markets): - dl_mock = mocker.patch('freqtrade.utils.download_pair_history', + dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) patch_exchange(mocker) mocker.patch( @@ -152,3 +85,21 @@ def test_download_data_keyboardInterrupt(mocker, caplog, markets): start_download_data(get_args(args)) assert dl_mock.call_count == 1 + + +def test_download_data_no_markets(mocker, caplog): + dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data', + MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) + patch_exchange(mocker) + mocker.patch( + 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) + ) + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC", + "--days", "20" + ] + start_download_data(get_args(args)) + assert dl_mock.call_args[1]['timerange'].starttype == "date" + assert log_has("Pairs [ETH/BTC,XRP/BTC] not available on exchange binance.", caplog) From 513e84880e4a0e60b3f1e04edd8637908c874818 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Aug 2019 20:38:51 +0200 Subject: [PATCH 11/15] Don't escape ticks where it's not needed --- freqtrade/freqtradebot.py | 6 +++--- freqtrade/optimize/hyperopt.py | 6 +++--- freqtrade/persistence.py | 4 ++-- freqtrade/tests/optimize/test_hyperopt.py | 4 ++-- freqtrade/tests/test_configuration.py | 10 +++++----- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e88b9db6a..8857b95da 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -216,7 +216,7 @@ class FreqtradeBot(object): if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: open_trades = len(Trade.get_open_trades()) if open_trades >= self.config['max_open_trades']: - logger.warning('Can\'t open a new trade: max number of trades is reached') + logger.warning("Can't open a new trade: max number of trades is reached") return None return available_amount / (self.config['max_open_trades'] - open_trades) @@ -351,8 +351,8 @@ class FreqtradeBot(object): min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested) if min_stake_amount is not None and min_stake_amount > stake_amount: logger.warning( - f'Can\'t open a new trade for {pair_s}: stake amount ' - f'is too small ({stake_amount} < {min_stake_amount})' + f"Can't open a new trade for {pair_s}: stake amount " + f"is too small ({stake_amount} < {min_stake_amount})" ) return False diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 62a6ab27b..9c3f085b6 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -124,14 +124,14 @@ class Hyperopt: Save hyperopt trials to file """ if self.trials: - logger.info('Saving %d evaluations to \'%s\'', len(self.trials), self.trials_file) + logger.info("Saving %d evaluations to '%s'", len(self.trials), self.trials_file) dump(self.trials, self.trials_file) def read_trials(self) -> List: """ Read hyperopt trials file """ - logger.info('Reading Trials from \'%s\'', self.trials_file) + logger.info("Reading Trials from '%s'", self.trials_file) trials = load(self.trials_file) self.trials_file.unlink() return trials @@ -379,7 +379,7 @@ class Hyperopt: self.load_previous_results() cpus = cpu_count() - logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!') + logger.info(f"Found {cpus} CPU cores. Let's make them scream!") config_jobs = self.config.get('hyperopt_jobs', -1) logger.info(f'Number of parallel jobs set as: {config_jobs}') diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index c844bbc4c..dff7e4ff6 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -48,8 +48,8 @@ def init(db_url: str, clean_open_orders: bool = False) -> None: try: engine = create_engine(db_url, **kwargs) except NoSuchModuleError: - raise OperationalException(f'Given value for db_url: \'{db_url}\' ' - f'is no valid database URL! (See {_SQL_DOCS_URL})') + raise OperationalException(f"Given value for db_url: '{db_url}' " + f"is no valid database URL! (See {_SQL_DOCS_URL})") session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) Trade.session = session() diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 7b525454c..f9ebf552d 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -381,7 +381,7 @@ def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None: hyperopt.save_trials() trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') - assert log_has('Saving 1 evaluations to \'{}\''.format(trials_file), caplog) + assert log_has("Saving 1 evaluations to '{}'".format(trials_file), caplog) mock_dump.assert_called_once() @@ -390,7 +390,7 @@ def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None: mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials) hyperopt_trial = hyperopt.read_trials() trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') - assert log_has('Reading Trials from \'{}\''.format(trials_file), caplog) + assert log_has("Reading Trials from '{}'".format(trials_file), caplog) assert hyperopt_trial == trials mock_load.assert_called_once() diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 10ce7e8cf..d98ff9ad0 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -41,14 +41,14 @@ def test_load_config_invalid_pair(default_conf) -> None: def test_load_config_missing_attributes(default_conf) -> None: default_conf.pop('exchange') - with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'): + with pytest.raises(ValidationError, match=r".*'exchange' is a required property.*"): validate_config_schema(default_conf) def test_load_config_incorrect_stake_amount(default_conf) -> None: default_conf['stake_amount'] = 'fake' - with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'): + with pytest.raises(ValidationError, match=r".*'fake' does not match 'unlimited'.*"): validate_config_schema(default_conf) @@ -472,7 +472,7 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: assert 'spaces' in config assert config['spaces'] == ['all'] - assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog) + assert log_has("Parameter -s/--spaces detected: ['all']", caplog) assert "runmode" in config assert config['runmode'] == RunMode.HYPEROPT @@ -722,7 +722,7 @@ def test_load_config_default_exchange(all_conf) -> None: assert 'exchange' not in all_conf with pytest.raises(ValidationError, - match=r'\'exchange\' is a required property'): + match=r"'exchange' is a required property"): validate_config_schema(all_conf) @@ -736,7 +736,7 @@ def test_load_config_default_exchange_name(all_conf) -> None: assert 'name' not in all_conf['exchange'] with pytest.raises(ValidationError, - match=r'\'name\' is a required property'): + match=r"'name' is a required property"): validate_config_schema(all_conf) From e5da5f7fe79e7ae2a2957f22bce426b97445dd20 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2019 08:07:03 +0000 Subject: [PATCH 12/15] Bump mkdocs-material from 4.4.0 to 4.4.1 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 4.4.0 to 4.4.1. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/4.4.0...4.4.1) Signed-off-by: dependabot-preview[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index ce76d52e5..10ea068ad 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1 +1 @@ -mkdocs-material==4.4.0 \ No newline at end of file +mkdocs-material==4.4.1 \ No newline at end of file From 75e3d22043abb0e96a03e2f2201c39261b970de2 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2019 08:07:07 +0000 Subject: [PATCH 13/15] Bump pytest from 5.1.0 to 5.1.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.1.0 to 5.1.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.1.0...5.1.1) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6436c60e4..a7b0d358d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 mypy==0.720 -pytest==5.1.0 +pytest==5.1.1 pytest-asyncio==0.10.0 pytest-cov==2.7.1 pytest-mock==1.10.4 From 6b233eb862d79eaca0dd9a6734d4e6e287bbb3da Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2019 08:07:24 +0000 Subject: [PATCH 14/15] Bump ccxt from 1.18.1068 to 1.18.1085 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1068 to 1.18.1085. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.18.1068...1.18.1085) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 3d80c3ef5..c8a9c2f74 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.18.1068 +ccxt==1.18.1085 SQLAlchemy==1.3.7 python-telegram-bot==11.1.0 arrow==0.14.5 From 6af51358025a9a318cc04c2122b856485ac9f3ce Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2019 08:07:30 +0000 Subject: [PATCH 15/15] Bump pandas from 0.25.0 to 0.25.1 Bumps [pandas](https://github.com/pandas-dev/pandas) from 0.25.0 to 0.25.1. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v0.25.0...v0.25.1) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9d558b5b8..e5015d620 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ -r requirements-common.txt numpy==1.17.0 -pandas==0.25.0 +pandas==0.25.1 scipy==1.3.1